@bridge-ds/core 0.12.0 → 0.15.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.
@@ -1,4 +1,4 @@
1
- import { cg as tokenBump, aQ as changeKind, L as LayeredTokenSet, aH as aliasOf, bZ as rgbaToDtcg, bn as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bq as inheritedTypeAt, u as upsertToken, H as readTokenAt, aW as comboKey, bY as renderedColor, b2 as contrastRatio, z as rgbaToHex, U as classifyRisk, W as RISK_LABELS, V as CONFIG_FILE, N as loadConfig, ak as ConfigError, _ as DEFAULT_TOKEN_PATHS, bl as globToRegExp, ae as slugify, Q as validateLayered, bI as parseJson, c1 as serializeJson, bN as plainEqual, cf as toPlain, bk as getEntry, bi as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aP as canShip, cn as whoCanShipSentence, ci as unreadableShipTeams, aL as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-BdMPKffE.js";
1
+ import { ck as tokenBump, aR as changeKind, L as LayeredTokenSet, aI as aliasOf, b_ as rgbaToDtcg, bn as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bq as inheritedTypeAt, u as upsertToken, H as readTokenAt, aX as comboKey, bZ as renderedColor, b3 as contrastRatio, z as rgbaToHex, U as classifyRisk, W as RISK_LABELS, V as CONFIG_FILE, N as loadConfig, ag as ConfigError, _ as DEFAULT_TOKEN_PATHS, bl as globToRegExp, ae as slugify, Q as validateLayered, bJ as parseJson, c2 as serializeJson, bO as plainEqual, cj as toPlain, bk as getEntry, bi as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aQ as canShip, cr as whoCanShipSentence, cm as unreadableShipTeams, c5 as soloApprover, aM as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-BjXFIz74.js";
2
2
  function changesetBump(changes) {
3
3
  return tokenBump(changes) ?? "patch";
4
4
  }
@@ -425,6 +425,82 @@ function classifyAuth(status, detail, opts = {}) {
425
425
  }
426
426
  return {};
427
427
  }
428
+ function memoryEtagStore(max = 500) {
429
+ const map = /* @__PURE__ */ new Map();
430
+ return {
431
+ get(key) {
432
+ const e = map.get(key);
433
+ if (e) {
434
+ map.delete(key);
435
+ map.set(key, e);
436
+ }
437
+ return e;
438
+ },
439
+ set(key, entry) {
440
+ map.delete(key);
441
+ map.set(key, entry);
442
+ while (map.size > max) map.delete(map.keys().next().value);
443
+ },
444
+ clear: () => map.clear()
445
+ };
446
+ }
447
+ function webStorageEtagStore(storage, opts = {}) {
448
+ const prefix = opts.prefix ?? "bridge.gh.etag:";
449
+ const maxBytes = opts.maxEntryBytes ?? 512e3;
450
+ return {
451
+ get(key) {
452
+ try {
453
+ const raw = storage.getItem(prefix + key);
454
+ if (!raw) return void 0;
455
+ const e = JSON.parse(raw);
456
+ return typeof e?.etag === "string" ? e : void 0;
457
+ } catch {
458
+ return void 0;
459
+ }
460
+ },
461
+ set(key, entry) {
462
+ try {
463
+ const raw = JSON.stringify(entry);
464
+ if (raw.length > maxBytes) return;
465
+ storage.setItem(prefix + key, raw);
466
+ } catch {
467
+ }
468
+ },
469
+ clear() {
470
+ try {
471
+ const keys = [];
472
+ for (let i = 0; i < storage.length; i++) {
473
+ const k = storage.key(i);
474
+ if (k?.startsWith(prefix)) keys.push(k);
475
+ }
476
+ for (const k of keys) storage.removeItem(k);
477
+ } catch {
478
+ }
479
+ }
480
+ };
481
+ }
482
+ const sharedEtags = memoryEtagStore();
483
+ function clearSharedEtags() {
484
+ sharedEtags.clear();
485
+ }
486
+ function tokenFingerprint(token) {
487
+ if (!token) return "anon";
488
+ let a = 2166136261;
489
+ let b = 16777619 ^ token.length;
490
+ for (let i = 0; i < token.length; i++) {
491
+ const c = token.charCodeAt(i);
492
+ a = Math.imul(a ^ c, 16777619) >>> 0;
493
+ b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
494
+ }
495
+ return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
496
+ }
497
+ function installationFor(list, owner) {
498
+ const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
499
+ if (!inst) return { reach: "no" };
500
+ const selection = inst.repositorySelection ?? inst.repository_selection;
501
+ return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
502
+ }
503
+ const MAX_PAGES = 10;
428
504
  function rateLimitMessage(x) {
429
505
  const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
430
506
  if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
@@ -432,8 +508,11 @@ function rateLimitMessage(x) {
432
508
  return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
433
509
  }
434
510
  const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
511
+ const hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
435
512
  const isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
436
513
  const API = "https://api.github.com";
514
+ const ACCEPT_JSON = "application/vnd.github+json";
515
+ const ACCEPT_RAW = "application/vnd.github.raw";
437
516
  const RAW = "https://raw.githubusercontent.com";
438
517
  function parseRepo(repo) {
439
518
  const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
@@ -452,6 +531,7 @@ function base64ToUtf8(b64) {
452
531
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
453
532
  return new TextDecoder("utf-8").decode(bytes);
454
533
  }
534
+ const copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
455
535
  const encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
456
536
  class GitHubClient {
457
537
  constructor(repo, token, fetchImpl = (url, init) => fetch(url, init), retry = DEFAULT_RETRY, options = {}) {
@@ -464,6 +544,8 @@ class GitHubClient {
464
544
  this.name = r.name;
465
545
  this.api = (options.apiUrl ?? API).replace(/\/$/, "");
466
546
  this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
547
+ this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
548
+ this.etagScope = options.etagScope ?? tokenFingerprint(token);
467
549
  }
468
550
  token;
469
551
  fetchImpl;
@@ -474,6 +556,15 @@ class GitHubClient {
474
556
  blobCache = /* @__PURE__ */ new Map();
475
557
  api;
476
558
  protectedBranches;
559
+ etags;
560
+ etagScope;
561
+ rate;
562
+ /** What the conditional request cache did for this client. */
563
+ etagStats = { conditional: 0, notModified: 0, stale: 0 };
564
+ /** GitHub's rate-limit headers from the last API response, or undefined before any response carried them. */
565
+ get rateLimit() {
566
+ return this.rate;
567
+ }
477
568
  /** "owner/name". */
478
569
  get fullName() {
479
570
  return `${this.owner}/${this.name}`;
@@ -497,45 +588,77 @@ class GitHubClient {
497
588
  get repoPath() {
498
589
  return `/repos/${this.owner}/${this.name}`;
499
590
  }
500
- async request(method, path, body, retryable = method === "GET") {
591
+ async request(method, path, body, retryable = method === "GET", opts = {}) {
501
592
  const attempts = retryable ? Math.max(1, this.retry.attempts) : 1;
502
593
  for (let attempt = 1; ; attempt++) {
503
594
  try {
504
- return await this.once(method, path, body);
595
+ return await this.once(method, path, body, opts);
505
596
  } catch (err) {
506
597
  const e = err;
507
- if (attempt >= attempts || !(e instanceof GitHubError) || !isTransient(e.status, e.detail ?? "")) throw err;
598
+ if (attempt >= attempts || !(e instanceof GitHubError) || hourlyLimitErrors.has(e) || !isTransient(e.status, e.detail ?? "")) throw err;
508
599
  await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
509
600
  }
510
601
  }
511
602
  }
512
- async once(method, path, body) {
603
+ noteRateLimit(headers) {
604
+ const remaining = headers?.get("x-ratelimit-remaining");
605
+ if (remaining == null) return;
606
+ const limit = headers?.get("x-ratelimit-limit");
607
+ const reset = headers?.get("x-ratelimit-reset");
608
+ this.rate = {
609
+ limit: limit != null ? Number(limit) : void 0,
610
+ remaining: Number(remaining),
611
+ resetsAt: reset ? new Date(Number(reset) * 1e3) : void 0,
612
+ resource: headers?.get("x-ratelimit-resource") ?? void 0
613
+ };
614
+ }
615
+ async once(method, path, body, opts = {}) {
616
+ const accept = opts.accept ?? ACCEPT_JSON;
513
617
  const headers = {
514
- Accept: "application/vnd.github+json",
618
+ Accept: accept,
515
619
  "X-GitHub-Api-Version": "2022-11-28"
516
620
  };
517
621
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
518
622
  if (body !== void 0) headers["Content-Type"] = "application/json";
623
+ const key = method === "GET" && this.etags && opts.etag !== false ? `${this.etagScope} ${accept} ${this.api}${path}` : void 0;
624
+ const cached = key ? this.etags.get(key) : void 0;
625
+ if (cached && this.token) {
626
+ headers["If-None-Match"] = cached.etag;
627
+ this.etagStats.conditional++;
628
+ }
519
629
  let res;
520
630
  try {
521
631
  res = await this.fetchImpl(`${this.api}${path}`, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
522
632
  } catch (err) {
633
+ if (err instanceof GitHubError) throw err;
523
634
  throw new GitHubError(0, "Could not reach GitHub. Check your connection, then try again.", err.message);
524
635
  }
636
+ this.noteRateLimit(res.headers);
637
+ if (res.status === 304 && cached && key && this.token) {
638
+ this.etagStats.notModified++;
639
+ this.etags.set(key, { ...cached, at: Date.now() });
640
+ return copy(cached.body);
641
+ }
525
642
  if (res.status === 204) return void 0;
526
643
  if (!res.ok) {
527
644
  let detail = "";
528
645
  try {
529
- const data = await res.json();
530
- detail = [data.message, ...(data.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
646
+ const data2 = await res.json();
647
+ detail = [data2.message, ...(data2.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
531
648
  } catch {
532
649
  detail = "";
533
650
  }
534
651
  const remaining = res.headers?.get("x-ratelimit-remaining");
535
652
  const reset = res.headers?.get("x-ratelimit-reset");
536
653
  if ((res.status === 403 || res.status === 429) && remaining === "0") {
654
+ if (cached && this.options.serveStaleOnRateLimit) {
655
+ this.etagStats.stale++;
656
+ return copy(cached.body);
657
+ }
537
658
  const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
538
- throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
659
+ const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
660
+ hourlyLimitErrors.add(limited);
661
+ throw limited;
539
662
  }
540
663
  const info = classifyAuth(res.status, detail, {
541
664
  path,
@@ -544,7 +667,24 @@ class GitHubClient {
544
667
  });
545
668
  throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
546
669
  }
547
- return await res.json();
670
+ const data = opts.text ? await res.text() : await res.json();
671
+ const etag = key ? res.headers?.get("etag") : null;
672
+ if (key && etag) this.etags.set(key, { etag, body: copy(data), at: Date.now() });
673
+ return data;
674
+ }
675
+ /**
676
+ * Every item of a paged listing (`per_page=100`), at most 10 pages. `pick` takes the items out of one page (the
677
+ * installation endpoints wrap them in an object with `total_count`).
678
+ */
679
+ async paged(path, pick) {
680
+ const out = [];
681
+ const sep = path.includes("?") ? "&" : "?";
682
+ for (let page = 1; page <= MAX_PAGES; page++) {
683
+ const { items, total } = pick(await this.request("GET", `${path}${sep}per_page=100&page=${page}`));
684
+ out.push(...items);
685
+ if (items.length < 100 || total !== void 0 && out.length >= total) break;
686
+ }
687
+ return out;
548
688
  }
549
689
  async getRepo() {
550
690
  const r = await this.request(
@@ -927,6 +1067,14 @@ class GitHubClient {
927
1067
  */
928
1068
  async readFile(path, ref) {
929
1069
  if (this.token) return this.getFileText(path, ref);
1070
+ return this.readRawFile(path, ref);
1071
+ }
1072
+ /**
1073
+ * Text of a file from the raw host (raw.githubusercontent.com), sent without any header so the browser makes a
1074
+ * simple CORS request (no preflight) and nothing counts against the API limit. Works for public repositories only:
1075
+ * a private file answers 404 there, like a missing one. Undefined on 404.
1076
+ */
1077
+ async readRawFile(path, ref) {
930
1078
  const url = `${(this.options.rawUrl ?? RAW).replace(/\/$/, "")}/${this.owner}/${this.name}/${encodeRef(ref)}/${encodeRef(path)}`;
931
1079
  let res;
932
1080
  try {
@@ -938,6 +1086,76 @@ class GitHubClient {
938
1086
  if (!res.ok) throw new GitHubError(res.status, friendlyMessage(res.status, ""), `raw ${path}@${ref}`);
939
1087
  return res.text();
940
1088
  }
1089
+ /**
1090
+ * Text of a file at a ref through the contents API with the raw media type: the way to read a private file (the raw
1091
+ * host cannot take the token without a preflight). Counts against the API limit; conditional when cached.
1092
+ * Undefined when the file does not exist.
1093
+ */
1094
+ async getRawText(path, ref) {
1095
+ try {
1096
+ return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}?ref=${encodeURIComponent(ref)}`, void 0, true, { accept: ACCEPT_RAW, text: true });
1097
+ } catch (err) {
1098
+ if (err instanceof GitHubError && err.status === 404) return void 0;
1099
+ throw err;
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Text of a blob by its sha through the git blobs API with the raw media type: how a signed-in reader loads a file
1104
+ * listed by a tree (the raw host refuses any request that carries a token). A blob sha names its content, so the
1105
+ * answer is cached for the client's life and never revalidated. Undefined on 404.
1106
+ */
1107
+ async getBlobRaw(sha) {
1108
+ const cached = this.blobCache.get(sha);
1109
+ if (cached !== void 0) return cached;
1110
+ try {
1111
+ const text = await this.request("GET", `${this.repoPath}/git/blobs/${encodeURIComponent(sha)}`, void 0, true, { accept: ACCEPT_RAW, text: true, etag: false });
1112
+ this.blobCache.set(sha, text);
1113
+ return text;
1114
+ } catch (err) {
1115
+ if (err instanceof GitHubError && err.status === 404) return void 0;
1116
+ throw err;
1117
+ }
1118
+ }
1119
+ // --- GitHub App installations (user-to-server tokens) ---------------------------------------
1120
+ /**
1121
+ * Installations of the signing-in GitHub App that the signed-in user can reach (GET /user/installations, every
1122
+ * page). Needs a user-to-server token of that same App: a personal or OAuth token is refused (403), and each App
1123
+ * sees only its own installations. GitHub lists only installations on accounts and repositories the user has
1124
+ * explicit permission on: a public repository where the user is not a collaborator is never listed, installed or
1125
+ * not. (GET /repos/{o}/{r}/installation would say, but it needs the App's private key, which Bridge never holds.)
1126
+ */
1127
+ async listUserInstallations() {
1128
+ this.assertToken("Reading where Bridge is installed");
1129
+ const list = await this.paged("/user/installations", (p) => ({ items: p.installations ?? [], total: p.total_count }));
1130
+ return list.map((i) => ({
1131
+ id: i.id,
1132
+ account: i.account?.login ? { login: i.account.login, type: i.account.type ?? "User" } : null,
1133
+ repositorySelection: i.repository_selection ?? "selected",
1134
+ appSlug: i.app_slug
1135
+ }));
1136
+ }
1137
+ /** Repositories of one installation the signed-in user can reach (GET /user/installations/{id}/repositories, every page). */
1138
+ async listInstallationRepos(installationId) {
1139
+ this.assertToken("Reading where Bridge is installed");
1140
+ const list = await this.paged(`/user/installations/${encodeURIComponent(String(installationId))}/repositories`, (p) => ({
1141
+ items: p.repositories ?? [],
1142
+ total: p.total_count
1143
+ }));
1144
+ return list.map((r) => ({ owner: r.owner?.login ?? r.full_name.split("/")[0], name: r.name, fullName: r.full_name, private: Boolean(r.private) }));
1145
+ }
1146
+ /**
1147
+ * The App installation that gives the signed-in user this repository (this client's by default): its id, or null
1148
+ * when the App is not installed there or the user cannot reach it. One call when the installation covers all of
1149
+ * the owner's repositories, two (paged) when it covers selected ones.
1150
+ */
1151
+ async findInstallation(owner = this.owner, repo = this.name) {
1152
+ const found = installationFor(await this.listUserInstallations(), owner);
1153
+ if (found.reach === "no") return null;
1154
+ if (found.reach === "yes") return { id: found.id, selection: "all" };
1155
+ const repos = await this.listInstallationRepos(found.id);
1156
+ const hit = repos.some((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.name.toLowerCase() === repo.toLowerCase());
1157
+ return hit ? { id: found.id, selection: "selected" } : null;
1158
+ }
941
1159
  /** The repository's default branch. */
942
1160
  async getDefaultBranch() {
943
1161
  return (await this.getRepo()).defaultBranch;
@@ -2804,7 +3022,7 @@ function summarizeCi(runs, statuses = []) {
2804
3022
  const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
2805
3023
  return { state, total, passed, failed, pending };
2806
3024
  }
2807
- function summarizeApprovals(reviews, owners, author) {
3025
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
2808
3026
  const latest = /* @__PURE__ */ new Map();
2809
3027
  const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
2810
3028
  for (const r of sorted) {
@@ -2822,7 +3040,8 @@ function summarizeApprovals(reviews, owners, author) {
2822
3040
  let tech;
2823
3041
  if (d.length || t.length) {
2824
3042
  design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
2825
- const soloBoth = (x) => a.length === 1 && d.includes(x);
3043
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
3044
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
2826
3045
  tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
2827
3046
  } else {
2828
3047
  design = a.length >= 1;
@@ -3088,7 +3307,7 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
3088
3307
  latestOld.delete(c.author);
3089
3308
  reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
3090
3309
  });
3091
- const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0);
3310
+ const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
3092
3311
  const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
3093
3312
  return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
3094
3313
  }
@@ -4425,7 +4644,7 @@ async function planSuggestionApply(input) {
4425
4644
  for (const p of new Set(targets)) if (await ws.isSymlink(p)) links.push(`"${p}" is a symbolic link (or inside one): Bridge never writes through links.`);
4426
4645
  if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
4427
4646
  }
4428
- const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null };
4647
+ const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
4429
4648
  const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
4430
4649
  const review = suggestionReviewState(payload.tier.tier, approvals);
4431
4650
  const missing = pendingApprovals(payload.tier.tier, approvals);
@@ -4551,6 +4770,51 @@ function suggestionApplyTable(plan) {
4551
4770
  const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
4552
4771
  return ["| Change | Result | Detail |", "| --- | --- | --- |", ...plan.ops.map((o) => `| \`${cell2(o.path)}\` | ${label[o.state]} | ${cell2(o.detail)} |`)].join("\n");
4553
4772
  }
4773
+ function normalizeRepoPath(path) {
4774
+ return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
4775
+ }
4776
+ function isUnder(path, prefix) {
4777
+ const p = normalizeRepoPath(prefix ?? "");
4778
+ return !p || path === p || path.startsWith(`${p}/`);
4779
+ }
4780
+ const parentDir = (path) => {
4781
+ const p = normalizeRepoPath(path);
4782
+ const i = p.lastIndexOf("/");
4783
+ return i < 0 ? "" : p.slice(0, i);
4784
+ };
4785
+ const joinRepoPath = (...parts) => normalizeRepoPath(parts.filter(Boolean).join("/"));
4786
+ function memorySource(files, ref = {}) {
4787
+ const map = new Map(Object.entries(files).map(([k, v]) => [normalizeRepoPath(k), v]));
4788
+ const paths = [...map.keys()].sort();
4789
+ return {
4790
+ ref: { owner: ref.owner ?? "", repo: ref.repo ?? "", sha: ref.sha ?? "memory" },
4791
+ list: async (prefix) => paths.filter((p) => isUnder(p, prefix)),
4792
+ text: async (path) => map.get(normalizeRepoPath(path)) ?? null
4793
+ };
4794
+ }
4795
+ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, err) => {
4796
+ throw err;
4797
+ }, onProgress) {
4798
+ const unique = [...new Set(paths)];
4799
+ const out = /* @__PURE__ */ new Map();
4800
+ onProgress?.(0, unique.length);
4801
+ let next = 0;
4802
+ const worker = async () => {
4803
+ while (next < unique.length) {
4804
+ const path = unique[next++];
4805
+ let body;
4806
+ try {
4807
+ body = await source.text(path);
4808
+ } catch (err) {
4809
+ body = onError(path, err);
4810
+ }
4811
+ out.set(path, body);
4812
+ onProgress?.(out.size, unique.length);
4813
+ }
4814
+ };
4815
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, unique.length)) }, worker));
4816
+ return out;
4817
+ }
4554
4818
  export {
4555
4819
  figmaSpecPath as $,
4556
4820
  explorationPrTitle as A,
@@ -4581,7 +4845,7 @@ export {
4581
4845
  branchWithJiraKey as Z,
4582
4846
  titleWithJiraKey as _,
4583
4847
  parseChangelog as a,
4584
- changeTable as a$,
4848
+ assignVersions as a$,
4585
4849
  formatFigmaSpec as a0,
4586
4850
  figmaDraftBrief as a1,
4587
4851
  parseFigmaOrigin as a2,
@@ -4592,33 +4856,33 @@ export {
4592
4856
  withFigmaOrigin as a7,
4593
4857
  WAITING_FOR_CLAUDE_LABEL as a8,
4594
4858
  DRAFTED_BY_CLAUDE_LABEL as a9,
4595
- REVIEW_STATUS_ORDER as aA,
4596
- SPEC_FIELDS as aB,
4597
- SUGGESTION_APPLIED_MARK as aC,
4598
- SUGGESTION_APPLY_EXIT as aD,
4599
- SUGGESTION_KINDS as aE,
4600
- SUGGESTION_LABEL as aF,
4601
- SUGGESTION_LIMITS as aG,
4602
- SUGGESTION_REVIEW_MARK as aH,
4603
- SUGGESTION_STATE_LABELS as aI,
4604
- SUGGESTION_STATUS_LABEL as aJ,
4605
- SUGGESTION_TOO_LARGE as aK,
4606
- SuggestionError as aL,
4607
- TOKEN_REJECTED_MESSAGE as aM,
4608
- TOKEN_SETTINGS_URL as aN,
4609
- TRIGGER_LABELS as aO,
4610
- UNRELEASED as aP,
4611
- acceptDocsDraft as aQ,
4612
- acceptDocsText as aR,
4613
- allowedMergeMethods as aS,
4614
- anatomyLabel as aT,
4615
- assertSafeBranch as aU,
4616
- assignRequestOnGitHub as aV,
4617
- assignVersions as aW,
4618
- base64ToUtf8 as aX,
4619
- bridgeMcpInstallCommand as aY,
4620
- canOverride as aZ,
4621
- canonicalJson as a_,
4859
+ GitHubClient as aA,
4860
+ PULL_STATE_PILL as aB,
4861
+ ProtectedBranchError as aC,
4862
+ REDUCED_MOTION_NOTE as aD,
4863
+ REQUEST_STATUS_LABEL as aE,
4864
+ REVIEW_STATUS_ORDER as aF,
4865
+ SPEC_FIELDS as aG,
4866
+ SUGGESTION_APPLIED_MARK as aH,
4867
+ SUGGESTION_APPLY_EXIT as aI,
4868
+ SUGGESTION_KINDS as aJ,
4869
+ SUGGESTION_LABEL as aK,
4870
+ SUGGESTION_LIMITS as aL,
4871
+ SUGGESTION_REVIEW_MARK as aM,
4872
+ SUGGESTION_STATE_LABELS as aN,
4873
+ SUGGESTION_STATUS_LABEL as aO,
4874
+ SUGGESTION_TOO_LARGE as aP,
4875
+ SuggestionError as aQ,
4876
+ TOKEN_REJECTED_MESSAGE as aR,
4877
+ TOKEN_SETTINGS_URL as aS,
4878
+ TRIGGER_LABELS as aT,
4879
+ UNRELEASED as aU,
4880
+ acceptDocsDraft as aV,
4881
+ acceptDocsText as aW,
4882
+ allowedMergeMethods as aX,
4883
+ anatomyLabel as aY,
4884
+ assertSafeBranch as aZ,
4885
+ assignRequestOnGitHub as a_,
4622
4886
  planComponentPush as aa,
4623
4887
  figmaOriginSection as ab,
4624
4888
  figmaBranchTitle as ac,
@@ -4626,174 +4890,185 @@ export {
4626
4890
  figmaDraftBody as ae,
4627
4891
  REQUEST_LABEL as af,
4628
4892
  figmaNodeUrl as ag,
4629
- BULK_LIMIT as ah,
4630
- COLOR_FIELDS as ai,
4631
- COMPONENT_SPECS_VERSION as aj,
4632
- CONFLICT_REASON as ak,
4633
- DEFAULT_COMPONENT_SPECS_FILE as al,
4634
- DEFAULT_PROTECTED_BRANCHES as am,
4635
- DOCS_CHANGE_BUMP as an,
4636
- DOCS_EDITOR_ORIGIN as ao,
4637
- DOCS_KEYS as ap,
4638
- DOCS_MCP_ORIGIN as aq,
4639
- EXPLORATION_CHECK as ar,
4640
- EXPLORATION_STATUS_LABEL as as,
4641
- EXPLORATION_TIER_LABEL as at,
4642
- EXPLORATION_USE_CASES as au,
4643
- GitHubClient as av,
4644
- PULL_STATE_PILL as aw,
4645
- ProtectedBranchError as ax,
4646
- REDUCED_MOTION_NOTE as ay,
4647
- REQUEST_STATUS_LABEL as az,
4893
+ parentDir as ah,
4894
+ parseComponentSpecs as ai,
4895
+ readRepoTexts as aj,
4896
+ normalizeRepoPath as ak,
4897
+ isUnder as al,
4898
+ BULK_LIMIT as am,
4899
+ COLOR_FIELDS as an,
4900
+ COMPONENT_SPECS_VERSION as ao,
4901
+ CONFLICT_REASON as ap,
4902
+ DEFAULT_COMPONENT_SPECS_FILE as aq,
4903
+ DEFAULT_PROTECTED_BRANCHES as ar,
4904
+ DOCS_CHANGE_BUMP as as,
4905
+ DOCS_EDITOR_ORIGIN as at,
4906
+ DOCS_KEYS as au,
4907
+ DOCS_MCP_ORIGIN as av,
4908
+ EXPLORATION_CHECK as aw,
4909
+ EXPLORATION_STATUS_LABEL as ax,
4910
+ EXPLORATION_TIER_LABEL as ay,
4911
+ EXPLORATION_USE_CASES as az,
4648
4912
  buildReport as b,
4649
- linkedPullsOf as b$,
4650
- changesetBump as b0,
4651
- classifyAuth as b1,
4652
- cleanDocs as b2,
4653
- closingIssueNumbers as b3,
4654
- commitFileOnGitHub as b4,
4655
- componentContrastPairs as b5,
4656
- componentMotion as b6,
4657
- componentsUsingMotionToken as b7,
4658
- composeChangeset as b8,
4659
- composeSuggestionAppliedComment as b9,
4660
- explorationShareMessage as bA,
4661
- explorationSlugFromBranch as bB,
4662
- figmaCell as bC,
4663
- fileModes as bD,
4664
- filterHistory as bE,
4665
- findOpenDocsProposal as bF,
4666
- findOpenDocsProposalOnGitHub as bG,
4667
- firstParagraph as bH,
4668
- formatDocsJson as bI,
4669
- friendlyMessage as bJ,
4670
- getSuggestionOnGitHub as bK,
4671
- hasSuggestionBlock as bL,
4672
- hasWriteAccess as bM,
4673
- hashSpec as bN,
4674
- iconComponentName as bO,
4675
- isActiveExploration as bP,
4676
- isAutomationPull as bQ,
4677
- isDocsProposalBranch as bR,
4678
- isGuardrailPath as bS,
4679
- isJiraKey as bT,
4680
- isMissingPermission as bU,
4681
- isReleasePull as bV,
4682
- isTokenRejected as bW,
4683
- jiraIssueFields as bX,
4684
- jiraKeyOf as bY,
4685
- linkRequestToExplorationOnGitHub as bZ,
4686
- linkSuggestionPrOnGitHub as b_,
4687
- composeSuggestionIssue as ba,
4688
- composeSuggestionReviewComment as bb,
4689
- continueDocsProposalOnGitHub as bc,
4690
- contrastDeltaTable as bd,
4691
- countChanges as be,
4692
- coverageSummary as bf,
4693
- cssModuleClasses as bg,
4694
- declineComment as bh,
4695
- declineReasonProblems as bi,
4696
- declineRequestOnGitHub as bj,
4697
- declineSuggestionOnGitHub as bk,
4698
- defaultDocsFooter as bl,
4699
- docsAuthorLine as bm,
4700
- docsFileText as bn,
4701
- docsProposal as bo,
4702
- docsSectionShapes as bp,
4703
- docsSlug as bq,
4704
- docsSuggestionPayload as br,
4705
- docsText as bs,
4706
- draftChangeLine as bt,
4707
- draftPushDescription as bu,
4708
- draftRequestText as bv,
4709
- effectiveTier as bw,
4710
- eligibleReviewers as bx,
4711
- explorationBranch as by,
4712
- explorationRules as bz,
4913
+ isMissingPermission as b$,
4914
+ base64ToUtf8 as b0,
4915
+ bridgeMcpInstallCommand as b1,
4916
+ canOverride as b2,
4917
+ canonicalJson as b3,
4918
+ changeTable as b4,
4919
+ changesetBump as b5,
4920
+ classifyAuth as b6,
4921
+ cleanDocs as b7,
4922
+ clearSharedEtags as b8,
4923
+ closingIssueNumbers as b9,
4924
+ draftPushDescription as bA,
4925
+ draftRequestText as bB,
4926
+ effectiveTier as bC,
4927
+ eligibleReviewers as bD,
4928
+ explorationBranch as bE,
4929
+ explorationRules as bF,
4930
+ explorationShareMessage as bG,
4931
+ explorationSlugFromBranch as bH,
4932
+ figmaCell as bI,
4933
+ fileModes as bJ,
4934
+ filterHistory as bK,
4935
+ findOpenDocsProposal as bL,
4936
+ findOpenDocsProposalOnGitHub as bM,
4937
+ firstParagraph as bN,
4938
+ formatDocsJson as bO,
4939
+ friendlyMessage as bP,
4940
+ getSuggestionOnGitHub as bQ,
4941
+ hasSuggestionBlock as bR,
4942
+ hasWriteAccess as bS,
4943
+ hashSpec as bT,
4944
+ iconComponentName as bU,
4945
+ installationFor as bV,
4946
+ isActiveExploration as bW,
4947
+ isAutomationPull as bX,
4948
+ isDocsProposalBranch as bY,
4949
+ isGuardrailPath as bZ,
4950
+ isJiraKey as b_,
4951
+ commitFileOnGitHub as ba,
4952
+ componentContrastPairs as bb,
4953
+ componentMotion as bc,
4954
+ componentsUsingMotionToken as bd,
4955
+ composeChangeset as be,
4956
+ composeSuggestionAppliedComment as bf,
4957
+ composeSuggestionIssue as bg,
4958
+ composeSuggestionReviewComment as bh,
4959
+ continueDocsProposalOnGitHub as bi,
4960
+ contrastDeltaTable as bj,
4961
+ countChanges as bk,
4962
+ coverageSummary as bl,
4963
+ cssModuleClasses as bm,
4964
+ declineComment as bn,
4965
+ declineReasonProblems as bo,
4966
+ declineRequestOnGitHub as bp,
4967
+ declineSuggestionOnGitHub as bq,
4968
+ defaultDocsFooter as br,
4969
+ docsAuthorLine as bs,
4970
+ docsFileText as bt,
4971
+ docsProposal as bu,
4972
+ docsSectionShapes as bv,
4973
+ docsSlug as bw,
4974
+ docsSuggestionPayload as bx,
4975
+ docsText as by,
4976
+ draftChangeLine as bz,
4713
4977
  collectHistory as c,
4714
- suggestionSubject as c$,
4715
- listSuggestionsOnGitHub as c0,
4716
- loadRepoFiles as c1,
4717
- mergeGate as c2,
4718
- missingPermissionMessage as c3,
4719
- missingProse as c4,
4720
- modeLabel as c5,
4721
- needsDocsReview as c6,
4722
- openPulls as c7,
4723
- overrideReasonProblem as c8,
4724
- parseChangesetText as c9,
4725
- requestBrief as cA,
4726
- requestImagePath as cB,
4727
- requestIssueTitle as cC,
4728
- requestStatus as cD,
4729
- requiredApprovals as cE,
4730
- requiredPermission as cF,
4731
- reviewQueue as cG,
4732
- reviewStatusKind as cH,
4733
- reviewSuggestionOnGitHub as cI,
4734
- reviewersToAsk as cJ,
4735
- sectionsNeedingReview as cK,
4736
- sha256Hex as cL,
4737
- shortVariant as cM,
4738
- shortVariants as cN,
4739
- specHash as cO,
4740
- specsUsage as cP,
4741
- suggestionAllowedRoots as cQ,
4742
- suggestionAppliedComment as cR,
4743
- suggestionApplyTable as cS,
4744
- suggestionIssueTitle as cT,
4745
- suggestionKindFromLabels as cU,
4746
- suggestionLabels as cV,
4747
- suggestionPathProblems as cW,
4748
- suggestionPayloadSha as cX,
4749
- suggestionReviewState as cY,
4750
- suggestionShapeProblems as cZ,
4751
- suggestionStateLabel as c_,
4752
- parseComponentSpecs as ca,
4753
- parseJiraKey as cb,
4754
- parseOverrideComment as cc,
4755
- parseRepo as cd,
4756
- parseRequestIssue as ce,
4757
- parseStructureChange as cf,
4758
- parseSuggestionAppliedComment as cg,
4759
- parseSuggestionIssue as ch,
4760
- parseSuggestionReviewComment as ci,
4761
- parseTier as cj,
4762
- patchDocsJson as ck,
4763
- pendingApprovals as cl,
4764
- permissionLabel as cm,
4765
- planRestore as cn,
4766
- planRevert as co,
4767
- planSuggestionApply as cp,
4768
- planToJson as cq,
4769
- prNumberFromSubject as cr,
4770
- prefillDocs as cs,
4771
- proposeDocsOnGitHub as ct,
4772
- propsTable as cu,
4773
- pullState as cv,
4774
- pushChangeRows as cw,
4775
- rateLimitMessage as cx,
4776
- releasePull as cy,
4777
- reopenEligibility as cz,
4978
+ suggestionApplyTable as c$,
4979
+ isReleasePull as c0,
4980
+ isTokenRejected as c1,
4981
+ jiraIssueFields as c2,
4982
+ jiraKeyOf as c3,
4983
+ joinRepoPath as c4,
4984
+ linkRequestToExplorationOnGitHub as c5,
4985
+ linkSuggestionPrOnGitHub as c6,
4986
+ linkedPullsOf as c7,
4987
+ listSuggestionsOnGitHub as c8,
4988
+ loadRepoFiles as c9,
4989
+ prNumberFromSubject as cA,
4990
+ prefillDocs as cB,
4991
+ proposeDocsOnGitHub as cC,
4992
+ propsTable as cD,
4993
+ pullState as cE,
4994
+ pushChangeRows as cF,
4995
+ rateLimitMessage as cG,
4996
+ releasePull as cH,
4997
+ reopenEligibility as cI,
4998
+ requestBrief as cJ,
4999
+ requestImagePath as cK,
5000
+ requestIssueTitle as cL,
5001
+ requestStatus as cM,
5002
+ requiredApprovals as cN,
5003
+ requiredPermission as cO,
5004
+ reviewQueue as cP,
5005
+ reviewStatusKind as cQ,
5006
+ reviewSuggestionOnGitHub as cR,
5007
+ reviewersToAsk as cS,
5008
+ sectionsNeedingReview as cT,
5009
+ sha256Hex as cU,
5010
+ shortVariant as cV,
5011
+ shortVariants as cW,
5012
+ specHash as cX,
5013
+ specsUsage as cY,
5014
+ suggestionAllowedRoots as cZ,
5015
+ suggestionAppliedComment as c_,
5016
+ memoryEtagStore as ca,
5017
+ memorySource as cb,
5018
+ mergeGate as cc,
5019
+ missingPermissionMessage as cd,
5020
+ missingProse as ce,
5021
+ modeLabel as cf,
5022
+ needsDocsReview as cg,
5023
+ openPulls as ch,
5024
+ overrideReasonProblem as ci,
5025
+ parseChangesetText as cj,
5026
+ parseJiraKey as ck,
5027
+ parseOverrideComment as cl,
5028
+ parseRepo as cm,
5029
+ parseRequestIssue as cn,
5030
+ parseStructureChange as co,
5031
+ parseSuggestionAppliedComment as cp,
5032
+ parseSuggestionIssue as cq,
5033
+ parseSuggestionReviewComment as cr,
5034
+ parseTier as cs,
5035
+ patchDocsJson as ct,
5036
+ pendingApprovals as cu,
5037
+ permissionLabel as cv,
5038
+ planRestore as cw,
5039
+ planRevert as cx,
5040
+ planSuggestionApply as cy,
5041
+ planToJson as cz,
4778
5042
  applyTokenEdits as d,
4779
- summarizeApprovals as d0,
4780
- summarizeCi as d1,
4781
- summarizeSuggestionApprovals as d2,
4782
- syncSuggestionStateLabel as d3,
4783
- titleOf as d4,
4784
- toChangeSummary as d5,
4785
- tokenCssVar as d6,
4786
- tokenHistory as d7,
4787
- unsafePathProblem as d8,
4788
- utf8ToBase64 as d9,
4789
- validateComponentSpec as da,
4790
- validateDocs as db,
4791
- validateIntegrations as dc,
4792
- variantName as dd,
4793
- versionTimeline as de,
4794
- withAuthorLine as df,
4795
- withRequestExploration as dg,
4796
- withoutJiraKey as dh,
5043
+ suggestionIssueTitle as d0,
5044
+ suggestionKindFromLabels as d1,
5045
+ suggestionLabels as d2,
5046
+ suggestionPathProblems as d3,
5047
+ suggestionPayloadSha as d4,
5048
+ suggestionReviewState as d5,
5049
+ suggestionShapeProblems as d6,
5050
+ suggestionStateLabel as d7,
5051
+ suggestionSubject as d8,
5052
+ summarizeApprovals as d9,
5053
+ summarizeCi as da,
5054
+ summarizeSuggestionApprovals as db,
5055
+ syncSuggestionStateLabel as dc,
5056
+ titleOf as dd,
5057
+ toChangeSummary as de,
5058
+ tokenCssVar as df,
5059
+ tokenFingerprint as dg,
5060
+ tokenHistory as dh,
5061
+ unsafePathProblem as di,
5062
+ utf8ToBase64 as dj,
5063
+ validateComponentSpec as dk,
5064
+ validateDocs as dl,
5065
+ validateIntegrations as dm,
5066
+ variantName as dn,
5067
+ versionTimeline as dp,
5068
+ webStorageEtagStore as dq,
5069
+ withAuthorLine as dr,
5070
+ withRequestExploration as ds,
5071
+ withoutJiraKey as dt,
4797
5072
  buildHistoryEntry as e,
4798
5073
  coerceValue as f,
4799
5074
  checkLayeredPair as g,
@@ -4817,4 +5092,4 @@ export {
4817
5092
  explorationNote as y,
4818
5093
  explorationNotePath as z
4819
5094
  };
4820
- //# sourceMappingURL=suggestion-apply-DxXRGQrO.js.map
5095
+ //# sourceMappingURL=repo-source-CNZLq_xK.js.map