app/lib/health/checks.server.ts
309 lines and 37 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| readContentSides | 47 to 53 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:48> | 48 to 48 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readProcedureSides | 61 to 71 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| listMarkdownFiles | 74 to 78 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:76> | 76 to 76 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:77> | 77 to 77 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readDictionarySides | 81 to 88 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readPageSides | 91 to 97 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readCvSides | 100 to 107 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readRosterSides | 110 to 116 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readPhageSides | 119 to 125 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readRegistrySides | 132 to 147 | 7 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readPublicationSides | 150 to 156 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readLlmsSides | 163 to 173 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:165> | 165 to 165 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| askStatusOf | 191 to 196 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| runHealthChecks | 204 to 334 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| guard | 208 to 209 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:209> | 209 to 209 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:212> | 212 to 212 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:217> | 217 to 217 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:221> | 221 to 224 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:228> | 228 to 232 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:236> | 236 to 240 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:244> | 244 to 248 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:252> | 252 to 256 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:260> | 260 to 264 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:268> | 268 to 272 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:276> | 276 to 280 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:284> | 284 to 288 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:292> | 292 to 297 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:301> | 301 to 304 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:308> | 308 to 312 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:316> | 316 to 329 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:333> | 333 to 333 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| guardOne | 336 to 344 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:341> | 341 to 341 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/db/cv.ts, app/kb/procedures/parse.mjs, app/kb/registry/compile.mjs, app/kb/registry/kinds.mjs, app/lib/content/hashes.mjs, app/lib/cv/parse.mjs, app/lib/cv/pdf-html.mjs, app/lib/cv/pdf.server.ts, app/lib/dictionary/parse.mjs, app/lib/editor/github.server.ts, app/lib/health/verdicts.mjs, app/lib/llms/validate.mjs, app/lib/media/backup.server.ts, app/lib/media/rebuild.server.ts, app/lib/pages/compile.mjs, app/lib/phages/compile.mjs, app/lib/publications/parse.mjs, app/lib/roster/compile.mjs, app/lib/search/ask.server.ts, app/lib/timing.ts. Imported by app/lib/operator/sync-tools.server.ts, app/routes/admin._index.tsx, app/routes/api.health.ts.
Source
// Every decision lives in verdicts.mjs: check:tests cannot reach a module that imports a binding.
import {
CHECK_TIMEOUT_MS,
askDriftVerdict,
contentDriftVerdict,
procedureDriftVerdict,
cvDriftVerdict,
cvPdfDriftVerdict,
dictionaryDriftVerdict,
pageDriftVerdict,
publicationDriftVerdict,
llmsDriftVerdict,
rosterDriftVerdict,
phageDriftVerdict,
registryDriftVerdict,
ftsEqualityVerdict,
mediaDriftVerdict,
mediaBackupDriftVerdict,
withTimeout,
} from "~/lib/health/verdicts.mjs";
import { askIndexStatus, type AskIndexStatus } from "~/lib/search/ask.server";
import { timed, type Timings } from "~/lib/timing";
import { GitHubError, listDirectory, listPostFiles } from "~/lib/editor/github.server";
import { REGISTRY_DIR } from "~/kb/registry/compile.mjs";
import { KINDS } from "~/kb/registry/kinds.mjs";
import { PROCEDURES_DIR } from "~/kb/procedures/parse.mjs";
import { gitBlobSha } from "~/lib/content/hashes.mjs";
import { LLMS_PATH, LLMS_SETTING_KEY } from "~/lib/llms/validate.mjs";
import { readCv } from "~/db/cv";
import { cvFingerprint } from "~/lib/cv/pdf-html.mjs";
import { storedCvPdfFingerprint } from "~/lib/cv/pdf.server";
import { CV_DIR } from "~/lib/cv/parse.mjs";
import { DICTIONARY_DIR } from "~/lib/dictionary/parse.mjs";
import { PAGES_DIR } from "~/lib/pages/compile.mjs";
import { PUBLICATIONS_DIR } from "~/lib/publications/parse.mjs";
import { PHAGES_DIR } from "~/lib/phages/compile.mjs";
import { ROSTER_DIR } from "~/lib/roster/compile.mjs";
import { mediaIndexStatus } from "~/lib/media/rebuild.server";
import { backupStatus } from "~/lib/media/backup.server";
/**
* The two sides the content-drift comparison reads: the repository's post files with their blob
* shas, and D1's rows with the sha each was rendered from. The health check and sync_posts both
* read them here, so they cannot compare different things.
*/
export async function readContentSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = (await listPostFiles(env)).map((e) => ({ slug: e.slug, sha: e.sha, path: e.path }));
const rows = await env.DB.prepare(
"SELECT slug, source_blob_sha FROM posts WHERE source_path IS NOT NULL",
).all<{ slug: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/**
* The same two sides for procedures: `content/procedures/*.md` with their blob shas, and the table's
* rows with the sha each was compiled from. The check and sync_procedures both read them here. A published
* version with no sealed frozen copy reads as having no sha, so it is drift and the sync that repairs it
* freezes the copy (drizzle/0027_procedure_versions.sql).
*/
export async function readProcedureSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, PROCEDURES_DIR);
const rows = await env.DB.prepare(
`SELECT p.slug, p.path,
CASE WHEN p.status = 'published' AND p.version IS NOT NULL AND p.version <> ''
AND NOT EXISTS (SELECT 1 FROM procedure_versions v WHERE v.slug = p.slug AND v.version = p.version AND v.sealed = 1)
THEN NULL ELSE p.source_blob_sha END AS source_blob_sha
FROM procedures p WHERE p.source_path IS NOT NULL`,
).all<{ slug: string; path: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/** The markdown files of a directory, each with its blob sha, the slug being the file name without .md. */
async function listMarkdownFiles(env: Env & { GITHUB_TOKEN?: string }, dir: string) {
return (await listDirectory(env, dir))
.filter((e) => e.type === "file" && e.name.endsWith(".md"))
.map((e) => ({ slug: e.name.slice(0, -".md".length), sha: e.sha, path: e.path }));
}
/** The same two sides for dictionary entries: `content/dictionary/*.md` and the dictionary_entries table's rows. */
export async function readDictionarySides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, DICTIONARY_DIR);
// `key` answers as `slug`, the name every kind's drift comparison and convergence loop reads a row by.
const rows = await env.DB.prepare(
"SELECT key AS slug, path, source_blob_sha FROM dictionary_entries",
).all<{ slug: string; path: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/** The same two sides for pages: `content/pages/*.md` and the pages table's rows. */
export async function readPageSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, PAGES_DIR);
const rows = await env.DB.prepare(
"SELECT slug, path, source_blob_sha FROM pages",
).all<{ slug: string; path: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/** The same two sides for the CV: `content/cv/*.md` and the cv table's rows. */
export async function readCvSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, CV_DIR);
const rows = await env.DB.prepare("SELECT slug, source_blob_sha FROM cv").all<{
slug: string;
source_blob_sha: string | null;
}>();
return { files, rows: rows.results ?? [] };
}
/** The same two sides for the roster: `content/roster/*.md` and the roster table's rows. */
export async function readRosterSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, ROSTER_DIR);
const rows = await env.DB.prepare(
"SELECT slug, source_blob_sha FROM roster",
).all<{ slug: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/** The same two sides for the phages: `content/phages/*.md` and the phages table's rows. */
export async function readPhageSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, PHAGES_DIR);
const rows = await env.DB.prepare(
"SELECT slug, source_blob_sha FROM phages",
).all<{ slug: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/**
* The same two sides for the registry: `content/registry/<kind>/<id>.md` for every kind, and the registry table's rows,
* each read by its address `<kind>/<id>`. A kind's directory that does not exist yet is an empty kind, not a fault: a
* kind is defined before its first file is written.
*/
export async function readRegistrySides(env: Env & { GITHUB_TOKEN?: string }) {
const files: Array<{ slug: string; sha: string; path: string }> = [];
for (const kind of Object.keys(KINDS)) {
try {
for (const file of await listMarkdownFiles(env, `${REGISTRY_DIR}/${kind}`)) {
files.push({ ...file, slug: `${kind}/${file.slug}` });
}
} catch (error) {
if (!(error instanceof GitHubError && error.status === 404)) throw error;
}
}
const rows = await env.DB.prepare(
"SELECT kind || '/' || id AS slug, source_blob_sha FROM registry",
).all<{ slug: string; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/** The same two sides for publications: `content/publications/*.md` and the publications table's rows. */
export async function readPublicationSides(env: Env & { GITHUB_TOKEN?: string }) {
const files = await listMarkdownFiles(env, PUBLICATIONS_DIR);
const rows = await env.DB.prepare(
"SELECT slug, doi_key, source_blob_sha FROM publications",
).all<{ slug: string; doi_key: string | null; source_blob_sha: string | null }>();
return { files, rows: rows.results ?? [] };
}
/**
* The same two sides for llms.txt: the one file with its blob sha, and the settings row /llms.txt is served
* from, given the blob sha of the bytes it holds (the row has no column for one). The slug is a name for the
* pair, so the shared comparison applies.
*/
export async function readLlmsSides(env: Env & { GITHUB_TOKEN?: string }) {
const dir = LLMS_PATH.slice(0, LLMS_PATH.lastIndexOf("/"));
const file = (await listDirectory(env, dir)).find((e) => e.type === "file" && e.path === LLMS_PATH);
const row = await env.DB.prepare("SELECT value FROM settings WHERE key = ?1")
.bind(LLMS_SETTING_KEY)
.first<{ value: string }>();
return {
files: file ? [{ slug: "llms", sha: file.sha, path: file.path }] : [],
rows: row ? [{ slug: "llms", source_blob_sha: await gitBlobSha(row.value) }] : [],
};
}
/** @public The admin Overview's loader returns these, so the route's inferred type names it. */
export interface HealthCheck {
name: string;
ok: boolean;
detail: string;
/** Only on failure: the one part of a failing check besides its name that reaches the wire. */
counts?: { expected: number; present: number };
}
/** What a caller may share with the run: the request's timing marks, and a memoized read of the Ask index. */
export interface HealthOptions {
timings?: Timings;
askStatus?: () => Promise<AskIndexStatus | null>;
}
/** The Ask index's status: the caller's shared read when it has one, else a fresh listing. Unreadable is a failed check. */
async function askStatusOf(env: Env, options: HealthOptions): Promise<AskIndexStatus> {
if (!options.askStatus) return askIndexStatus(env);
const status = await options.askStatus();
if (!status) throw new Error("the Ask index could not be listed");
return status;
}
interface HealthRun {
checks: HealthCheck[];
failed: HealthCheck[];
}
/** A check that throws is reported as failed, or one broken check would silence every other. */
export async function runHealthChecks(env: Env, options: HealthOptions = {}): Promise<HealthRun> {
/* Every check is independent and each has its own timeout, so they run together: the run takes as long as its slowest
check, not the sum of all of them. The order of the answers is the order below. */
const pending: Array<Promise<HealthCheck>> = [];
const guard = (name: string, run: Parameters<typeof guardOne>[1]) =>
timed(options.timings, `health_${name}`, () => guardOne(name, run));
pending.push(
guard("ask-index-drift", async () => askDriftVerdict(await askStatusOf(env, options))),
);
// The INDEX; media-backup-drift below compares BYTES. Easy to confuse.
pending.push(
guard("media-index-drift", async () => mediaDriftVerdict(await mediaIndexStatus(env))),
);
pending.push(
guard("media-backup-drift", async () => {
// Both buckets listed in full: a truncated read would report a clean sweep of the part it saw.
return mediaBackupDriftVerdict(await backupStatus(env));
}),
);
pending.push(
guard("content-drift", async () => {
// An unreadable repository fails this check, which the repair plan refuses to act on alone.
const { files, rows } = await readContentSides(env);
return contentDriftVerdict(files, rows);
}),
);
pending.push(
guard("procedures-drift", async () => {
// A procedure edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readProcedureSides(env);
return procedureDriftVerdict(files, rows);
}),
);
pending.push(
guard("dictionary-drift", async () => {
// An entry edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readDictionarySides(env);
return dictionaryDriftVerdict(files, rows);
}),
);
pending.push(
guard("roster-drift", async () => {
// A cohort edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readRosterSides(env);
return rosterDriftVerdict(files, rows);
}),
);
pending.push(
guard("phage-drift", async () => {
// A phage edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readPhageSides(env);
return phageDriftVerdict(files, rows);
}),
);
pending.push(
guard("registry-drift", async () => {
// An item edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readRegistrySides(env);
return registryDriftVerdict(files, rows);
}),
);
pending.push(
guard("pages-drift", async () => {
// A page edited, added or deleted through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readPageSides(env);
return pageDriftVerdict(files, rows);
}),
);
pending.push(
guard("cv-drift", async () => {
// A CV file edited through git, or a save whose D1 write failed, shows here.
const { files, rows } = await readCvSides(env);
return cvDriftVerdict(files, rows);
}),
);
pending.push(
guard("cv-pdf-drift", async () => {
// The PDF is drawn from D1's CV, so it is compared with that and not with the repository: a stale CV row
// is cv-drift's finding, and sync_cv runs before sync_cv_pdf.
const [stored, cv] = await Promise.all([storedCvPdfFingerprint(env), readCv(env)]);
return cvPdfDriftVerdict({ stored, expected: await cvFingerprint(cv) });
}),
);
pending.push(
guard("publications-drift", async () => {
const { files, rows } = await readPublicationSides(env);
return publicationDriftVerdict(files, rows);
}),
);
pending.push(
guard("llms-drift", async () => {
// A file edited through git, or a save whose D1 write failed, shows here; the served row is derived.
const { files, rows } = await readLlmsSides(env);
return llmsDriftVerdict(files, rows);
}),
);
pending.push(
guard("fts-equality", async () => {
// Index counts come from the _docsize shadows: COUNT(*) on an external-content fts5 table reads
// through to its content table and can never disagree with it.
const row = await env.DB.prepare(
"SELECT (SELECT COUNT(*) FROM posts) AS posts, " +
"(SELECT COUNT(*) FROM posts_fts_docsize) AS postsFts, " +
"(SELECT COUNT(*) FROM search_docs) AS docs, " +
"(SELECT COUNT(*) FROM search_identity_docsize) AS identity, " +
"(SELECT COUNT(*) FROM search_prose_docsize) AS prose",
).first<Record<string, unknown>>();
// A null row reaches the verdict as unreadable counts: a sentence, not a stack.
return ftsEqualityVerdict(row ?? {});
}),
);
const checks = await Promise.all(pending);
return { checks, failed: checks.filter((c) => !c.ok) };
}
async function guardOne(
name: string,
run: () => Promise<{ ok: boolean; detail: string; counts?: { expected: number; present: number } }>,
): Promise<HealthCheck> {
// The async wrapper turns a SYNCHRONOUS throw in run into a rejection withTimeout can guard.
const started = (async () => run())();
const { ok, detail, counts } = await withTimeout(started, CHECK_TIMEOUT_MS, name);
return counts ? { name, ok, detail, counts } : { name, ok, detail };
}