Skip to the content

Capsid menu

app/kb/procedures/compile.mjs

108 lines and 12 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of compile.mjs
FunctionLinesComplexityCoveredCRAPMutation
compileProcedure34 to 1123494%CriticalCRAP 34.3Critical24.6%
<anonymous:40>40 to 401100%OkCRAP 1Critical0%
<anonymous:46>46 to 462100%OkCRAP 2Critical0%
<anonymous:47>47 to 472100%OkCRAP 2Critical0%
<anonymous:49>49 to 492100%OkCRAP 2Critical0%
<anonymous:51>51 to 512100%OkCRAP 2Critical55.6%
<anonymous:53>53 to 532100%OkCRAP 2Critical55.6%
<anonymous:83>83 to 832100%OkCRAP 2Critical0%
<anonymous:85>85 to 852100%OkCRAP 2Critical0%
<anonymous:87>87 to 872100%OkCRAP 2Critical33.3%
<anonymous:89>89 to 892100%OkCRAP 2Critical33.3%
<anonymous:92>92 to 953100%OkCRAP 3Critical0%

Imports

It imports app/kb/procedures/parse.mjs, app/kb/procedures/render.mjs, app/kb/procedures/taxonomy.mjs, app/kb/procedures/validate.mjs, app/kb/registry/catalog.mjs, app/lib/content/hashes.mjs. Imported by app/kb/procedures/save.server.ts.

Source

// One door from a procedure file to everything derived from it: parse, validate, render, the markdown
// twin and the search input. check:protocols, sync:content and the operator API's save_procedure all
// call compileProcedure, so a file CI passes is the file the save tool accepts and the page draws.

import { gitBlobSha } from "../../lib/content/hashes.mjs";
import { itemPath } from "../registry/catalog.mjs";
import { NON_STRAIN_ORGANISMS } from "./taxonomy.mjs";
import { parseProcedure, procedurePath } from "./parse.mjs";
import { procedureSearchInput, renderProcedure } from "./render.mjs";
import { validateProcedure } from "./validate.mjs";

/** The file name a slug is written in; the same rule as a post's. */
const PROCEDURE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

/**
 * @param {{
 *   slug: string,
 *   raw: string,
 *   pipeline: { renderBody: import("./render.mjs").RenderBody, findWideDashes: (text: string) => Array<{ line: number, column: number, char: string, excerpt: string }> },
 *   resolveImage?: (src: string) => Promise<{ width: number, height: number }>,
 *   registry?: {
 *     primers: (ids: string[]) => Promise<Map<string, import("./render.mjs").StoredPrimer>>,
 *     strains: (ids: string[]) => Promise<Map<string, import("./render.mjs").StoredStrain>>,
 *     reagents: (ids: string[]) => Promise<Map<string, import("./render.mjs").StoredReagent>>,
 *     equipment: (ids: string[]) => Promise<Map<string, import("./render.mjs").StoredEquipment>>,
 *   },
 * }} input
 * @returns {Promise<
 *   | { ok: false, errors: string[], gaps: Array<{ field: string, reason: string }> }
 *   | { ok: true, errors: [], gaps: Array<{ field: string, reason: string }>, record: import("./render.mjs").ProcedureRecord,
 *       markdown: string, searchInput: ReturnType<typeof procedureSearchInput>, sourcePath: string, sourceBlobSha: string }
 * >}
 */
