app/components/lab.tsx
466 lines and 38 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| inventorySentence | 37 to 44 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:42> | 42 to 42 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabInventory | 46 to 67 | 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:58> | 58 to 62 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| sequenceCell | 69 to 69 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabPrimers | 71 to 98 | 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:84> | 84 to 88 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| product | 89 to 89 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| Fact | 100 to 107 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| NotFound | 110 to 112 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| publishedVersusComputed | 115 to 120 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:117> | 117 to 117 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabPrimer | 122 to 266 | 29 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:164> | 164 to 168 | 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 238 | 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 254 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabStrains | 268 to 289 | 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:280> | 280 to 284 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabStrain | 291 to 341 | 11 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:312> | 312 to 316 | 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:331> | 331 to 335 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabReagents | 343 to 389 | 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:356> | 356 to 360 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| supplier | 362 to 362 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| number | 363 to 363 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| link | 364 to 373 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| protocols | 374 to 383 | 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:376> | 376 to 381 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabEquipment | 391 to 427 | 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:404> | 404 to 408 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| manufacturer | 410 to 410 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| rotor | 411 to 411 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| protocols | 412 to 421 | 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:414> | 414 to 419 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| UsedIn | 430 to 443 | 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:435> | 435 to 439 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabReagent | 449 to 472 | 8 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| LabEquipmentItem | 478 to 486 | 5 | 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/kb/registry/align.mjs, app/kb/registry/catalog.mjs, app/kb/registry/kinds.mjs. Imported by app/routes/lab.kind.tsx, app/routes/lab.tsx.
Source
import { Catalog } from "capsomer/react/catalog";
import type { CatalogResult } from "capsomer/behaviour/catalog";
import {
INVENTORY,
EQUIPMENT_CATALOG,
PRIMERS,
REAGENTS,
STRAINS,
kindLabel,
mateOf,
pairProduct,
phageYearsText,
productSpanText,
productText,
siteText,
reagentLabel,
reagentSource,
tmNote,
type EquipmentRow,
type InventoryRow,
type PrimerListRow,
type PrimerRow,
type ReagentRow,
type StrainRow,
} from "~/kb/registry/catalog.mjs";
import { MAX_MISMATCHES } from "~/kb/registry/align.mjs";
import { KINDS } from "~/kb/registry/kinds.mjs";
/**
* The registry's pages (docs/REGISTRY.md): the master inventory, a kind's own catalog and one item. Each reads the same
* rows the markdown twin does, and each fact it prints is the record's or is computed from it, so no page types a
* number the record could contradict.
*/
/** One sentence under the inventory's title: how many of what, counted from the rows. */
function inventorySentence(rows: InventoryRow[]) {
if (rows.length === 0) return "Nothing is recorded yet.";
const counts = new Map<string, number>();
for (const row of rows) counts.set(row.kind, (counts.get(row.kind) ?? 0) + 1);
// A kind's plural is the registry's own (equipment has none other than itself); a phage's is its name and an s.
const parts = [...counts].map(([kind, n]) => `${n} ${n === 1 ? kindLabel(kind).toLowerCase() : (KINDS[kind]?.plural ?? `${kindLabel(kind).toLowerCase()}s`)}`);
return `${parts.join(", ")}.`;
}
export function LabInventory({ rows, result }: { rows: InventoryRow[]; result: CatalogResult<InventoryRow> }) {
return (
<>
<p className="registry-lede">
The lab’s reference catalog: its primers, the bacterial strains its phages grow on, the reagents and the equipment its protocols use, with the phages it has isolated. It records what a thing is,
never how much of it the lab has or where it sits. {inventorySentence(rows)}
</p>
<div className="registry site-catalog site-catalog-wide">
<Catalog
definition={INVENTORY}
result={result}
labelledBy="registry-title"
title={(row) => (
<a className="cap-table-open" href={row.path}>
{row.name}
</a>
)}
/>
</div>
</>
);
}
const sequenceCell = (p: PrimerListRow) => (p.sequence ? <code className="registry-seq">{p.sequence}</code> : "");
export function LabPrimers({ result }: { result: CatalogResult<PrimerListRow> }) {
const note = tmNote();
return (
<>
<p className="registry-lede">
The primers the lab’s protocols use, with each sequence as stored. Length, GC content and melting temperature are
computed from the sequence, and a pair’s product is computed from where its primers bind a reference sequence.
</p>
<div className="registry site-catalog site-catalog-wide">
<Catalog
definition={PRIMERS}
result={result}
labelledBy="registry-title"
title={(p) => (
<a className="cap-table-open" href={p.path}>
{p.name}
</a>
)}
cells={{ sequence: sequenceCell, product: (p) => productText(p.product) ?? "" }}
/>
</div>
<p className="registry-note">
{note.statement} The Tm estimate is not an annealing temperature. For the annealing temperature of a particular polymerase, use the{" "}
<a href={note.calculator.url}>{note.calculator.name}</a>.
</p>
</>
);
}
function Fact({ term, children }: { term: string; children: React.ReactNode }) {
return (
<>
<dt>{term}</dt>
<dd>{children}</dd>
</>
);
}
/** A fact that could not be found, with the reason it could not, so it is listed rather than left out. */
function NotFound({ reason }: { reason: string | null }) {
return <span className="registry-missing">Not found: {reason ?? "not recorded"}</span>;
}
/** The size a paper states, compared with the one computed, only where the paper states an exact size. */
function publishedVersusComputed(published: string, computed: Array<{ length: number }>) {
const exact = /^([0-9]+) bp$/.exec(published);
const sizes = [...new Set(computed.map((p) => p.length))];
if (!exact || sizes.length !== 1) return null;
return Number(exact[1]) === sizes[0] ? "equal to the computed size" : `different from the computed size, ${sizes[0]} bp`;
}
export function LabPrimer({
primer,
primers,
usedBy,
}: {
primer: PrimerRow;
primers: PrimerRow[];
usedBy: Array<{ path: string; title: string }>;
}) {
const mate = mateOf(primer, primers);
const pair = pairProduct(primer, primers);
const note = tmNote();
return (
<>
<dl className="registry-facts">
{primer.sequence ? (
<Fact term="Sequence (5′ to 3′)">
<code className="registry-seq">{primer.sequence}</code>
</Fact>
) : null}
{primer.reverseComplement ? (
<Fact term="Reverse complement (5′ to 3′)">
<code className="registry-seq">{primer.reverseComplement}</code>
</Fact>
) : null}
{primer.direction ? <Fact term="Direction">{primer.direction}</Fact> : null}
{primer.target ? <Fact term="Target">{primer.target}</Fact> : null}
{primer.set ? <Fact term="Set">{primer.set}</Fact> : null}
{mate ? (
<Fact term="Pairs with">
<a href={mate.path}>{mate.name}</a>
</Fact>
) : null}
{primer.length ? <Fact term="Length">{primer.length} nt</Fact> : null}
{primer.gcPercent !== null ? <Fact term="GC content">{primer.gcPercent}%</Fact> : null}
{primer.tm !== null ? <Fact term="Melting temperature estimate (Tm)">{primer.tm} °C</Fact> : null}
<Fact term="Annealing temperature">
Depends on the polymerase: <a href={note.calculator.url}>{note.calculator.name}</a>
</Fact>
{usedBy.length > 0 ? (
<Fact term={usedBy.length === 1 ? "Used in" : "Used in these protocols"}>
<ul className="registry-used">
{usedBy.map((protocol) => (
<li key={protocol.path}>
<a href={protocol.path}>{protocol.title}</a>
</li>
))}
</ul>
</Fact>
) : null}
</dl>
{primer.tm !== null ? (
<p className="registry-note">
{note.statement} The Tm estimate is not an annealing temperature. For the annealing temperature of a particular polymerase, use the{" "}
<a href={note.calculator.url}>{note.calculator.name}</a>.
</p>
) : null}
<h2 id="literature">In the literature</h2>
<dl className="registry-facts">
<Fact term="Published in">
{primer.publishedIn ? (
primer.publishedDoi ? (
<a href={`https://doi.org/${primer.publishedDoi}`}>{primer.publishedIn}</a>
) : primer.publishedUrl ? (
<a href={primer.publishedUrl}>{primer.publishedIn}</a>
) : (
primer.publishedIn
)
) : (
<NotFound reason={primer.publishedInMissing} />
)}
</Fact>
<Fact term="Name in the paper">{primer.publishedName ?? <NotFound reason={primer.publishedNameMissing} />}</Fact>
<Fact term="Sequence in the paper">
{primer.publishedSequence ? (
<>
<code className="registry-seq">{primer.publishedSequence}</code>{" "}
{primer.matchesPublished ? (
<span>Identical to the stored sequence.</span>
) : (
<strong className="registry-differs">Differs from the stored sequence.</strong>
)}
</>
) : (
<NotFound reason={primer.publishedSequenceMissing} />
)}
</Fact>
{primer.publishedProduct || primer.direction === "forward" ? (
<Fact term="Product in the paper">
{primer.publishedProduct ? (
<>
{primer.publishedProduct}
{pair && publishedVersusComputed(primer.publishedProduct, pair.products)
? `, ${publishedVersusComputed(primer.publishedProduct, pair.products)}`
: null}
</>
) : (
<NotFound reason={primer.publishedProductMissing} />
)}
</Fact>
) : null}
</dl>
<h2 id="reference">On the reference</h2>
<dl className="registry-facts">
{primer.placement ? (
<>
<Fact term="Reference">
<a href={`https://www.ncbi.nlm.nih.gov/nuccore/${primer.placement.reference.accession}`}>{primer.placement.reference.id}</a>:{" "}
{primer.placement.reference.description}
</Fact>
<Fact term={primer.placement.sites.length === 1 ? "Position" : "Positions"}>
<ul className="registry-used">
{primer.placement.sites.map((site) => (
<li key={`${site.start}-${site.strand}`}>{siteText(site)}</li>
))}
</ul>
</Fact>
</>
) : (
<Fact term="Reference">
{primer.reference
? `${primer.reference}: the primer binds it nowhere within ${MAX_MISMATCHES} mismatches.`
: "None recorded."}
</Fact>
)}
{pair ? (
<Fact term={`Product with ${mate?.name ?? "its pair"}`}>
<ul className="registry-used">
{pair.products.map((product) => (
<li key={product.start}>{productSpanText(product)}</li>
))}
</ul>
</Fact>
) : null}
</dl>
<p className="registry-note">
Positions and product sizes are computed from the sequence and the reference by the site’s own tested primer code, in
the accession’s own coordinates, and are not copied from a paper. A primer is looked for exactly first, then with
more mismatches up to {MAX_MISMATCHES}, and any mismatch is shown.
</p>
</>
);
}
export function LabStrains({ result }: { result: CatalogResult<StrainRow> }) {
return (
<>
<p className="registry-lede">
The bacterial hosts the lab’s phages are isolated on. Each strain’s phages are counted from the phages table,
so a phage added there is counted here with no edit to the strain.
</p>
<div className="registry site-catalog site-catalog-wide">
<Catalog
definition={STRAINS}
result={result}
labelledBy="registry-title"
title={(s) => (
<a className="cap-table-open" href={s.path}>
{s.name}
</a>
)}
/>
</div>
</>
);
}
export function LabStrain({ strain, usedBy }: { strain: StrainRow; usedBy: Array<{ path: string; title: string }> }) {
const years = phageYearsText(strain);
return (
<>
<dl className="registry-facts">
{strain.organism ? <Fact term="Organism">{strain.organism}</Fact> : null}
{strain.strain ? <Fact term="Strain">{strain.strain}</Fact> : null}
{strain.collection && strain.collectionNumber ? (
<Fact term="Culture collection">
{strain.collection} {strain.collectionNumber}
</Fact>
) : null}
{strain.guideUrl ? (
<Fact term="SEA-PHAGES Guide page">
<a href={strain.guideUrl}>{strain.guideUrl.replace(/^https:\/\//, "")}</a>
</Fact>
) : null}
<Fact term="Biosafety level">{strain.biosafetyLevel ?? <NotFound reason={strain.biosafetyLevelMissing} />}</Fact>
{usedBy.length > 0 ? (
<Fact term={usedBy.length === 1 ? "Used in" : "Used in these protocols"}>
<ul className="registry-used">
{usedBy.map((protocol) => (
<li key={protocol.path}>
<a href={protocol.path}>{protocol.title}</a>
</li>
))}
</ul>
</Fact>
) : null}
</dl>
<h2 id="phages">Phages isolated on it</h2>
{strain.phages.length === 0 ? (
<p>No phage in the lab’s table names this host.</p>
) : (
<>
<p>
{strain.phages.length} phage{strain.phages.length === 1 ? "" : "s"}, found {years}, counted from the phages table.
</p>
<ul className="registry-used registry-phages">
{strain.phages.map((phage) => (
<li key={phage.name}>
<a href={phage.path}>{phage.name}</a>, {phage.year}
</li>
))}
</ul>
</>
)}
</>
);
}
export function LabReagents({ result }: { result: CatalogResult<ReagentRow> }) {
return (
<>
<p className="registry-lede">
The substances the lab’s protocols use, apart from the samples they work on, in one table. A reagent is bought (a supplier
and a catalog number, where a record in the repository states them) or prepared in the lab (a link to its recipe). Each row
lists the protocols that use it, and each protocol’s own table has its amount, stock and final concentration.
</p>
<div className="registry site-catalog site-catalog-wide">
<Catalog
definition={REAGENTS}
result={result}
labelledBy="registry-title"
title={(r) => (
<a className="cap-table-open" href={r.path}>
{reagentLabel(r)}
</a>
)}
cells={{
supplier: (r) => (reagentSource(r) ?? <NotFound reason={r.supplierMissing} />),
number: (r) => (r.preparedInLab ? "" : (r.catalogNumber ?? <NotFound reason={r.catalogNumberMissing} />)),
link: (r) =>
r.productUrl ? (
<a href={r.productUrl}>Product page</a>
) : r.recipePath ? (
<a href={r.recipePath}>Recipe</a>
) : r.preparedInLab ? (
<NotFound reason={r.recipeMissing} />
) : (
""
),
protocols: (r) => (
<>
{r.uses.map((u, i) => (
<span key={u.path}>
{i > 0 ? "; " : null}
<a href={u.path}>{u.title}</a>
</span>
))}
</>
),
}}
/>
</div>
</>
);
}
export function LabEquipment({ result }: { result: CatalogResult<EquipmentRow> }) {
return (
<>
<p className="registry-lede">
The instruments and labware the lab’s protocols list under Equipment, in one table. A manufacturer appears where a record in
the repository states it, and a centrifuge’s rotor is recorded as a gap until Dustin names it. Each row lists the protocols
that use it, and each protocol’s own table has its words and settings for the item.
</p>
<div className="registry site-catalog site-catalog-wide">
<Catalog
definition={EQUIPMENT_CATALOG}
result={result}
labelledBy="registry-title"
title={(e) => (
<a className="cap-table-open" href={e.path}>
{e.name}
</a>
)}
cells={{
manufacturer: (e) => e.manufacturer ?? <NotFound reason={e.manufacturerMissing} />,
rotor: (e) => (e.rotor ?? (e.rotorMissing ? <NotFound reason={e.rotorMissing} /> : "")),
protocols: (e) => (
<>
{e.uses.map((u, i) => (
<span key={u.path}>
{i > 0 ? "; " : null}
<a href={u.path}>{u.title}</a>
</span>
))}
</>
),
}}
/>
</div>
</>
);
}
/** The protocols that use an item, each linked: computed from the protocols that name it, never stored on the item. */
function UsedIn({ uses }: { uses: Array<{ path: string; title: string }> }) {
if (uses.length === 0) return <Fact term="Used in">No published protocol names it yet.</Fact>;
return (
<Fact term={uses.length === 1 ? "Used in" : "Used in these protocols"}>
<ul className="registry-used">
{uses.map((protocol) => (
<li key={protocol.path}>
<a href={protocol.path}>{protocol.title}</a>
</li>
))}
</ul>
</Fact>
);
}
/**
* One reagent (docs/KNOWLEDGE-BASE.md, A1): what it is, where it comes from (a supplier and a catalog number, or the lab's recipe),
* every gap with its reason, and the protocols that use it. Each protocol's own table has its amount, stock and final.
*/
export function LabReagent({ reagent: r }: { reagent: ReagentRow }) {
return (
<dl className="registry-facts">
{r.abbreviation ? <Fact term="Abbreviation">{r.abbreviation}</Fact> : null}
{r.contents ? <Fact term="Contents">{r.contents}</Fact> : null}
{r.preparedInLab ? (
<Fact term="Source">
Prepared in the lab{r.recipePath ? <>, from its <a href={r.recipePath}>recipe</a></> : <>; <NotFound reason={r.recipeMissing} /></>}
</Fact>
) : (
<>
<Fact term="Supplier">{r.supplier ?? <NotFound reason={r.supplierMissing} />}</Fact>
<Fact term="Catalog number">{r.catalogNumber ?? <NotFound reason={r.catalogNumberMissing} />}</Fact>
</>
)}
{r.productUrl ? (
<Fact term="Product page">
<a href={r.productUrl}>{r.productUrl.replace(/^https:\/\//, "")}</a>
</Fact>
) : null}
<UsedIn uses={r.uses} />
</dl>
);
}
/**
* One item of equipment (docs/KNOWLEDGE-BASE.md, A1): who makes it, a centrifuge's rotor, every gap with its reason, and the
* protocols that use it. Each protocol's own table has its words and settings for the item.
*/
export function LabEquipmentItem({ equipment: e }: { equipment: EquipmentRow }) {
return (
<dl className="registry-facts">
<Fact term="Manufacturer">{e.manufacturer ?? <NotFound reason={e.manufacturerMissing} />}</Fact>
{e.rotor || e.rotorMissing ? <Fact term="Rotor">{e.rotor ?? <NotFound reason={e.rotorMissing} />}</Fact> : null}
<UsedIn uses={e.uses} />
</dl>
);
}