@bridge-ds/cli 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1679 -136
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -13,8 +13,9 @@ var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width",
13
13
  var ALIAS_RE = /^\{([^{}]+)\}$/;
14
14
  var isAlias = (v) => typeof v === "string" && ALIAS_RE.test(v);
15
15
  var aliasTarget = (v) => ALIAS_RE.exec(v)?.[1] ?? v;
16
- function hexToRgba(hex) {
17
- const m = /^#?([0-9a-f]{3,8})$/i.exec(hex.trim());
16
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
17
+ function hexToRgba(hex2) {
18
+ const m = /^#?([0-9a-f]{3,8})$/i.exec(hex2.trim());
18
19
  if (!m) return void 0;
19
20
  let h = m[1];
20
21
  if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
@@ -22,6 +23,65 @@ function hexToRgba(hex) {
22
23
  const n = (idx) => parseInt(h.slice(idx, idx + 2), 16) / 255;
23
24
  return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
24
25
  }
26
+ var to255 = (n) => Math.round(clamp01(n) * 255);
27
+ var rgbaToHex = (c) => "#" + [c.r, c.g, c.b].map((x) => to255(x).toString(16).padStart(2, "0")).join("");
28
+ function parseColor(value) {
29
+ if (typeof value === "string") return isAlias(value) ? void 0 : hexToRgba(value);
30
+ if (!value || typeof value !== "object") return void 0;
31
+ const v = value;
32
+ const alpha = typeof v.alpha === "number" ? v.alpha : 1;
33
+ if (v.colorSpace === "srgb" && Array.isArray(v.components) && v.components.length === 3) {
34
+ const [r, g, b] = v.components;
35
+ if ([r, g, b].every((x) => typeof x === "number")) return { r, g, b, a: alpha };
36
+ }
37
+ if (typeof v.hex === "string") {
38
+ const c = hexToRgba(v.hex);
39
+ if (c) return { ...c, a: alpha };
40
+ }
41
+ return void 0;
42
+ }
43
+ function colorsEqual(a, b) {
44
+ return rgbaToHex(a) === rgbaToHex(b) && Math.abs(a.a - b.a) < 2e-3;
45
+ }
46
+ function parseDimension(value) {
47
+ if (value && typeof value === "object" && !Array.isArray(value)) {
48
+ const v = value;
49
+ if (typeof v.value === "number" && typeof v.unit === "string") return { value: v.value, unit: v.unit };
50
+ }
51
+ if (typeof value === "string") {
52
+ const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
53
+ if (m) return { value: Number(m[1]), unit: m[2] };
54
+ }
55
+ if (typeof value === "number") return { value, unit: "px" };
56
+ return void 0;
57
+ }
58
+ var numbersEqual = (a, b) => Math.abs(a - b) < 1e-4;
59
+ function deepEqual(a, b) {
60
+ if (a === b) return true;
61
+ if (typeof a === "number" && typeof b === "number") return numbersEqual(a, b);
62
+ if (typeof a !== typeof b || !a || !b || typeof a !== "object") return false;
63
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
64
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, idx) => deepEqual(v, b[idx]));
65
+ const ka = Object.keys(a).filter((k) => a[k] !== void 0);
66
+ const kb = Object.keys(b).filter((k) => b[k] !== void 0);
67
+ if (ka.length !== kb.length) return false;
68
+ return ka.every((k) => deepEqual(a[k], b[k]));
69
+ }
70
+ function valuesEqual(type, a, b) {
71
+ if (a === void 0 || b === void 0) return a === b;
72
+ if (isAlias(a) || isAlias(b)) return isAlias(a) && isAlias(b) && aliasTarget(a) === aliasTarget(b);
73
+ if (type === "color") {
74
+ const ca = parseColor(a);
75
+ const cb = parseColor(b);
76
+ if (ca && cb) return colorsEqual(ca, cb);
77
+ }
78
+ if (type === "dimension") {
79
+ const da = parseDimension(a);
80
+ const db = parseDimension(b);
81
+ if (da && db) return da.unit === db.unit && numbersEqual(da.value, db.value);
82
+ }
83
+ return deepEqual(a, b);
84
+ }
25
85
 
26
86
  // ../core/src/audit/color.ts
