src/unmapped-repos.ts
75 lines and 10 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| parseOnPurpose | 29 to 36 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| <anonymous:33> | 33 to 33 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| suggestNamespace | 40 to 45 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| <anonymous:43> | 43 to 43 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:43> | 43 to 43 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| unmappedRepos | 48 to 53 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:50> | 50 to 50 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:51> | 51 to 51 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:52> | 52 to 52 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| readRepoMap | 58 to 83 | 10 | 100% | WatchCRAP 10 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/env.ts, src/github/client.ts. Imported by src/watcher-detect.ts, src/watcher-gather.ts.
Source
import type { Env } from "./env";
import { listInstalledRepos, type InstalledRepo } from "./github/client";
// Repos the GitHub App can see that no namespace maps. A repo is reachable through
// Capsid's tools only if a namespace maps it, and the mapping is the authorization
// boundary, so this module only finds the gap. It never
// maps: the seat or Dustin does, through capsid_admin_manage_namespaces action update.
// Repos that are unmapped on purpose (archived, frozen, or deleted soon), one owner/name
// per line, `#` comments allowed. An unsigned document is enough: it can only silence a
// finding, never map or reach a repo.
const UNMAPPED_ON_PURPOSE_NS = "capsid";
const UNMAPPED_ON_PURPOSE_PATH = "unmapped-repos.md";
export interface RepoMapRead {
installed: InstalledRepo[];
installations: number;
// Lowercased owner/name of every repo any namespace maps.
mapped: Set<string>;
onPurpose: Set<string>;
namespaces: string[];
}
export interface UnmappedRepo extends InstalledRepo {
suggested: string | null;
}
/** The lines of the on-purpose document as lowercased owner/name. */
export function parseOnPurpose(body: string | null): Set<string> {
const out = new Set<string>();
for (const raw of (body ?? "").split("\n")) {
const line = raw.replace(/#.*/, "").trim().toLowerCase();
if (/^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(line) && line.split("/").every((part) => part !== "." && part !== "..")) out.add(line);
}
return out;
}
/** The namespace a repo most likely belongs in, from its name alone: the repo named
* for a namespace, else the longest namespace the name starts with up to a dash. */
export function suggestNamespace(fullName: string, namespaces: string[]): string | null {
const name = (fullName.split("/")[1] ?? "").toLowerCase();
if (namespaces.includes(name)) return name;
const prefixed = namespaces.filter((ns) => name.startsWith(`${ns}-`)).sort((a, b) => b.length - a.length);
return prefixed[0] ?? null;
}
/** Installed repos that no namespace maps and the on-purpose list does not name. */
export function unmappedRepos(read: RepoMapRead): UnmappedRepo[] {
return read.installed
.filter((r) => !read.mapped.has(r.full_name.toLowerCase()) && !read.onPurpose.has(r.full_name.toLowerCase()))
.map((r) => ({ ...r, suggested: suggestNamespace(r.full_name, read.namespaces) }))
.sort((a, b) => a.full_name.localeCompare(b.full_name));
}
/** The App's repos against the namespace mapping. Throws, rather than returning a
* partial answer, on anything it cannot read whole: an empty list from the App is not
* believed either, because "0 unmapped" over "0 listed" says nothing. */
export async function readRepoMap(env: Env): Promise<RepoMapRead> {
const { repos, installations } = await listInstalledRepos(env);
if (repos.length === 0) {
throw new Error(`the App's ${installations} installation(s) list no repositories at all, which is not believable`);
}
const { results } = await env.DB.prepare("SELECT namespace, repos FROM namespaces ORDER BY namespace").all<{ namespace: string; repos: string | null }>();
const mapped = new Set<string>();
const namespaces: string[] = [];
for (const row of results ?? []) {
namespaces.push(row.namespace);
let list: unknown;
try {
list = JSON.parse(row.repos || "[]");
} catch {
throw new Error(`namespace ${row.namespace} has a corrupt repos mapping, so what is unmapped cannot be judged`);
}
if (!Array.isArray(list)) throw new Error(`namespace ${row.namespace} has a corrupt repos mapping (not an array)`);
for (const entry of list as Array<{ repo?: unknown }>) {
if (typeof entry?.repo === "string") mapped.add(entry.repo.toLowerCase());
}
}
const doc = await env.DB.prepare("SELECT body FROM documents WHERE namespace = ?1 AND path = ?2")
.bind(UNMAPPED_ON_PURPOSE_NS, UNMAPPED_ON_PURPOSE_PATH)
.first<{ body: string | null }>();
return { installed: repos, installations, mapped, onPurpose: parseOnPurpose(doc?.body ?? null), namespaces };
}