export async function compileProcedure({ slug, raw, pipeline, resolveImage, registry }) {
  if (!PROCEDURE_SLUG.test(slug)) {
    return { ok: false, errors: [`"${slug}" is not a lowercase kebab-case slug`], gaps: [] };
  }
  const file = procedurePath(slug);
  const dashes = pipeline.findWideDashes(raw).map(
    (hit) => `line ${hit.line}, column ${hit.column}: wide dash ${hit.char}; house style uses commas, periods, parentheses or colons ("${hit.excerpt}")`,
  );
  const parsed = parseProcedure({ file, raw });
  // A protocol names its primers by their id in the lab registry and stores no sequence (docs/REGISTRY.md); the registry's
  // rows are read here, so the record carries them and a frozen version freezes them. A caller with no registry cannot
  // compile a protocol that names primers: writing a record with no primer table would be silent.
  const named = Array.isArray(parsed.data.primers) ? parsed.data.primers.flatMap((p) => (typeof p?.primer === "string" ? [p.primer] : [])) : [];
  const namedStrains = Array.isArray(parsed.data.host_strain) ? parsed.data.host_strain.flatMap((s) => (typeof s?.strain === "string" ? [s.strain] : [])) : [];
  // An organism that is not a non-strain organism is a strain of the registry, so it is read from there too.
  const organismIds = Array.isArray(parsed.data.organism) ? parsed.data.organism.filter((id) => typeof id === "string" && !Object.hasOwn(NON_STRAIN_ORGANISMS, id)) : [];
  // A material that is a reagent of the registry names it by id; its facts are the registry's, not typed in the protocol.
  const namedReagents = Array.isArray(parsed.data.materials) ? parsed.data.materials.flatMap((m) => (typeof m?.reagent === "string" ? [m.reagent] : [])) : [];
  // An equipment entry that is an item of the registry names it by id.
  const namedEquipment = Array.isArray(parsed.data.equipment) ? parsed.data.equipment.flatMap((e) => (typeof e?.equipment === "string" ? [e.equipment] : [])) : [];
  if ((named.length > 0 || namedStrains.length > 0 || organismIds.length > 0 || namedReagents.length > 0 || namedEquipment.length > 0) && !registry) {
    return { ok: false, errors: ["this compile was given no lab registry, so the primers, host strains, organisms, reagents and equipment the file names cannot be read"], gaps: [] };
  }
  const primerRows = named.length > 0 && registry ? await registry.primers(named) : new Map();
  const strainIds = [...new Set([...namedStrains, ...organismIds])];
  const strainRows = strainIds.length > 0 && registry ? await registry.strains(strainIds) : new Map();
  const equipmentIds = [...new Set(namedEquipment)];
  const equipmentRows = equipmentIds.length > 0 && registry ? await registry.equipment(equipmentIds) : new Map();
  const reagentIds = [...new Set(namedReagents)];
  const reagentRows = reagentIds.length > 0 && registry ? await registry.reagents(reagentIds) : new Map();
  const { errors, gaps } = validateProcedure(parsed, { slug, primers: new Set(primerRows.keys()), strains: new Set(strainRows.keys()), reagents: new Set(reagentRows.keys()), equipment: new Set(equipmentRows.keys()) });
  // A draft primer is the admin's alone, so a published protocol may not print it.
  if (parsed.data.draft !== true) {
    for (const [id, row] of primerRows) if (row.status === "draft") errors.push(`primers[${id}] is a draft in the lab registry, so a published protocol cannot print it`);
    for (const [id, row] of strainRows) if (row.status === "draft") errors.push(`strain ${id} is a draft in the lab registry, so a published protocol cannot name it`);
    for (const [id, row] of reagentRows) if (row.status === "draft") errors.push(`reagent ${id} is a draft in the lab registry, so a published protocol cannot name it`);
    for (const [id, row] of equipmentRows) if (row.status === "draft") errors.push(`equipment ${id} is a draft in the lab registry, so a published protocol cannot name it`);
  }
  errors.unshift(...dashes);
  if (errors.length > 0) return { ok: false, errors, gaps };

  let record;
  try {
    record = await renderProcedure({
      slug,
      parsed,
      gaps,
      renderBody: pipeline.renderBody,
      resolveImage,
      primerRows: named.flatMap((id) => (primerRows.has(id) ? [primerRows.get(id)] : [])),
      // The strain's page is where the protocol links it, computed from its id here and not typed anywhere.
      strainRows: namedStrains.flatMap((id) => (strainRows.has(id) ? [{ ...strainRows.get(id), path: itemPath("strain", id) }] : [])),
      // The reagents the materials name, as the registry states them, so the table shows where each comes from and types none.
      reagentRows: reagentIds.flatMap((id) => (reagentRows.has(id) ? [reagentRows.get(id)] : [])),
      // The equipment the list names, as the registry states it, so the table shows who makes each and types none.
      equipmentRows: equipmentIds.flatMap((id) => (equipmentRows.has(id) ? [equipmentRows.get(id)] : [])),
      // The words for each organism id the protocol names, from the registry (or the non-strain list), so the library needs no list of its own.
      organismNames: Object.fromEntries(
        (Array.isArray(parsed.data.organism) ? parsed.data.organism : []).flatMap((id) => {
          const words = Object.hasOwn(NON_STRAIN_ORGANISMS, id) ? /** @type {Record<string, string>} */ (NON_STRAIN_ORGANISMS)[id] : strainRows.get(id)?.organism;
          return words ? [[id, words]] : [];
        }),
      ),
    });
  } catch (error) {
    return { ok: false, errors: [error instanceof Error ? error.message : String(error)], gaps };
  }
  const searchInput = procedureSearchInput(record, parsed);
  return {
    ok: true,
    errors: [],
    gaps,
    record,
    markdown: searchInput.markdown,
    searchInput,
    sourcePath: file,
    sourceBlobSha: await gitBlobSha(raw),
  };
}