@bridge-ds/cli 0.11.2 → 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 +1639 -134
  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";
@@ -4089,10 +4518,33 @@ var REQUEST_KINDS = [
4089
4518
  { value: "other", label: "Other", hint: "Anything else." }
4090
4519
  ];
4091
4520
  var REQUEST_LABEL = "component-change";
4521
+ var MARK = "bridge-request";
4092
4522
  function requestIssueTitle(r) {
4093
4523
  return subjectTitle(r.component, r.title);
4094
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";
4541
+ }
4095
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
+ }
4096
4548
  function validateIntegrations(i) {
4097
4549
  const out = [];
4098
4550
  if (!i?.jira) return out;
@@ -4121,7 +4573,175 @@ function jiraIssueFields(r, jira, issueUrl) {
4121
4573
  };
4122
4574
  }
4123
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
+
4124
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"];
4125
4745
  var SUGGESTION_STATE_LABELS = {
4126
4746
  "needs-review": "suggestion: needs review",
4127
4747
  "changes-requested": "suggestion: changes requested",
@@ -4141,7 +4761,328 @@ var SUGGESTION_STATUS_LABEL = {
4141
4761
  declined: "Declined",
4142
4762
  outdated: "Outdated"
4143
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
+ }
4144
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
+ }
4145
5086
 
4146
5087
  // ../core/src/suggestion-apply.ts