27
87
  var NAMED_COLORS = {
@@ -1119,9 +1179,9 @@ var DesignSystemIndex = class {
1119
1179
  return generic ? this.make(generic, 1, 0.3) : void 0;
1120
1180
  }
1121
1181
  case "shadow": {
1122
- const norm4 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
1182
+ const norm6 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
1123
1183
  const cands = this.entries(["shadow"]);
1124
- const exact = cands.find((e) => norm4(e.css) === norm4(value));
1184
+ const exact = cands.find((e) => norm6(e.css) === norm6(value));
1125
1185
  if (exact) return this.make(exact, 0, 1);
1126
1186
  const t = shadowNums(value);
1127
1187
  const scored = cands.map((e) => {
@@ -1572,16 +1632,16 @@ function applyAllowlist(entries, findings, components) {
1572
1632
  return { findings: keep(findings), components: keep(components), ignored, used };
1573
1633
  }
1574
1634
  function parseAllowlist(json) {
1575
- const fail = (msg) => {
1635
+ const fail2 = (msg) => {
1576
1636
  throw new Error(`allowlist: ${msg}`);
1577
1637
  };
1578
- if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
1638
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail2('expected { "entries": [...] }');
1579
1639
  return json.entries.map((e, i) => {
1580
1640
  const x = e;
1581
- if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
1582
- if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
1583
- if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1584
- if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1641
+ if (typeof x.file !== "string" || !x.file) fail2(`entry ${i} needs "file"`);
1642
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail2(`entry ${i} (${x.file}) needs a "line" number`);
1643
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail2(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1644
+ if (x.what !== void 0 && typeof x.what !== "string") fail2(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1585
1645
  return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
1586
1646
  });
1587
1647
  }
@@ -2176,8 +2236,8 @@ function parseOwnerEntry(raw) {
2176
2236
  var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
2177
2237
  function ownerEntryProblems(raw) {
2178
2238
  if (raw === void 0) return [];
2179
- const isObj3 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
2180
- if (!isObj3(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
2239
+ const isObj4 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
2240
+ if (!isObj4(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
2181
2241
  const out = [];
2182
2242
  for (const [k, v] of Object.entries(raw)) {
2183
2243
  if (k !== "design" && k !== "tech") {
@@ -2220,6 +2280,31 @@ function teamsIn(owners) {
2220
2280
  return [...out.values()];
2221
2281
  }
2222
2282
  var norm = (x) => x.trim().toLowerCase().replace(/^@/, "");
2283
+ function expandEntries(entries, teams) {
2284
+ const out = [];
2285
+ const seen = /* @__PURE__ */ new Set();
2286
+ const add = (x) => {
2287
+ const k = norm(x);
2288
+ if (k && !seen.has(k)) {
2289
+ seen.add(k);
2290
+ out.push(x.replace(/^@(?=[^/]+$)/, ""));
2291
+ }
2292
+ };
2293
+ for (const raw of entries) {
2294
+ const e = parseOwnerEntry(raw);
2295
+ if (!e) continue;
2296
+ if (e.kind === "person") add(e.login);
2297
+ else {
2298
+ const state = teams?.[teamKey(e.org, e.slug)];
2299
+ if (state?.status === "ok") state.members.forEach(add);
2300
+ else add(teamKey(e.org, e.slug));
2301
+ }
2302
+ }
2303
+ return out;
2304
+ }
2305
+ function expandOwners(owners, teams) {
2306
+ return { design: expandEntries(owners.design, teams), tech: expandEntries(owners.tech, teams) };
2307
+ }
2223
2308
  var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
2224
2309
  function entryLabel(raw, teams) {
2225
2310
  const e = parseOwnerEntry(raw);
@@ -2229,6 +2314,9 @@ function entryLabel(raw, teams) {
2229
2314
  const name = `@${e.org}/${e.slug}`;
2230
2315
  return state?.status === "ok" ? `${name} (${people(state.members.length)})` : name;
2231
2316
  }
2317
+ function unreadableTeams(teams) {
2318
+ return Object.values(teams ?? {}).filter((t) => t.status === "unreadable");
2319
+ }
2232
2320
  var TeamDirectory = class {
2233
2321
  constructor(reader, opts = {}) {
2234
2322
  this.reader = reader;
@@ -2351,6 +2439,8 @@ var TeamDirectory = class {
2351
2439
  var OWNER_GROUPS = ["design", "tech"];
2352
2440
  var RIGHTS = ["approveDesign", "approveTech", "ship"];
2353
2441
  var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
2442
+ var norm2 = (x) => x.trim().toLowerCase().replace(/^@/, "");
2443
+ var uniq = (xs) => [...new Set(xs)];
2354
2444
  function normalizeRights(input) {
2355
2445
  const pick = (r) => {
2356
2446
  const v = input?.[r];
@@ -2370,6 +2460,43 @@ function rightsProblems(raw) {
2370
2460
  }
2371
2461
  return out;
2372
2462
  }
2463
+ function holdersOf(right, owners, rights) {
2464
+ const groups = (rights ?? DEFAULT_RIGHTS)[right];
2465
+ return uniq(groups.flatMap((g) => owners[g] ?? []));
2466
+ }
2467
+ function approverLists(owners, rights, teams) {
2468
+ const entries = approverEntries(owners, rights);
2469
+ return teams === void 0 ? entries : expandOwners(entries, teams);
2470
+ }
2471
+ function approverEntries(owners, rights) {
2472
+ if (!rights) return { design: [...owners.design], tech: [...owners.tech] };
2473
+ return { design: holdersOf("approveDesign", owners, rights), tech: holdersOf("approveTech", owners, rights) };
2474
+ }
2475
+ function soloApprover(owners) {
2476
+ const d = [...new Set(owners.design.map(norm2))];
2477
+ const t = [...new Set(owners.tech.map(norm2))];
2478
+ if (d.length !== 1 || t.length !== 1 || d[0] !== t[0] || !d[0] || d[0].includes("/")) return null;
2479
+ return d[0];
2480
+ }
2481
+ function unreadableShipTeams(policy) {
2482
+ if (!policy?.teams) return [];
2483
+ const keys = new Set((policy.entries ?? []).map((e) => e.trim().toLowerCase().replace(/^@?/, "@")));
2484
+ return unreadableTeams(policy.teams).filter((t) => keys.has(t.key));
2485
+ }
2486
+ function canShip(login, policy) {
2487
+ if (!policy) return true;
2488
+ if (!login || login.includes("/")) return false;
2489
+ return policy.logins.map(norm2).includes(norm2(login));
2490
+ }
2491
+ function groupsPhrase(groups) {
2492
+ const g = OWNER_GROUPS.filter((x) => groups.includes(x));
2493
+ if (g.length === 1) return `the ${g[0]} owner`;
2494
+ return `a ${g.join(" or ")} owner`;
2495
+ }
2496
+ function whoCanShipSentence(policy) {
2497
+ const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
2498
+ return `Only ${groupsPhrase(policy.groups)} can merge this${names ? ` (${names})` : ""}.`;
2499
+ }
2373
2500
  function visualImpactProblems(raw) {
2374
2501
  if (raw === void 0) return [];
2375
2502
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"visualImpact" must be { "mode": "auto" | "on-demand" }'];
@@ -2740,6 +2867,116 @@ function loadConfig(source) {
2740
2867
  }
2741
2868
  var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
2742
2869
 
2870
+ // ../core/src/scopes.ts
2871
+ var has = (segments, ...names) => names.some((n) => segments.includes(n));
2872
+ function defaultScopes(path, type, tier) {
2873
+ const s = path.split(".");
2874
+ switch (type) {
2875
+ case "color":
2876
+ if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
2877
+ if (has(s, "text")) return ["TEXT_FILL"];
2878
+ if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
2879
+ if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
2880
+ if (has(s, "shadow")) return ["EFFECT_COLOR"];
2881
+ return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
2882
+ case "dimension":
2883
+ if (has(s, "radius")) return ["CORNER_RADIUS"];
2884
+ if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
2885
+ if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
2886
+ if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
2887
+ if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
2888
+ return ["WIDTH_HEIGHT", "GAP"];
2889
+ case "fontWeight":
2890
+ return ["FONT_WEIGHT"];
2891
+ case "fontFamily":
2892
+ return ["FONT_FAMILY"];
2893
+ case "number":
2894
+ if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
2895
+ if (has(s, "opacity")) return ["OPACITY"];
2896
+ return ["WIDTH_HEIGHT", "GAP"];
2897
+ case "string":
2898
+ return ["TEXT_CONTENT"];
2899
+ default:
2900
+ return [];
2901
+ }
2902
+ }
2903
+ var sameScopes = (a, b) => {
2904
+ const sa = [...new Set(a)].sort();
2905
+ const sb = [...new Set(b)].sort();
2906
+ return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
2907
+ };
2908
+ var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
2909
+
2910
+ // ../core/src/diff-engine.ts
2911
+ var normDesc = (d) => (d ?? "").trim();
2912
+ function fieldChanges(a, b) {
2913
+ const out = [];
2914
+ if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
2915
+ if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
2916
+ const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
2917
+ for (const mode2 of modes) {
2918
+ if (!valuesEqual(b.type, a.values[mode2], b.values[mode2])) out.push({ field: "value", mode: mode2, before: a.values[mode2], after: b.values[mode2] });
2919
+ }
2920
+ if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
2921
+ const sa = effectiveScopes(a);
2922
+ const sb = effectiveScopes(b);
2923
+ if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
2924
+ return out;
2925
+ }
2926
+
2927
+ // ../core/src/risk.ts
2928
+ var RISK_LABELS = {
2929
+ 1: "Tier 1: low risk, one code owner",
2930
+ 2: "Tier 2: medium risk, design review then design + tech approval",
2931
+ 3: "Tier 3: token change, needs design + tech approval"
2932
+ };
2933
+ var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
2934
+ var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
2935
+ var isSummary = (c) => "kind" in c;
2936
+ function summarizeTokenChange(c) {
2937
+ if (isSummary(c)) return c;
2938
+ const path = (c.target ?? c.current).path;
2939
+ if (!c.target) return { kind: "removed", path };
2940
+ if (!c.current) return { kind: "added", path };
2941
+ if (c.current.path !== c.target.path) return { kind: "renamed", path };
2942
+ const fields = fieldChanges(c.current, c.target);
2943
+ return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
2944
+ }
2945
+ function tokenBump(changes) {
2946
+ let bump;
2947
+ for (const c of changes.map(summarizeTokenChange)) {
2948
+ const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
2949
+ if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
2950
+ }
2951
+ return bump;
2952
+ }
2953
+ function isGuardrail(path, config) {
2954
+ const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
2955
+ return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
2956
+ }
2957
+ function classifyRisk(input) {
2958
+ const reasons = [];
2959
+ const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
2960
+ for (const c of tokenChanges) {
2961
+ if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
2962
+ else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
2963
+ }
2964
+ const bump = tokenBump(tokenChanges);
2965
+ const componentsDir = input.config?.components?.dir;
2966
+ for (const f of input.files ?? []) {
2967
+ const p = f.path.replace(/^\/+/, "");
2968
+ if (p.endsWith(".tokens.json")) {
2969
+ if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
2970
+ } else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
2971
+ else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
2972
+ else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
2973
+ else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
2974
+ }
2975
+ reasons.sort((a, b) => b.tier - a.tier);
2976
+ const tier = reasons[0]?.tier ?? 1;
2977
+ return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
2978
+ }
2979
+
2743
2980
  // ../core/src/node/git-local.ts
2744
2981
  import { execFile } from "node:child_process";
2745
2982
 
@@ -2843,6 +3080,198 @@ function diffLayered(before, after) {
2843
3080
  for (const d of added) changes.push({ kind: "added", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], after: d.value, afterCss: css(after, d), ...d.description ? { descriptionAfter: d.description } : {}, ...tierOf(after, d.file) });
2844
3081
  return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
2845
3082
  }
3083
+ var toChangeSummary = (c) => ({
3084
+ kind: c.kind,
3085
+ path: c.path,
3086
+ ...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
3087
+ });
3088
+ function classifyLayered(changes, otherFiles = [], config) {
3089
+ const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
3090
+ return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
3091
+ }
3092
+
3093
+ // ../core/src/history.ts
3094
+ var BULK_LIMIT = 60;
3095
+ var UNRELEASED = "Unreleased";
3096
+ var SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
3097
+ function prNumberFromSubject(subject) {
3098
+ for (const re of SUBJECT_PR) {
3099
+ const m = re.exec(subject);
3100
+ if (m) return Number(m[1]);
3101
+ }
3102
+ return void 0;
3103
+ }
3104
+ function parseChangesetText(path, text) {
3105
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
3106
+ if (!m) return null;
3107
+ const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
3108
+ const order = ["major", "minor", "patch"];
3109
+ const bump = order.find((b) => bumps.some((x) => x.bump === b));
3110
+ const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
3111
+ return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
3112
+ }
3113
+ function firstParagraph(text) {
3114
+ if (!text) return "";
3115
+ const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
3116
+ for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
3117
+ const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
3118
+ const s = lines.join(" ").replace(/\s+/g, " ").trim();
3119
+ if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
3120
+ }
3121
+ return "";
3122
+ }
3123
+ function countChanges(changes) {
3124
+ const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
3125
+ for (const x of changes) c[x.kind]++;
3126
+ return c;
3127
+ }
3128
+ var shortSha = (sha) => sha.slice(0, 7);
3129
+ function titleOf(commit) {
3130
+ if (commit.pr?.title) return commit.pr.title;
3131
+ if (/^Merge pull request #\d+/.test(commit.subject)) {
3132
+ const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
3133
+ if (first) return first.trim();
3134
+ }
3135
+ return commit.subject;
3136
+ }
3137
+ function buildHistoryEntry(input) {
3138
+ const { config, commit } = input;
3139
+ const beforeSet = new LayeredTokenSet(config, input.before);
3140
+ const afterSet = new LayeredTokenSet(config, input.after);
3141
+ const changes = diffLayered(beforeSet, afterSet);
3142
+ if (!changes.length) return null;
3143
+ const tier = classifyLayered(changes, [], config);
3144
+ const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
3145
+ const pkg = config.changesets?.package;
3146
+ const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
3147
+ const title = titleOf(commit);
3148
+ const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
3149
+ const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
3150
+ const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
3151
+ const changesetBump = relevant.map((c) => c.bump).find((b) => b === "major");
3152
+ const bump = changesetBump ?? tier.bump;
3153
+ return {
3154
+ id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
3155
+ sha: commit.sha,
3156
+ ...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
3157
+ date: commit.date,
3158
+ author: commit.pr?.author ?? commit.author,
3159
+ title,
3160
+ why,
3161
+ ...pr ? { pr } : {},
3162
+ changes,
3163
+ counts: countChanges(changes),
3164
+ tier: tier.tier,
3165
+ bump,
3166
+ breaking: bump === "major",
3167
+ reasons: tier.reasons,
3168
+ changesets: relevant,
3169
+ ...changes.length > BULK_LIMIT ? { bulk: true } : {},
3170
+ ...input.demo ? { demo: true } : {}
3171
+ };
3172
+ }
3173
+ async function collectHistory(config, backend, opts) {
3174
+ const root = config.tokens.root.replace(/\/$/, "");
3175
+ const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
3176
+ const seen = /* @__PURE__ */ new Set();
3177
+ const picked = [];
3178
+ for (const c of commits) {
3179
+ const n = c.pr?.number;
3180
+ if (n !== void 0) {
3181
+ if (seen.has(n)) continue;
3182
+ seen.add(n);
3183
+ }
3184
+ picked.push(c);
3185
+ }
3186
+ const entries = [];
3187
+ let done = 0;
3188
+ for (const c of picked) {
3189
+ const parent = c.parents[0];
3190
+ const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
3191
+ let changesets = [];
3192
+ if (backend.changesetPaths && backend.readFile && config.changesets) {
3193
+ const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
3194
+ const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
3195
+ changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
3196
+ }
3197
+ const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
3198
+ if (e) entries.push(e);
3199
+ opts.onProgress?.(++done, picked.length);
3200
+ }
3201
+ return entries;
3202
+ }
3203
+ function parseChangelog(text) {
3204
+ const out = [];
3205
+ const parts = text.split(/^##\s+/m).slice(1);
3206
+ for (const p of parts) {
3207
+ const [head, ...rest] = p.split(/\r?\n/);
3208
+ const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
3209
+ if (!version) continue;
3210
+ const body = rest.join("\n");
3211
+ const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
3212
+ const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
3213
+ const date2 = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
3214
+ out.push({ version, ...date2 ? { date: date2 } : {}, shas, prs });
3215
+ }
3216
+ return out;
3217
+ }
3218
+ function releasesFromTags(tags, pkg) {
3219
+ const out = [];
3220
+ for (const t of tags) {
3221
+ const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
3222
+ if (!m) continue;
3223
+ if (pkg && m[1] && m[1] !== pkg) continue;
3224
+ out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
3225
+ }
3226
+ return out;
3227
+ }
3228
+ function assignVersions(entries, releases) {
3229
+ const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
3230
+ return entries.map((e) => {
3231
+ const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
3232
+ const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
3233
+ return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
3234
+ });
3235
+ }
3236
+ var BUMP_RANK2 = { patch: 0, minor: 1, major: 2 };
3237
+ function versionTimeline(entries, releases = []) {
3238
+ const groups = /* @__PURE__ */ new Map();
3239
+ for (const e of entries) {
3240
+ const v = e.version ?? UNRELEASED;
3241
+ let g = groups.get(v);
3242
+ if (!g) {
3243
+ const rel = releases.find((r) => r.version === v);
3244
+ g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
3245
+ groups.set(v, g);
3246
+ }
3247
+ g.entries.push(e.id);
3248
+ if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
3249
+ g.tokenChanges += e.counts.total;
3250
+ g.breaking ||= e.breaking;
3251
+ if (BUMP_RANK2[e.bump] > BUMP_RANK2[g.bump]) g.bump = e.bump;
3252
+ if (!g.authors.includes(e.author)) g.authors.push(e.author);
3253
+ if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
3254
+ }
3255
+ return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
3256
+ }
3257
+ function packageDirCandidates(config) {
3258
+ const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
3259
+ const out = [];
3260
+ for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
3261
+ return out;
3262
+ }
3263
+ function mergeReleases(changelog, tags) {
3264
+ const byVersion = /* @__PURE__ */ new Map();
3265
+ for (const r of [...changelog, ...tags]) {
3266
+ const prev = byVersion.get(r.version);
3267
+ byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
3268
+ }
3269
+ return [...byVersion.values()];
3270
+ }
3271
+ function buildReport(args) {
3272
+ const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
3273
+ return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
3274
+ }
2846
3275
 
2847
3276
  // ../core/src/node/audit.ts
2848
3277
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -4050,6 +4479,33 @@ var EXPLORATION_USE_CASES = [
4050
4479
  ];
4051
4480
  var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
4052
4481
 
4482
+ // ../core/src/titles.ts
4483
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4484
+ function properWord(word, names) {
4485
+ const bare = word.replace(/[^\p{L}\p{N}]+$/u, "");
4486
+ if (!bare) return false;
4487
+ if (names.some((n) => n === bare)) return true;
4488
+ return new RegExp("\\p{Lu}", "u").test(bare.slice(1));
4489
+ }
4490
+ function lowerFirstWord(text, names = []) {
4491
+ const first = text.split(/\s+/)[0] ?? "";
4492
+ if (!first || properWord(first, names)) return text;
4493
+ return `${text.charAt(0).toLowerCase()}${text.slice(1)}`;
4494
+ }
4495
+ function subjectTitle(subject, title, names = []) {
4496
+ const t = title.trim().replace(/\.$/, "");
4497
+ const keep = [subject, ...names];
4498
+ const s = subject.trim();
4499
+ if (!s) return t;
4500
+ const m = new RegExp(`^${escape(s)}(?=$|[^\\p{L}\\p{N}])\\s*(:)?\\s*`, "iu").exec(t);
4501
+ if (m) {
4502
+ const rest = t.slice(m[0].length);
4503
+ if (!rest) return s;
4504
+ return m[1] ? `${s}: ${lowerFirstWord(rest, keep)}` : `${s} ${rest}`;
4505
+ }
4506
+ return `${s}: ${lowerFirstWord(t, keep)}`;
4507
+ }
4508
+
4053
4509
  // ../core/src/requests.ts
4054
4510
  var REQUEST_KINDS = [
4055
4511
  { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
@@ -4062,11 +4518,33 @@ var REQUEST_KINDS = [
4062
4518
  { value: "other", label: "Other", hint: "Anything else." }
4063
4519
  ];
4064
4520
  var REQUEST_LABEL = "component-change";
4521
+ var MARK = "bridge-request";
4065
4522
  function requestIssueTitle(r) {
4066
- const t = r.title.trim().replace(/\.$/, "");
4067
- return `${r.component}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
4523
+ return subjectTitle(r.component, r.title);
4524
+ }
4525
+ function parseRequestIssue(body) {
4526
+ if (!body) return void 0;
4527
+ const m = new RegExp(`<!-- ${MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
4528
+ if (!m) return void 0;
4529
+ try {
4530
+ const r = JSON.parse(m[1]);
4531
+ return r && typeof r.component === "string" && typeof r.title === "string" ? { ...r, changes: Array.isArray(r.changes) ? r.changes : [] } : void 0;
4532
+ } catch {
4533
+ return void 0;
4534
+ }
4535
+ }
4536
+ function requestStatus(issue) {
4537
+ const prs = issue.linkedPrs ?? [];
4538
+ if (issue.state === "open") return prs.some((p) => p.state === "open") ? "in-progress" : "requested";
4539
+ if (prs.some((p) => p.merged) || issue.stateReason === "completed") return "done";
4540
+ return "declined";
4068
4541
  }
4069
4542
  var REQUEST_STATUS_LABEL = { requested: "Requested", "in-progress": "In progress", done: "Done", declined: "Declined" };
4543
+ function closingIssueNumbers(body) {
4544
+ const out = /* @__PURE__ */ new Set();
4545
+ for (const m of (body ?? "").matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi)) out.add(Number(m[1]));
4546
+ return [...out];
4547
+ }
4070
4548
  function validateIntegrations(i) {
4071
4549
  const out = [];
4072
4550
  if (!i?.jira) return out;
@@ -4095,7 +4573,185 @@ function jiraIssueFields(r, jira, issueUrl) {
4095
4573
  };
4096
4574
  }
4097
4575
 
4576
+ // ../core/src/review-gate.ts
4577
+ function parseTier(labels, body) {
4578
+ const found = [];
4579
+ for (const l of labels) {
4580
+ const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
4581
+ if (m) found.push(Number(m[1]));
4582
+ }
4583
+ for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
4584
+ for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
4585
+ return found.length ? Math.max(...found) : null;
4586
+ }
4587
+ function pullState(p) {
4588
+ if (p.merged_at || p.merged) return "merged";
4589
+ if (p.state && p.state !== "open") return "closed";
4590
+ if (p.draft) return "draft";
4591
+ return "open";
4592
+ }
4593
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
4594
+ const latest = /* @__PURE__ */ new Map();
4595
+ const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
4596
+ for (const r of sorted) {
4597
+ const login = r.user?.login;
4598
+ if (!login || login === author) continue;
4599
+ if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
4600
+ }
4601
+ const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
4602
+ const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
4603
+ const lower = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
4604
+ const d = lower(owners.design);
4605
+ const t = lower(owners.tech);
4606
+ const a = lower(approvers);
4607
+ let design;
4608
+ let tech;
4609
+ if (d.length || t.length) {
4610
+ design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
4611
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
4612
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
4613
+ tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
4614
+ } else {
4615
+ design = a.length >= 1;
4616
+ tech = a.length >= 2;
4617
+ }
4618
+ return { approvers, changesRequested, design, tech };
4619
+ }
4620
+ var normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
4621
+ function canOverride(login, policy) {
4622
+ if (!login || !policy?.allowed.length) return false;
4623
+ return policy.allowed.map(normLogin).includes(normLogin(login));
4624
+ }
4625
+ function mergeGate(input) {
4626
+ const blocking = [];
4627
+ const missing = [];
4628
+ const approvalReasons = [];
4629
+ const lifecycle = pullState(input);
4630
+ if (lifecycle === "merged") blocking.push("This branch is already merged.");
4631
+ else if (lifecycle === "closed") blocking.push("This branch is closed.");
4632
+ if (input.exploration && lifecycle !== "merged" && lifecycle !== "closed") blocking.push("It is an exploration: promote it to review first.");
4633
+ if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
4634
+ if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
4635
+ if (input.ci.state === "pending") blocking.push("Tests are still running.");
4636
+ if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
4637
+ const tier = input.tier ?? 3;
4638
+ if (tier >= 2) {
4639
+ if (!input.approvals.design) {
4640
+ approvalReasons.push("It needs a design owner approval.");
4641
+ missing.push("design approval");
4642
+ }
4643
+ if (!input.approvals.tech) {
4644
+ approvalReasons.push("It needs a tech owner approval.");
4645
+ missing.push("tech approval");
4646
+ }
4647
+ } else if (!input.approvals.approvers.length) {
4648
+ approvalReasons.push("It needs one approval.");
4649
+ missing.push("approval");
4650
+ }
4651
+ const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
4652
+ const shipper = !input.viewer || canShip(input.viewer, input.ship);
4653
+ const ship = !shipper && lifecycle !== "merged" && lifecycle !== "closed" ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
4654
+ const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
4655
+ const allowed = reasons.length === 0;
4656
+ const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
4657
+ return { allowed, reasons, overridable, missing, canShip: shipper };
4658
+ }
4659
+ var CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
4660
+ function effectiveTier(labels, body, files) {
4661
+ const labelled = parseTier(labels, body);
4662
+ if (!files) return labelled;
4663
+ const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
4664
+ return Math.max(labelled ?? 0, computed);
4665
+ }
4666
+ var NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
4667
+ var ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
4668
+ function pendingApprovals(tier, approvals) {
4669
+ if (tier == null || !approvals) return [];
4670
+ return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
4671
+ }
4672
+
4673
+ // ../core/src/suggestion-review.ts
4674
+ var SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
4675
+ var SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
4676
+ var EVENTS = ["approve", "request-changes", "comment"];
4677
+ function parseSuggestionReviewComment(body) {
4678
+ const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4679
+ if (!m) return null;
4680
+ try {
4681
+ const r = JSON.parse(m[1]);
4682
+ if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
4683
+ return {
4684
+ event: r.event,
4685
+ ...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
4686
+ payloadSha: r.payloadSha,
4687
+ at: typeof r.at === "string" ? r.at : ""
4688
+ };
4689
+ } catch {
4690
+ return null;
4691
+ }
4692
+ }
4693
+ function parseSuggestionAppliedComment(body) {
4694
+ const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4695
+ if (!m) return null;
4696
+ try {
4697
+ const r = JSON.parse(m[1]);
4698
+ return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
4699
+ } catch {
4700
+ return null;
4701
+ }
4702
+ }
4703
+ var norm3 = (l) => l.trim().replace(/^@/, "").toLowerCase();
4704
+ var COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
4705
+ function summarizeSuggestionApprovals(comments, gov, opts) {
4706
+ const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
4707
+ const owners = new Set([...lists.design, ...lists.tech].map(norm3));
4708
+ const anyOwner = owners.size > 0;
4709
+ const reviews = [];
4710
+ const latestOld = /* @__PURE__ */ new Map();
4711
+ const ignored = /* @__PURE__ */ new Set();
4712
+ const ordered = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
4713
+ ordered.forEach(({ c }, i) => {
4714
+ const r = parseSuggestionReviewComment(c.body);
4715
+ if (!r || r.event === "comment" || !c.author) return;
4716
+ const login = norm3(c.author);
4717
+ if (opts.author && login === norm3(opts.author)) return;
4718
+ const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
4719
+ if (!counts) {
4720
+ ignored.add(c.author);
4721
+ return;
4722
+ }
4723
+ const at = String(i).padStart(6, "0");
4724
+ if (r.payloadSha !== opts.payloadSha) {
4725
+ latestOld.set(c.author, r.event);
4726
+ reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
4727
+ return;
4728
+ }
4729
+ latestOld.delete(c.author);
4730
+ reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
4731
+ });
4732
+ const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
4733
+ const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
4734
+ return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
4735
+ }
4736
+ function suggestionReviewState(tier, approvals) {
4737
+ if (approvals.changesRequested.length) return "changes-requested";
4738
+ return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
4739
+ }
4740
+
4098
4741
  // ../core/src/suggestions.ts
4742
+ var SUGGESTION_SCHEMA = "bridge.suggestion/1";
4743
+ var SUGGESTION_LABEL = "bridge-suggestion";
4744
+ var SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
4745
+ var SUGGESTION_STATE_LABELS = {
4746
+ "needs-review": "suggestion: needs review",
4747
+ "changes-requested": "suggestion: changes requested",
4748
+ approved: "suggestion: approved",
4749
+ applying: "suggestion: applying"
4750
+ };
4751
+ var SUGGESTION_FINAL_LABELS = {
4752
+ applied: "suggestion: applied",
4753
+ declined: "suggestion: declined"
4754
+ };
4099
4755
  var SUGGESTION_STATUS_LABEL = {
4100
4756
  "needs-review": "Needs review",
4101
4757
  "changes-requested": "Changes requested",
@@ -4105,6 +4761,328 @@ var SUGGESTION_STATUS_LABEL = {
4105
4761
  declined: "Declined",
4106
4762
  outdated: "Outdated"
4107
4763
  };
4764
+ var SUGGESTION_LIMITS = {
4765
+ body: 65536,
4766
+ /** The readable part of the body (tables are cut with "and N more"). */
4767
+ human: 12e3,
4768
+ /** The JSON block in the body. */
4769
+ inline: 48e3,
4770
+ /** The JSON block of one continuation comment. */
4771
+ part: 6e4,
4772
+ /** Continuation comments at most (about 480 KB of file contents). */
4773
+ maxParts: 8
4774
+ };
4775
+ var utf8 = (text) => new TextEncoder().encode(text);
4776
+ var hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
4777
+ function padded(bytes) {
4778
+ const len = bytes.length;
4779
+ const total = Math.ceil((len + 9) / 64) * 64;
4780
+ const buf = new Uint8Array(total);
4781
+ buf.set(bytes);
4782
+ buf[len] = 128;
4783
+ const view = new DataView(buf.buffer);
4784
+ view.setUint32(total - 8, Math.floor(len / 536870912), false);
4785
+ view.setUint32(total - 4, len * 8 >>> 0, false);
4786
+ return view;
4787
+ }
4788
+ var K256 = [
4789
+ 1116352408,
4790
+ 1899447441,
4791
+ 3049323471,
4792
+ 3921009573,
4793
+ 961987163,
4794
+ 1508970993,
4795
+ 2453635748,
4796
+ 2870763221,
4797
+ 3624381080,
4798
+ 310598401,
4799
+ 607225278,
4800
+ 1426881987,
4801
+ 1925078388,
4802
+ 2162078206,
4803
+ 2614888103,
4804
+ 3248222580,
4805
+ 3835390401,
4806
+ 4022224774,
4807
+ 264347078,
4808
+ 604807628,
4809
+ 770255983,
4810
+ 1249150122,
4811
+ 1555081692,
4812
+ 1996064986,
4813
+ 2554220882,
4814
+ 2821834349,
4815
+ 2952996808,
4816
+ 3210313671,
4817
+ 3336571891,
4818
+ 3584528711,
4819
+ 113926993,
4820
+ 338241895,
4821
+ 666307205,
4822
+ 773529912,
4823
+ 1294757372,
4824
+ 1396182291,
4825
+ 1695183700,
4826
+ 1986661051,
4827
+ 2177026350,
4828
+ 2456956037,
4829
+ 2730485921,
4830
+ 2820302411,
4831
+ 3259730800,
4832
+ 3345764771,
4833
+ 3516065817,
4834
+ 3600352804,
4835
+ 4094571909,
4836
+ 275423344,
4837
+ 430227734,
4838
+ 506948616,
4839
+ 659060556,
4840
+ 883997877,
4841
+ 958139571,
4842
+ 1322822218,
4843
+ 1537002063,
4844
+ 1747873779,
4845
+ 1955562222,
4846
+ 2024104815,
4847
+ 2227730452,
4848
+ 2361852424,
4849
+ 2428436474,
4850
+ 2756734187,
4851
+ 3204031479,
4852
+ 3329325298
4853
+ ];
4854
+ function sha256Hex(text) {
4855
+ const view = padded(utf8(text));
4856
+ const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
4857
+ const w = new Array(64);
4858
+ const rotr = (x, n) => x >>> n | x << 32 - n;
4859
+ for (let off = 0; off < view.byteLength; off += 64) {
4860
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
4861
+ for (let i = 16; i < 64; i++) {
4862
+ const a2 = w[i - 15];
4863
+ const b2 = w[i - 2];
4864
+ const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
4865
+ const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
4866
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
4867
+ }
4868
+ let [a, b, c, d, e, f, g, hh] = h;
4869
+ for (let i = 0; i < 64; i++) {
4870
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
4871
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
4872
+ hh = g;
4873
+ g = f;
4874
+ f = e;
4875
+ e = d + t1 | 0;
4876
+ d = c;
4877
+ c = b;
4878
+ b = a;
4879
+ a = t1 + t2 | 0;
4880
+ }
4881
+ h[0] = h[0] + a | 0;
4882
+ h[1] = h[1] + b | 0;
4883
+ h[2] = h[2] + c | 0;
4884
+ h[3] = h[3] + d | 0;
4885
+ h[4] = h[4] + e | 0;
4886
+ h[5] = h[5] + f | 0;
4887
+ h[6] = h[6] + g | 0;
4888
+ h[7] = h[7] + hh | 0;
4889
+ }
4890
+ return hex(h);
4891
+ }
4892
+ function canonicalJson(value) {
4893
+ return JSON.stringify(sortKeys(value));
4894
+ }
4895
+ function sortKeys(v) {
4896
+ if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
4897
+ if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
4898
+ const out = {};
4899
+ for (const k of Object.keys(v).sort()) {
4900
+ const x = v[k];
4901
+ if (x !== void 0) out[k] = sortKeys(x);
4902
+ }
4903
+ return out;
4904
+ }
4905
+ return v;
4906
+ }
4907
+ function shaForm(p) {
4908
+ if (!p.files) return p;
4909
+ return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
4910
+ }
4911
+ function suggestionPayloadSha(p) {
4912
+ return sha256Hex(canonicalJson(shaForm(p)));
4913
+ }
4914
+ function suggestionKindFromLabels(labels) {
4915
+ for (const l of labels) {
4916
+ const m = /^suggestion: (.+)$/.exec(l);
4917
+ if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
4918
+ }
4919
+ return void 0;
4920
+ }
4921
+ function suggestionStateFromLabels(labels) {
4922
+ return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
4923
+ }
4924
+ function suggestionStatus(i) {
4925
+ const prs = i.linkedPrs ?? [];
4926
+ if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
4927
+ if (prs.some((p) => p.state === "open") || i.applied) return "applying";
4928
+ if (i.outdated) return "outdated";
4929
+ return i.review ?? "needs-review";
4930
+ }
4931
+ var SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
4932
+ var MARK2 = "bridge-suggestion";
4933
+ var PART_SCHEMA = "bridge.suggestion-part/1";
4934
+ function fencedAfter(text, from) {
4935
+ const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
4936
+ return m?.[1];
4937
+ }
4938
+ var fail = (reason, message) => ({ ok: false, reason, message });
4939
+ function parseSuggestionIssue(body, opts = {}) {
4940
+ const text = body ?? "";
4941
+ const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK2}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
4942
+ const marker = markers[markers.length - 1];
4943
+ if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
4944
+ if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
4945
+ const json = fencedAfter(text, marker.index + marker[0].length);
4946
+ if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
4947
+ let raw;
4948
+ try {
4949
+ raw = JSON.parse(json);
4950
+ } catch {
4951
+ return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
4952
+ }
4953
+ const schema = raw?.schema;
4954
+ if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
4955
+ const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
4956
+ return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
4957
+ }
4958
+ const problems = suggestionShapeProblems(raw);
4959
+ if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
4960
+ const payload = raw;
4961
+ const markedSha = marker[2];
4962
+ const expected = marker[3] ? Number(marker[3]) : 0;
4963
+ let edited = false;
4964
+ if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
4965
+ const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
4966
+ const found = /* @__PURE__ */ new Map();
4967
+ const author = opts.author?.toLowerCase();
4968
+ for (const c of opts.comments ?? []) {
4969
+ if (author && c.author && c.author.toLowerCase() !== author) continue;
4970
+ const m = new RegExp(`^<!-- ${MARK2}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
4971
+ if (!m) continue;
4972
+ const i = Number(m[1]);
4973
+ if (found.has(i)) continue;
4974
+ const pj = fencedAfter(c.body, m.index + m[0].length);
4975
+ if (pj === void 0) continue;
4976
+ if (sha256Hex(pj) !== m[3]) edited = true;
4977
+ try {
4978
+ const part = JSON.parse(pj);
4979
+ if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
4980
+ found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
4981
+ } catch {
4982
+ continue;
4983
+ }
4984
+ }
4985
+ for (let i = 1; i <= want; i++) {
4986
+ if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
4987
+ }
4988
+ const files = (payload.files ?? []).map((f, idx) => {
4989
+ if (f.part === void 0) return f;
4990
+ let content = "";
4991
+ for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
4992
+ const { part: _part, ...rest } = f;
4993
+ return { ...rest, content };
4994
+ });
4995
+ payload.files = files;
4996
+ }
4997
+ const sha = suggestionPayloadSha(payload);
4998
+ return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
4999
+ }
5000
+ function suggestionShapeProblems(raw) {
5001
+ const out = [];
5002
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
5003
+ const p = raw;
5004
+ if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
5005
+ if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
5006
+ if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
5007
+ if (typeof p.summary !== "string") out.push('"summary" must be text.');
5008
+ if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
5009
+ if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
5010
+ const base = p.base;
5011
+ if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
5012
+ const tier = p.tier;
5013
+ if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
5014
+ const checks = p.checks;
5015
+ if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
5016
+ out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
5017
+ const tokens2 = p.tokens;
5018
+ if (tokens2 !== void 0 && (!tokens2 || !Array.isArray(tokens2.edits) || !Array.isArray(tokens2.changes))) out.push('"tokens" must be { "edits", "changes" }.');
5019
+ if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
5020
+ const spec = p.componentSpec;
5021
+ if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
5022
+ const nodes = p.links?.figma?.nodes;
5023
+ if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
5024
+ if (p.files !== void 0) {
5025
+ if (!Array.isArray(p.files)) out.push('"files" must be a list.');
5026
+ else {
5027
+ const seen = /* @__PURE__ */ new Set();
5028
+ for (const f of p.files) {
5029
+ const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
5030
+ if (!ok) {
5031
+ out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
5032
+ break;
5033
+ }
5034
+ if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
5035
+ seen.add(f.path);
5036
+ }
5037
+ }
5038
+ }
5039
+ return out;
5040
+ }
5041
+
5042
+ // ../core/src/suggestions-github.ts
5043
+ function linkedPullsOf(n, pulls) {
5044
+ return pulls.filter((p) => closingIssueNumbers(p.body).includes(n)).map((p) => ({ number: p.number, url: p.url, state: p.state === "open" ? "open" : "closed", merged: p.state === "merged" }));
5045
+ }
5046
+ async function listSuggestionsOnGitHub(gh, opts = {}) {
5047
+ const issues = await gh.listIssues({ label: SUGGESTION_LABEL, state: opts.state ?? "open", perPage: opts.perPage ?? 50 });
5048
+ const pulls = opts.pulls ?? (issues.length ? await gh.listPullRequests("all").catch(() => []) : []);
5049
+ return issues.map((i) => listItem(i, pulls));
5050
+ }
5051
+ function listItem(i, pulls) {
5052
+ const linkedPrs = linkedPullsOf(i.number, pulls);
5053
+ const cached = suggestionStateFromLabels(i.labels);
5054
+ const t = i.labels.map((l) => /^tier: ([123])$/.exec(l)?.[1]).find(Boolean);
5055
+ return {
5056
+ number: i.number,
5057
+ title: i.title,
5058
+ url: i.url,
5059
+ author: i.author,
5060
+ createdAt: i.createdAt,
5061
+ state: i.state,
5062
+ stateReason: i.stateReason,
5063
+ labels: i.labels,
5064
+ kind: suggestionKindFromLabels(i.labels),
5065
+ tier: t ? Number(t) : void 0,
5066
+ linkedPrs,
5067
+ status: suggestionStatus({ state: i.state, stateReason: i.stateReason, linkedPrs, applied: cached === "applying", review: cached === "applying" ? void 0 : cached })
5068
+ };
5069
+ }
5070
+ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
5071
+ const [issue, comments, pulls] = await Promise.all([gh.getIssue(n), gh.listIssueComments(n), opts.pulls ? Promise.resolve(opts.pulls) : gh.listPullRequests("all").catch(() => [])]);
5072
+ const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
5073
+ const linkedPrs = linkedPullsOf(n, pulls);
5074
+ const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
5075
+ const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
5076
+ const status = suggestionStatus({
5077
+ state: issue.state,
5078
+ stateReason: issue.stateReason,
5079
+ linkedPrs,
5080
+ applied: parsed.ok && applied.some((a) => a.payloadSha === parsed.sha),
5081
+ outdated: opts.outdated,
5082
+ review: parsed.ok && approvals ? suggestionReviewState(parsed.payload.tier.tier, approvals) : void 0
5083
+ });
5084
+ return { issue, parsed, comments, approvals, linkedPrs, applied, status };
5085
+ }
4108
5086
 
4109
5087
  // ../core/src/suggestion-apply.ts
4110
5088
  var SUGGESTION_APPLY_EXIT = {
@@ -4311,7 +5289,7 @@ function importsIn(text) {
4311
5289
  for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
4312
5290
  return [...out];
4313
5291
  }
4314
- var norm2 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
5292
+ var norm4 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
4315
5293
  var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
4316
5294
  var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
4317
5295
  var isCss = (p) => /\.(css|scss|less)$/.test(p);
@@ -4329,14 +5307,14 @@ function componentOf(from, spec, componentsDir) {
4329
5307
  return abs.slice(componentsDir.length + 1).split("/")[0] || null;
4330
5308
  }
4331
5309
  function componentForPath(path, componentsDir) {
4332
- const dir = norm2(componentsDir);
4333
- const p = norm2(path);
5310
+ const dir = norm4(componentsDir);
5311
+ const p = norm4(path);
4334
5312
  if (!p.startsWith(`${dir}/`)) return null;
4335
5313
  const rest = p.slice(dir.length + 1).split("/");
4336
5314
  return rest.length >= 2 ? rest[0] : null;
4337
5315
  }
4338
5316
  function buildUsageIndex(files, componentsDir) {
4339
- const dir = norm2(componentsDir);
5317
+ const dir = norm4(componentsDir);
4340
5318
  const modules = {};
4341
5319
  const vars = /* @__PURE__ */ new Map();
4342
5320
  const imports = /* @__PURE__ */ new Map();
@@ -4344,7 +5322,7 @@ function buildUsageIndex(files, componentsDir) {
4344
5322
  const globalVars = {};
4345
5323
  const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
4346
5324
  for (const f of files) {
4347
- const path = norm2(f.path);
5325
+ const path = norm4(f.path);
4348
5326
  const id = componentForPath(path, dir);
4349
5327
  if (!id) {
4350
5328
  if (isCss(path)) {
@@ -4388,7 +5366,7 @@ function impactedModules(index, input) {
4388
5366
  for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
4389
5367
  for (const f of input.files ?? []) {
4390
5368
  const id = componentForPath(f, index.componentsDir);
4391
- if (id && index.modules[id]) add(id, `file changed: ${norm2(f)}`, false);
5369
+ if (id && index.modules[id]) add(id, `file changed: ${norm4(f)}`, false);
4392
5370
  }
4393
5371
  const renderedBy = /* @__PURE__ */ new Map();
4394
5372
  for (const m of Object.values(index.modules)) for (const d of m.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m.id);
@@ -4406,6 +5384,131 @@ function impactedModules(index, input) {
4406
5384
  return { modules, globalHits };
4407
5385
  }
4408
5386
 
5387
+ // ../core/src/history-github.ts
5388
+ function gitHubHistoryBackend(client, config, opts = {}) {
5389
+ const trees = /* @__PURE__ */ new Map();
5390
+ const tree = (sha) => {
5391
+ let t = trees.get(sha);
5392
+ if (!t) {
5393
+ t = client.getCommitTree(sha).then((ts2) => client.getTree(ts2, true)).then((r) => r.entries);
5394
+ trees.set(sha, t);
5395
+ }
5396
+ return t;
5397
+ };
5398
+ const body = async (sha, e) => (!client.canWrite ? await client.readRawFile(e.path, sha).catch(() => void 0) : void 0) ?? client.getBlobText(e.sha);
5399
+ const root = config.tokens.root.replace(/\/$/, "");
5400
+ const prefix = root === "." || !root ? "" : `${root}/`;
5401
+ const limit = Math.max(1, opts.concurrency ?? 4);
5402
+ const pool = async (items, fn) => {
5403
+ const out = new Array(items.length);
5404
+ let i = 0;
5405
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
5406
+ for (let k = i++; k < items.length; k = i++) out[k] = await fn(items[k]);
5407
+ }));
5408
+ return out;
5409
+ };
5410
+ return {
5411
+ tree,
5412
+ async listCommits({ ref, paths, limit: max }) {
5413
+ const commits = [];
5414
+ for (let page = 1; commits.length < max && page <= 10; page++) {
5415
+ const batch = await client.listCommits({ sha: ref, path: paths[0], perPage: Math.min(100, max), page });
5416
+ for (const c of batch) {
5417
+ const [subject, ...rest] = c.message.split("\n");
5418
+ commits.push({ sha: c.sha, parents: c.parents, date: c.date, author: c.author, subject: subject ?? "", body: rest.join("\n").trim() });
5419
+ }
5420
+ if (batch.length < Math.min(100, max)) break;
5421
+ }
5422
+ const list2 = commits.slice(0, max);
5423
+ const merges = /* @__PURE__ */ new Map();
5424
+ const enriched = await pool(list2, async (c) => {
5425
+ const prs = await client.listPullRequestsForCommit(c.sha).catch(() => []);
5426
+ const pr = prs.find((p) => p.mergedAt && p.baseRef === ref) ?? prs.find((p) => p.mergedAt);
5427
+ if (!pr) return c;
5428
+ const prInfo = { number: pr.number, title: pr.title, url: pr.url, author: pr.author, ...pr.mergedAt ? { mergedAt: pr.mergedAt } : {}, body: pr.body };
5429
+ if (pr.mergeCommitSha && pr.mergeCommitSha !== c.sha) {
5430
+ let m = merges.get(pr.mergeCommitSha);
5431
+ if (!m) {
5432
+ m = client.getCommitInfo(pr.mergeCommitSha).then(
5433
+ (info) => {
5434
+ const [subject, ...rest] = info.message.split("\n");
5435
+ return { sha: info.sha, parents: info.parents, date: pr.mergedAt ?? info.date, author: info.author, subject: subject ?? "", body: rest.join("\n").trim() };
5436
+ },
5437
+ () => void 0
5438
+ );
5439
+ merges.set(pr.mergeCommitSha, m);
5440
+ }
5441
+ const merge = await m;
5442
+ if (merge) return { ...merge, pr: prInfo };
5443
+ }
5444
+ return { ...c, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
5445
+ });
5446
+ return enriched;
5447
+ },
5448
+ async tokenFiles(sha) {
5449
+ const entries = await tree(sha);
5450
+ const blobs = entries.filter((e) => e.type === "blob" && e.path.startsWith(prefix));
5451
+ const picked = new Set(selectTokenFiles(config, blobs.map((e) => e.path)));
5452
+ const wanted = blobs.filter((e) => picked.has(prefix ? e.path.slice(prefix.length) : e.path));
5453
+ const texts = await pool(wanted, async (e) => [prefix ? e.path.slice(prefix.length) : e.path, await body(sha, e)]);
5454
+ return new Map(texts);
5455
+ },
5456
+ async changesetPaths(sha) {
5457
+ const dir = config.changesets?.dir.replace(/\/$/, "");
5458
+ if (!dir) return [];
5459
+ return (await tree(sha)).filter((e) => e.type === "blob" && e.path.startsWith(`${dir}/`)).map((e) => e.path);
5460
+ },
5461
+ async readFile(sha, path) {
5462
+ const e = (await tree(sha)).find((x) => x.type === "blob" && x.path === path);
5463
+ return e ? body(sha, e) : null;
5464
+ }
5465
+ };
5466
+ }
5467
+ async function loadGitHubHistory(client, config, opts = {}) {
5468
+ const ref = opts.ref ?? config.repo?.defaultBranch ?? await client.getDefaultBranch();
5469
+ const limit = opts.limit ?? 30;
5470
+ const backend = gitHubHistoryBackend(client, config);
5471
+ let listed = 0;
5472
+ const counted = { ...backend, listCommits: async (o) => {
5473
+ const r = await backend.listCommits(o);
5474
+ listed = r.length;
5475
+ return r;
5476
+ } };
5477
+ const entries = await collectHistory(config, counted, { ref, limit, ...opts.onProgress ? { onProgress: opts.onProgress } : {} });
5478
+ const head = await client.getBranchSha(ref).catch(() => void 0);
5479
+ let changelog = [];
5480
+ let currentVersion;
5481
+ if (head) {
5482
+ const files = await backend.tree(head).catch(() => []);
5483
+ for (const dir of packageDirCandidates(config)) {
5484
+ const p = (f) => dir ? `${dir}/${f}` : f;
5485
+ const cl = files.find((e) => e.path === p("CHANGELOG.md"));
5486
+ const pkg = files.find((e) => e.path === p("package.json"));
5487
+ if (!cl && !pkg) continue;
5488
+ if (cl) changelog = parseChangelog(await client.getBlobText(cl.sha));
5489
+ if (pkg) {
5490
+ try {
5491
+ const json = JSON.parse(await client.getBlobText(pkg.sha));
5492
+ if (!config.changesets || json.name === config.changesets.package) currentVersion = json.version;
5493
+ } catch {
5494
+ }
5495
+ }
5496
+ break;
5497
+ }
5498
+ }
5499
+ const tagList = await client.listTags(30).catch(() => []);
5500
+ const tagReleases = releasesFromTags(tagList, config.changesets?.package);
5501
+ await Promise.all(
5502
+ tagReleases.slice(0, 10).map(async (r) => {
5503
+ if (!r.sha) return;
5504
+ const info = await client.getCommitInfo(r.sha).catch(() => void 0);
5505
+ if (info?.date) r.date = info.date;
5506
+ })
5507
+ );
5508
+ const releases = mergeReleases(changelog, tagReleases);
5509
+ return buildReport({ source: "github", label: `${client.fullName}@${ref}`, ref, entries, releases, ...currentVersion ? { currentVersion } : {}, truncated: listed >= limit });
5510
+ }
5511
+
4409
5512
  // ../core/src/fidelity-references.ts
4410
5513
  var FIGMA_REFERENCES_DIR = "figma-references";
4411
5514
  var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
@@ -4678,10 +5781,10 @@ function colorCss(c) {
4678
5781
  if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
4679
5782
  const v = c;
4680
5783
  const [r = 0, g = 0, b = 0] = v.components;
4681
- const hex = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
5784
+ const hex2 = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
4682
5785
  const alpha = v.alpha ?? 1;
4683
- if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
4684
- return { css: hex, hex };
5786
+ if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex: hex2 };
5787
+ return { css: hex2, hex: hex2 };
4685
5788
  }
4686
5789
  var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
4687
5790
  function shadowCss(s) {
@@ -4735,13 +5838,13 @@ function buildTokenRows(config, readText, listFiles) {
4735
5838
  ...componentFiles
4736
5839
  ];
4737
5840
  const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
4738
- const { css, hex } = tokenValueCss(t.type, t.value);
5841
+ const { css, hex: hex2 } = tokenValueCss(t.type, t.value);
4739
5842
  return {
4740
5843
  path: t.path.join("."),
4741
5844
  segments: t.path,
4742
5845
  type: t.type,
4743
5846
  css,
4744
- hex,
5847
+ hex: hex2,
4745
5848
  cssVar: tokenRowCssVar(t.path),
4746
5849
  figma: tokenRowFigmaName(t.path),
4747
5850
  description: t.description,
@@ -4785,8 +5888,8 @@ function buildTokenRows(config, readText, listFiles) {
4785
5888
  current = hit.value;
4786
5889
  currentType = hit.type || currentType;
4787
5890
  }
4788
- const { css, hex } = tokenValueCss(currentType, current);
4789
- return { alias: chain[0] ?? null, chain, css, hex };
5891
+ const { css, hex: hex2 } = tokenValueCss(currentType, current);
5892
+ return { alias: chain[0] ?? null, chain, css, hex: hex2 };
4790
5893
  };
4791
5894
  const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
4792
5895
  const build = (t, tier, valueFor) => {
@@ -4846,8 +5949,8 @@ var channel = (c) => {
4846
5949
  const s = c / 255;
4847
5950
  return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
4848
5951
  };
4849
- function hexLuminance(hex) {
4850
- const n = parseInt(hex.slice(1), 16);
5952
+ function hexLuminance(hex2) {
5953
+ const n = parseInt(hex2.slice(1), 16);
4851
5954
  return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
4852
5955
  }
4853
5956
  function hexContrastRatio(a, b) {
@@ -5038,7 +6141,7 @@ function scanComponents(input) {
5038
6141
  const docsSuffix = input.docsSuffix ?? ".docs.json";
5039
6142
  const styleSuffix = input.styleSuffix ?? ".module.css";
5040
6143
  const fileSet = new Set(input.files);
5041
- const has8 = (p) => fileSet.has(p);
6144
+ const has9 = (p) => fileSet.has(p);
5042
6145
  const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
5043
6146
  const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
5044
6147
  const names = [
@@ -5051,7 +6154,7 @@ function scanComponents(input) {
5051
6154
  const folder = `${dir}/${name}`;
5052
6155
  const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
5053
6156
  let docs = null;
5054
- if (has8(files.docs)) {
6157
+ if (has9(files.docs)) {
5055
6158
  try {
5056
6159
  docs = JSON.parse(readText(files.docs) ?? "");
5057
6160
  } catch (err) {
@@ -5061,10 +6164,10 @@ function scanComponents(input) {
5061
6164
  const tsxSrc = readText(files.tsx) ?? "";
5062
6165
  const tsx = parseComponentTsx(name, tsxSrc);
5063
6166
  const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
5064
- const cssText = has8(files.css) ? readText(files.css) : void 0;
6167
+ const cssText = has9(files.css) ? readText(files.css) : void 0;
5065
6168
  const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
5066
6169
  const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
5067
- const storiesText = has8(files.stories) ? readText(files.stories) : void 0;
6170
+ const storiesText = has9(files.stories) ? readText(files.stories) : void 0;
5068
6171
  return {
5069
6172
  name,
5070
6173
  slug: componentSlug(name),
@@ -5459,6 +6562,389 @@ function snapshotLlmsTxt(model, snapshot) {
5459
6562
  `;
5460
6563
  }
5461
6564
 
6565
+ // ../core/src/activity.ts
6566
+ var ACTIVITY_SCHEMA_VERSION = 1;
6567
+ var ACTIVITY_FILE = "activity.json";
6568
+ var ACTIVITY_MAX_BYTES = 3e6;
6569
+ var ACTIVITY_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
6570
+ var ACTIVITY_LIMITS = {
6571
+ /** Open pull requests recorded in full (the page of each; the list carries every open one). */
6572
+ branches: 30,
6573
+ /** Open suggestions read with their comments (parsed payload, counted approvals). */
6574
+ suggestions: 30,
6575
+ /** Request issues listed (open and closed); open ones are read with their comments. */
6576
+ requests: 30,
6577
+ /** Merged pull requests summarized. */
6578
+ merged: 20,
6579
+ /** Token history entries (core loadGitHubHistory limit). */
6580
+ history: 20,
6581
+ /** History entries whose two commits' file lists are recorded, so their before/after opens without the API. */
6582
+ historyTrees: 10
6583
+ };
6584
+ var ActivityError = class extends Error {
6585
+ };
6586
+ var isObj3 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
6587
+ var API2 = "https://api.github.com";
6588
+ function activityKey(url) {
6589
+ let u;
6590
+ try {
6591
+ u = new URL(url, API2);
6592
+ } catch {
6593
+ return null;
6594
+ }
6595
+ if (u.origin !== API2) return null;
6596
+ const parts = u.pathname.split("/");
6597
+ if (parts[1] === "repos" && parts.length > 3) {
6598
+ parts[2] = parts[2].toLowerCase();
6599
+ parts[3] = parts[3].toLowerCase();
6600
+ } else if (parts[1] === "users" && parts.length > 2) parts[2] = parts[2].toLowerCase();
6601
+ const q = [...u.searchParams].sort(([a, av], [b, bv]) => a === b ? av < bv ? -1 : 1 : a < b ? -1 : 1);
6602
+ const search = q.length ? `?${new URLSearchParams(q).toString()}` : "";
6603
+ return parts.join("/") + search;
6604
+ }
6605
+ var KEPT_URLS = /* @__PURE__ */ new Set(["html_url", "avatar_url", "target_url", "details_url", "environment_url", "raw_url"]);
6606
+ var DROPPED = /* @__PURE__ */ new Set(["_links", "patch", "body_html", "body_text", "reactions", "performed_via_github_app", "node_id", "gravatar_id", "user_view_type", "site_admin", "event", "sub_issues_summary", "issue_dependencies_summary", "pull_request_review_id"]);
6607
+ var LONG_TEXT = 6e4;
6608
+ var trimUser = (u) => ({
6609
+ login: u.login,
6610
+ ...u.id !== void 0 ? { id: u.id } : {},
6611
+ ...u.avatar_url ? { avatar_url: u.avatar_url } : {},
6612
+ ...u.html_url ? { html_url: u.html_url } : {},
6613
+ ...u.type ? { type: u.type } : {}
6614
+ });
6615
+ var trimRepo = (r) => ({
6616
+ ...r.id !== void 0 ? { id: r.id } : {},
6617
+ name: r.name,
6618
+ full_name: r.full_name,
6619
+ ...r.private !== void 0 ? { private: r.private } : {},
6620
+ ...r.fork !== void 0 ? { fork: r.fork } : {},
6621
+ ...r.default_branch ? { default_branch: r.default_branch } : {},
6622
+ ...isObj3(r.owner) ? { owner: trimUser(r.owner) } : {},
6623
+ ...r.html_url ? { html_url: r.html_url } : {}
6624
+ });
6625
+ function trimGitHub(value, key = "") {
6626
+ if (typeof value === "string") return value.length > LONG_TEXT ? value.slice(0, LONG_TEXT) : value;
6627
+ if (Array.isArray(value)) return value.map((v) => trimGitHub(v, key));
6628
+ if (!isObj3(value)) return value;
6629
+ if (key === "tree" && typeof value.path === "string" && typeof value.type === "string") return { path: value.path, type: value.type, sha: value.sha, ...value.mode ? { mode: value.mode } : {} };
6630
+ if ((key === "repo" || key === "repository" || key === "head_repository") && typeof value.full_name === "string") return trimRepo(value);
6631
+ if (typeof value.login === "string" && ("avatar_url" in value || "type" in value)) return trimUser(value);
6632
+ const out = {};
6633
+ for (const [k, v] of Object.entries(value)) {
6634
+ if (DROPPED.has(k)) continue;
6635
+ if ((k === "url" || k.endsWith("_url")) && !KEPT_URLS.has(k)) continue;
6636
+ if (k === "output" && isObj3(v)) {
6637
+ out[k] = { title: v.title ?? null, summary: typeof v.summary === "string" ? v.summary.slice(0, 2e3) : null, annotations_count: v.annotations_count ?? 0 };
6638
+ continue;
6639
+ }
6640
+ if (k === "app" && isObj3(v)) {
6641
+ out[k] = { id: v.id, slug: v.slug, name: v.name };
6642
+ continue;
6643
+ }
6644
+ out[k] = trimGitHub(v, k);
6645
+ }
6646
+ return out;
6647
+ }
6648
+ var JSON_ACCEPT = /application\/(vnd\.github\+)?json/;
6649
+ var SKIP2 = /* @__PURE__ */ Symbol("skip");
6650
+ function activityTreeFilter(config) {
6651
+ const under = (dir) => {
6652
+ const d = (dir ?? "").replace(/^\.\/|\/$/g, "");
6653
+ return d && d !== "." ? `${d}/` : "";
6654
+ };
6655
+ const tokens2 = under(config.tokens.root);
6656
+ const changesets = config.changesets ? under(config.changesets.dir) : null;
6657
+ const pkg = new Set(packageDirCandidates(config).flatMap((d) => [d ? `${d}/CHANGELOG.md` : "CHANGELOG.md", d ? `${d}/package.json` : "package.json"]));
6658
+ return (p) => p === "bridge.config.json" || pkg.has(p) || (tokens2 ? p.startsWith(tokens2) : p.endsWith(".json")) || changesets !== null && changesets !== "" && p.startsWith(changesets);
6659
+ }
6660
+ var UNREAD = [/\/git\/blobs\//, /\/commits\/[^/]+\/pulls$/, /\/commits\?/, /\/tags\?/, /\/contents\//];
6661
+ function activityShaper(config) {
6662
+ const keep = activityTreeFilter(config);
6663
+ return (key, body) => {
6664
+ if (UNREAD.some((r) => r.test(key))) return SKIP2;
6665
+ if (key.includes("/git/trees/") && isObj3(body) && Array.isArray(body.tree)) return { sha: body.sha, tree: body.tree.filter((e) => keep(e.path)), truncated: Boolean(body.truncated) };
6666
+ if (key.includes("/compare/") && isObj3(body)) {
6667
+ const c = body;
6668
+ return {
6669
+ status: c.status,
6670
+ ahead_by: c.ahead_by,
6671
+ behind_by: c.behind_by,
6672
+ total_commits: c.total_commits,
6673
+ merge_base_commit: c.merge_base_commit ? { sha: c.merge_base_commit.sha } : null,
6674
+ base_commit: c.base_commit ? { sha: c.base_commit.sha, commit: { tree: { sha: c.base_commit.commit?.tree?.sha } } } : null,
6675
+ files: Array.isArray(c.files) ? c.files.map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {}, sha: f.sha, additions: f.additions, deletions: f.deletions, changes: f.changes })) : []
6676
+ };
6677
+ }
6678
+ return body;
6679
+ };
6680
+ }
6681
+ var HISTORY_MAX_CHANGES = 100;
6682
+ function capHistory(report, max = HISTORY_MAX_CHANGES) {
6683
+ return {
6684
+ ...report,
6685
+ entries: report.entries.map((e) => e.changes.length > max || (e.reasons?.length ?? 0) > max ? { ...e, changes: e.changes.slice(0, max), ...e.reasons ? { reasons: e.reasons.slice(0, max) } : {} } : e)
6686
+ };
6687
+ }
6688
+ function recordingFetch(fetchImpl, into, shape = (_k, b) => b) {
6689
+ return async (url, init2) => {
6690
+ const res = await fetchImpl(url, init2);
6691
+ const method = (init2?.method ?? "GET").toUpperCase();
6692
+ const accept = init2?.headers?.Accept ?? init2?.headers?.accept ?? "application/vnd.github+json";
6693
+ const key = activityKey(url);
6694
+ if (method !== "GET" || !key || !JSON_ACCEPT.test(accept) || res.status !== 200 && res.status !== 404) return res;
6695
+ const text = await res.text();
6696
+ let body;
6697
+ try {
6698
+ body = JSON.parse(text);
6699
+ } catch {
6700
+ return { ok: res.ok, status: res.status, headers: res.headers, json: async () => JSON.parse(text), text: async () => text };
6701
+ }
6702
+ const shaped = shape(key, trimGitHub(body));
6703
+ if (shaped !== SKIP2) into.set(key, { status: res.status, body: shaped });
6704
+ return { ok: res.ok, status: res.status, ...res.headers ? { headers: res.headers } : {}, json: async () => JSON.parse(text), text: async () => text };
6705
+ };
6706
+ }
6707
+ var labelObjs = (labels) => labelNames(labels).map((name) => ({ name }));
6708
+ function labelNames(labels) {
6709
+ return (labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
6710
+ }
6711
+ var byUpdated = (a, b) => a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0;
6712
+ async function collectActivity(input) {
6713
+ const limits = { ...ACTIVITY_LIMITS, ...input.limits };
6714
+ const responses = /* @__PURE__ */ new Map();
6715
+ const rec = recordingFetch(input.fetch, responses, activityShaper(input.config));
6716
+ const client = new GitHubClient(`${input.owner}/${input.repo}`, input.token, rec, { attempts: 2, baseDelayMs: 500, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }, { etags: false });
6717
+ const R = `/repos/${input.owner}/${input.repo}`;
6718
+ const problems = [];
6719
+ const truncated = [];
6720
+ let limited = false;
6721
+ const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", ...input.token ? { Authorization: `Bearer ${input.token}` } : {} };
6722
+ const get = async (path) => {
6723
+ if (limited) return void 0;
6724
+ try {
6725
+ const res = await rec(`${API2}${path}`, { headers });
6726
+ if (res.status === 404) return void 0;
6727
+ if (!res.ok) {
6728
+ if ((res.status === 403 || res.status === 429) && res.headers?.get("x-ratelimit-remaining") === "0") {
6729
+ limited = true;
6730
+ problems.push("GitHub hourly limit reached while reading activity: the rest was left out.");
6731
+ } else problems.push(`GitHub answered ${res.status} for ${path}`);
6732
+ return void 0;
6733
+ }
6734
+ return await res.json();
6735
+ } catch (err) {
6736
+ problems.push(`Could not read ${path}: ${err.message}`);
6737
+ return void 0;
6738
+ }
6739
+ };
6740
+ const safely = async (what, fn, fallback) => {
6741
+ if (limited) return fallback;
6742
+ try {
6743
+ return await fn();
6744
+ } catch (err) {
6745
+ const msg = err.message;
6746
+ if (/limit/i.test(msg) && err.status === 429) limited = true;
6747
+ problems.push(`${what}: ${msg}`);
6748
+ return fallback;
6749
+ }
6750
+ };
6751
+ const info = await safely("Repository", () => client.getRepo(), null);
6752
+ const branch = info?.defaultBranch ?? input.config.repo?.defaultBranch ?? "main";
6753
+ const headSha = await safely("Default branch", () => client.getBranchSha(branch), void 0) ?? "";
6754
+ const open = await get(`${R}/pulls?state=open&per_page=100`) ?? [];
6755
+ const closed = await get(`${R}/pulls?state=closed&sort=updated&direction=desc&per_page=30`) ?? [];
6756
+ await get(`${R}/pulls?state=closed&per_page=50&sort=updated&direction=desc`);
6757
+ await get(`${R}/rules/branches/${encodeURIComponent(branch)}`);
6758
+ await get(`${R}/git/matching-refs/heads/bridge-visual`);
6759
+ await safely("Owner account", () => client.getAccountType(input.owner), null);
6760
+ const prev = input.previous ?? null;
6761
+ const sameRepoPrev = Boolean(prev && sameRepo(prev, input));
6762
+ const prevOk = sameRepoPrev && Boolean(headSha) && prev.headSha === headSha;
6763
+ const kept = async (path, reuse) => {
6764
+ const key = reuse ? activityKey(`${API2}${path}`) : null;
6765
+ const hit = key ? prev.responses[key] : void 0;
6766
+ if (key && hit) {
6767
+ responses.set(key, hit);
6768
+ return hit.status === 200 ? hit.body : void 0;
6769
+ }
6770
+ return get(path);
6771
+ };
6772
+ const branches4 = [];
6773
+ const sorted = [...open].sort(byUpdated);
6774
+ if (sorted.length > limits.branches) truncated.push(`${sorted.length - limits.branches} open branches beyond the ${limits.branches} most recently updated are listed without their pages.`);
6775
+ for (const [i, p] of sorted.entries()) {
6776
+ const full = i < limits.branches;
6777
+ const base = {
6778
+ number: p.number,
6779
+ title: p.title,
6780
+ author: p.user?.login ?? "",
6781
+ url: p.html_url,
6782
+ branch: p.head.ref,
6783
+ base: p.base.ref,
6784
+ headSha: p.head.sha,
6785
+ baseSha: p.base.sha,
6786
+ state: p.draft ? "draft" : "open",
6787
+ labels: labelNames(p.labels),
6788
+ tier: effectiveTier(labelObjs(p.labels), p.body ?? null, null),
6789
+ createdAt: p.created_at,
6790
+ updatedAt: p.updated_at,
6791
+ comments: null,
6792
+ files: null,
6793
+ previews: [],
6794
+ checks: null
6795
+ };
6796
+ if (!full || limited) {
6797
+ branches4.push(base);
6798
+ continue;
6799
+ }
6800
+ const n = p.number;
6801
+ const was = prevOk ? prev.branches.find((b) => b.number === n) : void 0;
6802
+ const same2 = Boolean(was && was.updatedAt === p.updated_at && was.headSha === p.head.sha && was.comments !== null);
6803
+ const [pull, files, , , , , status] = await Promise.all([
6804
+ kept(`${R}/pulls/${n}`, same2),
6805
+ kept(`${R}/pulls/${n}/files?per_page=100`, same2),
6806
+ kept(`${R}/pulls/${n}/reviews?per_page=100`, same2),
6807
+ kept(`${R}/issues/${n}/comments?per_page=100`, same2),
6808
+ kept(`${R}/pulls/${n}/comments?per_page=100`, same2),
6809
+ get(`${R}/commits/${p.head.sha}/check-runs?per_page=100`),
6810
+ get(`${R}/commits/${p.head.sha}/status`),
6811
+ kept(`${R}/compare/${encodeURIComponent(p.base.ref)}...${p.head.sha}`, same2),
6812
+ kept(`${R}/git/trees/${p.base.sha}?recursive=1`, sameRepoPrev),
6813
+ kept(`${R}/git/trees/${p.head.sha}?recursive=1`, sameRepoPrev),
6814
+ kept(`${R}/issues/${n}/labels`, same2)
6815
+ ]);
6816
+ const root = input.config.tokens.root.replace(/\/$/, "");
6817
+ branches4.push({
6818
+ ...base,
6819
+ tier: effectiveTier(labelObjs((pull ?? p).labels), (pull ?? p).body ?? null, files ?? null),
6820
+ comments: pull ? (pull.comments ?? 0) + (pull.review_comments ?? 0) : null,
6821
+ files: files ? { changed: files.length, tokens: files.map((f) => f.filename).filter((f) => f.startsWith(`${root}/`) && f.endsWith(".json")) } : null,
6822
+ previews: (status?.statuses ?? []).filter((s) => s.state === "success" && s.target_url && /^https:\/\//.test(s.target_url)).map((s) => ({ context: s.context, url: s.target_url })),
6823
+ checks: status ? status.state === "success" || status.state === "failure" || status.state === "pending" ? status.state : "none" : null
6824
+ });
6825
+ }
6826
+ const gov = { owners: input.config.governance.owners, rights: input.config.governance.rights, soloOwnerCanApproveBoth: input.config.governance.soloOwnerCanApproveBoth };
6827
+ const items = await safely("Suggestions", () => listSuggestionsOnGitHub(client, { state: "open" }), []);
6828
+ const pullsAll = items.length ? await safely("Pull requests", () => client.listPullRequests("all"), []) : [];
6829
+ if (items.length > limits.suggestions) truncated.push(`${items.length - limits.suggestions} open suggestions beyond the ${limits.suggestions} newest are listed without their pages.`);
6830
+ const suggestions2 = [];
6831
+ for (const [i, s] of items.entries()) {
6832
+ const d = i < limits.suggestions ? await safely(`Suggestion #${s.number}`, () => getSuggestionOnGitHub(client, s.number, gov, { pulls: pullsAll }), null) : null;
6833
+ suggestions2.push({
6834
+ number: s.number,
6835
+ title: s.title,
6836
+ author: s.author,
6837
+ url: s.url,
6838
+ createdAt: s.createdAt,
6839
+ kind: s.kind ?? null,
6840
+ status: d?.status ?? s.status,
6841
+ labels: s.labels,
6842
+ comments: d ? d.comments.length : null,
6843
+ payload: d?.parsed.ok ? d.parsed.payload : null,
6844
+ approvers: d?.approvals?.approvers ?? []
6845
+ });
6846
+ }
6847
+ const issues = await safely("Requests", () => client.listIssues({ label: REQUEST_LABEL, state: "all", perPage: 50 }), []);
6848
+ const requests3 = [];
6849
+ const openIssues = issues.filter((i) => i.state === "open");
6850
+ if (openIssues.length > limits.requests) truncated.push(`${openIssues.length - limits.requests} open requests beyond the ${limits.requests} newest are listed without their pages.`);
6851
+ for (const [i, r] of issues.slice(0, Math.max(limits.requests, 0)).entries()) {
6852
+ if (r.state === "open" && i < limits.requests) {
6853
+ await get(`${R}/issues/${r.number}`);
6854
+ await get(`${R}/issues/${r.number}/comments?per_page=100`);
6855
+ }
6856
+ const parsed = parseRequestIssue(r.body);
6857
+ const linked = [...open, ...closed].filter((p) => new RegExp(`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${r.number}\\b`, "i").test(p.body ?? ""));
6858
+ requests3.push({
6859
+ number: r.number,
6860
+ title: r.title,
6861
+ author: r.author,
6862
+ url: r.url,
6863
+ state: r.state,
6864
+ status: requestStatus({ state: r.state, stateReason: r.stateReason, linkedPrs: linked.map((p) => ({ state: p.state === "open" ? "open" : "closed", merged: Boolean(p.merged_at) })) }),
6865
+ kind: parsed?.kind ?? null,
6866
+ component: parsed?.component ?? null,
6867
+ labels: r.labels,
6868
+ createdAt: r.createdAt
6869
+ });
6870
+ }
6871
+ const merged = closed.filter((p) => p.merged_at).slice(0, limits.merged).map((p) => ({ number: p.number, title: p.title, author: p.user?.login ?? "", url: p.html_url, mergedAt: p.merged_at, labels: labelNames(p.labels), tier: effectiveTier(labelObjs(p.labels), p.body ?? null, null) }));
6872
+ let history = null;
6873
+ if (prevOk && prev.history) {
6874
+ history = prev.history;
6875
+ for (const [k, v] of Object.entries(prev.responses)) if (isHistoryKey(k) && !responses.has(k)) responses.set(k, v);
6876
+ } else if (!limited && headSha) {
6877
+ const read = await safely("Token history", () => loadGitHubHistory(client, input.config, { ref: branch, limit: limits.history }), null);
6878
+ history = read && capHistory(read);
6879
+ if (history) {
6880
+ const backend = gitHubHistoryBackend(client, input.config, { concurrency: 2 });
6881
+ const shas = [...new Set(history.entries.filter((e) => !e.bulk).slice(0, limits.historyTrees).flatMap((e) => [e.sha, ...e.baseSha ? [e.baseSha] : []]))];
6882
+ for (const sha of shas) await safely(`Files at ${sha.slice(0, 7)}`, () => backend.tree(sha), []);
6883
+ }
6884
+ }
6885
+ const activity = {
6886
+ kind: "bridge-activity",
6887
+ schemaVersion: ACTIVITY_SCHEMA_VERSION,
6888
+ tool: { name: "bridge", version: input.version },
6889
+ generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6890
+ repo: { owner: input.owner, name: input.repo, branch },
6891
+ headSha,
6892
+ branches: branches4,
6893
+ suggestions: suggestions2,
6894
+ requests: requests3,
6895
+ merged,
6896
+ history,
6897
+ responses: Object.fromEntries([...responses].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
6898
+ truncated,
6899
+ problems
6900
+ };
6901
+ return fitActivity(activity);
6902
+ }
6903
+ var sameRepo = (a, b) => a.repo.owner.toLowerCase() === b.owner.toLowerCase() && a.repo.name.toLowerCase() === b.repo.toLowerCase();
6904
+ var isHistoryKey = (k) => /\/git\/commits\/[0-9a-f]+$/.test(k) || /\/git\/trees\/[0-9a-f]+\?recursive=1$/.test(k);
6905
+ var activityBytes = (a) => new TextEncoder().encode(`${JSON.stringify(a)}
6906
+ `).length;
6907
+ function fitActivity(activity, max = ACTIVITY_MAX_BYTES) {
6908
+ if (activityBytes(activity) <= max) return activity;
6909
+ const out = { ...activity, responses: { ...activity.responses }, truncated: [...activity.truncated] };
6910
+ const drop = (test) => {
6911
+ for (const k of Object.keys(out.responses)) if (test(k)) delete out.responses[k];
6912
+ };
6913
+ drop((k) => k.includes("/git/trees/"));
6914
+ out.truncated.push("File lists were left out to keep activity.json small: a branch or history change reads them from GitHub.");
6915
+ if (activityBytes(out) <= max) return out;
6916
+ const old = [...out.branches].sort((a, b) => a.updatedAt < b.updatedAt ? -1 : 1);
6917
+ for (const b of old) {
6918
+ drop((k) => new RegExp(`/(pulls|issues)/${b.number}(/|$|\\?)`).test(k) || k.includes(`/commits/${b.headSha}/`) || k.includes(`...${b.headSha}`));
6919
+ if (activityBytes(out) <= max) {
6920
+ out.truncated.push("The pages of the least recently updated branches were left out: they read from GitHub.");
6921
+ return out;
6922
+ }
6923
+ }
6924
+ out.responses = {};
6925
+ out.truncated.push("Every recorded read was left out: pages read GitHub live.");
6926
+ return out;
6927
+ }
6928
+ function parseActivity(input) {
6929
+ let data = input;
6930
+ if (typeof input === "string") {
6931
+ try {
6932
+ data = JSON.parse(input);
6933
+ } catch (err) {
6934
+ throw new ActivityError(`${ACTIVITY_FILE} is not valid JSON (${err.message})`);
6935
+ }
6936
+ }
6937
+ if (!isObj3(data) || data.kind !== "bridge-activity") throw new ActivityError(`${ACTIVITY_FILE} is not a Bridge activity file`);
6938
+ if (data.schemaVersion !== ACTIVITY_SCHEMA_VERSION) throw new ActivityError(`${ACTIVITY_FILE} is schema version ${String(data.schemaVersion)}; this version of Bridge reads ${ACTIVITY_SCHEMA_VERSION}`);
6939
+ const repo = data.repo;
6940
+ if (!isObj3(repo) || typeof repo.owner !== "string" || typeof repo.name !== "string" || typeof repo.branch !== "string") throw new ActivityError(`${ACTIVITY_FILE}: repo needs owner, name and branch`);
6941
+ if (typeof data.generatedAt !== "string" || Number.isNaN(Date.parse(data.generatedAt))) throw new ActivityError(`${ACTIVITY_FILE}: generatedAt is missing`);
6942
+ for (const k of ["branches", "suggestions", "requests", "merged", "truncated", "problems"]) if (!Array.isArray(data[k])) throw new ActivityError(`${ACTIVITY_FILE}: ${k} must be a list`);
6943
+ if (!isObj3(data.responses) || !Object.values(data.responses).every((r) => isObj3(r) && typeof r.status === "number")) throw new ActivityError(`${ACTIVITY_FILE}: responses must map paths to { status, body }`);
6944
+ if (data.history !== null && !(isObj3(data.history) && Array.isArray(data.history.entries))) throw new ActivityError(`${ACTIVITY_FILE}: history must be null or a history report`);
6945
+ return data;
6946
+ }
6947
+
5462
6948
  // ../visual/src/capture.ts
5463
6949
  import { mkdir, writeFile } from "node:fs/promises";
5464
6950
  import { dirname as dirname2 } from "node:path";
@@ -5859,17 +7345,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
5859
7345
 
5860
7346
  // ../visual/src/select.ts
5861
7347
  var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
5862
- var norm3 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
7348
+ var norm5 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
5863
7349
  var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
5864
7350
  function storyComponent(s, componentsDir) {
5865
- return componentForPath(norm3(s.importPath), componentsDir);
7351
+ return componentForPath(norm5(s.importPath), componentsDir);
5866
7352
  }
5867
7353
  function selectImpact(input) {
5868
7354
  const { usage, stories } = input;
5869
7355
  const dir = usage.componentsDir;
5870
7356
  const lib = input.librarySrc.replace(/\/$/, "");
5871
7357
  const cap = input.maxStoriesPerComponent ?? 4;
5872
- const visualFiles = input.files.map(norm3).filter((f) => !NO_VISUAL.test(f));
7358
+ const visualFiles = input.files.map(norm5).filter((f) => !NO_VISUAL.test(f));
5873
7359
  const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
5874
7360
  const impact2 = impactedModules(usage, { vars, files: visualFiles });
5875
7361
  let fallback = null;
@@ -5909,7 +7395,7 @@ function selectImpact(input) {
5909
7395
  return {
5910
7396
  tokens: input.tokens,
5911
7397
  vars,
5912
- files: input.files.map(norm3),
7398
+ files: input.files.map(norm5),
5913
7399
  components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
5914
7400
  stories: [...new Set(picked)],
5915
7401
  fallback,
@@ -5928,7 +7414,7 @@ function storiesFromSource(files) {
5928
7414
  if (!title) continue;
5929
7415
  for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
5930
7416
  const name = m[1];
5931
- out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm3(f.path)}` });
7417
+ out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm5(f.path)}` });
5932
7418
  }
5933
7419
  }
5934
7420
  return out;
@@ -6556,7 +8042,7 @@ async function jiraSync(input) {
6556
8042
  log(`Bridge Jira sync: ${reason}`);
6557
8043
  return { status: "skipped", reason };
6558
8044
  };
6559
- const fail = (reason) => {
8045
+ const fail2 = (reason) => {
6560
8046
  log(`::error::Bridge Jira sync: ${reason}`);
6561
8047
  return { status: "error", reason };
6562
8048
  };
@@ -6571,7 +8057,7 @@ async function jiraSync(input) {
6571
8057
  if (!n || !repo) return skip("not an issues event.");
6572
8058
  if (!["opened", "labeled", "closed"].includes(action)) return skip(`nothing to do on "${action}".`);
6573
8059
  if (action === "labeled" && event.label?.name !== REQUEST_LABEL) return skip(`label "${event.label?.name ?? ""}" is not ${REQUEST_LABEL}.`);
6574
- if (!env.GITHUB_TOKEN) return fail("GITHUB_TOKEN is missing: pass secrets.GITHUB_TOKEN to the step.");
8060
+ if (!env.GITHUB_TOKEN) return fail2("GITHUB_TOKEN is missing: pass secrets.GITHUB_TOKEN to the step.");
6575
8061
  const ghFetch = (url2, init2) => f(url2, init2);
6576
8062
  const gh = new GitHubClient(repo, env.GITHUB_TOKEN, ghFetch);
6577
8063
  const issue = await gh.getIssue(n);
@@ -6600,14 +8086,14 @@ async function jiraSync(input) {
6600
8086
  const text = `Closed in GitHub (#${n}), ${outcome}`;
6601
8087
  const body = { body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text, marks: [{ type: "link", attrs: { href: issue.url } }] }] }] } };
6602
8088
  const res2 = await f(`${base}/rest/api/3/issue/${encodeURIComponent(key)}/comment`, { method: "POST", headers, body: JSON.stringify(body) });
6603
- if (!res2.ok) return fail(await jiraError(res2));
8089
+ if (!res2.ok) return fail2(await jiraError(res2));
6604
8090
  log(`Bridge Jira sync: commented on ${key}: ${text}.`);
6605
8091
  return { status: "commented", key };
6606
8092
  }
6607
8093
  if (key) return skip(`#${n} is already linked to ${key}.`);
6608
8094
  const request = requests.requestFromIssue(issue);
6609
8095
  const res = await f(`${base}/rest/api/3/issue`, { method: "POST", headers, body: JSON.stringify({ fields: jiraIssueFields(request, jira, issue.url) }) });
6610
- if (!res.ok) return fail(await jiraError(res));
8096
+ if (!res.ok) return fail2(await jiraError(res));
6611
8097
  const created = await res.json();
6612
8098
  const url = `${base}/browse/${created.key}`;
6613
8099
  await gh.addComment(n, `Jira: ${created.key} ${url}`);
@@ -6637,7 +8123,7 @@ Exit codes: 0 ok, 1 not a request or Claude Code not found, 2 usage or setup err
6637
8123
  var RequestUsageError = class extends Error {
6638
8124
  };
6639
8125
  var flag = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6640
- var has = (a, k) => Boolean(a.flags[k]);
8126
+ var has2 = (a, k) => Boolean(a.flags[k]);
6641
8127
  var defaultIO = () => ({
6642
8128
  print: (s) => process.stdout.write(`${s}
6643
8129
  `),
@@ -6697,9 +8183,9 @@ async function getOrExplain(ctx, n, io, branch) {
6697
8183
  }
6698
8184
  async function runRequest(a, root, io = defaultIO()) {
6699
8185
  const sub = a._[1];
6700
- if (!sub || sub === "help" || has(a, "help")) {
8186
+ if (!sub || sub === "help" || has2(a, "help")) {
6701
8187
  io.print(REQUEST_HELP);
6702
- return sub || has(a, "help") ? 0 : 2;
8188
+ return sub || has2(a, "help") ? 0 : 2;
6703
8189
  }
6704
8190
  switch (sub) {
6705
8191
  case "list": {
@@ -6708,7 +8194,7 @@ async function runRequest(a, root, io = defaultIO()) {
6708
8194
  const ctx = await context(root, io, false);
6709
8195
  const component = flag(a, "component");
6710
8196
  const r = await requests2.listRequests(ctx, { status, ...component ? { component } : {} });
6711
- if (has(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8197
+ if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
6712
8198
  if (!r.requests.length) return io.print(`No ${status === "all" ? "" : `${status} `}component change request in ${r.repo}${component ? ` for ${component}` : ""}.`), 0;
6713
8199
  const w = Math.max(...r.requests.map((x) => x.component.length));
6714
8200
  for (const x of r.requests) {
@@ -6722,7 +8208,7 @@ async function runRequest(a, root, io = defaultIO()) {
6722
8208
  const ctx = await context(root, io, false);
6723
8209
  const r = await getOrExplain(ctx, n, io);
6724
8210
  if (!r) return 1;
6725
- if (has(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8211
+ if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
6726
8212
  const q = r.request;
6727
8213
  io.print(`#${r.number} ${q.component}: ${q.title}`);
6728
8214
  io.print(`${REQUEST_STATUS_LABEL[r.status]}, ${q.kind}, from ${q.source}${q.requestedBy ? ` by @${q.requestedBy}` : ""}`);
@@ -6752,7 +8238,7 @@ Next: bridge request implement ${r.number} --claude`);
6752
8238
  const branch = flag(a, "branch");
6753
8239
  const r = await getOrExplain(ctx, n, io, branch);
6754
8240
  if (!r) return 1;
6755
- if (!has(a, "claude")) return io.print(r.brief), 0;
8241
+ if (!has2(a, "claude")) return io.print(r.brief), 0;
6756
8242
  if (!io.hasClaude()) {
6757
8243
  io.print(r.brief);
6758
8244
  io.error(`
@@ -6770,7 +8256,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
6770
8256
  const usage = 'bridge request create --component Button --kind new-variant --title "Add a danger ghost variant" --summary "..."';
6771
8257
  if (!component || !kind || !title || !summary) throw new RequestUsageError(`Usage: ${usage}`);
6772
8258
  if (!REQUEST_KINDS.some((k) => k.value === kind)) throw new RequestUsageError(`--kind takes ${REQUEST_KINDS.map((k) => k.value).join(", ")}, got "${kind}"`);
6773
- const dryRun = has(a, "dry-run");
8259
+ const dryRun = has2(a, "dry-run");
6774
8260
  const ctx = await context(root, io, !dryRun);
6775
8261
  const figma2 = flag(a, "figma");
6776
8262
  const exploration = flag(a, "exploration");
@@ -6786,7 +8272,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
6786
8272
  source: "cli",
6787
8273
  dryRun
6788
8274
  });
6789
- if (has(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8275
+ if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
6790
8276
  if (r.status === "dry-run") {
6791
8277
  io.print(`Dry run, nothing created.
6792
8278
 
@@ -6845,7 +8331,7 @@ Work on one: git switch explore/<name>, then \`bridge propose --branch explore/<
6845
8331
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`.
6846
8332
  Exit codes: 0 ok, 2 usage or setup error.`;
6847
8333
  var flag2 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6848
- var has2 = (a, k) => Boolean(a.flags[k]);
8334
+ var has3 = (a, k) => Boolean(a.flags[k]);
6849
8335
  async function context2(root, io) {
6850
8336
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
6851
8337
  return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -6854,11 +8340,11 @@ var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "cla
6854
8340
  var date = (iso) => iso ? iso.slice(0, 10) : "";
6855
8341
  async function runExplore(a, root, io = defaultIO()) {
6856
8342
  const sub = a._[1];
6857
- if (!sub || sub === "help" || has2(a, "help")) {
8343
+ if (!sub || sub === "help" || has3(a, "help")) {
6858
8344
  io.print(EXPLORE_HELP);
6859
- return sub || has2(a, "help") ? 0 : 2;
8345
+ return sub || has3(a, "help") ? 0 : 2;
6860
8346
  }
6861
- const json = has2(a, "json");
8347
+ const json = has3(a, "json");
6862
8348
  switch (sub) {
6863
8349
  case "start": {
6864
8350
  const name = a._.slice(2).join(" ").trim() || flag2(a, "name");
@@ -6866,7 +8352,7 @@ async function runExplore(a, root, io = defaultIO()) {
6866
8352
  if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
6867
8353
  const useCase = flag2(a, "use-case");
6868
8354
  const ctx = await context2(root, io);
6869
- const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, ...flag2(a, "product") ? { product: flag2(a, "product") } : {}, dryRun: has2(a, "dry-run") });
8355
+ const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, ...flag2(a, "product") ? { product: flag2(a, "product") } : {}, dryRun: has3(a, "dry-run") });
6870
8356
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6871
8357
  if (r.status === "dry-run") {
6872
8358
  io.print(`Dry run, nothing written.
@@ -6890,7 +8376,7 @@ Next: ${r.next}`);
6890
8376
  }
6891
8377
  case "list": {
6892
8378
  const ctx = await context2(root, io);
6893
- const r = await explorations.listExplorations(ctx, { state: has2(a, "all") ? "all" : "open" });
8379
+ const r = await explorations.listExplorations(ctx, { state: has3(a, "all") ? "all" : "open" });
6894
8380
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6895
8381
  io.print(r.summary);
6896
8382
  for (const x of r.explorations) {
@@ -6911,7 +8397,7 @@ Local only: ${b} (push it and open a draft pull request labeled exploration)`);
6911
8397
  const ref = a._.slice(2).join(" ").trim();
6912
8398
  if (!ref) throw new RequestUsageError("Usage: bridge explore promote <name|#pr> [--dry-run]");
6913
8399
  const ctx = await context2(root, io);
6914
- const r = await explorations.promoteExploration(ctx, { ref, dryRun: has2(a, "dry-run") });
8400
+ const r = await explorations.promoteExploration(ctx, { ref, dryRun: has3(a, "dry-run") });
6915
8401
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6916
8402
  io.print(`${r.status === "dry-run" ? "Dry run: would promote" : "Promoted"} #${r.number} "${r.title}" to review: Tier ${r.tier}.`);
6917
8403
  io.print(`Labels: +${r.addLabels.join(", +")}, -${r.removeLabels.join(", -")}${r.removeFiles.length ? `. Removes ${r.removeFiles.join(", ")}` : ""}.`);
@@ -6965,10 +8451,10 @@ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Readi
6965
8451
  Exit codes (apply): 0 applied, already applied or a clean dry run; 1 not approved, blocked by a check or refused;
6966
8452
  2 usage or setup error; 3 conflict with today's code.`;
6967
8453
  var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6968
- var has3 = (a, k) => Boolean(a.flags[k]);
8454
+ var has4 = (a, k) => Boolean(a.flags[k]);
6969
8455
  function changesFlag(a) {
6970
- if (has3(a, "suggest") && has3(a, "direct")) throw new SuggestUsageError("Pass --suggest or --direct, not both.");
6971
- return has3(a, "direct") ? "direct" : has3(a, "suggest") ? "suggest" : void 0;
8456
+ if (has4(a, "suggest") && has4(a, "direct")) throw new SuggestUsageError("Pass --suggest or --direct, not both.");
8457
+ return has4(a, "direct") ? "direct" : has4(a, "suggest") ? "suggest" : void 0;
6972
8458
  }
6973
8459
  function tokenFrom2(io) {
6974
8460
  return io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
@@ -6986,7 +8472,7 @@ var STATUSES = ["needs-review", "changes-requested", "approved", "applying", "ap
6986
8472
  var KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
6987
8473
  async function runSuggestions(a, root, io = defaultIO()) {
6988
8474
  const sub = a._[1] ?? "list";
6989
- if (sub === "help" || has3(a, "help")) return io.print(SUGGESTIONS_HELP), 0;
8475
+ if (sub === "help" || has4(a, "help")) return io.print(SUGGESTIONS_HELP), 0;
6990
8476
  switch (sub) {
6991
8477
  case "list": {
6992
8478
  const status = flag3(a, "status") ?? "all";
@@ -6995,7 +8481,7 @@ async function runSuggestions(a, root, io = defaultIO()) {
6995
8481
  if (kind && !KINDS.includes(kind)) throw new SuggestUsageError(`--kind takes ${KINDS.join(", ")}, got "${kind}"`);
6996
8482
  const ctx = await context3(root, io);
6997
8483
  const r = await suggestions.listSuggestionsOp(ctx, { status, ...kind ? { kind } : {} });
6998
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8484
+ if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
6999
8485
  if (!r.suggestions.length) return io.print(`No ${status === "all" ? "open " : `${status} `}suggestion in ${r.repo}.`), 0;
7000
8486
  for (const x of r.suggestions) {
7001
8487
  const prs = x.linkedPrs.length ? ` PR ${x.linkedPrs.map((p) => `#${p.number}${p.merged ? " merged" : ` ${p.state}`}`).join(", ")}` : "";
@@ -7009,7 +8495,7 @@ ${r.next}`);
7009
8495
  const n = issueNumber2(a._[2], "bridge suggestions show <number>");
7010
8496
  const ctx = await context3(root, io);
7011
8497
  const r = await suggestions.getSuggestionOp(ctx, { number: n });
7012
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8498
+ if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
7013
8499
  io.print(`#${r.number} ${r.title}`);
7014
8500
  io.print(`${r.said}${r.kind ? `, ${r.kind}` : ""}${r.tier ? `, tier ${r.tier} (${r.bump})` : ""}${r.source ? `, from ${r.source}` : ""}${r.suggestedBy ? ` by @${r.suggestedBy}` : ""}`);
7015
8501
  io.print(r.url);
@@ -7047,7 +8533,7 @@ Next: ${r.next}`);
7047
8533
  if (!pr && !branch) throw new SuggestUsageError("Usage: bridge suggestions link <number> --pr <m> (or --branch <name>)");
7048
8534
  const ctx = await context3(root, io);
7049
8535
  const r = await suggestions.linkSuggestionOp(ctx, { number: n, ...pr ? { pr } : {}, ...branch ? { branch } : {} });
7050
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8536
+ if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
7051
8537
  io.print(`#${n} is being applied${pr ? ` in #${pr}` : branch ? ` on ${branch}` : ""}.`);
7052
8538
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
7053
8539
  return 0;
@@ -7060,23 +8546,23 @@ ${SUGGESTIONS_HELP}`);
7060
8546
  }
7061
8547
  var fmt = (v) => v === void 0 ? "(none)" : typeof v === "string" ? v : JSON.stringify(v);
7062
8548
  async function runApply(a, root, io = defaultIO()) {
7063
- if (has3(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
8549
+ if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
7064
8550
  const n = issueNumber2(a._[1], "bridge apply <number> [--write | --patch] [--dry-run]");
7065
- if (has3(a, "write") && has3(a, "patch")) throw new SuggestUsageError("Pass --write or --patch, not both.");
7066
- const mode2 = has3(a, "write") ? "write" : has3(a, "patch") ? "patch" : "branch";
8551
+ if (has4(a, "write") && has4(a, "patch")) throw new SuggestUsageError("Pass --write or --patch, not both.");
8552
+ const mode2 = has4(a, "write") ? "write" : has4(a, "patch") ? "patch" : "branch";
7067
8553
  const ctx = await context3(root, io);
7068
8554
  if (ctx.mode !== "local" || !ctx.isGit) throw new SuggestUsageError("bridge apply runs in a git checkout of the design system: cd into it, or pass --root.");
7069
8555
  const branch = flag3(a, "branch");
7070
8556
  const r = await suggestions.applySuggestionOp(ctx, {
7071
8557
  number: n,
7072
- mode: has3(a, "dry-run") ? "dry-run" : mode2,
7073
- acceptEdited: has3(a, "accept-edited"),
7074
- forceUnapproved: has3(a, "force-unapproved"),
8558
+ mode: has4(a, "dry-run") ? "dry-run" : mode2,
8559
+ acceptEdited: has4(a, "accept-edited"),
8560
+ forceUnapproved: has4(a, "force-unapproved"),
7075
8561
  ...branch ? { branch } : {}
7076
8562
  });
7077
8563
  const code = typeof r.exitCode === "number" ? r.exitCode : SUGGESTION_APPLY_EXIT[r.outcome] ?? 1;
7078
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), code;
7079
- if (mode2 === "patch" && r.patch && !has3(a, "dry-run")) {
8564
+ if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), code;
8565
+ if (mode2 === "patch" && r.patch && !has4(a, "dry-run")) {
7080
8566
  io.print(r.patch.trimEnd());
7081
8567
  io.error(r.message);
7082
8568
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
@@ -7100,10 +8586,10 @@ Next: ${r.next}`);
7100
8586
  return code;
7101
8587
  }
7102
8588
  async function runReview(a, root, io = defaultIO()) {
7103
- if (has3(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
8589
+ if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
7104
8590
  const usage = 'bridge review <number> --approve | --request-changes --message "..." | --decline --message "..."';
7105
8591
  const n = issueNumber2(a._[1], usage);
7106
- const events = ["approve", "request-changes", "decline", "comment"].filter((e) => has3(a, e));
8592
+ const events = ["approve", "request-changes", "decline", "comment"].filter((e) => has4(a, e));
7107
8593
  if (events.length !== 1) throw new SuggestUsageError(`Usage: ${usage}`);
7108
8594
  const event = events[0];
7109
8595
  const inline = flag3(a, event);
@@ -7120,7 +8606,7 @@ async function runReview(a, root, io = defaultIO()) {
7120
8606
  ...role ? { role } : {},
7121
8607
  ...version ? { payloadSha: version } : {}
7122
8608
  });
7123
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
8609
+ if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
7124
8610
  const done = event === "approve" ? "Approved" : event === "request-changes" ? "Changes requested on" : event === "decline" ? "Declined" : "Commented on";
7125
8611
  io.print(`${done} #${n}${r.version ? ` (version ${r.version})` : ""}. Now: ${SUGGESTION_STATUS_LABEL[r.status] ?? r.status}.`);
7126
8612
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
@@ -7159,7 +8645,7 @@ Usage:
7159
8645
  Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
7160
8646
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7161
8647
  var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
7162
- var has4 = (a, k) => Boolean(a.flags[k]);
8648
+ var has5 = (a, k) => Boolean(a.flags[k]);
7163
8649
  async function context4(root, io) {
7164
8650
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7165
8651
  return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7181,11 +8667,11 @@ function pairs2(list2, what) {
7181
8667
  var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
7182
8668
  async function runBranch(a, root, io = defaultIO()) {
7183
8669
  const sub = a._[1];
7184
- if (!sub || sub === "help" || has4(a, "help")) {
8670
+ if (!sub || sub === "help" || has5(a, "help")) {
7185
8671
  io.print(BRANCH_HELP);
7186
- return sub || has4(a, "help") ? 0 : 2;
8672
+ return sub || has5(a, "help") ? 0 : 2;
7187
8673
  }
7188
- const json = has4(a, "json");
8674
+ const json = has5(a, "json");
7189
8675
  const ctx = await context4(root, io);
7190
8676
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7191
8677
  switch (sub) {
@@ -7203,6 +8689,7 @@ async function runBranch(a, root, io = defaultIO()) {
7203
8689
  io.print(`#${r.pr} ${r.title} (${r.product.name})`);
7204
8690
  io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
7205
8691
  if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
8692
+ io.print(`${r.tests.label}.`);
7206
8693
  if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
7207
8694
  io.print(`Next: ${r.next}`);
7208
8695
  io.print(r.url);
@@ -7225,14 +8712,14 @@ async function runBranch(a, root, io = defaultIO()) {
7225
8712
  }
7226
8713
  case "review": {
7227
8714
  const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
7228
- const decision = has4(a, "approve") ? "approve" : has4(a, "request-changes") ? "request_changes" : has4(a, "comment") ? "comment" : null;
8715
+ const decision = has5(a, "approve") ? "approve" : has5(a, "request-changes") ? "request_changes" : has5(a, "comment") ? "comment" : null;
7229
8716
  if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
7230
8717
  const body = decision === "approve" ? flag4(a, "body") : flag4(a, decision === "request_changes" ? "request-changes" : "comment");
7231
8718
  const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
7232
8719
  return out(r, () => io.print(r.said));
7233
8720
  }
7234
8721
  case "ask": {
7235
- const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has4(a, "dry-run") });
8722
+ const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has5(a, "dry-run") });
7236
8723
  return out(r, () => io.print(r.said));
7237
8724
  }
7238
8725
  case "comment": {
@@ -7246,7 +8733,7 @@ async function runBranch(a, root, io = defaultIO()) {
7246
8733
  case "merge": {
7247
8734
  const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
7248
8735
  const reason = flag4(a, "override");
7249
- const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has4(a, "dry-run") });
8736
+ const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has5(a, "dry-run") });
7250
8737
  return out(r, () => io.print(r.said));
7251
8738
  }
7252
8739
  case "close": {
@@ -7261,7 +8748,7 @@ async function runBranch(a, root, io = defaultIO()) {
7261
8748
  const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
7262
8749
  const choices = pairs2(a.flags.choice, "choice");
7263
8750
  for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
7264
- const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has4(a, "dry-run") });
8751
+ const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has5(a, "dry-run") });
7265
8752
  return out(r, () => {
7266
8753
  io.print(said(r));
7267
8754
  for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
@@ -7294,7 +8781,7 @@ ${BRANCH_HELP}`);
7294
8781
  }
7295
8782
  async function runStudioRead(cmd, a, root, io = defaultIO()) {
7296
8783
  const ctx = await context4(root, io);
7297
- const json = has4(a, "json");
8784
+ const json = has5(a, "json");
7298
8785
  if (cmd === "products") {
7299
8786
  const r2 = await branches.listProducts(ctx);
7300
8787
  if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
@@ -7345,9 +8832,9 @@ async function runDocsSave(a, root, io = defaultIO()) {
7345
8832
  ...flag4(a, "product") ? { product: flag4(a, "product") } : {},
7346
8833
  ...changes ? { changes } : {},
7347
8834
  source: "cli",
7348
- dryRun: has4(a, "dry-run")
8835
+ dryRun: has5(a, "dry-run")
7349
8836
  });
7350
- if (has4(a, "json")) io.print(JSON.stringify(r, null, 2));
8837
+ if (has5(a, "json")) io.print(JSON.stringify(r, null, 2));
7351
8838
  else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
7352
8839
 
7353
8840
  ${r.path}:
@@ -7372,7 +8859,7 @@ Usage:
7372
8859
  bridge pieces switch <n> once the library shipped: main into the product branch
7373
8860
 
7374
8861
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7375
- var has5 = (a, k) => Boolean(a.flags[k]);
8862
+ var has6 = (a, k) => Boolean(a.flags[k]);
7376
8863
  async function context5(root, io) {
7377
8864
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7378
8865
  return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7389,11 +8876,11 @@ function piece(a, at, usage) {
7389
8876
  }
7390
8877
  async function runPieces(a, root, io = defaultIO()) {
7391
8878
  const sub = a._[1];
7392
- if (!sub || sub === "help" || has5(a, "help")) {
8879
+ if (!sub || sub === "help" || has6(a, "help")) {
7393
8880
  io.print(PIECES_HELP);
7394
- return sub || has5(a, "help") ? 0 : 2;
8881
+ return sub || has6(a, "help") ? 0 : 2;
7395
8882
  }
7396
- const json = has5(a, "json");
8883
+ const json = has6(a, "json");
7397
8884
  const ctx = await context5(root, io);
7398
8885
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7399
8886
  switch (sub) {
@@ -7410,7 +8897,7 @@ async function runPieces(a, root, io = defaultIO()) {
7410
8897
  case "open": {
7411
8898
  const usage = "bridge pieces open <n> <piece>";
7412
8899
  const changes = changesFlag(a);
7413
- const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has5(a, "no-preview"), dryRun: has5(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
8900
+ const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has6(a, "no-preview"), dryRun: has6(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
7414
8901
  return out(r, () => {
7415
8902
  io.print(`${r.status === "dry-run" ? "Would open" : r.status === "already-linked" ? "Already linked" : r.status === "suggested" ? "Suggested" : "Opened"} a ${r.opens}: ${r.title}, needed by ${r.neededBy}.`);
7416
8903
  if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
@@ -7429,7 +8916,7 @@ async function runPieces(a, root, io = defaultIO()) {
7429
8916
  return out(r, () => io.print(r.said));
7430
8917
  }
7431
8918
  default: {
7432
- const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has5(a, "record") });
8919
+ const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has6(a, "record") });
7433
8920
  return out(r, () => {
7434
8921
  io.print(`#${r.pr} ${r.branch} (${r.product}): ${r.pieces.length ? `${r.pieces.length} ${r.pieces.length === 1 ? "piece is" : "pieces are"} missing from the library` : "nothing is missing from the library"}, ${r.files} files read.`);
7435
8922
  for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
@@ -7459,7 +8946,7 @@ Usage:
7459
8946
  token change, or a branch with the spec for Claude AI
7460
8947
 
7461
8948
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7462
- var has6 = (a, k) => Boolean(a.flags[k]);
8949
+ var has7 = (a, k) => Boolean(a.flags[k]);
7463
8950
  async function context6(root, io) {
7464
8951
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7465
8952
  return BridgeContext7.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7471,17 +8958,17 @@ function number3(a, usage) {
7471
8958
  }
7472
8959
  async function runFigma(a, root, io = defaultIO()) {
7473
8960
  const sub = a._[1];
7474
- if (sub === "help" || has6(a, "help")) {
8961
+ if (sub === "help" || has7(a, "help")) {
7475
8962
  io.print(FIGMA_HELP);
7476
8963
  return 0;
7477
8964
  }
7478
- const json = has6(a, "json");
8965
+ const json = has7(a, "json");
7479
8966
  const ctx = await context6(root, io);
7480
8967
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7481
8968
  switch (sub) {
7482
8969
  case void 0:
7483
8970
  case "list": {
7484
- const r = await figma.figmaChangesOp(ctx, { state: has6(a, "all") ? "all" : "open" });
8971
+ const r = await figma.figmaChangesOp(ctx, { state: has7(a, "all") ? "all" : "open" });
7485
8972
  return out(r, () => {
7486
8973
  for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
7487
8974
  io.print(`Next: ${r.next}`);
@@ -7489,7 +8976,7 @@ async function runFigma(a, root, io = defaultIO()) {
7489
8976
  }
7490
8977
  case "show": {
7491
8978
  const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
7492
- if (has6(a, "draft")) {
8979
+ if (has7(a, "draft")) {
7493
8980
  if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
7494
8981
  if (!io.hasClaude()) throw new RequestUsageError("`claude` is not on PATH: install Claude Code, or paste the brief from `bridge figma show <n>` into Claude.");
7495
8982
  return io.runClaude(r.brief, root);
@@ -7515,7 +9002,7 @@ ${r.brief}`);
7515
9002
  throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
7516
9003
  }
7517
9004
  const changes = changesFlag(a);
7518
- const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has6(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
9005
+ const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has7(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
7519
9006
  return out(r, () => {
7520
9007
  io.print(`${r.component}: ${r.route}. ${r.said}`);
7521
9008
  for (const x of r.reasons ?? []) io.print(` ${x}`);
@@ -7601,7 +9088,20 @@ async function writeSnapshot(opts) {
7601
9088
  `);
7602
9089
  written.push(summary.path);
7603
9090
  }
7604
- return { snapshot, written, bytes };
9091
+ let activity;
9092
+ if (opts.activity) {
9093
+ const [owner, name] = (opts.activity.repo ?? `${snapshot.repo.owner}/${snapshot.repo.name}`).split("/");
9094
+ if (!owner || !name) throw new Error('bridge snapshot --activity needs the repository: --repo owner/name, GITHUB_REPOSITORY, or "repo" in bridge.config.json.');
9095
+ const fetchImpl = opts.activity.fetch ?? ((url, init2) => fetch(url, init2));
9096
+ const previous = await new GitHubClient(`${owner}/${name}`, opts.activity.token, fetchImpl, { attempts: 1, baseDelayMs: 0, sleep: async () => {
9097
+ } }, { etags: false }).getRawText(ACTIVITY_FILE, SNAPSHOT_BRANCH).then((t) => t ? parseActivity(t) : null).catch(() => null);
9098
+ const a = await collectActivity({ fetch: fetchImpl, token: opts.activity.token, owner, repo: name, config: model.config, version: opts.version, ...opts.generatedAt ? { generatedAt: opts.generatedAt } : {}, previous });
9099
+ writeFileSync2(join7(opts.out, ACTIVITY_FILE), `${JSON.stringify(a)}
9100
+ `);
9101
+ written.push(ACTIVITY_FILE);
9102
+ activity = { activity: a, bytes: activityBytes(a), reused: Boolean(previous && a.history && previous.history === a.history) };
9103
+ }
9104
+ return { snapshot, written, bytes, ...activity ? { activity } : {} };
7605
9105
  }
7606
9106
  function snapshotLines(r, out) {
7607
9107
  const s = r.snapshot;
@@ -7614,7 +9114,24 @@ function snapshotLines(r, out) {
7614
9114
  ` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
7615
9115
  ` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
7616
9116
  ` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
7617
- ...s.problems.map((p) => ` problem ${p}`)
9117
+ ...s.problems.map((p) => ` problem ${p}`),
9118
+ ...activityLines(r, out)
9119
+ ];
9120
+ }
9121
+ function activityLines(r, out) {
9122
+ if (!r.activity) return [];
9123
+ const a = r.activity.activity;
9124
+ const kb = Math.max(1, Math.round(r.activity.bytes / 1024));
9125
+ const reads = Object.keys(a.responses).length;
9126
+ return [
9127
+ `Wrote ${join7(out, ACTIVITY_FILE)} (${kb} KB): ${reads} GitHub reads recorded for signed-out visitors.`,
9128
+ ` branches ${a.branches.length} open`,
9129
+ ` suggestions ${a.suggestions.length} open`,
9130
+ ` requests ${a.requests.length}`,
9131
+ ` merged ${a.merged.length} recent`,
9132
+ ` history ${a.history ? `${a.history.entries.length} token changes${r.activity.reused ? " (kept: the default branch did not move)" : ""}` : "not read"}`,
9133
+ ...a.truncated.map((t) => ` left out ${t}`),
9134
+ ...a.problems.map((p) => ` problem ${p}`)
7618
9135
  ];
7619
9136
  }
7620
9137
 
@@ -7651,7 +9168,7 @@ async function rolesReport(opts) {
7651
9168
 
7652
9169
  // src/index.ts
7653
9170
  import { brand, BridgeContext as BridgeContext8, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson } from "@bridge-ds/mcp";
7654
- var VERSION = true ? "0.11.1" : "dev";
9171
+ var VERSION = true ? "0.12.0" : "dev";
7655
9172
  var MCP_PACKAGE = "@bridge-ds/mcp";
7656
9173
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
7657
9174
 
@@ -7712,11 +9229,14 @@ Usage:
7712
9229
  bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
7713
9230
  Dry run unless --write
7714
9231
  bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
7715
- [--branch main] [--json]
9232
+ [--branch main] [--activity [--repo owner/name]] [--json]
7716
9233
  the repo model of this checkout for hosted Bridge (Studio and Library):
7717
9234
  snapshot.json (token, contrast and component files, Figma coverage,
7718
9235
  contrast results) plus audits/<id>.json. --audit runs bridge audit on a
7719
9236
  product folder; --audit-report carries reports already written.
9237
+ --activity also writes activity.json (open branches, suggestions,
9238
+ requests, recent merges, token history) read with GITHUB_TOKEN, so
9239
+ signed-out visitors of hosted Studio spend no GitHub API request.
7720
9240
  The bridge-data workflow publishes the folder to the bridge-data branch
7721
9241
  bridge docs draft <Component> [--json]
7722
9242
  docs.json pre-fill from the code (props, tokens, anatomy, contrast,
@@ -7792,7 +9312,7 @@ function parse(argv) {
7792
9312
  return out;
7793
9313
  }
7794
9314
  var flag5 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
7795
- var has7 = (a, k) => Boolean(a.flags[k]);
9315
+ var has8 = (a, k) => Boolean(a.flags[k]);
7796
9316
  var print = (s) => process.stdout.write(`${s}
7797
9317
  `);
7798
9318
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
@@ -7808,7 +9328,7 @@ function printSent(r) {
7808
9328
  async function init(a, root) {
7809
9329
  const cfgPath = join8(root, CONFIG_FILE2);
7810
9330
  let config;
7811
- if (existsSync8(cfgPath) && !has7(a, "force")) {
9331
+ if (existsSync8(cfgPath) && !has8(a, "force")) {
7812
9332
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
7813
9333
  config = (await (await BridgeContext8.create({ root })).config()).config;
7814
9334
  } else {
@@ -7842,10 +9362,10 @@ async function check(a, root) {
7842
9362
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
7843
9363
  const c = await ops.checkContrast(ctx, {});
7844
9364
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
7845
- const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has7(a, "strict") && warnings.length > 0;
9365
+ const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has8(a, "strict") && warnings.length > 0;
7846
9366
  const protection = await protectionOf(a, root, ctx);
7847
9367
  const roles = await rolesOf(a, root, ctx);
7848
- if (has7(a, "json")) {
9368
+ if (has8(a, "json")) {
7849
9369
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
7850
9370
  return failed ? 1 : 0;
7851
9371
  }
@@ -7864,7 +9384,7 @@ async function check(a, root) {
7864
9384
  return failed ? 1 : 0;
7865
9385
  }
7866
9386
  async function protectionOf(a, root, ctx) {
7867
- if (has7(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
9387
+ if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
7868
9388
  const { config } = await ctx.config();
7869
9389
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
7870
9390
  if (!repo) return void 0;
@@ -7872,7 +9392,7 @@ async function protectionOf(a, root, ctx) {
7872
9392
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
7873
9393
  }
7874
9394
  async function rolesOf(a, root, ctx) {
7875
- if (has7(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
9395
+ if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
7876
9396
  const { config } = await ctx.config();
7877
9397
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
7878
9398
  if (!repo) return void 0;
@@ -7884,7 +9404,7 @@ async function tokens(a, root) {
7884
9404
  if (!q) throw new UsageError("bridge tokens <query>");
7885
9405
  const ctx = await BridgeContext8.create({ root });
7886
9406
  const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag5(a, "limit") ?? 20) });
7887
- if (has7(a, "json")) return print(JSON.stringify(r, null, 2));
9407
+ if (has8(a, "json")) return print(JSON.stringify(r, null, 2));
7888
9408
  if (!r.results.length) return print(`No token matches "${q}".`);
7889
9409
  const w = Math.max(...r.results.map((x) => x.path.length));
7890
9410
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -7901,11 +9421,11 @@ async function diff(a, root) {
7901
9421
  const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
7902
9422
  const tier = core.classify(changes, [], config);
7903
9423
  const imp = core.impact(before, after, changes, 30);
7904
- if (has7(a, "json")) {
9424
+ if (has8(a, "json")) {
7905
9425
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
7906
9426
  return 0;
7907
9427
  }
7908
- if (has7(a, "md")) {
9428
+ if (has8(a, "md")) {
7909
9429
  print(`## Token changes since \`${base}\`
7910
9430
 
7911
9431
  ${core.tokenTable(changes)}
@@ -7971,9 +9491,9 @@ async function propose(a, root) {
7971
9491
  ...product ? { product } : {},
7972
9492
  ...changes ? { changes } : {},
7973
9493
  source: "cli",
7974
- dryRun: has7(a, "dry-run")
9494
+ dryRun: has8(a, "dry-run")
7975
9495
  });
7976
- if (has7(a, "json")) print(JSON.stringify(r, null, 2));
9496
+ if (has8(a, "json")) print(JSON.stringify(r, null, 2));
7977
9497
  else {
7978
9498
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
7979
9499
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
@@ -7991,7 +9511,7 @@ async function brandCmd(a, root) {
7991
9511
  const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
7992
9512
  if (sub === "list") {
7993
9513
  const r2 = await brand.listBrands(ctx);
7994
- if (has7(a, "json")) print(JSON.stringify(r2.brands, null, 2));
9514
+ if (has8(a, "json")) print(JSON.stringify(r2.brands, null, 2));
7995
9515
  else for (const b of r2.brands) print(`${b.default ? "*" : " "} ${b.slug.padEnd(14)} ${b.name.padEnd(18)} ${b.primary ?? ""}${b.overrides.length ? ` overrides: ${b.overrides.join(", ")}` : ""}`);
7996
9516
  return 0;
7997
9517
  }
@@ -8013,9 +9533,9 @@ async function brandCmd(a, root) {
8013
9533
  ...opt("notes") ? { notes: opt("notes") } : {},
8014
9534
  ...changes ? { changes } : {},
8015
9535
  source: "cli",
8016
- dryRun: has7(a, "dry-run")
9536
+ dryRun: has8(a, "dry-run")
8017
9537
  });
8018
- if (has7(a, "json")) {
9538
+ if (has8(a, "json")) {
8019
9539
  const { fileContents: _f, ...rest } = r;
8020
9540
  print(JSON.stringify(rest, null, 2));
8021
9541
  return r.status === "blocked" ? 1 : 0;
@@ -8049,9 +9569,9 @@ async function figmaReferences(a, root) {
8049
9569
  out: resolve6(root, flag5(a, "out") ?? "figma-references"),
8050
9570
  sha,
8051
9571
  ...only.length ? { only } : {},
8052
- log: has7(a, "json") ? void 0 : (line2) => print(line2)
9572
+ log: has8(a, "json") ? void 0 : (line2) => print(line2)
8053
9573
  });
8054
- if (has7(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
9574
+ if (has8(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
8055
9575
  return 0;
8056
9576
  }
8057
9577
  async function impact(a, root) {
@@ -8076,15 +9596,15 @@ async function impact(a, root) {
8076
9596
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
8077
9597
  ...imageBase !== void 0 ? { imageBase } : {},
8078
9598
  ...artifactUrl ? { artifactUrl } : {},
8079
- plan: has7(a, "plan"),
8080
- keepUnchanged: has7(a, "keep-unchanged")
9599
+ plan: has8(a, "plan"),
9600
+ keepUnchanged: has8(a, "keep-unchanged")
8081
9601
  });
8082
- if (has7(a, "json")) {
9602
+ if (has8(a, "json")) {
8083
9603
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
8084
9604
  return 0;
8085
9605
  }
8086
9606
  const sel = r.manifest.selection;
8087
- if (has7(a, "plan")) {
9607
+ if (has8(a, "plan")) {
8088
9608
  print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
8089
9609
  print(` tokens changing value: ${sel.tokens.length}${sel.tokens.length ? ` (${sel.tokens.filter((t) => t.kind === "direct").map((t) => t.path).slice(0, 6).join(", ")})` : ""}`);
8090
9610
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -8105,7 +9625,7 @@ async function historyCmd(a, root) {
8105
9625
  const limit = Number(flag5(a, "limit") ?? 20);
8106
9626
  if (token) {
8107
9627
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
8108
- if (has7(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
9628
+ if (has8(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
8109
9629
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
8110
9630
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
8111
9631
  `);
@@ -8117,8 +9637,8 @@ async function historyCmd(a, root) {
8117
9637
  return 0;
8118
9638
  }
8119
9639
  const tier = flag5(a, "tier");
8120
- const r = await hist.getHistory(ctx, { limit, ...flag5(a, "author") ? { author: flag5(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag5(a, "version") ? { version: flag5(a, "version") } : {}, ...has7(a, "breaking") ? { breaking: true } : {} });
8121
- if (has7(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
9640
+ const r = await hist.getHistory(ctx, { limit, ...flag5(a, "author") ? { author: flag5(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag5(a, "version") ? { version: flag5(a, "version") } : {}, ...has8(a, "breaking") ? { breaking: true } : {} });
9641
+ if (has8(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
8122
9642
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
8123
9643
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
8124
9644
  for (const v of r.versions) print(` ${v.version.padEnd(11)} ${day(v.date)} ${v.entries} change(s), ${v.tokenChanges} token edit(s)${v.breaking ? ", breaking" : ""}`);
@@ -8140,8 +9660,8 @@ async function revertCmd(a, root) {
8140
9660
  const token = changes !== "direct" ? githubToken() : void 0;
8141
9661
  const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
8142
9662
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
8143
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has7(a, "restore-conflicts"), dryRun: has7(a, "dry-run"), ...changes ? { changes } : {}, source: "cli" });
8144
- if (has7(a, "json")) {
9663
+ const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has8(a, "restore-conflicts"), dryRun: has8(a, "dry-run"), ...changes ? { changes } : {}, source: "cli" });
9664
+ if (has8(a, "json")) {
8145
9665
  print(JSON.stringify(r, null, 2));
8146
9666
  return r.status === "blocked" ? 1 : 0;
8147
9667
  }
@@ -8168,8 +9688,8 @@ async function audit(a, root) {
8168
9688
  const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
8169
9689
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
8170
9690
  const ds = loadDesignSystem(dsRoot);
8171
- if (has7(a, "fix")) {
8172
- const write = has7(a, "write");
9691
+ if (has8(a, "fix")) {
9692
+ const write = has8(a, "write");
8173
9693
  const changes = fixCss(target, ds.index, write);
8174
9694
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
8175
9695
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
@@ -8245,7 +9765,7 @@ async function audit(a, root) {
8245
9765
  code = 1;
8246
9766
  }
8247
9767
  }
8248
- if (has7(a, "no-regress") && report.baseline) {
9768
+ if (has8(a, "no-regress") && report.baseline) {
8249
9769
  const b = report.baseline;
8250
9770
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
8251
9771
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -8257,8 +9777,31 @@ async function audit(a, root) {
8257
9777
  async function snapshotCmd(a, root) {
8258
9778
  const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
8259
9779
  const branch = flag5(a, "branch");
8260
- const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {} });
8261
- if (has7(a, "json")) print(JSON.stringify({ out, written: r.written, bytes: r.bytes, sourceSha: r.snapshot.sourceSha, summary: r.snapshot.summary, audits: r.snapshot.audits.map((x) => x.path), problems: r.snapshot.problems }, null, 2));
9780
+ let activity;
9781
+ if (has8(a, "activity")) {
9782
+ const token = githubToken();
9783
+ if (!token) throw new UsageError("bridge snapshot --activity reads GitHub: set GITHUB_TOKEN (in a workflow, the run token is enough).");
9784
+ const repo = flag5(a, "repo") || process.env.GITHUB_REPOSITORY || void 0;
9785
+ activity = { token, ...repo ? { repo } : {} };
9786
+ }
9787
+ const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {}, ...activity ? { activity } : {} });
9788
+ if (has8(a, "json"))
9789
+ print(
9790
+ JSON.stringify(
9791
+ {
9792
+ out,
9793
+ written: r.written,
9794
+ bytes: r.bytes,
9795
+ sourceSha: r.snapshot.sourceSha,
9796
+ summary: r.snapshot.summary,
9797
+ audits: r.snapshot.audits.map((x) => x.path),
9798
+ problems: r.snapshot.problems,
9799
+ ...r.activity ? { activity: { bytes: r.activity.bytes, reads: Object.keys(r.activity.activity.responses).length, branches: r.activity.activity.branches.length, suggestions: r.activity.activity.suggestions.length, requests: r.activity.activity.requests.length, merged: r.activity.activity.merged.length, history: r.activity.activity.history?.entries.length ?? null, truncated: r.activity.activity.truncated, problems: r.activity.activity.problems } } : {}
9800
+ },
9801
+ null,
9802
+ 2
9803
+ )
9804
+ );
8262
9805
  else {
8263
9806
  const shown = relative5(process.cwd(), out);
8264
9807
  for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
@@ -8270,7 +9813,7 @@ async function docsCmd(a, root) {
8270
9813
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
8271
9814
  const ctx = await BridgeContext8.create({ root });
8272
9815
  const d = await ops.draftDocsOp(ctx, { name });
8273
- if (has7(a, "json")) {
9816
+ if (has8(a, "json")) {
8274
9817
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
8275
9818
  return 0;
8276
9819
  }
@@ -8312,10 +9855,10 @@ async function main() {
8312
9855
  if (cmd === "review") return runReview(a, root);
8313
9856
  if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
8314
9857
  if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
8315
- if (cmd === "version" || !cmd && has7(a, "version")) return print(VERSION), 0;
8316
- if (!cmd || has7(a, "help") || cmd === "help") {
9858
+ if (cmd === "version" || !cmd && has8(a, "version")) return print(VERSION), 0;
9859
+ if (!cmd || has8(a, "help") || cmd === "help") {
8317
9860
  print(HELP);
8318
- return cmd || has7(a, "help") ? 0 : 2;
9861
+ return cmd || has8(a, "help") ? 0 : 2;
8319
9862
  }
8320
9863
  switch (cmd) {
8321
9864
  case "init":