← Files OpsTruthARCHIVED FILE

src/github.js

34.2 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

import { bounded, unique } from "./utils.js";
import { PLUGIN_VERSION } from "./version.js";
import { createGithubAppClient } from "./github-app.js";

const MAX_TREE_ENTRIES = 20000;
// Keep repository, status and file fetches below Cloudflare's per-invocation subrequest ceiling.
const MAX_FILES = 30;
const MAX_FILE_BYTES = 1024 * 1024;
const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
const MAX_MIGRATION_FILES = 12;
const MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
const MAX_COVERAGE_PATHS = 100;
const MAX_GITHUB_API_BYTES = 16 * 1024 * 1024;
const MAX_ARCHIVE_COMPRESSED_BYTES = 64 * 1024 * 1024;
const MAX_ARCHIVE_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
const decoder = new TextDecoder();
const TEXT_EXTENSIONS = new Set([
  "js", "jsx", "ts", "tsx", "mjs", "cjs", "json", "jsonc", "md", "mdx", "yaml", "yml",
  "toml", "sql", "graphql", "gql", "html", "css", "scss", "txt", "sh", "py", "go", "rs",
]);

export function parseRepository(input) {
  const value = String(input || "").trim();
  let candidate = value.replace(/\.git$/, "");
  if (/^https?:\/\//i.test(candidate)) {
    let url;
    try {
      url = new URL(candidate);
    } catch {
      throw new Error("Repository URL is invalid");
    }
    if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") {
      throw new Error("Only public GitHub repositories are supported");
    }
    candidate = url.pathname.split("/").filter(Boolean).slice(0, 2).join("/");
  }
  if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/.test(candidate) || candidate.includes("..")) {
    throw new Error("Repository must be owner/name or a public GitHub URL");
  }
  const [owner, repo] = candidate.split("/");
  return { owner, repo, fullName: `${owner}/${repo}`, htmlUrl: `https://github.com/${owner}/${repo}` };
}

async function cachedFetch(request, ctx) {
  const cache = typeof caches !== "undefined" ? caches.default : null;
  if (cache) {
    const cached = await cache.match(request);
    if (cached) return cached;
  }
  const response = await fetch(request);
  if (cache && response.ok) {
    const headers = new Headers(response.headers);
    headers.set("cache-control", "public, max-age=300");
    const cacheable = new Response(response.clone().body, { status: response.status, headers });
    const write = cache.put(request, cacheable);
    if (ctx?.waitUntil) ctx.waitUntil(write);
    else await write;
  }
  return response;
}

function githubHeaders() {
  return {
    accept: "application/vnd.github+json",
    "user-agent": `opstruth-chatgpt-plugin/${PLUGIN_VERSION}`,
    "x-github-api-version": "2022-11-28",
  };
}

async function githubApi(path, ctx, env = {}, options = {}) {
  const headers = githubHeaders();
  const request = new Request(`https://api.github.com${path}`, { headers });
  const response = options.fresh ? await fetch(request) : await cachedFetch(request, ctx);
  if (!response.ok) {
    if (response.status === 404) throw new Error("Repository was not found or is not public");
    if (response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0") {
      const error = new Error("GitHub public rate limit reached");
      error.code = "GITHUB_RATE_LIMIT";
      throw error;
    }
    throw new Error(`GitHub request failed with status ${response.status}`);
  }
  const bytes = await readBounded(response.body, MAX_GITHUB_API_BYTES, "GitHub API response");
  try {
    return JSON.parse(decoder.decode(bytes));
  } catch {
    throw new Error("GitHub API returned invalid JSON");
  }
}

async function optionalGithubApi(path, ctx, env = {}, options = {}) {
  try {
    return { available: true, value: await githubApi(path, ctx, env, options), reason: null };
  } catch (error) {
    return { available: false, value: null, reason: error?.code === "GITHUB_RATE_LIMIT" ? "rate_limited" : error.message };
  }
}

function extension(path) {
  const name = path.split("/").at(-1) || "";
  return name.includes(".") ? name.split(".").at(-1).toLowerCase() : "";
}

function unsafeToRead(path) {
  const lower = path.toLowerCase();
  const base = lower.split("/").at(-1);
  return base === ".env" || base.startsWith(".env.") || /(?:^|\/)(?:id_rsa|id_ed25519|credentials|secrets?)(?:\.|$)/.test(lower)
    || /\.(?:pem|key|p12|pfx|keystore)$/i.test(lower);
}

