Skip to the content

Capsid menu

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

The functions of checks.server.ts
FunctionLinesComplexityCoveredCRAPMutation
readContentSides47 to 532No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:48>48 to 481No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readProcedureSides61 to 712No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
listMarkdownFiles74 to 781No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:76>76 to 762No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:77>77 to 771No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readDictionarySides81 to 882No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readPageSides91 to 972No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readCvSides100 to 1072No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readRosterSides110 to 1162No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readPhageSides119 to 1252No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readRegistrySides132 to 1477No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readPublicationSides150 to 1562No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readLlmsSides163 to 1733No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:165>165 to 1652No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
askStatusOf191 to 1963No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
runHealthChecks204 to 3341No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
guard208 to 2091No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:209>209 to 2091No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:212>212 to 2121No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:217>217 to 2171No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:221>221 to 2241No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:228>228 to 2321No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:236>236 to 2401No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:244>244 to 2481No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:252>252 to 2561No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:260>260 to 2641No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:268>268 to 2721No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:276>276 to 2801No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:284>284 to 2881No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:292>292 to 2971No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:301>301 to 3041No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:308>308 to 3121No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:316>316 to 3292No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:333>333 to 3331No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
guardOne336 to 3442No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:341>341 to 3411No data: no coverage report in this mapNo data: no coverage report in this mapNo 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 };
}