4147
5088
  var SUGGESTION_APPLY_EXIT = {
@@ -4348,7 +5289,7 @@ function importsIn(text) {
4348
5289
  for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
4349
5290
  return [...out];
4350
5291
  }
4351
- var norm2 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
5292
+ var norm4 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
4352
5293
  var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
4353
5294
  var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
4354
5295
  var isCss = (p) => /\.(css|scss|less)$/.test(p);
@@ -4366,14 +5307,14 @@ function componentOf(from, spec, componentsDir) {
4366
5307
  return abs.slice(componentsDir.length + 1).split("/")[0] || null;
4367
5308
  }
4368
5309
  function componentForPath(path, componentsDir) {
4369
- const dir = norm2(componentsDir);
4370
- const p = norm2(path);
5310
+ const dir = norm4(componentsDir);
5311
+ const p = norm4(path);
4371
5312
  if (!p.startsWith(`${dir}/`)) return null;
4372
5313
  const rest = p.slice(dir.length + 1).split("/");
4373
5314
  return rest.length >= 2 ? rest[0] : null;
4374
5315
  }
4375
5316
  function buildUsageIndex(files, componentsDir) {
4376
- const dir = norm2(componentsDir);
5317
+ const dir = norm4(componentsDir);
4377
5318
  const modules = {};
4378
5319
  const vars = /* @__PURE__ */ new Map();
4379
5320
  const imports = /* @__PURE__ */ new Map();
@@ -4381,7 +5322,7 @@ function buildUsageIndex(files, componentsDir) {
4381
5322
  const globalVars = {};
4382
5323
  const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
4383
5324
  for (const f of files) {
4384
- const path = norm2(f.path);
5325
+ const path = norm4(f.path);
4385
5326
  const id = componentForPath(path, dir);
4386
5327
  if (!id) {
4387
5328
  if (isCss(path)) {
@@ -4425,7 +5366,7 @@ function impactedModules(index, input) {
4425
5366
  for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
4426
5367
  for (const f of input.files ?? []) {
4427
5368
  const id = componentForPath(f, index.componentsDir);
4428
- 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);
4429
5370
  }
4430
5371
  const renderedBy = /* @__PURE__ */ new Map();
4431
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);
@@ -4443,6 +5384,131 @@ function impactedModules(index, input) {
4443
5384
  return { modules, globalHits };
4444
5385
  }
4445
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
+
4446
5512
  // ../core/src/fidelity-references.ts
4447
5513
  var FIGMA_REFERENCES_DIR = "figma-references";
4448
5514
  var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
@@ -4715,10 +5781,10 @@ function colorCss(c) {
4715
5781
  if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
4716
5782
  const v = c;
4717
5783
  const [r = 0, g = 0, b = 0] = v.components;
4718
- const hex = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
5784
+ const hex2 = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
4719
5785
  const alpha = v.alpha ?? 1;
4720
- if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
4721
- 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 };
4722
5788
  }
4723
5789
  var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
4724
5790
  function shadowCss(s) {
@@ -4772,13 +5838,13 @@ function buildTokenRows(config, readText, listFiles) {
4772
5838
  ...componentFiles
4773
5839
  ];
4774
5840
  const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
4775
- const { css, hex } = tokenValueCss(t.type, t.value);
5841
+ const { css, hex: hex2 } = tokenValueCss(t.type, t.value);
4776
5842
  return {
4777
5843
  path: t.path.join("."),
4778
5844
  segments: t.path,
4779
5845
  type: t.type,
4780
5846
  css,
4781
- hex,
5847
+ hex: hex2,
4782
5848
  cssVar: tokenRowCssVar(t.path),
4783
5849
  figma: tokenRowFigmaName(t.path),
4784
5850
  description: t.description,
@@ -4822,8 +5888,8 @@ function buildTokenRows(config, readText, listFiles) {
4822
5888
  current = hit.value;
4823
5889
  currentType = hit.type || currentType;
4824
5890
  }
4825
- const { css, hex } = tokenValueCss(currentType, current);
4826
- 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 };
4827
5893
  };
4828
5894
  const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
4829
5895
  const build = (t, tier, valueFor) => {
@@ -4883,8 +5949,8 @@ var channel = (c) => {
4883
5949
  const s = c / 255;
4884
5950
  return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
4885
5951
  };
4886
- function hexLuminance(hex) {
4887
- const n = parseInt(hex.slice(1), 16);
5952
+ function hexLuminance(hex2) {
5953
+ const n = parseInt(hex2.slice(1), 16);
4888
5954
  return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
4889
5955
  }
4890
5956
  function hexContrastRatio(a, b) {
@@ -5075,7 +6141,7 @@ function scanComponents(input) {
5075
6141
  const docsSuffix = input.docsSuffix ?? ".docs.json";
5076
6142
  const styleSuffix = input.styleSuffix ?? ".module.css";
5077
6143
  const fileSet = new Set(input.files);
5078
- const has8 = (p) => fileSet.has(p);
6144
+ const has9 = (p) => fileSet.has(p);
5079
6145
  const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
5080
6146
  const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
5081
6147
  const names = [
@@ -5088,7 +6154,7 @@ function scanComponents(input) {
5088
6154
  const folder = `${dir}/${name}`;
5089
6155
  const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
5090
6156
  let docs = null;
5091
- if (has8(files.docs)) {
6157
+ if (has9(files.docs)) {
5092
6158
  try {
5093
6159
  docs = JSON.parse(readText(files.docs) ?? "");
5094
6160
  } catch (err) {
@@ -5098,10 +6164,10 @@ function scanComponents(input) {
5098
6164
  const tsxSrc = readText(files.tsx) ?? "";
5099
6165
  const tsx = parseComponentTsx(name, tsxSrc);
5100
6166
  const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
5101
- const cssText = has8(files.css) ? readText(files.css) : void 0;
6167
+ const cssText = has9(files.css) ? readText(files.css) : void 0;
5102
6168
  const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
5103
6169
  const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
5104
- const storiesText = has8(files.stories) ? readText(files.stories) : void 0;
6170
+ const storiesText = has9(files.stories) ? readText(files.stories) : void 0;
5105
6171
  return {
5106
6172
  name,
5107
6173
  slug: componentSlug(name),
@@ -5496,6 +6562,389 @@ function snapshotLlmsTxt(model, snapshot) {
5496
6562
  `;
5497
6563
  }
5498
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
+
5499
6948
  // ../visual/src/capture.ts
5500
6949
  import { mkdir, writeFile } from "node:fs/promises";
5501
6950
  import { dirname as dirname2 } from "node:path";
@@ -5896,17 +7345,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
5896
7345
 
5897
7346
  // ../visual/src/select.ts
5898
7347
  var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
5899
- var norm3 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
7348
+ var norm5 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
5900
7349
  var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
5901
7350
  function storyComponent(s, componentsDir) {
5902
- return componentForPath(norm3(s.importPath), componentsDir);
7351
+ return componentForPath(norm5(s.importPath), componentsDir);
5903
7352
  }
5904
7353
  function selectImpact(input) {
5905
7354
  const { usage, stories } = input;
5906
7355
  const dir = usage.componentsDir;
5907
7356
  const lib = input.librarySrc.replace(/\/$/, "");
5908
7357
  const cap = input.maxStoriesPerComponent ?? 4;
5909
- 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));
5910
7359
  const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
5911
7360
  const impact2 = impactedModules(usage, { vars, files: visualFiles });
5912
7361
  let fallback = null;
@@ -5946,7 +7395,7 @@ function selectImpact(input) {
5946
7395
  return {
5947
7396
  tokens: input.tokens,
5948
7397
  vars,
5949
- files: input.files.map(norm3),
7398
+ files: input.files.map(norm5),
5950
7399
  components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
5951
7400
  stories: [...new Set(picked)],
5952
7401
  fallback,
@@ -5965,7 +7414,7 @@ function storiesFromSource(files) {
5965
7414
  if (!title) continue;
5966
7415
  for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
5967
7416
  const name = m[1];
5968
- 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)}` });
5969
7418
  }
5970
7419
  }
5971
7420
  return out;
@@ -6593,7 +8042,7 @@ async function jiraSync(input) {
6593
8042
  log(`Bridge Jira sync: ${reason}`);
6594
8043
  return { status: "skipped", reason };
6595
8044
  };
6596
- const fail = (reason) => {
8045
+ const fail2 = (reason) => {
6597
8046
  log(`::error::Bridge Jira sync: ${reason}`);
6598
8047
  return { status: "error", reason };
6599
8048
  };
@@ -6608,7 +8057,7 @@ async function jiraSync(input) {
6608
8057
  if (!n || !repo) return skip("not an issues event.");
6609
8058
  if (!["opened", "labeled", "closed"].includes(action)) return skip(`nothing to do on "${action}".`);
6610
8059
  if (action === "labeled" && event.label?.name !== REQUEST_LABEL) return skip(`label "${event.label?.name ?? ""}" is not ${REQUEST_LABEL}.`);
6611
- 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.");
6612
8061
  const ghFetch = (url2, init2) => f(url2, init2);
6613
8062
  const gh = new GitHubClient(repo, env.GITHUB_TOKEN, ghFetch);
6614
8063
  const issue = await gh.getIssue(n);
@@ -6637,14 +8086,14 @@ async function jiraSync(input) {
6637
8086
  const text = `Closed in GitHub (#${n}), ${outcome}`;
6638
8087
  const body = { body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text, marks: [{ type: "link", attrs: { href: issue.url } }] }] }] } };
6639
8088
  const res2 = await f(`${base}/rest/api/3/issue/${encodeURIComponent(key)}/comment`, { method: "POST", headers, body: JSON.stringify(body) });
6640
- if (!res2.ok) return fail(await jiraError(res2));
8089
+ if (!res2.ok) return fail2(await jiraError(res2));
6641
8090
  log(`Bridge Jira sync: commented on ${key}: ${text}.`);
6642
8091
  return { status: "commented", key };
6643
8092
  }
6644
8093
  if (key) return skip(`#${n} is already linked to ${key}.`);
6645
8094
  const request = requests.requestFromIssue(issue);
6646
8095
  const res = await f(`${base}/rest/api/3/issue`, { method: "POST", headers, body: JSON.stringify({ fields: jiraIssueFields(request, jira, issue.url) }) });
6647
- if (!res.ok) return fail(await jiraError(res));
8096
+ if (!res.ok) return fail2(await jiraError(res));
6648
8097
  const created = await res.json();
6649
8098
  const url = `${base}/browse/${created.key}`;
6650
8099
  await gh.addComment(n, `Jira: ${created.key} ${url}`);
@@ -6674,7 +8123,7 @@ Exit codes: 0 ok, 1 not a request or Claude Code not found, 2 usage or setup err
6674
8123
  var RequestUsageError = class extends Error {
6675
8124
  };
6676
8125
  var flag = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6677
- var has = (a, k) => Boolean(a.flags[k]);
8126
+ var has2 = (a, k) => Boolean(a.flags[k]);
6678
8127
  var defaultIO = () => ({
6679
8128
  print: (s) => process.stdout.write(`${s}
6680
8129
  `),
@@ -6734,9 +8183,9 @@ async function getOrExplain(ctx, n, io, branch) {
6734
8183
  }
6735
8184
  async function runRequest(a, root, io = defaultIO()) {
6736
8185
  const sub = a._[1];
6737
- if (!sub || sub === "help" || has(a, "help")) {
8186
+ if (!sub || sub === "help" || has2(a, "help")) {
6738
8187
  io.print(REQUEST_HELP);
6739
- return sub || has(a, "help") ? 0 : 2;
8188
+ return sub || has2(a, "help") ? 0 : 2;
6740
8189
  }
6741
8190
  switch (sub) {
6742
8191
  case "list": {
@@ -6745,7 +8194,7 @@ async function runRequest(a, root, io = defaultIO()) {
6745
8194
  const ctx = await context(root, io, false);
6746
8195
  const component = flag(a, "component");
6747
8196
  const r = await requests2.listRequests(ctx, { status, ...component ? { component } : {} });
6748
- 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;
6749
8198
  if (!r.requests.length) return io.print(`No ${status === "all" ? "" : `${status} `}component change request in ${r.repo}${component ? ` for ${component}` : ""}.`), 0;
6750
8199
  const w = Math.max(...r.requests.map((x) => x.component.length));
6751
8200
  for (const x of r.requests) {
@@ -6759,7 +8208,7 @@ async function runRequest(a, root, io = defaultIO()) {
6759
8208
  const ctx = await context(root, io, false);
6760
8209
  const r = await getOrExplain(ctx, n, io);
6761
8210
  if (!r) return 1;
6762
- 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;
6763
8212
  const q = r.request;
6764
8213
  io.print(`#${r.number} ${q.component}: ${q.title}`);
6765
8214
  io.print(`${REQUEST_STATUS_LABEL[r.status]}, ${q.kind}, from ${q.source}${q.requestedBy ? ` by @${q.requestedBy}` : ""}`);
@@ -6789,7 +8238,7 @@ Next: bridge request implement ${r.number} --claude`);
6789
8238
  const branch = flag(a, "branch");
6790
8239
  const r = await getOrExplain(ctx, n, io, branch);
6791
8240
  if (!r) return 1;
6792
- if (!has(a, "claude")) return io.print(r.brief), 0;
8241
+ if (!has2(a, "claude")) return io.print(r.brief), 0;
6793
8242
  if (!io.hasClaude()) {
6794
8243
  io.print(r.brief);
6795
8244
  io.error(`
@@ -6807,7 +8256,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
6807
8256
  const usage = 'bridge request create --component Button --kind new-variant --title "Add a danger ghost variant" --summary "..."';
6808
8257
  if (!component || !kind || !title || !summary) throw new RequestUsageError(`Usage: ${usage}`);
6809
8258
  if (!REQUEST_KINDS.some((k) => k.value === kind)) throw new RequestUsageError(`--kind takes ${REQUEST_KINDS.map((k) => k.value).join(", ")}, got "${kind}"`);
6810
- const dryRun = has(a, "dry-run");
8259
+ const dryRun = has2(a, "dry-run");
6811
8260
  const ctx = await context(root, io, !dryRun);
6812
8261
  const figma2 = flag(a, "figma");
6813
8262
  const exploration = flag(a, "exploration");
@@ -6823,7 +8272,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
6823
8272
  source: "cli",
6824
8273
  dryRun
6825
8274
  });
6826
- 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;
6827
8276
  if (r.status === "dry-run") {
6828
8277
  io.print(`Dry run, nothing created.
6829
8278
 
@@ -6882,7 +8331,7 @@ Work on one: git switch explore/<name>, then \`bridge propose --branch explore/<
6882
8331
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`.
6883
8332
  Exit codes: 0 ok, 2 usage or setup error.`;
6884
8333
  var flag2 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
6885
- var has2 = (a, k) => Boolean(a.flags[k]);
8334
+ var has3 = (a, k) => Boolean(a.flags[k]);
6886
8335
  async function context2(root, io) {
6887
8336
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
6888
8337
  return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -6891,11 +8340,11 @@ var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "cla
6891
8340
  var date = (iso) => iso ? iso.slice(0, 10) : "";
6892
8341
  async function runExplore(a, root, io = defaultIO()) {
6893
8342
  const sub = a._[1];
6894
- if (!sub || sub === "help" || has2(a, "help")) {
8343
+ if (!sub || sub === "help" || has3(a, "help")) {
6895
8344
  io.print(EXPLORE_HELP);
6896
- return sub || has2(a, "help") ? 0 : 2;
8345
+ return sub || has3(a, "help") ? 0 : 2;
6897
8346
  }
6898
- const json = has2(a, "json");
8347
+ const json = has3(a, "json");
6899
8348
  switch (sub) {
6900
8349
  case "start": {
6901
8350
  const name = a._.slice(2).join(" ").trim() || flag2(a, "name");
@@ -6903,7 +8352,7 @@ async function runExplore(a, root, io = defaultIO()) {
6903
8352
  if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
6904
8353
  const useCase = flag2(a, "use-case");
6905
8354
  const ctx = await context2(root, io);
6906
- 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") });
6907
8356
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6908
8357
  if (r.status === "dry-run") {
6909
8358
  io.print(`Dry run, nothing written.
@@ -6927,7 +8376,7 @@ Next: ${r.next}`);
6927
8376
  }
6928
8377
  case "list": {
6929
8378
  const ctx = await context2(root, io);
6930
- 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" });
6931
8380
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6932
8381
  io.print(r.summary);
6933
8382
  for (const x of r.explorations) {
@@ -6948,7 +8397,7 @@ Local only: ${b} (push it and open a draft pull request labeled exploration)`);
6948
8397
  const ref = a._.slice(2).join(" ").trim();
6949
8398
  if (!ref) throw new RequestUsageError("Usage: bridge explore promote <name|#pr> [--dry-run]");
6950
8399
  const ctx = await context2(root, io);
6951
- 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") });
6952
8401
  if (json) return io.print(JSON.stringify(r, null, 2)), 0;
6953
8402
  io.print(`${r.status === "dry-run" ? "Dry run: would promote" : "Promoted"} #${r.number} "${r.title}" to review: Tier ${r.tier}.`);
6954
8403
  io.print(`Labels: +${r.addLabels.join(", +")}, -${r.removeLabels.join(", -")}${r.removeFiles.length ? `. Removes ${r.removeFiles.join(", ")}` : ""}.`);
@@ -7002,10 +8451,10 @@ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Readi
7002
8451
  Exit codes (apply): 0 applied, already applied or a clean dry run; 1 not approved, blocked by a check or refused;
7003
8452
  2 usage or setup error; 3 conflict with today's code.`;
7004
8453
  var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
7005
- var has3 = (a, k) => Boolean(a.flags[k]);
8454
+ var has4 = (a, k) => Boolean(a.flags[k]);
7006
8455
  function changesFlag(a) {
7007
- if (has3(a, "suggest") && has3(a, "direct")) throw new SuggestUsageError("Pass --suggest or --direct, not both.");
7008
- 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;
7009
8458
  }
7010
8459
  function tokenFrom2(io) {
7011
8460
  return io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
@@ -7023,7 +8472,7 @@ var STATUSES = ["needs-review", "changes-requested", "approved", "applying", "ap
7023
8472
  var KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
7024
8473
  async function runSuggestions(a, root, io = defaultIO()) {
7025
8474
  const sub = a._[1] ?? "list";
7026
- 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;
7027
8476
  switch (sub) {
7028
8477
  case "list": {
7029
8478
  const status = flag3(a, "status") ?? "all";
@@ -7032,7 +8481,7 @@ async function runSuggestions(a, root, io = defaultIO()) {
7032
8481
  if (kind && !KINDS.includes(kind)) throw new SuggestUsageError(`--kind takes ${KINDS.join(", ")}, got "${kind}"`);
7033
8482
  const ctx = await context3(root, io);
7034
8483
  const r = await suggestions.listSuggestionsOp(ctx, { status, ...kind ? { kind } : {} });
7035
- 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;
7036
8485
  if (!r.suggestions.length) return io.print(`No ${status === "all" ? "open " : `${status} `}suggestion in ${r.repo}.`), 0;
7037
8486
  for (const x of r.suggestions) {
7038
8487
  const prs = x.linkedPrs.length ? ` PR ${x.linkedPrs.map((p) => `#${p.number}${p.merged ? " merged" : ` ${p.state}`}`).join(", ")}` : "";
@@ -7046,7 +8495,7 @@ ${r.next}`);
7046
8495
  const n = issueNumber2(a._[2], "bridge suggestions show <number>");
7047
8496
  const ctx = await context3(root, io);
7048
8497
  const r = await suggestions.getSuggestionOp(ctx, { number: n });
7049
- 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;
7050
8499
  io.print(`#${r.number} ${r.title}`);
7051
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}` : ""}`);
7052
8501
  io.print(r.url);
@@ -7084,7 +8533,7 @@ Next: ${r.next}`);
7084
8533
  if (!pr && !branch) throw new SuggestUsageError("Usage: bridge suggestions link <number> --pr <m> (or --branch <name>)");
7085
8534
  const ctx = await context3(root, io);
7086
8535
  const r = await suggestions.linkSuggestionOp(ctx, { number: n, ...pr ? { pr } : {}, ...branch ? { branch } : {} });
7087
- 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;
7088
8537
  io.print(`#${n} is being applied${pr ? ` in #${pr}` : branch ? ` on ${branch}` : ""}.`);
7089
8538
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
7090
8539
  return 0;
@@ -7097,23 +8546,23 @@ ${SUGGESTIONS_HELP}`);
7097
8546
  }
7098
8547
  var fmt = (v) => v === void 0 ? "(none)" : typeof v === "string" ? v : JSON.stringify(v);
7099
8548
  async function runApply(a, root, io = defaultIO()) {
7100
- 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;
7101
8550
  const n = issueNumber2(a._[1], "bridge apply <number> [--write | --patch] [--dry-run]");
7102
- if (has3(a, "write") && has3(a, "patch")) throw new SuggestUsageError("Pass --write or --patch, not both.");
7103
- 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";
7104
8553
  const ctx = await context3(root, io);
7105
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.");
7106
8555
  const branch = flag3(a, "branch");
7107
8556
  const r = await suggestions.applySuggestionOp(ctx, {
7108
8557
  number: n,
7109
- mode: has3(a, "dry-run") ? "dry-run" : mode2,
7110
- acceptEdited: has3(a, "accept-edited"),
7111
- 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"),
7112
8561
  ...branch ? { branch } : {}
7113
8562
  });
7114
8563
  const code = typeof r.exitCode === "number" ? r.exitCode : SUGGESTION_APPLY_EXIT[r.outcome] ?? 1;
7115
- if (has3(a, "json")) return io.print(JSON.stringify(r, null, 2)), code;
7116
- 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")) {
7117
8566
  io.print(r.patch.trimEnd());
7118
8567
  io.error(r.message);
7119
8568
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
@@ -7137,10 +8586,10 @@ Next: ${r.next}`);
7137
8586
  return code;
7138
8587
  }
7139
8588
  async function runReview(a, root, io = defaultIO()) {
7140
- 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;
7141
8590
  const usage = 'bridge review <number> --approve | --request-changes --message "..." | --decline --message "..."';
7142
8591
  const n = issueNumber2(a._[1], usage);
7143
- 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));
7144
8593
  if (events.length !== 1) throw new SuggestUsageError(`Usage: ${usage}`);
7145
8594
  const event = events[0];
7146
8595
  const inline = flag3(a, event);
@@ -7157,7 +8606,7 @@ async function runReview(a, root, io = defaultIO()) {
7157
8606
  ...role ? { role } : {},
7158
8607
  ...version ? { payloadSha: version } : {}
7159
8608
  });
7160
- 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;
7161
8610
  const done = event === "approve" ? "Approved" : event === "request-changes" ? "Changes requested on" : event === "decline" ? "Declined" : "Commented on";
7162
8611
  io.print(`${done} #${n}${r.version ? ` (version ${r.version})` : ""}. Now: ${SUGGESTION_STATUS_LABEL[r.status] ?? r.status}.`);
7163
8612
  for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
@@ -7196,7 +8645,7 @@ Usage:
7196
8645
  Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
7197
8646
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7198
8647
  var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
7199
- var has4 = (a, k) => Boolean(a.flags[k]);
8648
+ var has5 = (a, k) => Boolean(a.flags[k]);
7200
8649
  async function context4(root, io) {
7201
8650
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7202
8651
  return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7218,11 +8667,11 @@ function pairs2(list2, what) {
7218
8667
  var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
7219
8668
  async function runBranch(a, root, io = defaultIO()) {
7220
8669
  const sub = a._[1];
7221
- if (!sub || sub === "help" || has4(a, "help")) {
8670
+ if (!sub || sub === "help" || has5(a, "help")) {
7222
8671
  io.print(BRANCH_HELP);
7223
- return sub || has4(a, "help") ? 0 : 2;
8672
+ return sub || has5(a, "help") ? 0 : 2;
7224
8673
  }
7225
- const json = has4(a, "json");
8674
+ const json = has5(a, "json");
7226
8675
  const ctx = await context4(root, io);
7227
8676
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7228
8677
  switch (sub) {
@@ -7263,14 +8712,14 @@ async function runBranch(a, root, io = defaultIO()) {
7263
8712
  }
7264
8713
  case "review": {
7265
8714
  const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
7266
- 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;
7267
8716
  if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
7268
8717
  const body = decision === "approve" ? flag4(a, "body") : flag4(a, decision === "request_changes" ? "request-changes" : "comment");
7269
8718
  const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
7270
8719
  return out(r, () => io.print(r.said));
7271
8720
  }
7272
8721
  case "ask": {
7273
- 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") });
7274
8723
  return out(r, () => io.print(r.said));
7275
8724
  }
7276
8725
  case "comment": {
@@ -7284,7 +8733,7 @@ async function runBranch(a, root, io = defaultIO()) {
7284
8733
  case "merge": {
7285
8734
  const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
7286
8735
  const reason = flag4(a, "override");
7287
- 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") });
7288
8737
  return out(r, () => io.print(r.said));
7289
8738
  }
7290
8739
  case "close": {
@@ -7299,7 +8748,7 @@ async function runBranch(a, root, io = defaultIO()) {
7299
8748
  const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
7300
8749
  const choices = pairs2(a.flags.choice, "choice");
7301
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}".`);
7302
- 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") });
7303
8752
  return out(r, () => {
7304
8753
  io.print(said(r));
7305
8754
  for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
@@ -7332,7 +8781,7 @@ ${BRANCH_HELP}`);
7332
8781
  }
7333
8782
  async function runStudioRead(cmd, a, root, io = defaultIO()) {
7334
8783
  const ctx = await context4(root, io);
7335
- const json = has4(a, "json");
8784
+ const json = has5(a, "json");
7336
8785
  if (cmd === "products") {
7337
8786
  const r2 = await branches.listProducts(ctx);
7338
8787
  if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
@@ -7383,9 +8832,9 @@ async function runDocsSave(a, root, io = defaultIO()) {
7383
8832
  ...flag4(a, "product") ? { product: flag4(a, "product") } : {},
7384
8833
  ...changes ? { changes } : {},
7385
8834
  source: "cli",
7386
- dryRun: has4(a, "dry-run")
8835
+ dryRun: has5(a, "dry-run")
7387
8836
  });
7388
- if (has4(a, "json")) io.print(JSON.stringify(r, null, 2));
8837
+ if (has5(a, "json")) io.print(JSON.stringify(r, null, 2));
7389
8838
  else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
7390
8839
 
7391
8840
  ${r.path}:
@@ -7410,7 +8859,7 @@ Usage:
7410
8859
  bridge pieces switch <n> once the library shipped: main into the product branch
7411
8860
 
7412
8861
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7413
- var has5 = (a, k) => Boolean(a.flags[k]);
8862
+ var has6 = (a, k) => Boolean(a.flags[k]);
7414
8863
  async function context5(root, io) {
7415
8864
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7416
8865
  return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7427,11 +8876,11 @@ function piece(a, at, usage) {
7427
8876
  }
7428
8877
  async function runPieces(a, root, io = defaultIO()) {
7429
8878
  const sub = a._[1];
7430
- if (!sub || sub === "help" || has5(a, "help")) {
8879
+ if (!sub || sub === "help" || has6(a, "help")) {
7431
8880
  io.print(PIECES_HELP);
7432
- return sub || has5(a, "help") ? 0 : 2;
8881
+ return sub || has6(a, "help") ? 0 : 2;
7433
8882
  }
7434
- const json = has5(a, "json");
8883
+ const json = has6(a, "json");
7435
8884
  const ctx = await context5(root, io);
7436
8885
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7437
8886
  switch (sub) {
@@ -7448,7 +8897,7 @@ async function runPieces(a, root, io = defaultIO()) {
7448
8897
  case "open": {
7449
8898
  const usage = "bridge pieces open <n> <piece>";
7450
8899
  const changes = changesFlag(a);
7451
- 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 } : {} });
7452
8901
  return out(r, () => {
7453
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}.`);
7454
8903
  if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
@@ -7467,7 +8916,7 @@ async function runPieces(a, root, io = defaultIO()) {
7467
8916
  return out(r, () => io.print(r.said));
7468
8917
  }
7469
8918
  default: {
7470
- 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") });
7471
8920
  return out(r, () => {
7472
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.`);
7473
8922
  for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
@@ -7497,7 +8946,7 @@ Usage:
7497
8946
  token change, or a branch with the spec for Claude AI
7498
8947
 
7499
8948
  GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
7500
- var has6 = (a, k) => Boolean(a.flags[k]);
8949
+ var has7 = (a, k) => Boolean(a.flags[k]);
7501
8950
  async function context6(root, io) {
7502
8951
  const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
7503
8952
  return BridgeContext7.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
@@ -7509,17 +8958,17 @@ function number3(a, usage) {
7509
8958
  }
7510
8959
  async function runFigma(a, root, io = defaultIO()) {
7511
8960
  const sub = a._[1];
7512
- if (sub === "help" || has6(a, "help")) {
8961
+ if (sub === "help" || has7(a, "help")) {
7513
8962
  io.print(FIGMA_HELP);
7514
8963
  return 0;
7515
8964
  }
7516
- const json = has6(a, "json");
8965
+ const json = has7(a, "json");
7517
8966
  const ctx = await context6(root, io);
7518
8967
  const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
7519
8968
  switch (sub) {
7520
8969
  case void 0:
7521
8970
  case "list": {
7522
- 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" });
7523
8972
  return out(r, () => {
7524
8973
  for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
7525
8974
  io.print(`Next: ${r.next}`);
@@ -7527,7 +8976,7 @@ async function runFigma(a, root, io = defaultIO()) {
7527
8976
  }
7528
8977
  case "show": {
7529
8978
  const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
7530
- if (has6(a, "draft")) {
8979
+ if (has7(a, "draft")) {
7531
8980
  if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
7532
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.");
7533
8982
  return io.runClaude(r.brief, root);
@@ -7553,7 +9002,7 @@ ${r.brief}`);
7553
9002
  throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
7554
9003
  }
7555
9004
  const changes = changesFlag(a);
7556
- 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 } : {} });
7557
9006
  return out(r, () => {
7558
9007
  io.print(`${r.component}: ${r.route}. ${r.said}`);
7559
9008
  for (const x of r.reasons ?? []) io.print(` ${x}`);
@@ -7639,7 +9088,20 @@ async function writeSnapshot(opts) {
7639
9088
  `);
7640
9089
  written.push(summary.path);
7641
9090
  }
7642
- 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 } : {} };
7643
9105
  }
7644
9106
  function snapshotLines(r, out) {
7645
9107
  const s = r.snapshot;
@@ -7652,7 +9114,24 @@ function snapshotLines(r, out) {
7652
9114
  ` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
7653
9115
  ` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
7654
9116
  ` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
7655
- ...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}`)
7656
9135
  ];
7657
9136
  }
7658
9137
 
@@ -7689,7 +9168,7 @@ async function rolesReport(opts) {
7689
9168
 
7690
9169
  // src/index.ts
7691
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";
7692
- var VERSION = true ? "0.11.2" : "dev";
9171
+ var VERSION = true ? "0.12.0" : "dev";
7693
9172
  var MCP_PACKAGE = "@bridge-ds/mcp";
7694
9173
  var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
7695
9174
 
@@ -7750,11 +9229,14 @@ Usage:
7750
9229
  bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
7751
9230
  Dry run unless --write
7752
9231
  bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
7753
- [--branch main] [--json]
9232
+ [--branch main] [--activity [--repo owner/name]] [--json]
7754
9233
  the repo model of this checkout for hosted Bridge (Studio and Library):
7755
9234
  snapshot.json (token, contrast and component files, Figma coverage,
7756
9235
  contrast results) plus audits/<id>.json. --audit runs bridge audit on a
7757
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.
7758
9240
  The bridge-data workflow publishes the folder to the bridge-data branch
7759
9241
  bridge docs draft <Component> [--json]
7760
9242
  docs.json pre-fill from the code (props, tokens, anatomy, contrast,
@@ -7830,7 +9312,7 @@ function parse(argv) {
7830
9312
  return out;
7831
9313
  }
7832
9314
  var flag5 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
7833
- var has7 = (a, k) => Boolean(a.flags[k]);
9315
+ var has8 = (a, k) => Boolean(a.flags[k]);
7834
9316
  var print = (s) => process.stdout.write(`${s}
7835
9317
  `);
7836
9318
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
@@ -7846,7 +9328,7 @@ function printSent(r) {
7846
9328
  async function init(a, root) {
7847
9329
  const cfgPath = join8(root, CONFIG_FILE2);
7848
9330
  let config;
7849
- if (existsSync8(cfgPath) && !has7(a, "force")) {
9331
+ if (existsSync8(cfgPath) && !has8(a, "force")) {
7850
9332
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
7851
9333
  config = (await (await BridgeContext8.create({ root })).config()).config;
7852
9334
  } else {
@@ -7880,10 +9362,10 @@ async function check(a, root) {
7880
9362
  const v = await ops.validateTokens(ctx, { includeWarnings: true });
7881
9363
  const c = await ops.checkContrast(ctx, {});
7882
9364
  const warnings = "warnings" in v ? v.warnings ?? [] : [];
7883
- 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;
7884
9366
  const protection = await protectionOf(a, root, ctx);
7885
9367
  const roles = await rolesOf(a, root, ctx);
7886
- if (has7(a, "json")) {
9368
+ if (has8(a, "json")) {
7887
9369
  print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
7888
9370
  return failed ? 1 : 0;
7889
9371
  }
@@ -7902,7 +9384,7 @@ async function check(a, root) {
7902
9384
  return failed ? 1 : 0;
7903
9385
  }
7904
9386
  async function protectionOf(a, root, ctx) {
7905
- 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;
7906
9388
  const { config } = await ctx.config();
7907
9389
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
7908
9390
  if (!repo) return void 0;
@@ -7910,7 +9392,7 @@ async function protectionOf(a, root, ctx) {
7910
9392
  return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
7911
9393
  }
7912
9394
  async function rolesOf(a, root, ctx) {
7913
- 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;
7914
9396
  const { config } = await ctx.config();
7915
9397
  const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
7916
9398
  if (!repo) return void 0;
@@ -7922,7 +9404,7 @@ async function tokens(a, root) {
7922
9404
  if (!q) throw new UsageError("bridge tokens <query>");
7923
9405
  const ctx = await BridgeContext8.create({ root });
7924
9406
  const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag5(a, "limit") ?? 20) });
7925
- if (has7(a, "json")) return print(JSON.stringify(r, null, 2));
9407
+ if (has8(a, "json")) return print(JSON.stringify(r, null, 2));
7926
9408
  if (!r.results.length) return print(`No token matches "${q}".`);
7927
9409
  const w = Math.max(...r.results.map((x) => x.path.length));
7928
9410
  for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
@@ -7939,11 +9421,11 @@ async function diff(a, root) {
7939
9421
  const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
7940
9422
  const tier = core.classify(changes, [], config);
7941
9423
  const imp = core.impact(before, after, changes, 30);
7942
- if (has7(a, "json")) {
9424
+ if (has8(a, "json")) {
7943
9425
  print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
7944
9426
  return 0;
7945
9427
  }
7946
- if (has7(a, "md")) {
9428
+ if (has8(a, "md")) {
7947
9429
  print(`## Token changes since \`${base}\`
7948
9430
 
7949
9431
  ${core.tokenTable(changes)}
@@ -8009,9 +9491,9 @@ async function propose(a, root) {
8009
9491
  ...product ? { product } : {},
8010
9492
  ...changes ? { changes } : {},
8011
9493
  source: "cli",
8012
- dryRun: has7(a, "dry-run")
9494
+ dryRun: has8(a, "dry-run")
8013
9495
  });
8014
- if (has7(a, "json")) print(JSON.stringify(r, null, 2));
9496
+ if (has8(a, "json")) print(JSON.stringify(r, null, 2));
8015
9497
  else {
8016
9498
  print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
8017
9499
  if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
@@ -8029,7 +9511,7 @@ async function brandCmd(a, root) {
8029
9511
  const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
8030
9512
  if (sub === "list") {
8031
9513
  const r2 = await brand.listBrands(ctx);
8032
- if (has7(a, "json")) print(JSON.stringify(r2.brands, null, 2));
9514
+ if (has8(a, "json")) print(JSON.stringify(r2.brands, null, 2));
8033
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(", ")}` : ""}`);
8034
9516
  return 0;
8035
9517
  }
@@ -8051,9 +9533,9 @@ async function brandCmd(a, root) {
8051
9533
  ...opt("notes") ? { notes: opt("notes") } : {},
8052
9534
  ...changes ? { changes } : {},
8053
9535
  source: "cli",
8054
- dryRun: has7(a, "dry-run")
9536
+ dryRun: has8(a, "dry-run")
8055
9537
  });
8056
- if (has7(a, "json")) {
9538
+ if (has8(a, "json")) {
8057
9539
  const { fileContents: _f, ...rest } = r;
8058
9540
  print(JSON.stringify(rest, null, 2));
8059
9541
  return r.status === "blocked" ? 1 : 0;
@@ -8087,9 +9569,9 @@ async function figmaReferences(a, root) {
8087
9569
  out: resolve6(root, flag5(a, "out") ?? "figma-references"),
8088
9570
  sha,
8089
9571
  ...only.length ? { only } : {},
8090
- log: has7(a, "json") ? void 0 : (line2) => print(line2)
9572
+ log: has8(a, "json") ? void 0 : (line2) => print(line2)
8091
9573
  });
8092
- 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));
8093
9575
  return 0;
8094
9576
  }
8095
9577
  async function impact(a, root) {
@@ -8114,15 +9596,15 @@ async function impact(a, root) {
8114
9596
  ...maxStories ? { maxStoriesPerComponent: maxStories } : {},
8115
9597
  ...imageBase !== void 0 ? { imageBase } : {},
8116
9598
  ...artifactUrl ? { artifactUrl } : {},
8117
- plan: has7(a, "plan"),
8118
- keepUnchanged: has7(a, "keep-unchanged")
9599
+ plan: has8(a, "plan"),
9600
+ keepUnchanged: has8(a, "keep-unchanged")
8119
9601
  });
8120
- if (has7(a, "json")) {
9602
+ if (has8(a, "json")) {
8121
9603
  print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
8122
9604
  return 0;
8123
9605
  }
8124
9606
  const sel = r.manifest.selection;
8125
- if (has7(a, "plan")) {
9607
+ if (has8(a, "plan")) {
8126
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)})`);
8127
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(", ")})` : ""}`);
8128
9610
  print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
@@ -8143,7 +9625,7 @@ async function historyCmd(a, root) {
8143
9625
  const limit = Number(flag5(a, "limit") ?? 20);
8144
9626
  if (token) {
8145
9627
  const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
8146
- 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;
8147
9629
  if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
8148
9630
  print(`${r2.path}: ${r2.total} change(s) (${r2.source})
8149
9631
  `);
@@ -8155,8 +9637,8 @@ async function historyCmd(a, root) {
8155
9637
  return 0;
8156
9638
  }
8157
9639
  const tier = flag5(a, "tier");
8158
- 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 } : {} });
8159
- 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;
8160
9642
  print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
8161
9643
  if (!r.total) return print(r.note ?? "No token change matches."), 0;
8162
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" : ""}`);
@@ -8178,8 +9660,8 @@ async function revertCmd(a, root) {
8178
9660
  const token = changes !== "direct" ? githubToken() : void 0;
8179
9661
  const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
8180
9662
  const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
8181
- const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has7(a, "restore-conflicts"), dryRun: has7(a, "dry-run"), ...changes ? { changes } : {}, source: "cli" });
8182
- 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")) {
8183
9665
  print(JSON.stringify(r, null, 2));
8184
9666
  return r.status === "blocked" ? 1 : 0;
8185
9667
  }
@@ -8206,8 +9688,8 @@ async function audit(a, root) {
8206
9688
  const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
8207
9689
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
8208
9690
  const ds = loadDesignSystem(dsRoot);
8209
- if (has7(a, "fix")) {
8210
- const write = has7(a, "write");
9691
+ if (has8(a, "fix")) {
9692
+ const write = has8(a, "write");
8211
9693
  const changes = fixCss(target, ds.index, write);
8212
9694
  const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
8213
9695
  for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
@@ -8283,7 +9765,7 @@ async function audit(a, root) {
8283
9765
  code = 1;
8284
9766
  }
8285
9767
  }
8286
- if (has7(a, "no-regress") && report.baseline) {
9768
+ if (has8(a, "no-regress") && report.baseline) {
8287
9769
  const b = report.baseline;
8288
9770
  if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
8289
9771
  process.stderr.write("bridge audit: adoption regressed since the baseline\n");
@@ -8295,8 +9777,31 @@ async function audit(a, root) {
8295
9777
  async function snapshotCmd(a, root) {
8296
9778
  const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
8297
9779
  const branch = flag5(a, "branch");
8298
- const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {} });
8299
- 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
+ );
8300
9805
  else {
8301
9806
  const shown = relative5(process.cwd(), out);
8302
9807
  for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
@@ -8308,7 +9813,7 @@ async function docsCmd(a, root) {
8308
9813
  if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
8309
9814
  const ctx = await BridgeContext8.create({ root });
8310
9815
  const d = await ops.draftDocsOp(ctx, { name });
8311
- if (has7(a, "json")) {
9816
+ if (has8(a, "json")) {
8312
9817
  print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
8313
9818
  return 0;
8314
9819
  }
@@ -8350,10 +9855,10 @@ async function main() {
8350
9855
  if (cmd === "review") return runReview(a, root);
8351
9856
  if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
8352
9857
  if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
8353
- if (cmd === "version" || !cmd && has7(a, "version")) return print(VERSION), 0;
8354
- 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") {
8355
9860
  print(HELP);
8356
- return cmd || has7(a, "help") ? 0 : 2;
9861
+ return cmd || has8(a, "help") ? 0 : 2;
8357
9862
  }
8358
9863
  switch (cmd) {
8359
9864
  case "init":