function priority(path) {
  const lower = path.toLowerCase();
  const base = lower.split("/").at(-1);
  let score = 10;
  if (["package.json", "pyproject.toml", "cargo.toml", "go.mod", "pom.xml", "composer.json"].includes(base)) score = lower.includes("/") ? 88 : 100;
  else if (["wrangler.json", "wrangler.jsonc", "wrangler.toml", "vercel.json", "netlify.toml", "dockerfile", "tsconfig.json", "openapi.json", "openapi.yaml", "openapi.yml"].includes(base)) score = 100;
  else if (lower.startsWith(".github/workflows/") || ["readme.md", "contributing.md", "security.md"].includes(base)) score = 95;
  else if (/(?:^|\/)migrations?\//.test(lower)) score = 90;
  else if (/(?:^|\/)(?:app|pages)\/.+\/(?:page|route)\.(?:js|jsx|ts|tsx)$/.test(lower)) score = 92;
  else if (/(?:^|\/)(?:orchestrator|server|backend|api|worker)\/src\/(?:index|app|server|router|routes|api|worker|openapi)\.(?:js|jsx|ts|tsx|mjs|cjs)$/.test(lower)) score = 99;
  else if (/(?:^|\/)src\/(?:index|app|server|router|routes|api|worker)\.(?:js|jsx|ts|tsx|mjs|cjs)$/.test(lower)) score = 96;
  else if (/(?:router|routes|api|server|worker|index)\.(?:js|jsx|ts|tsx|mjs|cjs)$/.test(base)) score = 84;
  else if (/(?:^|\/)(?:src|app|pages|api|server|worker)\//.test(lower)) score = 62;
  else if (/\.(?:md|json|ya?ml|toml)$/.test(lower)) score = 30;

  if (/(?:^|\/)(?:node_modules|dist|build|coverage|vendor|third[_-]?party|\.next|\.cache|fixtures?|__snapshots__)(?:\/|$)/.test(lower)) return -100;
  if (/(?:^|\/)(?:openai-cookbook|openclaw-docs|cookbook)(?:\/|$)/.test(lower)) score -= 90;
  else if (/(?:^|\/)(?:test|tests|examples?|docs)(?:\/|$)/.test(lower) && score < 90) score -= 70;
  return score;
}

function selectFiles(tree) {
  return tree
    .filter((entry) => entry.type === "blob" && Number(entry.size || 0) <= MAX_FILE_BYTES)
    .filter((entry) => TEXT_EXTENSIONS.has(extension(entry.path)) || priority(entry.path) >= 90)
    .filter((entry) => !unsafeToRead(entry.path))
    .filter((entry) => priority(entry.path) > 0)
    .sort((left, right) => priority(right.path) - priority(left.path) || left.path.localeCompare(right.path))
    .slice(0, MAX_FILES);
}

function isMigrationPath(path) {
  return /(?:^|\/)migrations?\//i.test(path) || /migration/i.test(path.split("/").at(-1) || "");
}

function coverageList(paths) {
  const sorted = [...paths].sort();
  return { count: sorted.length, paths: sorted.slice(0, MAX_COVERAGE_PATHS), pathsTruncated: sorted.length > MAX_COVERAGE_PATHS };
}

function migrationCoverage(tree, files, omissions, treeTruncated, limitations = []) {
  const discoveredPaths = tree.filter((entry) => entry.type === "blob" && isMigrationPath(entry.path)).map((entry) => entry.path);
  const inspectedPaths = files.map((file) => file.path);
  const sortedOmissions = [...omissions].sort((left, right) => left.path.localeCompare(right.path) || left.reason.localeCompare(right.reason));
  return {
    complete: !treeTruncated && sortedOmissions.length === 0 && inspectedPaths.length === discoveredPaths.length,
    treeComplete: !treeTruncated,
    limits: { maxFiles: MAX_MIGRATION_FILES, maxFileBytes: MAX_FILE_BYTES, maxTotalBytes: MAX_MIGRATION_TOTAL_BYTES },
    discovered: coverageList(discoveredPaths),
    inspected: coverageList(inspectedPaths),
    omitted: {
      count: sortedOmissions.length,
      items: sortedOmissions.slice(0, MAX_COVERAGE_PATHS),
      itemsTruncated: sortedOmissions.length > MAX_COVERAGE_PATHS,
    },
    limitations: [...new Set([...(treeTruncated ? ["tree_truncated"] : []), ...limitations])].sort(),
  };
}

function tarString(bytes, start, length) {
  const field = bytes.subarray(start, start + length);
  const zero = field.indexOf(0);
  return decoder.decode(zero === -1 ? field : field.subarray(0, zero)).trim();
}

function tarNumber(bytes, start, length) {
  const value = tarString(bytes, start, length).replace(/\0/g, "").trim();
  if (!value) return 0;
  if (value.charCodeAt(0) & 0x80) {
    let result = BigInt(value.charCodeAt(0) & 0x7f);
    for (let index = 1; index < value.length; index += 1) result = (result << 8n) | BigInt(value.charCodeAt(index));
    return Number(result);
  }
  const parsed = Number.parseInt(value, 8);
  return Number.isFinite(parsed) ? parsed : 0;
}

function cleanArchivePath(path) {
  const normalized = String(path || "").replace(/^\.\//, "").replace(/\\/g, "/");
  const parts = normalized.split("/").filter(Boolean);
  if (parts.length < 2) return null;
  const clean = parts.slice(1).join("/");
  if (!clean || clean.startsWith("/") || clean.split("/").some((part) => part === "..")) return null;
  return clean;
}

function parsePaxPath(text) {
  let offset = 0;
  let path = null;
  while (offset < text.length) {
    const space = text.indexOf(" ", offset);
    if (space === -1) break;
    const length = Number.parseInt(text.slice(offset, space), 10);
    if (!Number.isFinite(length) || length <= 0) break;
    const record = text.slice(space + 1, offset + length).replace(/\n$/, "");
    const equals = record.indexOf("=");
    if (equals !== -1 && record.slice(0, equals) === "path") path = record.slice(equals + 1);
    offset += length;
  }
  return path;
}

function candidateOrder(left, right) {
  return priority(right.path) - priority(left.path) || left.path.localeCompare(right.path);
}

export function parseTarArchive(bytes) {
  const tree = [];
  const candidates = [];
  let offset = 0;
  let pendingPath = null;
  let treeTruncated = false;

  while (offset + 512 <= bytes.length) {
    const header = bytes.subarray(offset, offset + 512);
    if (header.every((byte) => byte === 0)) break;
    const name = tarString(header, 0, 100);
    const prefix = tarString(header, 345, 155);
    const size = tarNumber(header, 124, 12);
    const type = String.fromCharCode(header[156] || 48);
    const dataStart = offset + 512;
    const dataEnd = dataStart + size;
    if (!Number.isSafeInteger(size) || size < 0 || dataEnd > bytes.length) throw new Error("GitHub archive was malformed");

    const body = bytes.subarray(dataStart, dataEnd);
    if (type === "x") pendingPath = parsePaxPath(decoder.decode(body)) || pendingPath;
    else if (type === "L") pendingPath = decoder.decode(body).replace(/\0.*$/s, "").trim();
    else {
      const archivePath = pendingPath || [prefix, name].filter(Boolean).join("/");
      pendingPath = null;
      const path = cleanArchivePath(archivePath);
      if (path && (type === "0" || type === "\0" || type === "5")) {
        if (tree.length >= MAX_TREE_ENTRIES) {
          treeTruncated = true;
          break;
        }
        const isFile = type !== "5";
        tree.push({ path, type: isFile ? "blob" : "tree", size: isFile ? size : 0, sha: null });
        if (isFile && size <= MAX_FILE_BYTES && !unsafeToRead(path) && priority(path) > 0
          && (TEXT_EXTENSIONS.has(extension(path)) || priority(path) >= 90)) {
          candidates.push({ path, text: decoder.decode(body), truncated: false });
          candidates.sort(candidateOrder);
          if (candidates.length > MAX_FILES) candidates.pop();
        }
      }
    }
    offset = dataStart + Math.ceil(size / 512) * 512;
  }

  const files = [];
  let total = 0;
  for (const candidate of candidates.sort(candidateOrder)) {
    if (total + candidate.text.length > MAX_TOTAL_BYTES) break;
    files.push(candidate);
    total += candidate.text.length;
  }
  return { tree, files, treeTruncated };
}

async function readBounded(stream, limit, label) {
  if (!stream) throw new Error(`${label} response had no body`);
  const reader = stream.getReader();
  const chunks = [];
  let total = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    total += value.byteLength;
    if (total > limit) {
      await reader.cancel();
      throw new Error(`${label} exceeded the ${Math.floor(limit / (1024 * 1024))} MiB safety limit`);
    }
    chunks.push(value);
  }
  const bytes = new Uint8Array(total);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return bytes;
}

async function loadArchiveSnapshot(repository, ctx) {
  const headUrl = `https://github.com/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/archive/HEAD.tar.gz`;
  const headResponse = await fetch(new Request(headUrl, {
    redirect: "manual",
    headers: { "user-agent": `opstruth-chatgpt-plugin/${PLUGIN_VERSION}` },
  }));
  if (headResponse.status === 404) throw new Error("Repository was not found or is not public");

  let archiveUrl = `https://codeload.github.com/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/tar.gz/HEAD`;
  let headCommitSha = null;
  if (headResponse.status >= 300 && headResponse.status < 400) {
    const location = headResponse.headers.get("location");
    if (location) {
      const candidate = new URL(location);
      if (candidate.protocol === "https:" && candidate.hostname === "codeload.github.com") {
        archiveUrl = candidate.href;
        const ref = candidate.pathname.split("/").filter(Boolean).at(-1);
        if (/^[a-f0-9]{40,64}$/i.test(ref || "")) headCommitSha = ref;
      }
    }
  }

  const request = new Request(archiveUrl, { headers: { "user-agent": `opstruth-chatgpt-plugin/${PLUGIN_VERSION}` } });
  const response = await cachedFetch(request, ctx);
  if (response.status === 404) throw new Error("Repository was not found or is not public");
  if (!response.ok) throw new Error(`GitHub archive request failed with status ${response.status}`);
  const contentLength = Number(response.headers.get("content-length") || 0);
  if (contentLength > MAX_ARCHIVE_COMPRESSED_BYTES) throw new Error("GitHub archive exceeded the 64 MiB compressed safety limit");

  let decompressedStream;
  try {
    decompressedStream = response.body.pipeThrough(new DecompressionStream("gzip"));
  } catch {
    throw new Error("GitHub archive could not be decompressed");
  }
  const archive = await readBounded(decompressedStream, MAX_ARCHIVE_UNCOMPRESSED_BYTES, "GitHub archive contents");
  const parsed = parseTarArchive(archive);
  if (!parsed.tree.length) throw new Error("GitHub archive did not contain a readable public repository tree");
  const lowerPaths = new Set(parsed.tree.map((entry) => entry.path.toLowerCase()));

  const availableMigrationFiles = parsed.files.filter((file) => isMigrationPath(file.path)).sort((left, right) => left.path.localeCompare(right.path));
  const migrationFiles = [];
  const migrationOmissionReasons = new Map();
  let migrationBytes = 0;
  for (const file of availableMigrationFiles) {
    if (migrationFiles.length >= MAX_MIGRATION_FILES) migrationOmissionReasons.set(file.path, "file_count_limit");
    else {
      const bytes = new TextEncoder().encode(file.text).byteLength;
      if (migrationBytes + bytes > MAX_MIGRATION_TOTAL_BYTES) migrationOmissionReasons.set(file.path, "total_byte_limit");
      else {
        migrationFiles.push(file);
        migrationBytes += bytes;
      }
    }
  }
  const migrationFilePaths = new Set(migrationFiles.map((file) => file.path));
  const migrationOmissions = parsed.tree
    .filter((entry) => entry.type === "blob" && isMigrationPath(entry.path) && !migrationFilePaths.has(entry.path))
    .map((entry) => ({
      path: entry.path,
      reason: migrationOmissionReasons.get(entry.path)
        || (Number(entry.size || 0) > MAX_FILE_BYTES ? "file_byte_limit" : "archive_fallback_not_selected"),
    }));

  return {
    repository: {
      owner: repository.owner,
      name: repository.repo,
      fullName: repository.fullName,
      providerRepositoryId: null,
      htmlUrl: repository.htmlUrl,
      description: null,
      defaultBranch: "HEAD",
      headTreeSha: null,
      headCommitSha,
      visibility: "public",
      archived: null,
      fork: null,
      pushedAt: null,
      license: [...lowerPaths].some((path) => /(?:^|\/)licen[cs]e(?:\.|$)/i.test(path)) ? "Detected in tree" : null,
      metadataSource: "public-archive-fallback",
    },
    tree: parsed.tree,
    files: parsed.files,
    capabilityFiles: { migrations: migrationFiles },
    capabilityCoverage: {
      migrations: migrationCoverage(parsed.tree, migrationFiles, migrationOmissions, parsed.treeTruncated, ["archive_fallback_bounded_selection"]),
    },
    limits: {
      treeEntriesObserved: parsed.tree.length,
      treeTruncated: parsed.treeTruncated,
      filesRead: parsed.files.length,
      maxFiles: MAX_FILES,
      maxFileBytes: MAX_FILE_BYTES,
      maxTotalBytes: MAX_TOTAL_BYTES,
      archiveFallback: true,
    },
  };
}

async function fetchRawFile(repository, branch, entry) {
  const encodedPath = entry.path.split("/").map(encodeURIComponent).join("/");
  const url = `https://raw.githubusercontent.com/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/${encodeURIComponent(branch)}/${encodedPath}`;
  const response = await fetch(new Request(url, { headers: { "user-agent": `opstruth-chatgpt-plugin/${PLUGIN_VERSION}` } }));
  if (!response.ok) return null;
  const bytes = await readBounded(response.body, MAX_FILE_BYTES, "GitHub raw file");
  const text = decoder.decode(bytes);
  return { path: entry.path, text, truncated: Number(entry.size || 0) > bytes.byteLength };
}

async function fetchSelectedFiles(repository, branch, tree) {
  const selected = selectFiles(tree);
  const files = [];
  let total = 0;
  for (let index = 0; index < selected.length; index += 10) {
    const batch = await Promise.all(selected.slice(index, index + 10).map((entry) => fetchRawFile(repository, branch, entry)));
    for (const file of batch.filter(Boolean)) {
      if (total + file.text.length > MAX_TOTAL_BYTES) return files;
      files.push(file);
      total += file.text.length;
    }
  }
  return files;
}

async function fetchMigrationCapability(repository, branch, tree, treeTruncated) {
  const discovered = tree.filter((entry) => entry.type === "blob" && isMigrationPath(entry.path))
    .sort((left, right) => left.path.localeCompare(right.path));
  const omissions = [];
  const eligible = [];
  for (const entry of discovered) {
    if (unsafeToRead(entry.path)) omissions.push({ path: entry.path, reason: "unsafe_path" });
    else if (Number(entry.size || 0) > MAX_FILE_BYTES) omissions.push({ path: entry.path, reason: "file_byte_limit" });
    else eligible.push(entry);
  }
  const selected = eligible.slice(0, MAX_MIGRATION_FILES);
  for (const entry of eligible.slice(MAX_MIGRATION_FILES)) omissions.push({ path: entry.path, reason: "file_count_limit" });

  const files = [];
  let totalBytes = 0;
  for (let index = 0; index < selected.length; index += 6) {
    const batchEntries = selected.slice(index, index + 6);
    const batch = await Promise.all(batchEntries.map(async (entry) => {
      try {
        return { entry, file: await fetchRawFile(repository, branch, entry), reason: null };
      } catch {
        return { entry, file: null, reason: "read_error" };
      }
    }));
    for (const item of batch) {
      if (!item.file) {
        omissions.push({ path: item.entry.path, reason: item.reason || "unreadable" });
        continue;
      }
      const bytes = new TextEncoder().encode(item.file.text).byteLength;
      if (totalBytes + bytes > MAX_MIGRATION_TOTAL_BYTES) {
        omissions.push({ path: item.entry.path, reason: "total_byte_limit" });
        continue;
      }
      files.push(item.file);
      totalBytes += bytes;
    }
  }
  return { files, coverage: migrationCoverage(tree, files, omissions, treeTruncated) };
}

function unavailableGithubStatus(defaultBranch, reason) {
  return {
    source: "github-public-api",
    available: false,
    reason,
    defaultBranch,
    headCommitSha: null,
    branchProtection: { available: false, protected: null, reason },
    workflowRuns: { available: false, totalObserved: 0, latest: [], reason },
    checkRuns: { available: false, totalObserved: 0, latest: [], reason },
    commitStatus: { available: false, state: null, contexts: [], reason },
  };
}

async function loadGithubStatus(repository, defaultBranch, ctx, env = {}) {
  const encodedBranch = encodeURIComponent(defaultBranch);
  const [branchResult, workflowResult] = await Promise.all([
    optionalGithubApi(`/repos/${repository.owner}/${repository.repo}/branches/${encodedBranch}`, ctx, env),
    optionalGithubApi(`/repos/${repository.owner}/${repository.repo}/actions/runs?branch=${encodedBranch}&per_page=20&exclude_pull_requests=true`, ctx, env),
  ]);
  const headCommitSha = branchResult.value?.commit?.sha || null;
  const [checksResult, statusResult] = headCommitSha
    ? await Promise.all([
      optionalGithubApi(`/repos/${repository.owner}/${repository.repo}/commits/${encodeURIComponent(headCommitSha)}/check-runs?per_page=100`, ctx, env),
      optionalGithubApi(`/repos/${repository.owner}/${repository.repo}/commits/${encodeURIComponent(headCommitSha)}/status`, ctx, env),
    ])
    : [{ available: false, value: null, reason: branchResult.reason || "head_commit_unavailable" }, { available: false, value: null, reason: branchResult.reason || "head_commit_unavailable" }];

  const workflowRuns = Array.isArray(workflowResult.value?.workflow_runs)
    ? workflowResult.value.workflow_runs.slice(0, 20).map((run) => ({
      id: run.id,
      name: run.name || run.display_title || "Unnamed workflow",
      event: run.event || null,
      status: run.status || null,
      conclusion: run.conclusion || null,
      headSha: run.head_sha || null,
      runNumber: run.run_number || null,
      startedAt: run.run_started_at || run.created_at || null,
      updatedAt: run.updated_at || null,
      htmlUrl: run.html_url || null,
    }))
    : [];
  const checkRuns = Array.isArray(checksResult.value?.check_runs)
    ? checksResult.value.check_runs.slice(0, 100).map((run) => ({
      id: run.id,
      name: run.name || "Unnamed check",
      status: run.status || null,
      conclusion: run.conclusion || null,
      startedAt: run.started_at || null,
      completedAt: run.completed_at || null,
      htmlUrl: run.html_url || null,
    }))
    : [];
  const contexts = Array.isArray(statusResult.value?.statuses)
    ? statusResult.value.statuses.slice(0, 100).map((status) => ({
      context: status.context || null,
      state: status.state || null,
      updatedAt: status.updated_at || status.created_at || null,
      targetUrl: status.target_url || null,
    }))
    : [];
  const anyAvailable = branchResult.available || workflowResult.available || checksResult.available || statusResult.available;
  return {
    source: "github-public-api",
    available: anyAvailable,
    reason: anyAvailable ? null : branchResult.reason || workflowResult.reason || "github_status_unavailable",
    defaultBranch,
    headCommitSha,
    branchProtection: {
      available: branchResult.available,
      protected: branchResult.available ? Boolean(branchResult.value?.protected) : null,
      reason: branchResult.reason,
    },
    workflowRuns: {
      available: workflowResult.available,
      totalObserved: Number(workflowResult.value?.total_count || workflowRuns.length),
      latest: workflowRuns,
      reason: workflowResult.reason,
    },
    checkRuns: {
      available: checksResult.available,
      totalObserved: Number(checksResult.value?.total_count || checkRuns.length),
      latest: checkRuns,
      reason: checksResult.reason,
    },
    commitStatus: {
      available: statusResult.available,
      state: statusResult.value?.state || null,
      contexts,
      reason: statusResult.reason,
    },
  };
}

export async function loadRepositorySnapshot(input, env = {}, ctx = {}) {
  const repository = parseRepository(input);
  let metadata;
  let treePayload;
  try {
    metadata = await githubApi(`/repos/${repository.owner}/${repository.repo}`, ctx, env);
    if (metadata.private) throw new Error("Private repositories require a future authenticated lane");
    treePayload = await githubApi(`/repos/${repository.owner}/${repository.repo}/git/trees/${encodeURIComponent(metadata.default_branch)}?recursive=1`, ctx, env);
  } catch (error) {
    if (error?.code === "GITHUB_RATE_LIMIT") {
      const snapshot = await loadArchiveSnapshot(repository, ctx);
      snapshot.githubStatus = unavailableGithubStatus(snapshot.repository.defaultBranch, "rate_limited");
      return snapshot;
    }
    throw error;
  }
  const branch = metadata.default_branch;
  const completeTree = Array.isArray(treePayload.tree) ? treePayload.tree : [];
  const tree = bounded(completeTree, MAX_TREE_ENTRIES).map(({ path, type, size, sha }) => ({ path, type, size: size || 0, sha }));
  const treeTruncated = Boolean(treePayload.truncated) || completeTree.length > MAX_TREE_ENTRIES;
  const [files, githubStatus, migrationCapability] = await Promise.all([
    fetchSelectedFiles(repository, branch, tree),
    loadGithubStatus(repository, branch, ctx, env),
    fetchMigrationCapability(repository, branch, tree, treeTruncated),
  ]);
  return {
    repository: {
      owner: repository.owner,
      name: repository.repo,
      fullName: metadata.full_name,
      providerRepositoryId: metadata.id === undefined || metadata.id === null ? null : String(metadata.id),
      htmlUrl: metadata.html_url,
      description: metadata.description,
      defaultBranch: branch,
      headTreeSha: treePayload.sha || null,
      visibility: metadata.visibility,
      archived: Boolean(metadata.archived),
      fork: Boolean(metadata.fork),
      pushedAt: metadata.pushed_at,
      license: metadata.license?.spdx_id || null,
      headCommitSha: githubStatus.headCommitSha,
    },
    tree,
    files,
    capabilityFiles: { migrations: migrationCapability.files },
    capabilityCoverage: { migrations: migrationCapability.coverage },
    githubStatus,
    limits: {
      treeEntriesObserved: tree.length,
      treeTruncated,
      filesRead: files.length,
      maxFiles: MAX_FILES,
      maxFileBytes: MAX_FILE_BYTES,
      maxTotalBytes: MAX_TOTAL_BYTES,
    },
  };
}

export function fileMap(snapshot) {
  return new Map(snapshot.files.map((file) => [file.path, file.text]));
}

export function paths(snapshot) {
  return snapshot.tree.map((entry) => entry.path);
}

export function fileContents(snapshot) {
  return snapshot.files.map((file) => ({ path: file.path, text: file.text }));
}

export function declaredEnvironmentNames(snapshot) {
  const names = [];
  const patterns = [
    /process\.env\.([A-Z][A-Z0-9_]*)/g,
    /import\.meta\.env\.([A-Z][A-Z0-9_]*)/g,
    /env\.([A-Z][A-Z0-9_]*)/g,
  ];
  for (const file of snapshot.files) {
    for (const pattern of patterns) {
      for (const match of file.text.matchAll(pattern)) names.push(match[1]);
    }
  }
  return unique(names).sort();
}

function assertCommitSha(value, label) {
  if (!/^[a-f0-9]{40}$/i.test(String(value || ""))) throw new Error(`${label}_invalid`);
}

function assertVerificationPath(value) {
  const path = String(value || "");
  if (!path || path.length > 500 || path.startsWith("/") || path.includes("\\")
    || path.split("/").some((part) => !part || part === "..")) throw new Error("verification_path_invalid");
  return path;
}

function githubAppObservationReason(error) {
  if (error?.code === "GITHUB_APP_RATE_LIMIT") return "rate_limited";
  if (["GITHUB_APP_AUTH_FAILED", "GITHUB_APP_NOT_CONFIGURED", "GITHUB_APP_CONFIGURATION_INVALID", "GITHUB_APP_TOKEN_INVALID"].includes(error?.code)) {
    return "authentication_unavailable";
  }
  if (error?.code === "GITHUB_APP_PERMISSION_DENIED" || error?.code === "GITHUB_APP_SCOPE_INVALID") return "permission_denied";
  if (error?.code === "GITHUB_APP_NOT_FOUND") return "not_found";
  if (error?.code === "GITHUB_APP_RESPONSE_INVALID") return "provider_response_invalid";
  return "github_verification_unavailable";
}

async function optionalGithubAppApi(client, path, options = {}) {
  try {
    return { available: true, value: await client.json(path, options), reason: null };
  } catch (error) {
    return { available: false, value: null, reason: githubAppObservationReason(error) };
  }
}

function decodeContentsFile(payload, entry) {
  if (payload === null) return null;
  if (!payload || Array.isArray(payload) || payload.type !== "file" || payload.path !== entry.path
    || payload.sha !== entry.sha || payload.encoding !== "base64" || Number(payload.size) > MAX_FILE_BYTES) {
    const error = new Error("GitHub verification file response was invalid");
    error.code = "GITHUB_APP_RESPONSE_INVALID";
    throw error;
  }
  const encoded = typeof payload.content === "string" ? payload.content.replace(/\s+/g, "") : "";
  if (typeof payload.content !== "string" || encoded.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
    const error = new Error("GitHub verification file response was invalid");
    error.code = "GITHUB_APP_RESPONSE_INVALID";
    throw error;
  }
  let binary;
  try {
    binary = atob(encoded);
  } catch {
    const error = new Error("GitHub verification file response was invalid");
    error.code = "GITHUB_APP_RESPONSE_INVALID";
    throw error;
  }
  if (binary.length > MAX_FILE_BYTES || Number(payload.size) !== binary.length) {
    const error = new Error("GitHub verification file response exceeded its bound");
    error.code = "GITHUB_APP_RESPONSE_INVALID";
    throw error;
  }
  const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
  return { path: entry.path, text: decoder.decode(bytes), truncated: false };
}

async function fetchVerificationFile(client, repository, headSha, entry) {
  const encodedPath = entry.path.split("/").map(encodeURIComponent).join("/");
  const payload = await client.json(
    `/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/contents/${encodedPath}?ref=${encodeURIComponent(headSha)}`,
    { allowNotFound: true },
  );
  return decodeContentsFile(payload, entry);
}

export async function loadCommitVerificationEvidence({ repository: input, baseSha, headSha, paths: requestedPaths = [] }, env = {}, ctx = {}) {
  const repository = parseRepository(input);
  assertCommitSha(baseSha, "base_sha");
  assertCommitSha(headSha, "head_sha");
  const paths = [...new Set(requestedPaths.map(assertVerificationPath))].sort();
  if (paths.length > 20) throw new Error("verification_path_limit_exceeded");
  const client = createGithubAppClient(env, repository.fullName);
  const repositoryPrefix = `/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}`;
  const metadata = await client.json(repositoryPrefix);
  client.assertSelectedRepository(metadata);
  const encodedHead = encodeURIComponent(headSha);
  const encodedBase = encodeURIComponent(baseSha);
  const commitResult = await optionalGithubAppApi(client, `${repositoryPrefix}/commits/${encodedHead}`);
  const treeResult = await optionalGithubAppApi(client, `${repositoryPrefix}/git/trees/${encodedHead}?recursive=1`);
  const compareResult = await optionalGithubAppApi(client, `${repositoryPrefix}/compare/${encodedBase}...${encodedHead}`);
  const checksResult = await optionalGithubAppApi(client, `${repositoryPrefix}/commits/${encodedHead}/check-runs?per_page=100`);
  const statusResult = await optionalGithubAppApi(client, `${repositoryPrefix}/commits/${encodedHead}/status`);
  const tree = Array.isArray(treeResult.value?.tree)
    ? bounded(treeResult.value.tree, MAX_TREE_ENTRIES).map(({ path, type, size, sha }) => ({ path, type, size: size || 0, sha }))
    : [];
  const byPath = new Map(tree.map((entry) => [entry.path, entry]));
  const files = [];
  let totalBytes = 0;
  for (const path of paths) {
    const entry = byPath.get(path);
    if (!entry || entry.type !== "blob" || Number(entry.size || 0) > MAX_FILE_BYTES || unsafeToRead(path)) continue;
    const file = await fetchVerificationFile(client, repository, headSha, entry);
    if (!file) continue;
    const bytes = new TextEncoder().encode(file.text).byteLength;
    if (totalBytes + bytes > MAX_TOTAL_BYTES) break;
    files.push(file);
    totalBytes += bytes;
  }
  const changedFiles = Array.isArray(compareResult.value?.files)
    ? compareResult.value.files.slice(0, 300).map((file) => ({
      path: file.filename,
      status: file.status || null,
      additions: Number(file.additions || 0),
      deletions: Number(file.deletions || 0),
      changes: Number(file.changes || 0),
      blobUrl: file.blob_url || null,
    }))
    : [];
  const checkRuns = Array.isArray(checksResult.value?.check_runs)
    ? checksResult.value.check_runs.slice(0, 100).map((run) => ({
      name: run.name || "Unnamed check",
      status: run.status || null,
      conclusion: run.conclusion || null,
      htmlUrl: run.html_url || null,
    }))
    : [];
  const contexts = Array.isArray(statusResult.value?.statuses)
    ? statusResult.value.statuses.slice(0, 100).map((status) => ({
      name: status.context || "Unnamed status",
      state: status.state || null,
      targetUrl: status.target_url || null,
    }))
    : [];
  return {
    repository: {
      fullName: metadata.full_name,
      providerRepositoryId: metadata.id === undefined || metadata.id === null ? null : Number(metadata.id),
      htmlUrl: metadata.html_url,
      visibility: metadata.visibility,
      evidenceAuthority: client.authority,
    },
    subject: {
      baseSha: baseSha.toLowerCase(),
      headSha: headSha.toLowerCase(),
      commitAvailable: commitResult.available,
      observedHeadSha: commitResult.value?.sha?.toLowerCase() || null,
      commitUrl: commitResult.value?.html_url || `https://github.com/${repository.fullName}/commit/${headSha}`,
    },
    tree: {
      available: treeResult.available,
      complete: treeResult.available && !treeResult.value?.truncated && tree.length < MAX_TREE_ENTRIES,
      paths: tree.map((entry) => entry.path),
      reason: treeResult.reason,
    },
    files,
    compare: {
      available: compareResult.available,
      status: compareResult.value?.status || null,
      aheadBy: Number(compareResult.value?.ahead_by ?? -1),
      behindBy: Number(compareResult.value?.behind_by ?? -1),
      files: changedFiles,
      complete: compareResult.available && changedFiles.length < 300,
      htmlUrl: `https://github.com/${repository.fullName}/compare/${baseSha}...${headSha}`,
      reason: compareResult.reason,
    },
    checks: {
      available: checksResult.available || statusResult.available,
      checkRuns,
      contexts,
      combinedState: statusResult.value?.state || null,
      reason: checksResult.reason || statusResult.reason,
    },
  };
}

SHA-256: 6bff8c70aacf0f7a33abd87c25198cce80ea7bb27186a66910d870ccaa5242e3