Plasmid
The no-plasmid workspace uses the shared discovery entrance: local DNA paste is visible first, pUC19 and pBR322 are labelled opt-in examples, and file opening and explicit NCBI fetch sit in a secondary disclosure. Armed accessions and failed reads remain visible; an Explainers link still opens the empty explainer view. Once a plasmid opens, the subject and work tabs replace this entrance.
Plasmid (registry slug plasmid-map, product name "Drybench Plasmid") draws a plasmid as a circular or linear map, annotates it from a small library of NCBI-sourced parts, finds restriction sites under dam and dcm methylation, picks diagnostic digests with a schematic gel, finds sequencing-primer sites and computes bench amounts. This page describes how the code does each of these, how it is checked, and where it is incomplete or diverges from its design documents.
Purpose, users and status
The design brief (docs/design/brief-plasmid-map.md §1, §2) frames the app as the answer to "what is in this plasmid, and how do I check it?" for students and instructors, iGEM and synthetic-biology teams, bench scientists without a SnapGene seat, and sequencing or plasmid cores. It is not meant to compete with SnapGene or Benchling on cloning depth. Its intended edges are the diagnostic digest picker, numbers shown with their evidence, universal sequencing-primer sites, and maps that can be shared as links for small vectors.
| Registry field | Value (content/apps/plasmid-map.ts, content/apps/identity.ts) |
|---|---|
| Status, version, updated | preview, 0.11.0, 2026-10-11 |
| Names | "Drybench Plasmid", short name "Plasmid" (content/app-names.ts); the brief calls it a working name until Q-08 closes |
| Identity | accent lime, mark loop, workspace group bench, kind tool, working title "Plasmid map" |
| Landing page | Listed since D-0027 (6 October 2026), while still in preview |
| Shell | rail: "sections", inspector available (hidden until explicitly opened; D-0060), no stage, status chip network, orientation "New plasmid" |
| Data policy | compute: "browser+public-apis", storage: "browser-only", wasm: false, flags.workers: true, shareAlikeData: false |
| Objects | owns construct; reads and writes construct, sequence, target; object tabs for all three |
notMedicalAdvice |
true, with a biosafety limit line |
The registry's capability and FAQ copy (content/apps/plasmid-map.ts, content/tools/plasmid-map.ts) is the product's own description. Where this page and that copy differ, the code is described here and the difference is listed under Known gaps, defects and divergences.
Addresses and pages
Addresses come from the route map (content/apps/routes.ts; segment plasmid). See Registry and content for how tasks, explainers and the route helpers work.
| Address | What it is | Source |
|---|---|---|
/plasmid |
Product page | Registry page copy |
/plasmid/app |
The workspace; never indexed | src/apps/plasmid-map/ |
/plasmid/app#s=... |
Workspace state in the fragment | src/components/tools/plasmid-map/address.ts |
/plasmid/app?acc=<accession> |
Inbound accession; moved into the fragment and armed, never fetched automatically | HANDOFF_CHECKS.acc in src/lib/handoff/params.ts |
/plasmid/<task> |
Nine prerendered task pages: digest-picker, ng-to-pmol, restriction-cloning, golden-gate, gibson, crispr, open-dna-file, puc19, pbr322 |
src/apps/plasmid-map/tasks.marketing.tsx, examples.server.ts, islands/ |
/plasmid/explainers, /plasmid/explainers/<id> |
Ten explainers: how-to-read-a-plasmid-map, origin-of-replication, ampicillin-resistance, promoters, coding-sequences-and-reading-frames, restriction-enzymes, dam-dcm-methylation, diagnostic-restriction-digest, sequencing-primers, selection-markers |
content/apps/plasmid-map.explainer-index.ts, plasmid-map.explainers.ts |
/docs/plasmid |
Docs page, with generated verification tables | content/apps/plasmid-map.docs.ts |
/data/plasmid-map/{features,enzymes,primers}.<hash10>.json |
Versioned data files, served Cache-Control: public, max-age=31536000, immutable (registry cacheRules) |
public/data/plasmid-map/ |
/data/plasmid-map/examples/<id>.<version>.json |
The two shipped example sequences | public/data/plasmid-map/examples/ |
/verification/plasmid-map/<version>/ |
Published oracle fixtures, one write-once directory per app version (0.1.0 to 0.11.0) | pipeline/plasmid-map/publish.py |
src/apps/plasmid-map/page-ids.ts declares no workspace sub-pages (PAGE_IDS = []).
File map
| Path | Role |
|---|---|
content/apps/plasmid-map.ts |
Registry entry content: capabilities, limits order, sources, excluded sources, endpoint, oracles, verification sets, tasks, cache rules, FAQ |
content/apps/plasmid-map.entry.ts |
Joins the content with its identity (withIdentity); imported by the app chunk only |
content/tools/plasmid-map.ts |
The "one copy" of the write-up, sources and limits; enzyme and feature counts read from the manifests |
content/apps/plasmid-map.methods.ts |
METHODS, ASSEMBLY_METHODS (MethodRefs with verification state) and FORMULAE |
content/apps/plasmid-map.verification.ts |
FIXTURES_DATE, BIOPYTHON_VERSION, FIXTURE_COUNTS, verificationNotes |
content/apps/plasmid-map.{shell,workspace,words,tasks,docs,research,dna-words,explainers,explainer-index}.ts |
All other copy |
content/apps/enzyme-chooser.ts, enzyme-sites.ts, methylation-limits.ts |
The enzyme chooser's words, the site and host words, and the two methylation limit lines (a leaf with no imports), shared with Sequence since the chooser move of 17 October 2026; plasmid-map.words.ts and plasmid-map.workspace.ts re-export or read them |
src/apps/plasmid-map/definition.ts, entry.tsx, workspace.tsx |
Shell definition (rail, keys 1 to 6, palette commands, share spec), chunk entry, lazy Main |
src/apps/plasmid-map/store.ts |
publishPlasmid / usePlasmid: the workspace's published snapshot for shell-rendered parts |
src/apps/plasmid-map/views/plasmid-workspace.tsx |
The workspace (about 2,050 lines): inputs, arrivals, worker calls, keeping, autosave, share link, exports, Send to |
src/apps/plasmid-map/views/clone-evidence.tsx |
Researcher-entered clone observations |
src/apps/plasmid-map/views/feature-inspector.tsx |
The feature inspector card beside the map: the pick's headline rows, keys to the shell's inspector and to the Sequence view |
src/apps/plasmid-map/inspector.tsx |
Inspector content (feature, site or primer detail) |
src/apps/plasmid-map/objects.ts, object-tabs.ts, object-tab.tsx |
Object inbound rules, construct record shape and validator, object-page tabs |
src/apps/plasmid-map/exports.ts, provenance.ts |
Export menu items and provenance chips |
src/apps/plasmid-map/workspace-adapter.ts |
Recorded-result capture for Studies (plasmid-design schema) |
src/apps/plasmid-map/examples.server.ts, tasks.marketing.tsx, task-islands.tsx, islands/*.tsx, verification-table.marketing.tsx |
Task pages computed at build, their client islands, docs verification tables |
src/components/tools/plasmid-map/model.ts |
Plasmid, RecordFeature, FeatureEdit, ConstructData, ShownFeature; text and .dna readers into the model |
src/components/tools/plasmid-map/address.ts |
URL schema plasmidSchema and helpers |
src/components/tools/plasmid-map/data.ts |
Data-version resolution (lib) and loading |
src/components/tools/plasmid-map/protocol.ts, plasmid.worker.ts, worker-client.ts, dna-reader.ts |
Worker messages, worker, main-thread client, the one-shot .dna reader for the task island |
src/components/tools/plasmid-map/map-layout.ts, plasmid-svg.tsx, gel-svg.tsx, gel-caption.ts |
Map geometry, map renderer, gel renderer |
src/components/tools/plasmid-map/genbank-export.ts |
GenBank export rules (/provenance, notes, /translation) |
src/components/tools/plasmid-map/words.ts, format.ts, csv-rows.ts |
Result words, number formats and colours, CSV rows |
src/components/tools/enzyme-chooser/{words.ts,host-control.tsx}, src/lib/seq/enzyme-choice.ts |
Shared with Sequence: siteWords, enzymeGroups, siteCountText, findEnzyme, notInTable; HostControl, HostSummary, methylationIdFor; MAX_SHOWN (40). Plasmid's words.ts, panels/host-control.tsx and address.ts re-export them |
src/components/tools/plasmid-map/eutils-client.ts, examples.ts, file-limits.ts, cloning-example.ts |
NCBI client, example pins, 5 MB file cap, task-example types |
src/components/tools/plasmid-map/panels/*.tsx |
Input, map, features, enzymes, digest, sequence, numbers, explainers panels; host control; detail views; numbers-view.tsx, the Numbers view's on-demand wrapper |
src/components/tools/plasmid-map/numbers.ts |
The bench numbers' inputs, defaults and numbersFor (read by the exports; the panel loads on demand) |
src/lib/seq/enzymes.ts, digest.ts, methylation.ts, methylation-ids.ts, gel.ts |
Restriction engine |
src/lib/seq/features/{model,library,align,annotate,primers,genbank,genbank-write,extract,snapgene}.ts |
Annotation, primer sites and file formats |
src/lib/seq/{ds-fragment,restriction-cloning,golden-gate,gibson,crispr}.ts |
Assembly and guide engines (task pages only) |
src/lib/seq/{dna-amounts,dna-mw,pack,eutils,methods-plasmid}.ts |
Amounts, weight, link codec, E-utilities, method records |
src/lib/seq/sequence-cap.ts |
SEQUENCE_CAP (50,000) as a leaf with no imports, re-exported by eutils.ts and protocol.ts, so the worker does not load the E-utilities reader, the hand-off checks or the URL-state codec |
src/lib/seq/{enzymes-manifest,enzyme-names}.ts, features/{library,primers}-manifest.ts |
Generated manifests; do not edit |
pipeline/plasmid-map/ |
Records, curated CSVs, build scripts, Python oracles, fixtures, verify.ts, publish.py |
e2e/plasmid-map/*.spec.ts |
Browser tests |
scripts/size-budgets/plasmid-map.json |
Chunk budgets |
Workspace architecture
The shell loads src/apps/plasmid-map/entry.tsx (displayName drybench-entry:plasmid-map) only on this app's workspace; see Architecture. definition.ts supplies:
- one rail section of example actions (pUC19, pBR322);
- keys
1to6and palette commands that go to the six views and focus their headings, plus "Keep" and "Reset"; - a share spec built on
plasmidSchema, adescribeLinkfunction that turns ab:payload back into a base count, andnotInLink/onShareFileso that Copy link opens the share dialog with the GenBank file first when the view holds something a link cannot carry.
workspace.tsx loads views/plasmid-workspace.tsx with next/dynamic (ssr: false) inside a ToolBoundary. The workspace publishes a PlasmidSnapshot through store.ts (useSyncExternalStore), which is how the definition's commands, the share dialog and the inspector read and drive it.
All computation runs in one Web Worker (plasmid.worker.ts, marker drybench-worker:plasmid-analysis). The page fetches the three data files, hands them to the worker once per data release (load), and then sends analyse, digest or picker requests. The worker never fetches. A SnapGene .dna file is untrusted binary and is read in the same worker (dna, since 17 October 2026, the CTO's rulings 1 and 6 of that day): the page transfers the file's ArrayBuffer (PlasmidWorkerClient.readDna), the worker runs readSnapGene and posts back the file or its refusal; no data release is needed. The .dna task island has no client, so dna-reader.ts starts a one-shot worker of the same file, transfers the bytes and terminates it on the answer or a 30 s timeout. A worker error or timeout is shown as the "not a SnapGene file" refusal; nothing is half read.
The Explainers and Numbers views load with next/dynamic when first shown (markers drybench-island:plasmid-explainers and drybench-island:plasmid-numbers; the Numbers marker sits on the wrapper numbers-view.tsx, since the ng-to-pmol island renders the panel too). The Assembly view loads by its own import().
The shared enzyme chooser (17 October 2026). Under the CTO's ruling of 14 October 2026 (§2, "The move"; the memo is on the Sequence coordinator's branch), the enzyme chooser's words and pure functions moved, word for word, out of Plasmid's files into three shared content files and two shared modules, so that Sequence's view can read them without importing Plasmid's copy. content/apps/enzyme-chooser.ts holds the five chooser words (search, placeholder, "Show the unique cutters", "Show none", "None"), the two suggestion words, countWords, tableWords and the cap line chooserWords.capReached(shown, total, left, more, what, where). Its two slots are what total counts (Sequence "unique cutters", Plasmid "chosen enzymes") and what draws ("the view", "the map"); the caller passes at most CAP_NAMES (6) names and the count of the rest, which reads "{six names} and {m} more" (the CTO's ruling of 17 October 2026, Part A 4·4; the CPO's words and noun memos). capLine(chosen, cap, what, where) in src/components/tools/enzyme-chooser/words.ts fills it from a chosen list, or returns null when every one is drawn.
Chosen and drawn (REL-250, REL-267; 17 October 2026). Up to MAX_CHOSEN (100) enzymes may be chosen, and all of them are in the link, scanned and listed; the map draws the first MAX_SHOWN (40) in link order. A tick above 40 is accepted, and "Show the unique cutters" chooses every unique cutter. While more than 40 are chosen, the enzyme panel shows the shared cap line with "chosen enzymes" and "the map", then enzymes.notDrawnNote(n) ("Every enzyme you chose is still scanned and listed here; clear one from the map to draw it" or "another"), where n is chosen minus drawn; each chosen row the map does not draw carries enzymes.notOnMap ("not on the map"). "Enzymes shown on the map" counts what is drawn. At 100 chosen, the link's own limit, one paragraph after the count line gives enzymes.chosenLimit(MAX_CHOSEN) ("A link that shares this map carries at most 100 chosen enzymes, and 100 are chosen. Clear one to choose another. …", the CPO's link-limit memo; it never names the drawing cap), every unticked row is disabled with aria-describedby pointing at it, and ticked rows and the keys stay enabled; nothing is said before the limit. A sequence with more than 100 unique cutters would have "Show the unique cutters" choose its first 100, which no line yet says (none of the shipped plasmids comes near it; open with the CPO). content/apps/enzyme-sites.ts holds methylationWords (with the glyphs ✕ and ?) and hostWords, which reads limit and briefLimit from the leaf methylation-limits.ts. The two aliases into Plasmid's shell copy, hostWords.shareRow and tableWords.linkReason, were read by nothing and are removed; their words stay in plasmid-map.shell.ts. No app's definition chunk or entry imports enzyme-chooser.ts or enzyme-sites.ts; Plasmid's chunks take them through its re-exports, so no Plasmid component changed. content/apps/enzyme-chooser.test.ts checks the re-exports, the words and the cap line's two Sequence readings. A change to the shared words needs the CPO and lands in both apps at once.
| Request | Work | Timeout (client) |
|---|---|---|
load |
readLibrary, readPrimerFile, build the 12-mer index |
60 s |
analyse |
annotation, every enzyme's sites with methylation outcomes and the counterfactual for blocked sites, primer sites; returns per-stage milliseconds | 60 s |
digest |
the chosen enzymes' digest | 60 s |
picker |
the diagnostic picker, top 40 candidates and the enzymes left out for uncertain methylation | 120 s |
dna |
readSnapGene on the transferred bytes; no data release needed |
30 s |
worker-client.ts refuses any sequence over 50,000 bases (SEQUENCE_CAP) before posting (OverCap), and the worker checks again. A timeout or a worker error terminates the worker; the next request starts a fresh one and reloads the data. The worker validates the library and primer files but takes the enzyme table as given (no shape check).
Views and state
Views
The six views are shared line tabs: Map, Features, Digest, Sequence, Numbers, Explainers (VIEW_IDS in content/apps/plasmid-map.shell.ts). Visited views stay mounted and hidden, so zoom and unfinished inputs survive tab changes. Opening another plasmid increments viewSession and remounts them. With no plasmid open, the page shows an "Open plasmid" / "Explainers" pair of tabs over the input panel.
| View | Contents |
|---|---|
| Map | A row of stat cards (ReadoutStrip: length and topology, features on the map, enzymes in the table, the reference record or file); the circular or linear SVG map in a panel titled by its layout, with the layout control and the zoom and arm-the-wheel keys in its title row and the layer toggles (features, sites, primers) beneath the drawing; beside it the feature inspector card (views/feature-inspector.tsx); then the enzyme panel (cutter groups, search, host control, sites of the focused enzyme, enzymes shown on the map). The map and the card sit side by side from a 56rem main (@container/main) and stack below it; at phone width labels move to a list under the map |
| Features | Feature table with badges, add / edit / remove / restore; primer-site table with binding words and Tm |
| Digest | Three columns from a 60rem main, stacked below it (panels/digest-panel.tsx): "Choose a test" (the insert's from, to and feature, the enzymes of the digest, the methylation as set under Enzymes, the keys "Find distinguishing digests" and "View restriction sites"); "Expected gel · schematic" (the picker's three-construct gel for the candidate in focus with its band table, then the chosen digest of up to 3 enzymes with its gel, band table and fragment table, under [data-digest-result]); "Comparison summary" (the candidate listbox, the construct-by-construct fragment counts and band sizes of the candidate in focus, "Use this digest"). Both gels keep their export refs (plasmid.gel, the picker's figure). "Lanes and ladder" ([data-lanes-toggle]) loads panels/gel-lanes-panel.tsx on demand (marker drybench-island:plasmid-gel-lanes) in place of the chosen digest's gel: see "Several digests and the ladder" |
| Sequence | Letters 60 per line in groups of ten, with the features crossing each line listed as text |
| Numbers | Double-stranded weight, the 650 Da/bp approximation, pmol, copies/µL, ng for a copy number, ligation insert mass |
| Explainers | List of the ten explainers |
The address
Shared links put state in the #s= fragment (Storage and state, Security and network). view is the subject key: changing it pushes a history entry; every other key replaces.
| Key | Check | Meaning |
|---|---|---|
view |
one of the six ids, default map |
Current view |
seq, alphabet, name |
dictionary checks; alphabet must be dna or dna-circular |
The plasmid itself, plain, b: or z: encoded |
ex |
^[a-z0-9][a-z0-9-]{0,39}@[1-9]\d{0,2}$ |
A shipped example (puc19@1, pbr322@1); pins its own data versions and never carries lib |
acc |
INSDC or RefSeq nucleotide accession, optional version, upper-cased | Armed accession |
mine |
dictionary check | One of the person's constructs (this browser only); never in a share link |
id |
UniProt accession | A protein sent here: the workspace lists kept plasmids that mention it |
lib |
^f([1-9]\d{0,2})\.e([1-9]\d{0,2})\.p([1-9]\d{0,2})$ |
Data versions (features, enzymes, primers) |
meth |
dam-dcm (default, omitted), dam, dcm, none, unmethylated |
Methylation of the DNA to be digested |
enz |
at most 100 table names (MAX_CHOSEN; the CTO's A1 of 17 October 2026) |
The chosen enzymes, in order; the map draws the first 40 (MAX_SHOWN), and the panel says which it does not draw (REL-250, REL-267) |
dig |
at most 3 table names | The digest |
ins |
start-end, 1-based, end below start wraps |
Picker insert |
sel |
[fsp]:<id>@<first base> |
Picked feature, site or primer site |
ng, cn, cp, vng, ibp, ratio |
positive number as typed, at most 24 characters | Numbers inputs (defaults 100 ng, 10 ng/µL, 1e6 copies, 50 ng, 1,000 bp, 3) |
An enz or dig value naming any enzyme the table lacks drops the whole value, with the reason "“X” is not in this table (N enzymes)". There is no feat key: feature edits never travel in a link. SHARE_KEYS lists what a share link may carry; mine is never among them, and acc and seq are never written together.
Arrivals
The arrival effect handles, once each: mine (reads the construct from the object store through readConstruct, restores its saved state), ex (fetches the example file and checks its length against the pin; a lib beside ex is removed with a notice), seq (decoded with decodeSequenceParam; topology from alphabet; writes the current lib if absent), and acc (reported as armed only). A lib naming an unknown version opens the current versions with a notice; a withdrawn version opens its replacement with a notice naming both.
Selection and the inspector
pickKey writes sel. Library calls are keyed lib:<featureId>@<start><strand>; record features rec:<i>, .dna features file:<i>, added features add:<id>. The inspector (inspector.tsx) shows FeatureDetail, SiteDetail or PrimerDetail from the published snapshot and recomputes nothing. The Map view's feature inspector card (views/feature-inspector.tsx, the UI redesign of 13 October 2026) shows the same pick in short form: a "Selected feature / site / primer site" pill, the name in the serif face, and rows for the range, strand, length, the call's word (callWord, with a partial call's gloss), identity and coverage (percentText), mismatches, gaps and the source; for a site the position, cut, strand and methylation label (siteWords); for a primer site the position, strand, binding words and Tm. "See source evidence" opens the shell's inspector (the full detail, the source record and licence, the UniProt rule); "Inspect sequence" goes to the Sequence view. A click or Enter on the map still opens the shell's inspector, as before. With nothing picked the card lists the features on the map to pick from (from sm up; the phone keeps the list under the map). A key with a colon (rec:, file:, add:) is written to sel with the colon dropped and matched back in that same form (REL-27, 887e9c6c, 13 October 2026), so record, file and added features are picked and focused like library calls; e2e/plasmid-map/map-defects.spec.ts picks one. "Inspect sequence" passes the pick's first base to the Sequence view, which scrolls the line holding it to the top of its region (MNT-21, 17 October 2026).
Map rendering
The map is the app's own SVG (map-layout.ts for pure geometry, plasmid-svg.tsx for React). The brief rejected SeqViz and Open Vector Editor (§6).
The circle's centre carries the name in the suite's serif face (var(--font-serif), falling back to the viewer's serif in an exported SVG) over the length and topology; labels are 12 units of the 720-unit viewBox.
Circular map
Fixed geometry, in a 720 × 720 viewBox (CIRCLE):
| Constant | Value | Use |
|---|---|---|
backbone |
radius 200 | Backbone circle; origin tick at the top |
track, band |
20, 13 | Feature tracks step inwards by 20 from the backbone; each arc band is 13 thick |
primer |
222 | Primer arrows (6 thick) just outside the backbone |
tick, siteLabel |
236, 262 | Site ticks run from the backbone to 236; labels at 262 on a leader polyline |
featureLabel |
118 | Feature labels inside the ring (6 px further in per track, up to two tracks), on leader lines |
angleOf(p, n) = 2π (p − 1) / n, clockwise from the top. arcPath draws an annular sector with an arrowhead at the 3′ end; the head is min(0.4 × arc angle, 9 / mid-radius) radians. A full-circle feature is drawn to 2π − 10⁻⁴. Site ticks sit at angleOf(topAfter + 0.5).
Linear map
A 1,000-wide viewBox (LINE: margin 24, axis at y 150, track 22, band 14). The visible window is zoomWindow(n, zoom, centre): size = max(40, round(n / zoom)), centred on the focused feature (or the middle), clamped to 1..n. Zoom doubles or halves between 1 and 64 with +/-, buttons, or the wheel when armed. Axis ticks use the first step in 1, 2, 5, ..., 10,000 giving at most ten ticks. A feature across the origin is drawn as two pieces. A linear molecule always uses the linear map; a circular one can toggle.
Tracks and label placement
assignTrackssorts spans by start (longer first) and puts each in the lowest track where it overlaps nothing;overlaphandles wrapped spans.spreadLabels(circle) sorts wanted angles, pushes any label closer thangapto its predecessor, shifts the run back by half the mean displacement, repeats four times, and shrinksgapto 2π / count when the labels cannot fit. Gaps are 0.16 rad for features and 0.075 rad for sites.spreadRow(line) pushes labels right with a 4-unit gap, using an estimated width of(letters + 2) × 6.2, then pulls the run back to fit.
Feature colours come from featureColour (chart tokens by type). Partial matches are drawn at 0.35 opacity with a dashed outline. Blocked sites get a muted tone, a struck-through label and "✕"; "may be blocked" sites get a dashed tick, the warning tone and "?". One-mismatch primer sites are dashed and unfilled.
Keyboard and accessibility
The wrapper div is the focus target (role="img", aria-label, aria-describedby to the key description); the SVG itself is aria-hidden. Keys: arrows and [/] move between features in map order, Home/End, Enter opens the inspector, +/- zoom (linear only), r resets the zoom. Each move is announced through the shell's single live region. The feature table is the text equivalent. The wheel zooms only when armed.
Input formats
| Source | Path | Limits |
|---|---|---|
| Paste | readPlasmidText → shared parseInput (src/lib/seq/parse.ts) |
Raw letters, FASTA (first record; a toast when there are more) or GenBank; IUPAC DNA letters only (ACGTURYSWKMBDHVN, U becomes T, * and - dropped); protein refused; at most 50,000 bp; topology from the radio for raw/FASTA, from LOCUS for GenBank |
| File | openFile |
At most 5,000,000 bytes (FILE_MAX_BYTES); accepted extensions .gb .gbk .genbank .gbff .fa .fasta .fna .fas .txt .seq .dna; a .dna name, or a first byte 0x09 with more than 20 bytes, goes to the SnapGene reader, run in the plasmid worker; otherwise UTF-8 text, GenBank if any line starts with LOCUS, else FASTA; text files open as circular unless the record says otherwise |
| Example | loadExample |
Same-origin JSON; length checked against the pin |
| NCBI | fetchFromNcbi |
See NCBI E-utilities |
| Shared link | arrival effect | seq up to the dictionary's 1,800 characters |
| Workspace input picker | useMolecularInput |
A molecular input bound from a Study; residues and topology must round-trip exactly |
GenBank reader
src/lib/seq/features/genbank.ts is the suite's one GenBank reader (shared with Sequence; see Sequence). It reads LOCUS (name, length, bp/aa/rc, molecule type, topology), DEFINITION with continuation lines, ACCESSION, VERSION (a lone . reads as none), the FEATURES table and ORIGIN (digits and spaces removed, case kept). Records are split at // lines.
Locations: a..b, a single base, </> partial ends, complement(...), join(...), complement(join(...)), join(complement(...),complement(...)) (parts stored in plus-strand order), and joins across the origin. Mixed strands, nested complements, references to other records, between-base locations (1^2) and order(...) are kept as text with no parts; the map leaves such features out (recordFeaturesOf). Quoted qualifiers lose outer quotes and "" becomes "; /translation continuations are joined without spaces, other qualifiers with one space.
recordFeaturesOf drops source features, drops a gene that only repeats a CDS's span, names each feature from the first of label, gene, product, note, bound_moiety, regulatory_class, standard_name (cut to 80 characters) and drops any feature with a part outside the sequence. The full record is also kept as originalRecord (residues, features, aliases, COMMENT paragraphs and the original text) so an export can reproduce untouched locations and features the map does not draw.
A GenBank paste is named from LOCUS, not DEFINITION. The parser's "LOCUS says N bp, the ORIGIN holds M" warning is not surfaced by the plasmid workspace.
SnapGene .dna reader
src/lib/seq/features/snapgene.ts is a clean-room, read-only reader written from the public packet description (brief §10.1, §10.6). It runs only in the plasmid worker (plasmid.worker.ts, request dna), never on the main thread. A file is a run of packets: one type byte, a big-endian 32-bit length, data.
| Packet | Read as |
|---|---|
0x09 cookie |
"SnapGene", then sequence type (must be 1, DNA), export version, import version (each big-endian 16-bit); import version above MAX_IMPORT_VERSION = 19 is refused |
0x00 DNA |
flags byte (bit 0 = circular), then IUPAC bases |
0x0A features |
XML <Feature name type directionality> (directionality="2" is minus, anything else plus), <Segment range="a-b"> 1-based inclusive, a > b wraps, type="gap" segments skipped; `<Q name><V text |
0x05 primers |
XML <Primer name sequence>, <BindingSite location="a-b"> 0-based inclusive, boundStrand 0/1; turned into primer_bind features |
0x06 notes |
XML; kept as name/text pairs, retained in the inputs export, shown only on request |
Refusal kinds: not-snapgene, not-dna, unknown-version, unknown-packet (any other type byte), truncated, length-mismatch (declared DNA length disagreeing with the bases, detected by a base letter where the next packet's type byte should be, or a non-base inside the DNA packet), bad-xml, outside-sequence, no-dna, two-dna. The XML parser is a small hand-written one (no DOMParser in workers) handling elements, attributes in either quote style, entities, comments, CDATA and declarations. Markup inside values is stripped.
The reader has only been checked on synthetic files made by pipeline/plasmid-map/make-dna-fixtures.py from NCBI records. The same understanding of the format wrote those files and the reader, so they cannot show that real SnapGene files are read correctly. Real SnapGene files commonly carry packet types outside the five known ones and are refused until the pre-live check with real files (CSO option 3, docs/progress/app-plasmid.md). Nothing is ever written in .dna format.
Shipped examples
EXAMPLES in examples.ts pins each example's record version, sequence SHA-256, length, data versions (f1.e1.p1), sequence module version 1.1.0, plasmid engine version 1.0.0, and opening state.
| Key | Record | Length | Opening enzymes |
|---|---|---|---|
puc19@1 |
L09137.2, "Cloning vector pUC19c, complete sequence" | 2,686 bp | EcoRI, SacI, KpnI, SmaI, BamHI, XbaI, SalI, PstI, SphI, HindIII |
pbr322@1 |
J01749.1, "Cloning vector pBR322, complete sequence" | 4,361 bp | EcoRI, HindIII, BamHI, SalI, PstI |
Example files are written by pipeline/plasmid-map/build-examples.mts from saved records, so an ex link never contacts NCBI. Under library 1, pUC19 gets six calls (M13mp19 polylinker 396..452, lac promoter 470..591, lac operator 487..507, CAP site 554..591, RNA I 1309..1415, bla 1626..2486) and four primer sites. pBR322 gets four calls (tetA, rop, RNA I, bla) and no primer sites (pipeline/plasmid-map/fixtures/examples/*.json). pUC19's lacZα is not drawn: no submitter-annotated NCBI record of it was found (CSO ruling, recorded in docs/progress/app-plasmid.md).
NCBI E-utilities
eutils-client.ts and src/lib/seq/eutils.ts fetch one GenBank record by accession:
- The typed accession is normalised by the dictionary's nucleotide-accession regex (RefSeq
XX_forms and INSDC shapes, optional.version). The shape cannot tell protein from nucleotide accessions; a wrong one fails at NCBI. requestConsentshows the shared consent dialog (host, item, terms URL, budget "one accession: 1 request"). Declining sets a problem message and sends nothing. An inboundacconly fills the field.fetchWithTimeoutrequestshttps://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore&id=<acc>&rettype=gbwithparts&retmode=text&tool=drybench-plasmid&email=<maker email>under serviceeutils: consentalways, one lane, concurrency 1, at least 400 ms apart, 20 s timeout, the layer's default retry policy (src/components/suite/net/limiter.ts,fetch.ts). No API key.readEfetchGenBankaccepts exactly one LOCUS record of nucleotides, not empty, with no protein-only letters and notaa, at most 50,000 bases. Refusals:not-genbank,several,empty,not-dna,too-long.
The endpoint is declared in the registry with workspaceOnly: true, so the object pages' CSP omits NCBI (Security and network). The opt-in fetch sends the accession and the user's IP address to NCBI; the limits copy says so.
Data files and versions
Three data files load from this site, once per session and only after a plasmid is open (loadData in data.ts). Each published version is an immutable file named by a 10-hex content hash and listed in a generated manifest.
| Data | Version 1 | Raw / gzip size (measured 10 Oct 2026) | Manifest |
|---|---|---|---|
| Enzyme table (with methylation) | 223 enzymes, built 2026-10-06, enzymes.8829d35c83.json |
81,519 / 7,773 bytes | src/lib/seq/enzymes-manifest.ts |
| Annotation library | 50 features from 33 NCBI records, built 2026-10-06, features.a4999a99f8.json |
60,437 / 18,920 bytes | src/lib/seq/features/library-manifest.ts |
| Primer set | 6 primers, built 2026-10-06, primers.3ebeea616e.json |
4,381 / 1,535 bytes | src/lib/seq/features/primers-manifest.ts |
Annotation library 2, the current version since verification 0.14.0: 90 features from 58 NCBI records, features.a7b9c65fae.json, 110,685 / 30,635 bytes, with the 14 marked pairs in the file. It was first built on 17 October 2026 as features.f71a11f8d6.json and rebuilt in place under the same number on 20 October 2026, before either file reached main, when the CSO renamed pucOri from "pUC ori" to "ColE1-type origin" (board/memos/2026-10-20-cso-puc-ori-call.md; the version ruling is board/memos/2026-10-20-cto-library-rebuild-version.md): only that entry's name and note changed, and f71a11f8d6 was never published. Version 1 stays published, and a map link with lib=1 reopens with it; the fixtures written against library 1 (the 0.12.0 annotation cases, the GenBank writer samples and the golden examples puc19@1 and pbr322@1) are checked with library 1 by name, and the golden examples puc19@2 and pbr322@2 with library 2. The docs page's fixture tables name the library each set is checked against. The size note on the app's pages is computed from the manifest's byte count.
resolveLib turns a lib value into versions: absent means current; a malformed or unpublished value means current plus an "unknown" notice; a withdrawn version follows replacedBy (falling back to current) with a notice per step. Withdrawn data are never computed with. No version is withdrawn today.
pipeline/plasmid-map/versions.mts holds the lifecycle: a new version is a draft, rewritten in place until --freeze; after that a changed build is refused unless --bump publishes the next version, and old files stay. src/lib/seq/plasmid-data.test.ts fails on any draft unless ALLOW_DRAFT_DATA=1. See Data pipeline.
Each version entry also carries what copy and the gates need rather than having it typed by hand: records (how many source records the data was built from), bytes and gzipBytes (the published file at gzip level 9). src/lib/seq/plasmid-data.test.ts holds every published library version to the CTO's gate of 256 KB raw and 64 KB gzipped; library 1 measures 60,437 bytes raw and 18,998 gzipped at level 9. scripts/plasmid-annotation-timing.mjs reports the CTO's three timing measures (load ≤ 300 ms, annotate pUC19 ≤ 250 ms, annotate 50 kb ≤ 2.5 s) in Node; against library 1 on a 2026 cloud container they were 20.0, 44.0 and 400.5 ms, medians of five runs after a warm-up. Under D-0074 a measure over its ceiling is reported, not a failure, unless the script is run with --strict.
Enzyme table
pipeline/plasmid-map/enzymes.csv is typed by hand from published facts (CSO ruling, 6 October 2026), never generated from Biopython, EMBOSS or REBASE files. Selection: commercially available Type II enzymes in common use for mapping and cloning, isoschizomers kept as separate rows where the cut or methylation sensitivity differs. Type IIB, nicking, homing and Type III enzymes are excluded. exclusions.csv lists BaeI, BcgI, BsaXI, CspCI (Type IIB) and BssKI (not sold) with their reasons.
Cut convention: top and bottom count bases from the 5′ end of the site as written, in top-strand coordinates; negative values lie 5′ of the site. Examples from the CSV header: EcoRI G^AATT_C is 1,5; KpnI is 5,1; SmaI 3,3; BsaI GGTCTC(1/5) is 7,11; BsmI 7,5; TspRI 7,−2. In table 1, 42 enzymes cut outside their site.
methylation-absences.csv is generated by build-enzymes.mts from enzymes.csv and methylation.csv and lists the enzymes whose site can carry a dam or dcm base but that have no methylation.csv row — 25 of the 82 eligible among the table's 223 rows, counts recomputed at every build so they cannot go stale as the table grows (methylation.csv's own "81 of 222" had already drifted by one and is corrected). The rule is canBeMethylated, the same function the engine classifies with, and reference.py absences checks it with a second, independent implementation that agrees row by row, its output committed as fixtures/methylation-absence-rule.json. The lines close the "no entry in Table I" reason only: they say nothing about an enzyme that has rows none of which covers the mark that landed, which rests on the transcription's completeness instead, because a missing row cannot tell "Table I has no entry" apart from "the entry was missed" (the CSO, 17 October 2026). An empty checkedOn means the check has not been made, never a checked absence; the file is a queue of checks owed, is not published as an artefact, and the library never cites it as evidence. It sits outside pipeline/plasmid-map/fixtures/ on purpose: files there are write-once protected and these dates are meant to be filled in later. src/lib/seq/plasmid-data.test.ts enforces what its preamble claims — the list equals the generated set, a row with a note and no date is an error, and nothing the app ships reads the path. Nothing a scientist reads depends on the dates: an enzyme with no methylation row is already classified "sensitivity not recorded", which makes the digest refuse to present a clean result, the same answer a checked absence would give.
build-enzymes.mts requires every row to carry the check line reference.py wrote ("agrees with Biopython 1.88 Bio.Restriction, REBASE release 404, <date>"), maps each methylation.csv entry onto the site with parseEntryPattern, and classifies each enzyme with canBeMethylated: 141 never, 57 with Table I entries (115 entries in all), 25 not-recorded. DpnI carries requiresMethylation. It also writes src/lib/seq/enzyme-names.ts so the address can validate names without loading the table. Each row links to its REBASE page; the 40 golden-set enzymes' pages carry a read date.
Annotation library
pipeline/plasmid-map/features.manifest.csv names, per feature, an NCBI record, the record's own location, the record's feature key and a label its qualifiers must contain. build-features.mts checks that the saved record (pipeline/plasmid-map/records/<accession>.gb) has exactly that feature, extracts the bases, translates each CDS with the record's table and compares it with /translation, and accepts a uniprot value only when UniProt cross-references it to the CDS's protein_id and the CDS translates to exactly the entry's canonical sequence (records/uniprot.json, records/uniprot-sequences.json).
Library 1: 25 CDS, 9 promoters, 4 replication origins, 3 terminators, 2 poly(A) signals, 2 recombination sites, 2 protein-binding sites, 1 enhancer, 1 intron, 1 misc_feature. Five entries are under 30 bases (short). 17 CDS carry a UniProt entry; aph3Ia, hsvTK and lacI lose theirs because their translations differ from the cross-referenced entry. The longest entry is lacZ at 3,075 bp. The library rule (CSO third addendum) takes a part's own sequence as its submitters annotated it, no wider than the part; pTZ19R's lacZ fragment and pACYC184's p15A origin were removed under it.
Library 2 (Stage C, from 17 October 2026) adds 40 rows for 90 in all, from 58 records (the count the build writes into the library manifest and plasmid-data.test.ts ties to the file; an earlier draft of this sentence said 57), every one of them from the CSO's candidate tables of 11 October 2026 and its ruling 1 of 17 October: the nine gap parts ruled in (T7 terminator, bGH poly(A), EF-1α, PGK, U6 and H1 promoters, WPRE and both lentiviral LTRs), the p15A origin, the ColE1-type origin (pucOri, see below), lacY, lacA, two further polylinkers, thirteen more named fluorescent proteins, the araBAD promoter segment named as a segment, GmR, the pBBR-type Rep, the FLAG and calmodulin-binding coding segments, and seven yeast parts. The CSO's estimate of 120 to 160 was an estimate; the reachable count from eligible records is lower, which it said is acceptable.
The CSO's corrections of 17 October 2026 (board/memos/2026-10-17-cso-library-review-records.md, ...-cso-cross-check-and-overhang.md), applied before the version 2 bump. pauseSite is retyped from terminator to misc_feature: neither AY738222.1's class (polyA_signal_sequence) nor its note ("synthetic; transcriptional pause site") says terminator. The id repA101 becomes repA, because the bases are wild-type pSC101 repA and an id that names an allele travels into exports, share links and pinned calls; the row's note says the library cannot distinguish the repA101 allele from wild-type repA. f1Ori becomes f1Region, displayed "f1 region", because 456 bases of the depositors' "phage f1 region" may not be displayed as an origin; the bounds and the rep_origin key are the depositors' and do not change. rnaI is displayed "RNA I (ColE1-type copy-number control)" so the name does not assert an origin, and its one-base 3′ uncertainty is carried as a note, never as an INSDC partial mark, which would say something else. aph3Ia's note carries the depositors' own "reading frame ?" so the hedge reaches the user and not only the review record. Version 1 stays published, so lib=1 links reopen with the old ids, names and types.
Review records. The build checks that a row matches a feature its record carries; it cannot check who wrote that feature. Every row therefore has a committed review record under pipeline/plasmid-map/reviews/<accession>.md (the CSO's ruling 1): the record as fetched with its file hash, who annotated it and the evidence, which qualifiers a database added later, the stop-list check, and per row the location, the qualifier, the name justification, the uncertainty and a verdict. Candidates kept out keep a record too, under "Not taken", so a hold is as traceable as an accept: 106 candidates reviewed, 90 accepted, 6 held, 10 rejected. src/lib/seq/plasmid-data.test.ts checks the two directions and that every manifest row is accepted. fetch-records.mts lists the candidates in CANDIDATE_RECORDS so their records stay in the index whatever the verdict. All 44 records held before Stage C were re-fetched on 17 October 2026 and came back byte for byte, so no accession version had changed.
How two calls at one place are shown. Two steps, in this order (the CSO, 17 October 2026). Step 1: do the calls meet at one locus? They meet when their overlap on the user's sequence, same strand, is at least DISPLAY_PARAMETERS.spanOverlapRatio of the shorter call's own span. Calls that do not meet are at two loci and both are shown, each with its own span and no label, whatever the marking. Step 2: the marking decides, never the bucket. An identity pair and a pair marked same-element, shared-subsequence or unsettled collapse to the best-scoring call, the other named as also within thresholds — whether the spans coincide, nest or sit a base or two apart, because an offset between two entries' extents is not evidence of two elements. Offsets are published in the cross-check output and never drawn. A different-element pair always shows both calls, and the span geometry decides only the label: contained gets inside naming its container, and where neither contains the other, neither is labelled.
The three display parameters are one frozen block, DISPLAY_PARAMETERS in annotate.ts, deliberately outside BADGE_THRESHOLDS and outside the methods file: they decide what is shown and never whether a call is made. spanOverlapRatio is the same 0.90 as coverageRatio, reused so that no third parameter appears — the two measure different things, an entry's own length and a call's span on the sequence, and the reuse is a choice against parameter proliferation rather than a measured equivalence. reference.py carries the same three names in one block with the same comment, so the oracle and the engine cannot drift, and specificity-v2.test.ts holds the invariant that matters: the parameters are read only by displayAtLoci, which takes them as a required argument; annotate always passes the frozen block, and neither annotate nor indexLibrary accepts other values (the CTO, 17 October 2026). The tests drive displayAtLoci directly: on constructed calls sharing 60 percent of a span the parameter moves what is shown and never edits a call, and on the real display-case sequences no setting moves anything, because every call collapsed beside another shares the whole of the shorter span, which the test asserts. Until 17 October indexLibrary still accepted a display argument that nothing read, left over from a refused design; it is removed.
Pairs. pipeline/plasmid-map/library-pairs.csv marks what geometry cannot decide about two entries that reach each other's thresholds at one place, and travels in the published library file so the display reads it instead of guessing. The bucket itself is geometry's: entries within 90 percent of each other's length are an identity pair (one best call shown, the others named as also within thresholds), a shorter entry found whole inside a longer one is containment, anything else is partial overlap and both calls stand. The 90 percent (COEXTENSIVE_RATIO in annotate.ts) is a display parameter and a judgement: no match threshold and no SEED_K depends on it, so the calls are identical whatever it is set to. For a containment pair the file says whether the two are elements their depositors annotated separately (different-element: both shown, the contained call carrying inside naming its container), one element and a variant or repeat of it (same-element: the best call alone, the other named), or two entries sharing a subsequence by construction (shared-subsequence, the two lentiviral LTRs sharing R and U5: the best call, the other named over the matched span alone, neither drawn inside the other). An unmarked containment pair takes the conservative default: the container alone, the contained entry named. Each marking is justified in the file and in both entries' review records.
The ColE1-type origin (the CSO's ruling of 20 October 2026). DQ059139.1's submitters note their 668-base origin as "pUC ori", but its base 498 is G, the pBR322 state: the entry is identical to pBR322 (J01749.1 2478..3145) and differs from pUC19 (L09137.2 811..1478) at pUC19 1308 alone, where pUC19 carries A. At the one-mismatch tolerance a call cannot tell the pUC origin from the pBR322 origin, so the entry is named at the family level, "ColE1-type origin", and both plasmids get a match. The id pucOri is kept, because ids travel in library-pairs.csv, share links and saved state and are never displayed. The Inspector's Evidence fact carries the entry's limit line, "Does not distinguish the pUC origin from the pBR322 origin.", on this entry's calls only (content/apps/plasmid-map.entry-limits.ts, keyed by library id; the CPO's placement, board/memos/2026-10-20-cpo-cole1-line-and-rel-258-placement.md). The exported GenBank file carries the same sentence once, as the last /note of those calls (genbank-export.ts; the CPO's ruling 5, confirmed by the CSO). No copy-number wording is shown on an origin call. Calling the allele would be a new method and is out of v0.
library-gaps.csv listed 13 common parts for library 1. Nine left the list when library 2 took them in; four stay: HSV TK poly(A) (the candidate record's annotations conflict around it), lacZα (both candidate records overlap engineered polylinkers), CAG (no eligible record found), and the complete araBAD promoter with its AraC and CRP regions (only the 39-base segment entered, under its own name). The app shows the list beside the feature table, so a missing call is not read as absence. The lentiviral LTR and WPRE entries add no gene-therapy claim, and the biosafety line stays. The brief planned 200 to 250 entries and Rfam-sourced RNA features; no Rfam source is used (RNA I comes from J01566.1).
Primer set
primers.csv rows with status=ship are written by build-primers.mts, each with a dated source naming it and a check location in a saved NCBI record where the primer must occur exactly. Six ship: M13 Forward (−20), M13 Reverse (Thermo Fisher MAN0000047), pUC/M13 Forward and Reverse (Promega TM042), T7 Promoter primer (pcDNA3.1 manual), CMV forward (MAN0007209). BGH Reverse, RVprimer3, RVprimer4, GLprimer2, SP6, T7 terminator and the two pGEX primers stay "To be checked".
Analysis methods
Method records live in src/lib/seq/methods-plasmid.ts (PLASMID_ENGINE_VERSION = "1.0.0", whose scope is the records the golden examples pin: restriction, methylation, digest, annotation, primer sites and amounts; the gel, GEL_METHOD 2.0.0 with GEL_RULES.version equal to it by test, and the picker, PICKER_METHOD 1.1.0, carry their own versions, pinned by the gel hand-case and trio v2 fixtures). Each engine re-exports its own record. The registry's METHODS mark restriction, methylation, digest, picker, annotation and amounts as "verified" (against named oracles, below; the picker against trio v2, every verdict agreeing), and the gel and primer sites as "unit-tested". For the shared scientific background see Science overview.
Restriction site search
findSites(sequence, enzyme, circular) in src/lib/seq/enzymes.ts:
- IUPAC matching uses 4-bit masks (A 1, C 2, G 4, T 8). A sequence letter matches only if its mask is a subset of the pattern's, so an ambiguity code in the plasmid never creates a site unless the site allows every base it stands for.
- A non-palindromic site is also searched as its reverse complement (strand
-); a palindromic one once. - On a circle the text is
sequence + sequence[0 .. L−2], scanning start positions 0..n−1, so a site across the origin is found once (acrossOrigin). - Cut positions are
topAfter,bottomAfter= number of top-strand bases before the cut. For+:p + top,p + bottom. For-:p + L − bottom,p + L − top. On a circle they wrap into 1..n (n is the origin); on a line a cut outside 0..n setsoffEnd. Biopython's position isafter % n + 1.
dam and dcm methylation
src/lib/seq/methylation.ts models only dam (G-m6A-TC, both strands) and dcm (C-m5C-WGG at the second C, both strands). methylationMap marks every motif, across the origin of a circle (text extended by four bases), under the host from hostOf(meth): dam-dcm both, dam or dcm one, none and unmethylated neither. The last two compute identically but are kept apart for the control's wording.
For each site occurrence, methylationOutcome:
- DpnI (
requiresMethylation): both strands' A methylated →needs-methylation, cuts; neither → does not cut; one strand only →not-recorded(hemimethylated, not modelled). - No mark on the site's bases →
unmethylated(cuts). - Enzyme with no Table I entries →
not-recorded(no-entry). - Marks are expressed in site coordinates (
siteorcomplementstrand, 1-based position); on a palindromic site both strands collapse ontosite. - Only entries whose printed bases fit this occurrence's actual bases apply.
- An entry naming exactly the observed marks decides, if usable (
qualifierempty, orhemi-slowwith the mark on both strands of a palindrome): only not-cut →blocked; only cut →not-blocked; both → conflict. - Otherwise each mark is judged by single-mark entries. Any blocking mark →
blocked. All marks covered by cut entries →not-blocked. Anything else →not-recordedwith the highest-ranked reason (conflict>hemi-slow>slow>not-listed) and the Table I pages.
Precedence is blocked over may-be-blocked over cuts (CSO ruling). cutsAt maps outcomes to true, false or null; null ("may be blocked") is never counted as a cut. For a blocked site the worker also computes cutsWithout: the outcome with the blocking methylase(s) switched off, for the "Without dam methylation, this site cuts." line (never for DpnI).
Not modelled (stated in the limits): EcoKI, CpG and other host methylation; methylation fractions or kinetics; star activity; hemimethylated DpnI. The scientific audit (docs/science/2026-10-scientific-audit.md, Plasmid) accepts these as stated limits and asks that DpnI wording not imply a kinetic fixture.
Digests and cutter classes
scanEnzymes runs every enzyme with one shared methylation map and gives each site an outcome, geometry first (REL-265, 17 October 2026): a cut past a linear molecule's end is off-end whatever its methylation; otherwise cuts, blocked (a methylase blocks it), needs-methylation (an enzyme that cuts only methylated DNA, DpnI, on an unmethylated site; its outcome now names the methylase that usually makes the mark) or uncertain. Each scan counts cutting, blocked, needsMethylation, offEnd and uncertain, which sum to its sites (tested). blocked was narrowed on 17 October 2026 (REL-257): it no longer counts DpnI's unmethylated sites, so the site table's count line reads "15 sites" for DpnI on unmethylated pUC19 instead of "15 blocked"; the worker passes cutting, blocked and uncertain only (plasmid.worker.ts, the Room track's file), so the needs-methylation count is not shown there until it passes the new field. An off-end site no longer counts as cutting, so a linear molecule's cutter class is from the cuts it actually has. cutterClass gives none, unique, two or more from cutting sites only. In the UI (enzymeGroups) any enzyme with an uncertain site goes into its own "depends" group, and blocked sites count in no group.
digestOf(scans, molecule) collects the cuts of the chosen enzymes by outcome (off-end sites first, adding nothing to the cuts, the blocked counts or the uncertainty; blocked and needs-methylation counted apart; uncertain sites set aside, each with the methylases whose effect is not recorded) and publishes the census: sites, cutting, blocked, needsMethylation, offEnd, uncertain, and census per enzyme with the blocking methylases (sites = the rest summed, tested against digest-causes-v1.json, whose counts come from reference.py's own motif scan). uncut is for the drawing only: a circle, every site known, no cut surviving the filter; it names no cause. Why a lane has no cut is digestCauses (src/lib/seq/digest-causes.ts): a list in the fixed order no-site, all-blocked, needs-methylation, off-end, uncertain, each with its enzymes; laneDrawing gives the one drawing (linear molecule whole: one band, or under 100 bp one molecule not drawn; uncut circle: its forms not modelled; two or more uncertain sites: not known). src/components/tools/plasmid-map/cause-words.ts composes the CPO's sentences from them for the single digest (LaneCauses in the Digest panel) and the lanes, each site-asserting cause with "See the {enzymes} sites", which opens the Map view's restriction sites with the shown enzymes set to exactly those the sentence names (onViewSitesFor, passed by the workspace; without it the key is not shown, since a key opening every site is a weaker route than its sentence implies, ruling of 17 October 2026). fragmentsFrom merges coincident cuts (enzyme names joined with /) and produces:
- circle, no cut: no fragments,
uncut: true; - circle, one cut: one fragment of length n;
- circle, k cuts: k fragments between consecutive cuts, the last wrapping;
- line: fragments between 0, the inside cuts and n.
With exactly one uncertain site the digest carries variants (ifCuts, ifBlocked) and the gel draws two lanes; with more than one, fragments is null and no bands are drawn.
Diagnostic digest picker
trioForInsert(molecule, start, end) builds three molecules from a 1-based insert range: correct (as given), empty (insert removed) and reversed (insert reverse-complemented). An insert across the origin is handled by rotating the circle so the insert starts at base 1.
pickDigests(trio, enzymes, host):
- Pool: enzymes with no uncertain site on any of the three constructs that cut the correct construct 1 to 6 times (
PICKER_POOL.maxCuts). - Candidates: every single enzyme in the pool, and every pair whose distinct cut positions on the correct construct number at most 8 (
pairMaxCuts). - A candidate leaving any construct uncut, or without fragments, is dropped.
- Each construct's sizes become schematic bands (
resolvedBands). Two lanes are "told apart" (toldApart) when their band counts differ or any pair of bands, largest first, differs by more than 5 percent of the larger. marginis the smallest, over the three pairs, of the largest relative difference between paired bands (1 when counts differ).- Sort: digests telling all three apart first, then singles before pairs, larger margin, fewer bands, then names.
The worker returns the pool, the number judged, the number distinguishing all three, the first 40 candidates, and leftOut: enzymes kept out only because a site may be blocked. The workspace offers the picker results as a keyboard listbox. The pool limits and ranking are the app's judgement (method record wording).
Schematic gel
src/lib/seq/gel.ts (GEL_RULES, rules 2.0.0, GEL_METHOD 2.0.0, 17 October 2026): fragments under 100 bp are not drawn; sizes are sorted largest first and a fragment joins the current band when it is within 5 percent, inclusive, of that band's largest fragment ((largest − size) × 20 ≤ largest, integer arithmetic, so 950 against 1,000 is on the boundary and joins), otherwise it starts a new band; a band's fragments are therefore within 5 percent of each other (the CSO's ruling 3; 5 percent is a judgement). Rules 1 compared each size with the band's smallest member and could chain 1,000, 960 and 922 bp into one band. toldApart uses the same integer test on band leaders; layoutGel places bands on one log10 scale between the largest and smallest visible size (markers included) with 4 percent margins, y = 0.04 + 0.92 × (log10 top − log10 size) / span. Markers are round sizes (10,000 to 100 bp), not a vendor ladder. A ladder lane (layoutGel's third argument, Gel.ladder) replaces the markers: each size from src/lib/seq/ladders.ts is one band at its own height on the samples' scale, drawn as listed (no grouping, no 100 bp floor, no intensity or mass), so a 75 bp ladder band is drawn where a 75 bp sample fragment is not. Ladders (LADDERS_VERSION 1, 17 October 2026): "1 kb ladder, 14 bands" (10,000 to 250 bp) and "Wide-range ladder, 15 bands" (20,000 to 75 bp), typed by hand from their supplier's product sheets into pipeline/plasmid-map/ladders.csv with supplier, product, catalogue number, document, revision, URL, date read and the sheet's SHA-256; the module ships only the generic name, sizes, document, revision and dates, and ladders.test.ts checks it against the CSV and that no supplier or product word ships. The licence line is "To be checked" (held for the General Counsel). The gel claims no band position, no sizing by interpolation, no "match" with a ladder band, no migration at any agarose percentage and no supercoiled or nicked behaviour (limit plasmid-map.limit.ladder). No millimetre or Rf positions are given and uncut forms (supercoiled, nicked) are not modelled. gel-svg.tsx draws lanes 92 units wide, wider when a heading needs it (a heading too wide is split after " + " or ", ", with the lane's note, its host, on a line under it), at most 420 CSS pixels wide on screen; the export copy carries the "schematic" caption inside the figure (gelCaptionLines wraps at an estimated 6.2 units per character).
Feature annotation
annotate(sequence, circular, index) in src/lib/seq/features/annotate.ts, with bandedLocalAlign in align.ts:
| Step | Detail |
|---|---|
| Index | 12-mer words (2 bits per base, SEED_K = 12) of every non-short library feature; non-ACGT letters break words |
| Circular extension | Text extended by min(n, longest − 1) bases (3,074 for library 1); hits starting in the extension are discarded as repeats |
| Strands | The extended text and its reverse complement are searched; minus-strand coordinates are mapped back |
| Short features (< 30 bp) | Exact, full-length indexOf matches only, scored 2 × length |
| Seeding | Every shared 12-mer adds a diagonal (text position − feature position) for that feature |
| Clustering | Sorted diagonals split where consecutive ones differ by more than slack = 16 + ceil(0.02 × L); a single-seed cluster is skipped when L ≥ 100 |
| Alignment | Smith-Waterman local alignment with Gotoh affine gaps in a diagonal band: half-width slack + ceil((hi − lo) / 2) around the cluster's mean diagonal, over the text window [lo − slack, hi + L + slack] |
| Scoring | match +2, mismatch −3, gap open −5, gap extend −2 (a gap of k scores −5 − 2(k − 1)) (ALIGNMENT_SCORING) |
| Ties | First maximum by feature row then text column; diagonal before a gap in the feature before a gap in the text; extension preferred over opening at equal score |
| Numbers | identity = identical columns / all columns (gaps included); coverage = aligned feature bases / feature length; mismatches; gap columns; gap openings |
Call (callFor, ε = 10⁻¹²) |
match: identity ≥ 0.99 and coverage ≥ 0.95; partial: identity ≥ 0.90, coverage ≥ 0.50 and ≥ 30 aligned feature bases; features under 30 bp are never partial |
| Gloss | partial: "truncated" when coverage < 0.95, "mutated" when identity < 0.99 |
| Duplicates | Per feature and strand, overlapping calls keep the highest score |
The thresholds are the app's own heuristics (the audit asks for that wording; FORMULAE.thresholds uses it). A part sharing no 12-mer with its library entry is not seeded and cannot be found, which is why "no call does not mean absent" is a limit line. Badges print identity and coverage truncated (never rounded up) to one decimal (percentText); "Exact match" appears only at 100 and 100.
The CTO's spike (docs/progress/app-plasmid.md, 6 October 2026) measured 0.45 to 0.95 s for 50 kb against 250 synthetic features in Node. There is no automated timing test for annotation.
UniProt link rule
uniprotFor attaches a library CDS's UniProt entry to a call only if the call is a match covering the whole entry (featureStart = 1, featureEnd = length) and the call's bases translate (entry's genetic code, initiator read as M, final stop dropped) to exactly the entry's canonical sequence. Otherwise uniprotWithheld records partial, or differs with the substitutions in UniProt numbering and both lengths when they differ. pUC19's bla differs from P62593 at V82I and A182V, so pUC19 sends no protein to Protein. pBR322's bla, tetA and rop carry P62593, P02981 and P03051 (CSO and CPO rulings, 6 October 2026).
Sequencing-primer sites
findPrimerSites scans each primer and its reverse complement (once if palindromic) over the sequence (extended by L − 1 bases on a circle), allowing at most one mismatch. A non-ACGT letter never pairs. Each site records the strand, 1-based start/end (wrapping), mismatchFrom3prime (1 = 3′-terminal base) and threePrimeAt, the base where a read starts. An exact site is never relisted as a one-mismatch site. primerTm uses the sequence module's oligoMelting at its default conditions against a perfect match; no Tm is shown for a mismatched site. Tm conditions are described on Sequence.
Bench numbers
src/lib/seq/dna-amounts.ts with nucleicAcidMass from dna-mw.ts:
MW = sum over both strands of dNMP − H2O residue formulas (CIAAW 2021 atomic weights);
circular: no end groups; linear: 5'-OH ends (+H2O − HPO3 per strand)
pmol = ng × 1000 / MW
copies = ng/µL × 1e-9 × N_A / MW, N_A = 6.02214076e23 mol⁻¹ (exact)
ng = copies × MW / N_A × 1e9
insert = vector ng × (insert bp / vector bp) × ratio
approx = n × 650 Da per bp (shown beside, never instead)
Any base other than A, C, G or T refuses the weight with the offending positions. pUC19 circular double-stranded is 1,659,693.51 g/mol, about 5 percent under the 650 Da/bp rule (docs/progress/app-plasmid.md). The Numbers view, the ng-to-pmol task page and any other caller use this one module. The brief's dna-concentration measurement is registered with plasmid-map as writer but nothing in the app writes it.
Assembly and guide design (worked task examples)
Five engines were added in Wave 2 (docs/design/pre-release-app-depth/molecular.md M5 to M7, and the CSO's Wave 2 ruling). They are pure TypeScript and verified against hand-worked fixtures, but the workspace does not call them. They are run at build time by src/apps/plasmid-map/examples.server.ts to produce one worked example per task page, and the client islands only display results, switch orientation or download them. A user cannot enter their own sequences into any of them today.
Double-stranded ends
src/lib/seq/ds-fragment.ts (DS_FRAGMENT_METHOD 1.3.0) types a cut as (topAfter, bottomAfter): topAfter < bottomAfter is a 5′ overhang of the bases between, > is a 3′ overhang, equal is blunt. An end's seq is the overhang read 5′→3′ on the protruding strand. compatibleEnds requires equal polarity and, for overhangs, equal length with one the reverse complement of the other; blunt joins blunt. flipEnd keeps an end's bases and swaps the protruding strand; reverseFragment swaps and flips both ends, so a fragment and its reverse complement anneal in the same places. Ends are never repaired, filled in or blunted.
Restriction cloning
src/lib/seq/restriction-cloning.ts (1.4.0): digestMolecule cuts a vector and an insert under a host (blocked sites make no cut; uncertain sites make a qualified cut whose ends carry the reason; off-end cuts make none). A circle cut once is opened into one fragment. restrictionClone takes the longest fragment with two enzyme-made ends from each molecule and calls ligate, which tries the insert both ways and returns every orientation whose ends anneal (ambiguous when two differ). assembleRing writes each core plus each shared overhang once (5′ overhang from the downstream end, 3′ from the upstream end) and throws if product length ≠ core bases + overhangs. Refusals: overlapping-cuts, off-end, no-insert-fragment, no-backbone-fragment, incompatible-ends. Only single-insert circles are produced; self-ligation and concatemers are not enumerated. The worked examples are EGFP (U55762.1 679..1398) with EcoRI/HindIII tails into pUC19 (directional, one product) and with BamHI tails (both orientations).
Golden Gate
src/lib/seq/golden-gate.ts (1.4.0): each part and the destination are digested with one Type IIS enzyme. A molecule with more than two sites, no enzyme-ended fragment, or not exactly one released fragment free of the recognition site is refused (internal-site, no-fragment). Every end must be a 5′ overhang (polarity) and non-palindromic. Starting from the destination backbone, the next piece is the unique part whose left end (as supplied or reversed) anneals; more than one candidate is duplicate-overhang, none is no-assembly; the cycle must close on the backbone and the normalised junction overhangs must be distinct. The engine accepts any enzyme; only BsaI and BsmBI are documented and verified. The worked example puts EGFP with BsaI tails (overhangs AATG, GCTT) into a 59-base synthetic demonstration destination.
Gibson-style assembly
src/lib/seq/gibson.ts (1.3.0), GIBSON_LIMITS: 2 to 6 fragments, product at most 50,000 bp, overlap at least 15 bp. terminalOverlap is the longest suffix of one fragment that is a prefix of the next, found exactly. Each overlap (and its reverse complement, unless self-complementary) must occur exactly twice across all fragments' top strands, else repeated-homology. Overlaps are collapsed once; circular plans also drop the closing overlap; material is asserted. No Tm or recommended overlap length is used, and fragments are never flipped. The worked example cuts pUC19 into three arcs with 25 bp overlaps.
The segment map
Each assembly product carries segments, which says where every product base came from: { productStart, length, input, inputStart, orientation }, all 0-based. productStart is the segment's first base on the product's top strand and may wrap the origin of a circular product; input indexes the method's own input list (restriction cloning: 0 the vector, 1 the insert; Golden Gate: 0 the destination then the parts as supplied; Gibson: the fragments as supplied); inputStart is the input base matching that first product base. With orientation "+" product base k is input base inputStart + k, and with "-" it is the complement of input base inputStart - k; both walks are taken modulo the input's length when the input is circular. Feature transfer maps a parent's annotation through this, base by base, rather than searching the product for it.
Segments are a cover, not a partition. Where two inputs are joined their shared bases belong to both, so neighbouring segments overlap by exactly the shared overlap or overhang. For Gibson those bases are the upstream fragment's suffix and the downstream fragment's prefix, equal by the exact-overlap test that makes the join. For restriction cloning and Golden Gate the junction's product letters are the downstream fragment's 5′ protrusion and, equally, the upstream fragment's own top-strand letters past its paired core: compatibleEnds requires one overhang read 5′→3′ to be the reverse complement of the other, which is what makes the two readings the same letters. So a feature that ends inside an overhang, or lies wholly within a Gibson overlap, is carried whole from each parent that holds it rather than reported as truncated by an arbitrary attribution; where both parents hold it, the duplicate rule carries it once and names both. Summing segment lengths double-counts the shared bases: the material accounting is in conserved, which counts each once.
Adding the map was additive — no product's residues move — and each method took a minor version bump for it. The oracle is pipeline/plasmid-map/segment-map.py, which builds its products by its own construction and never reads a junction record; src/lib/seq/segment-map.test.ts checks the engines' segments against it and re-checks every segment against the engine's own product base by base. The attribution of a shared overlap to both neighbours is with the CSO.
Cloning from two sites chosen on the map
src/lib/seq/restriction-from-map.ts (RESTRICTION_FROM_MAP_METHOD 1.0.0) wraps the restriction-cloning engine for the way the work actually starts: the person picks two sites on the circle, not two enzymes. That difference is the module's reason to exist. Every chosen enzyme is checked against every site it has on that molecule, and a chosen enzyme that also cuts the vector, or the insert body, elsewhere withholds the answer and names the position that was not chosen — that digest does not give the fragments the chosen pair implies. A site the host's marks block, a cut beyond the end of a line, and ends whose exposed bases do not pair also withhold it.
A chosen site whose methylation sensitivity the source does not record makes an uncertain cut: a product is shown carrying the uncertainty and clean is false, never a clean product (the CSO's red line 4). Where one enzyme opens both sides the insert anneals either way round, so both products come back with needsChoice and neither is picked. An insert from a PCR product always carries PCR_END_QUALIFICATION, "cutting near a DNA end is not assessed", with no base count: the room an enzyme needs beyond its site varies by enzyme and conditions and this app holds no data for it. One insert only; two-insert ligation, partial digests, fill-in, blunting and phosphatase treatment are not modelled, and self-ligation and concatemers are not enumerated.
Oracle: pipeline/plasmid-map/restriction-from-map.py (9 cases, 5 withheld). Its uncertain-site case is AsiSI, which is the one enzyme in the shipped table whose site (GCGATCGC) must carry a dam mark and which has no row in the hand transcription of McClelland's Table I — so the uncertainty is derived from the source, not assumed.
Golden Gate with several parts
goldenGateMultiPart in src/lib/seq/golden-gate.ts adds what a multi-part build needs over goldenGate: BsaI and BsmBI only, refused on the enzyme alone before any sequence is read; a cap on the pieces (GOLDEN_GATE_MULTI.partCap, 8 and marked provisional until the CSO sets it), checked on the count before any sequence work; and internal sites reported with their positions and, where a part's coding sequence is given, the codon each sits in. Reported, never changed: no part is domesticated and no synonymous codon suggested. internalSitesOf treats the outermost two sites of a part as the pair that releases it; on the circular destination the releasing pair lies on the discarded dropout and position does not say which, so every site is reported once there are more than two.
A recognition site the finished circle carries — including one made across a junction that none of the pieces holds — is now its own refusal, site-created, rather than being reported as an internal site. overhangFacts states which pairs of overhangs differ at exactly one position, as a fact about two sequences with no risk wording. The bases are reported as the junctions expose them, so the positions are relative to that reading and mirror on the other strand; how many positions differ does not. There is no fidelity, mis-ligation or efficiency figure, no recommended overhang and no high-fidelity set, and no datum of Potapov et al. 2018: the CSO's ruling keeps those out of v0 until the method has an independent oracle of its own.
Oracle: pipeline/plasmid-map/golden-gate-multi.py (10 cases, 6 withheld), which designs each circle first and then writes the parts that release its pieces, covering the cap on both sides of the boundary.
Carrying the parents' features onto a product
src/lib/seq/feature-transfer.ts (FEATURE_TRANSFER_METHOD 1.0.0) maps a parent's annotation onto an assembly product through the segment map above, base by base; the product is never searched for a feature's bases. It is a pure module with two imports (./features/genbank.ts types and ./revcomp.ts) and runs in the assembly chunk, never in the admission worker.
Record, file, added and edited features are carried. Library calls are not: the product is annotated afresh, so a carried call would be a claim about the product that nothing checked.
| Status | The words for the person | When |
|---|---|---|
whole |
Intact | Every base maps and the product holds them in one piece. A gap the parent's own location already had — a join — is kept inside that piece, because the parent never held those bases either. This may cross a junction, since a shared overhang's bases are in the product and the parent holds them |
split-at-junction |
Truncated at junction, per piece | Every base maps, but the product holds them in more than one piece — most often a vector feature with the insert ligated inside it. Each piece is its own row and its own GenBank feature, never a join across the bases between them, which the parent's feature never ran through; a piece that wraps the product's origin is one row written as a join, because there the circle is continuous. The accounting counts the parent feature once |
truncated |
Truncated at junction | Some bases map and some do not. Each surviving piece is kept with an INSDC partial mark at the cut end only, by coordinate side rather than by strand, so a minus-strand feature cut at its own 5′ end is marked at the high-coordinate end. cutEnd names the coordinate and the mark, never a strand-relative word. There is no length floor: pUC19's added feature at 396..454 opened with EcoRI and HindIII keeps a five-base piece and a seven-base piece of its fifty-nine, each a row, counting once |
dropped |
Not carried | No base maps. It is listed, never silently forgotten. A source feature is never carried at all, since it describes the record rather than a piece of DNA, and is counted apart as notCarriable |
For each parent, intact + split + truncated + not carried + duplicate equals that parent's features, library calls apart; the engine throws rather than return an accounting that does not balance. A feature is never stitched across two parents, because each is mapped through its own parent's segments alone: one formed at a junction can only appear as a fresh call on the product. Identical features from two parents on one interval are carried once, naming both.
Qualifiers: the parent's own partial marks are kept; protein_id, db_xref, locus_tag, old_locus_tag, inference, experiment and citation are dropped, as the edited-feature rule drops them. One /provenance="carried" is added, the token that extends the export's existing library/added/edited scheme and that migration 3.4 maps to FeatureSource; the whole of the provenance goes in one /note with the fixed prefix "Carried from", naming the parent, its accession, its own location and the method and version that made the product, so a tool that drops /provenance still keeps the sentence. Parent hashes stay in the file's COMMENT block. Where two parents carry one feature the note repeats, one per parent.
/translation is never copied: for an intact coding sequence it is recomputed through checkTranslation, which now lives in src/lib/seq/cds-translation.ts and which the GenBank export re-exports, and only if that check passes; a truncated coding sequence and one under transl_except get none. A coding sequence cut at its 5′ end has its /codon_start recomputed through codonStartAfterTrim, ((codonStart − 1 − trimmed) mod 3) + 1, with the lost bases counted in the feature's own reading direction so a minus-strand feature's moved end is its high coordinate.
The duplicates rule is for genuinely identical annotations. Where two parents annotate identical bases differently — a different name, type or qualifier — both are carried, each attributed to its own parent, and neither is merged under the other's name.
The worked parent is pBR322 (J01749.1), the real record, opened with HindIII and BamHI: bla at complement(3293..4153) carried intact, tet at 86..1276 losing the 290 bases before BamHI's cut so 901 of 1,191 are carried with /codon_start recomputed from 1 to 2, and the promoter P1 at complement(27..33) ending inside HindIII's overhang at 30..33 and so carried intact rather than cut back to complement(27..>29). The oracle asserts those numbers before it writes the fixture.
Carried features are stored as ordinary record features keyed carried:<inputIndex>:<parentKey> (carriedKey). The assembly receipts and admission are unchanged: a carried feature never goes into a receipt. The panel that shows them waits for claude/sequence-wave6-debt, and C2.2's pickKey fix must land before they reach the map.
The oracle is pipeline/plasmid-map/feature-transfer.py (12 cases: 10 assemblies and two metamorphic repeats of the first, 70 feature rows over all of them, 9 of which record a parent feature not carried), which works every case out through Biopython SeqFeature arithmetic, asserts extract() equality against the parent for each carried piece, and writes each product as GenBank and reads it back. Two of its cases are metamorphic: the same assembly read from another origin, and with both inputs supplied on the other strand. A row is one piece the product holds, so a feature split at a junction is two rows and two GenBank features; the counts published in the verification copy are tied to this file by src/lib/seq/transfer-verification-counts.test.ts.
SpCas9 guide sites
src/lib/seq/crispr.ts (1.0.0) enumerates every 20 nt protospacer immediately 5′ of NGG, on both strands (minus-strand sites found from CC on the top strand), wrapping on a circle. Each guide records 1-based start/end, strand, protospacer, PAM, GC fraction, longest homopolymer run, exact occurrences elsewhere in the given sequence on both strands (its own site excluded), and acrossOrigin (the 23-base site crosses the origin). Windows with non-ACGT letters are skipped. There is deliberately no on-target or off-target score, no mismatch-tolerant search and no genome statement. The worked example is the first 80 bp of pUC19, shown as linear.
Several digests and the ladder (17 October 2026)
Built to the screen docs/design/plasmid-stage/digest-lanes-and-figures.md with the CPO's design rulings 1 to 8 (and screens 3 to 6 ruling 3) and the CSO's design rulings 3, 5 and 6, 17 October 2026. Three modules, two of them on demand:
src/components/tools/plasmid-map/gel-lanes.ts(pure,gel-lanes.test.tson pUC19's real digests): the lanes,MAX_LANES6 carried andMAX_DRAWN_LANES4 drawn (dig-lanes.ts); only drawn lanes enterlayoutGel, so a fifth or sixth lane never stretches the scale, and both are named (notDrawnLabels) and listed in the band table ("not drawn on the gel") and their fragment tables. Labels are composed (effectiveLabelForm): the enzymes; "{construct} · {enzymes}", forced whenever two lanes differ by construct; or the lane number with a legend line. The host is each lane's note unless one host covers every lane and the person states it once.laneStatementgives "Lane {k}: {construct} ({identity}), {enzymes}; host {host}; {hidden}." only when the construct and its identity are known (constructIdentity: the accession with its version, else "sequence SHA-256 {12 hex}", never a file name). An uncut circular lane draws no band and "not drawn" inside it, with the CSO's circular sentence; an uncut linear lane is one molecule, whose band is held behindUNCUT_LINEAR_BAND = falseuntil the CSO confirms it (the well and the first sentence only; the band table's row reads "whole molecule, uncut"). Lanes with the same enzymes, host and construct collapse (laneKey).panels/digest-lanes.tsx(column 1, markerdrybench-island:plasmid-digest-lanes, loaded with the Digest view): "Add as a lane" moves the digest chosen now into the lanes list and clears the draft; "{k} of 6 lanes" and the cap line stated before use; at 6 the key is disabled, described by "6 of 6 lanes. Remove a lane to add another."; each lane its number, its enzymes and Remove (no label field); the ladder select by composition name, its data loaded when the select is opened or the first lane is added.panels/gel-lanes-panel.tsx(column 2, markerdrybench-island:plasmid-gel-lanes, loaded with the first lane, in place of the single digest's gel): one reference lane (the markers, or the ladder in their place with the round sizes kept as the axis text;Gel.axis), the CSO's grouping example, the ladder's sizes, sheet sentence and limit, the band table of every lane with its host, a fragment table per lane captioned by its statement, and Figure settings (lane labels, host in labels, sizes shown, the provenance line, Reset), which change the drawn gel and its export copy and no band, size or table, and are neither saved nor shared. Background and format are the Export dialog's (the panel cannot build the provenance block); the export copy carries the lanes' data (data-gel-export-data) for the writer.
Kept lanes are page state: dig carries the digest chosen now, so old links are unchanged; they are cleared when the plasmid's name or length changes. Not built yet: the diagnostic picker prefilled from a designed product's junctions and compared with the parent vector (waits for the plan record and the engine's end test).
The construct and its identity reach the panel through the construct prop: the workspace (src/apps/plasmid-map/views/plasmid-workspace.tsx) names it as the figure writer does, the accession with its version or else the bases' SHA-256 (constructIdentity), computed once per sequence. Without it (before the hash resolves) the statements, the lane-number form, the construct form and the on-screen provenance line are withheld; the export's provenance line (written by figure/export.ts from the open plasmid) carries them either way. Copy: column 1's and the lanes' strings are in content/apps/plasmid-map.gel.ts, loaded with them; the gel's limit text (digest.gel.schematic, the CSO's verbatim five limits) stays in plasmid-map.workspace.ts because the single digest's gel shows it too. At 375 px the gel sits in its own horizontal scroll box (DS §8.5); the page never scrolls sideways.
The address grammar for several digests is ruled (the CTO's ruling 6 and addendum A3, 17 October 2026) and built as src/components/tools/plasmid-map/dig-lanes.ts (pure, dig-lanes.test.ts): lanes by ;, enzymes by ,; one lane is today's dig value byte for byte (tested against plasmidSchema's check); MAX_DIGEST 3 a lane, MAX_LANES 6 in a link, MAX_DRAWN_LANES 4 drawn; an empty, overfull or seventh lane, or a name the table lacks, drops the whole value with the reason; lanes with the same enzymes collapse; lanesFrom returns all, firstLane the digest the picker, objects and hand-offs read; lad=<id> by pattern ^[a-z0-9][a-z0-9.-]{0,31}$. It is wired into address.ts, SHARE_KEYS and the workspace after the Room track's chooser move reaches main; until then kept lanes stay page state.
Link budget and sharing
SEQ_LINK_MAX = 1800 (the dictionary's SEQ_MAX_LENGTH, against the suite's 2,000-character LINK_MAX_LENGTH). The workspace encodes the sequence with encodeSequenceParam(sequence, "dna") (the shorter of plain letters and the 2-bit b: codec in src/lib/seq/pack.ts; z: deflate only if b: does not round-trip). If the value exceeds 1,800 characters the link carries no sequence. For pure A/C/G/T the largest sequence that fits is 5,376 bp (computed with packSequence; each IUPAC exception costs about two more bytes). pUC19 packs to 904 characters and pBR322 to 1,462. The copy rounds this to "about 5,500 bp".
shareHref writes ex for examples (dropping lib), acc for NCBI records, or seq/alphabet/name when the sequence fits. Over the budget, notInLink returns "This plasmid is too long for a link. Share a GenBank file, or send it to another app on this browser.", and the share dialog's file action exports GenBank. Feature edits, a file's own features and a pasted record's features also produce a "Not in this link" message. A link never carries badges, cut calls, bands or edits.
Objects, Studies and Send to
See Storage and state for the object store, Studies and hand-off records.
- Keep.
keep()creates amine:construct/<uuid>withConstructData(v 1): residues, alphabet, name, source, record features, edits,meth,enz,dig,insert, data versions, a filtered copy of the address state, the GenBank text of the current map (annotatedText), and optionally.dnanotes,originalRecord,physicalLabel,cloneEvidenceand Study bindings. Badges, cut calls and bands are never stored. Editing an unkept plasmid keeps it silently. Autosave writes the record 1.2 s after a change, except for a construct a named task owns (see Kept edits). - Links. Once the analysis matching the current inputs is in,
writeLinksstores the plasmid'sseq:DNA object (carry; withgenbank:<accession>aliases for examples and NCBI records) linkedmade-from, and auniprot:<accession>target for each call that carries an entry, linkedmentions. - Reading back.
readConstructvalidates the record field by field (lengths, positions, state keys against the schema, at most 50,000 residues) and also accepts Sequence's constructs with asequenceWorkspacetext that round-trips exactly. - Inbound objects (
objectInbound): a construct opens bymine(private); a DNA sequence byseq/alphabet(protein and RNA are not offered); a target byid, which lists the kept plasmids that mention it. - Object tabs (
object-tab.tsx): on a construct, a compact circular map with badges, unique cutters under the construct's methylation setting and the selection marker (from a fixed list of resistance-gene ids); on a DNA sequence within 50 kb, the same map from the current data versions; on a target, the kept plasmids that mention it. - Send to goes through the shared
ObjectActionsmenu. The payload addsseqandalphabetonly for Sequence and only when the sequence fits a link; Sequence opens a kept construct from itsminerecord, so length does not limit it (browser test with a 7,477 bp plasmid). Assay receives the construct by hand-off record as a label. H13 to Protein uses the registry's link builder with the UniProt accession asid. The Lab calculators hand-offs (H16, H18, H19) areplannedin the registry and not built. - Recorded results.
WorkCaptureControlsrecords a result only when an analysis (and any chosen digest) matching the saved inputs exactly has completed.capturePlasmidWorkspaceResultwrites an input snapshot (schemaplasmid-designv1, evidence originmanual-assertion, executionplanned) and a result (plasmid-map.recorded-resultv1) with method ids and versions, data versions with content hashes, a summary, and qualifications (plasmid-design-prediction,annotation-origins,recorded-operation-scope, plusambiguous-design,methylation-uncertain,clone-evidence-design-mismatchand others when they apply). Warnings make the resultqualified. - Kept edits. Edits to a named task are kept as its draft beside the record, never written into it by the timer or by leaving (D-0068 rule 8; Storage and state). When a construct opens by
mine(a resumed task, an object page or an address alone),keptConstructfinds the live task that owns it (readOpenWork) and reads its draft (readOpenWorkDraft); the draft is opened in place of the record only whenreadConstruct(with admission for assembly provenance) reads it as the app's own design, andappliedDraftis set withloadedMineand the address (a refused draft is offered as unshown). The owning task becomes the research context'sworkId, so an address alone shows the offer, andownedRefkeeps the autosave off for a construct a task owns.reloadOpenConstructservesonDraftResolved: Discard reopens the saved construct, its Undo the draft, each only while the same construct is open in the same scope and no other input has opened since. The payload carriesannotatedText, a GenBank text built from the analysis's calls and a written date, so the screen differs from the record until the analysis is in: a named task's saved construct put on screen is held insavedSettle(the save control isdisabledandsettling, so its timer writes no draft and leaving is not held meanwhile) until the analysis is in, thensavedOnScreenis bumped when the plasmid object and themeth,enz,dig,insandlibkeys are unchanged since it opened. A change to the plasmid, or to those keys once the address has shown them, ends the wait at once, so an edit made during the analysis is kept and leaving holds for it (REL-41 b). The written date, on the COMMENT and the LOCUS line alike (writeGenBank'sdate), is the opened record's or draft's own (writtenOnreads its "Written by … on" comment;writtenDate) while the plasmid and those keys are as opened, and today's date once anything changes, so a draft from an earlier day reopens as shown rather than edited (REL-41 a). After 30 s (SETTLE_MS) without an analysis the control is enabled with the record as its baseline.ownAddresssets the address a saved design opens with, so the screen's payload rebuilds the design's own state: the link-arrival keys (name, which a resumed task's hand-off carries,seq,alphabet,acc) are the design's own or cleared,methis cleared for the default host rather than left from another design, andlibis kept as saved for a design made from a shipped example (whose pins win overlib).markArrivedmarks the design'sex,seqandaccas handled, so the example a saved design was first opened from is not reopened over it (which dropped the task it was resumed with). - Clone evidence (
clone-evidence.tsx): up to 50 observations entered by the researcher (sequencing, digest, PCR or other; date, reference, outcome), each bound to a SHA-256 design hash. They are records of what the researcher entered, not inferred from the map.
Exports
exports.ts registers these Export-menu items. Each tabular file carries the shared provenance sidecar; each figure embeds the provenance block (app version, canonical address, data versions, host, sources with versions, methods, limits, statements).
| Item | Format | Notes |
|---|---|---|
| Features, sites, fragments, primer sites, numbers | CSV | Site rows carry cut_call (cuts/blocked/unknown), methylation token, methylase and host; fragments include both variant lanes when one site is uncertain |
| Map, schematic gel | SVG or PNG (a figure in millimetres) | Written by src/components/tools/plasmid-map/figure/export.ts, loaded on demand (marker drybench-export:plasmid-figure) through the shared menu's physical-figure dialog (FigureSpec.physical; the shared export-menu.tsx is unchanged): SVG, or PNG at 150, 300 or 600 dpi with a pHYs chunk (figure/png-phys.ts, pure TypeScript); its width the drawn figure's at 96 px to the inch. White unless transparent is chosen; light-theme colours whatever the screen's theme; the figure settings as drawn. The provenance block inside, and a footer: for the gel the provenance line "{app} {v} · {construct} ({identity}) · enzyme table {e} · gel rules {GEL_RULES.version}[ · ladder data {v}] · host {host} · schematic" (the identity from the open plasmid: the accession with its version, else its SHA-256), with the legend lines in the lane-number form; for the map the enzyme table and library versions. A gel with a ladder adds the ladder-sheets source. The gel item is offered whenever a gel is drawn, the chosen digest's or the lanes' |
| GenBank | .gb, chemical/seq-na-genbank |
See below |
| FASTA | .fasta |
70 per line; header name length bp circular/linear DNA |
| Saved inputs | JSON | The construct record |
| Clone evidence | JSON | Only when observations exist |
| Report | Switches to the map view, then prints |
GenBank export (genbank-export.ts, genbank-write.ts):
- LOCUS name is the plasmid name converted to ASCII, non
[A-Za-z0-9_.-]replaced by_, cut to 16 characters; divisionSYN; date of writing. ACCESSION and VERSION are always., so the file is never presented as an NCBI record. COMMENT names the source record ("Sequence from GenBank L09137.2."), aliases, the original record's comments, the writing app and version, and a provenance sentence, all ASCII. - Untouched record or file features are written as received, with the original location text when parts are unchanged. Features the map does not draw (
source, redundantgene) are re-added while the sequence is unchanged. - Library calls:
/label,/provenance="library",/inference="similar to DNA sequence:INSD:<accession>", a/notewith the library version and date, source location, badge and licence; a CDS gets/translationonly whencheckTranslationpasses. - Added features:
/provenance="added"and a note. Edited features:/provenance="edited", a note naming what changed, none ofprotein_id,db_xref,locus_tag,old_locus_tag,inference,experiment,citation; a CDS's translation is recomputed, never copied, and a sourcetransl_except/exceptionblocks the translation. checkTranslation: A/C/G/T only; whole codons unless partial; first codon an initiation codon of the table unless 5′-partial (the builder's addition so the file agrees with Biopythoncds=True); no internal stop; final stop unless 3′-partial.- Every
/noteis ASCII (asciiNote, insrc/lib/seq/features/genbank-ascii.tssince 12 October 2026 so Sequence's PCR product writer applies the same rule; re-exported fromgenbank-export.ts): accents dropped, typographic quotes and dashes plain, Greek letters spelled, µ asu, straight double quotes become single, anything else?. Qualifiers wrap at 79 columns;/translationwraps without spaces. Since 14 October 2026 (the CTO's ruling of 12 October, REL-379) every other value is wrapped only at its spaces: a word that does not fit the rest of a line starts the next one, and a word longer than a line, or a first word that cannot share the key's line (/source_record's reference, a JSON/predictionor/witnessreceipt), is written whole on one line that runs past column 79, never split. Since the batch 1 review (14 October 2026) a line or paragraph break, carriage return, tab, vertical tab, form feed, U+001C to U+001F or U+0085 in a value is written as a space (LINE_CHARACTERS; a raw one ended the feature table, made a linereadGenBankcannot match, or was stripped by Biopython where it decides a line's ends), and two breaks are never made, the lines either side joined instead: after the bare opening/key="(Biopython 1.88 reads the value with a leading space,readGenBankwithout its first character) and before a line starting with/(which Biopython reads as a new qualifier). A line ending in"elsewhere is left, so quoted prose still wraps at 79 columns: Biopython closes the value there but appends the later lines, reading the same value (narrowed on the delta review of 14 October 2026). Breaking only at spaces also means a doubled""is never split (REL-27). What the tests show:genbank-write.test.tsreads back withreadGenBankthe REL-379 cases at 70 offsets and the review's cases (line and paragraph breaks, a doubled quote before a/word, a leading space, a note of quoted words that must still wrap), and holds the writer to an offline Biopython round trip of 1,200 seeded values (prose, quotes and slashes, tokens longer than a line, leading spaces, line breaks, quoted URLs, JSON receipts;genbank-roundtrip-corpus.ts, read bypipeline/plasmid-map/genbank_roundtrip.pyintosrc/lib/seq/features/fixtures/genbank-roundtrip-v2.json(write-once;genbank-roundtrip.jsonkeeps the first fixed writer's reading)): the corpus's bytes are pinned, Biopython parsed it, and both readers give every value back exactly. Not covered, a limit of the format: a run of spaces at a line break reads back as one space in Biopython (it strips continuation lines), as it does for Biopython's own writer. Values passed through from a record (a primer set's notes, its label,/tm_qualifications, an imported file's qualifiers) are not made ASCII (REL-380). Lines past column 79 may fail strict INSDC submission validators; these files are never submissions (ACCESSION is always.).readGenBankstill joins a line after a bare opening quote without a space, for files written before the change.
Methods and checks dialog
The methods and checks dialog (D-0085; Shared shell, Registry and content) reads content/apps/plasmid-map.methods-sheet.ts: one group per view, with the methods its chips and exports name and the registry limits scoped to it by limitMeta id (Plasmid is the one app with limit ids today). The Assembly view lists ASSEMBLY_METHODS (end primitive, restriction cloning, Golden Gate, Gibson), the fixed terminal primer-tail method and annealing-region Tm; CRISPR guides are a task-page group; Sequence and Explainers carry no record. definition.methods.group() returns the store's view.
Verification
See Oracles and verification for the suite-wide approach and Build, test and CI for how tests run. The oracles are offline: Biopython 1.88 (pipeline/plasmid-map/requirements.txt; Bio.Restriction's data are REBASE emboss release 404) and Python's fractions. Nothing from Biopython or REBASE is shipped.
Oracle fixtures
Fixture (pipeline/plasmid-map/fixtures/) |
Written by | Cases | What the test asserts |
|---|---|---|---|
enzyme-check.json |
reference.py enzymes |
223 rows | Each row rendered in Biopython's elucidate() notation, overhang kind and length, and cuts on pUC19 and pBR322 agree; the shipped table's check lines came from this file |
digests.json |
reference.py enzymes |
446 single digests, 6 double, origin-spanning sites, 40-enzyme golden set | Cut positions and fragment lengths exact; golden set's pBR322 counts equal REBASE pages read by hand; NEBcutter 3 column is null ("to be recorded by hand") |
trio.json |
reference.py enzymes |
4,157 digests judged, pool 95, 1,710 distinguishing | Picker pool and every verdict exact (unmethylated DNA only) |
features.json |
reference.py features |
26 alignments | Identity and coverage within 0.1 percentage points; start, end, strand, call and score exact; UniProt carry/withhold and substitutions exact |
boundaries.json |
reference.py features |
12 synthetic inserts | Calls on each side of every threshold match PairwiseAligner numbers |
amounts.json |
reference.py amounts |
11 | Equal to exact rational arithmetic at 1e-12 relative |
methylation.json |
reference.py methylation |
19 hand cases + 2 plasmid cases | Outcomes, marks and reasons exact; MboI on pUC19 and EcoRII on pBR322 blocked in dam⁺ dcm⁺ |
dna-files.json, dna/*.dna |
make-dna-fixtures.py, reference.py dna |
6 synthetic files, 4 refusal files | Sequence, topology and every feature equal Biopython's SnapGene reader; refusals for the stated reason (not Biopython's, which skips unknown packets) |
genbank.json, genbank-samples/*.gb |
genbank-samples.mts, reference.py genbank |
2 sample exports | Writer reproduces the samples byte for byte (LOCUS date aside); Biopython reads ACCESSION ., keeps /provenance, and its cds=True translation equals each /translation or refuses where none is written |
examples/{puc19,pbr322}@1.json |
reference.py examples (write-once) |
2 | Engine with library 1 reproduces calls, UniProt checks, primer sites and every enzyme's cut count |
examples/{puc19,pbr322}@2.json |
reference.py examples (write-once) |
2 | The same pinned sequences called with library 2, each call also listing its mismatched bases (mismatchesAt): the engine with library 2 reproduces calls (7 on pUC19, 5 on pBR322) and mismatch counts; the ColE1-type origin is pinned at 811..1478 (667/668, mismatch at 1308) and 2478..3145 (668/668) |
ds-ends.json |
ds-ends.py |
5 cuts, 8 compatibility, 3 flips | Hand-worked ends re-derived from a two-strand drawing |
restriction-cloning.json |
restriction-cloning.py |
3 clones, 2 refusals | Products exact up to rotation and strand, orientations as recorded; Bio.Restriction digests each hand product back into its cores |
golden-gate.json |
golden-gate.py |
3 assemblies, 4 refusals | Scarless product; Bio.Restriction confirms released overhangs and no surviving site |
gibson.json |
gibson.py |
2 assemblies, 5 refusals | Product re-derived by independent concatenation; metamorphic reverse-complement and rotation checks |
crispr.json |
crispr.py |
5 sequences, 21 guides | Every field exact against a regex enumeration on both strands; the script refuses to write unless it agrees with one hand-worked case |
fixtures/manifest.json records each file's SHA-256 and case count, the generating script's hash, the date and the oracle versions. The first test in src/lib/seq/digest.test.ts checks every listed file's hash against the manifest, and the Wave 2 tests check their own fixture's hash again. publish.py <version> copies the manifest, its files, the examples manifest and every golden example it lists to public/verification/plasmid-map/<version>/, refusing to replace any file already on main. src/lib/seq/plasmid-data.test.ts asserts that the directory for the registry's current version holds byte-identical copies of every verification set.
Unit tests
Run individually with node --test <file>. On 10 October 2026 the 18 plasmid-related test files below ran 94 tests, all passing.
| File | Covers |
|---|---|
src/lib/seq/digest.test.ts |
Manifest hashes, table checks, cuts vs Biopython, golden set, double digests and origin-spanning sites, fragment geometry, Type IIS off-end, picker vs trio, unique cutters, uncertain sites |
src/lib/seq/methylation.test.ts |
Hand cases, "not recorded" never a cut, cited examples, never classification, reasons, precedence |
src/lib/seq/features/annotate.test.ts |
Library shape, 26 oracle alignments, UniProt rule both directions, affine scoring, gloss, thresholds and floor, specificity (10 random 10 kb sequences, golden plasmids reversed), sensitivity at boundaries |
src/lib/seq/features/primers.test.ts, snapgene.test.ts, eutils.test.ts, pack.test.ts |
Primer set and sites; .dna reads and refusals and a guard that src/ never imports the fixture writer; accession validator, address and answer reader; codec |
src/lib/seq/plasmid-data.test.ts, plasmid-examples.test.ts |
Version files and hashes, no drafts, lib resolution, published fixtures; golden examples |
src/lib/seq/dna-amounts.test.ts |
Avogadro, amounts cases, refusal |
src/lib/seq/{ds-fragment,restriction-cloning,golden-gate,gibson,crispr}.test.ts |
Wave 2 fixtures, metamorphic checks, refusals, no score field on guides |
src/components/tools/plasmid-map/{plasmid,genbank-export,gel-caption}.test.ts |
Words (badges, withheld UniProt sentence, cutter groups, not-in-table, primer binding); GenBank export; caption wrapping |
The gel model has unit coverage only; there is no oracle for band positions. The picker under dam/dcm settings is unit-tested only; the oracle trio is unmethylated.
Weekly NCBI check
pipeline/plasmid-map/verify.ts runs in .github/workflows/scheduled.yml. It re-fetches every saved record with the app's own address and reader (one request at a time, 400 ms apart), requires unchanged sequence SHA-256s and the same current version, checks that every shipped primer still sits at its binding site, and warns when the library is more than 12 months old. It exits 1 on failure.
Browser tests
e2e/plasmid-map/ (Playwright, strictOffline: true except where a test needs another app or object pages):
workspace.spec.ts: paste to map with one live region and no letters in the address; keyboard map navigation and pUC19's withheld UniProt link (V82I, A182V); pBR322's bla carrying P62593 to Protein; the picker by keyboard and gel SVG export with provenance;.dnaopen and refusal; GenBank open; inbound accession armed, consent dialog, E-utilities fixture; a feature edit making Copy link offer GenBank first, read back in a fresh browser; Send to Sequence and Assay; a 7,477 bp plasmid over the link budget; the object-page tabs; 375 px layout without sideways scroll.map-defects.spec.ts(17 October 2026, C2.2 and MNT-21): a GenBank record's own feature (rec:) picked from the map and the Features table, focused, and held insel; both pieces of an origin-crossing primer site on the linear map; the palette's Reset returning the zoom (desktop only: the palette is opened from the keyboard); "Inspect sequence" scrolling to the pick.enzyme-cap.spec.ts(17 October 2026, REL-250): with 40 enzymes chosen from a link, a 41st ticked is accepted, kept in the link and listed, not drawn, said in the cap line with its row marked; clearing a drawn one draws it; "Show the unique cutters" on pUC19 chooses all 42, draws 40 and names the other two, each marked.tasks.spec.ts: each task page and explainer, docs verification tables, and per-island chunk loading.e2e/objects/work-draft-plasmid.spec.ts: a resumed task shows its kept draft (Discard returns the saved plasmid, Undo the draft), a resume with no edits writes no draft, and an address alone opens the owning task with the draft while the autosave leaves its record alone.navigation.spec.ts: tab state preservation and the empty page's draft surviving a visit to the explainers.
scripts/size-budgets/plasmid-map.json sets chunk budgets: definition 23.4 KB, workspace 196 KB interim (measured 178.1 KB on 10 October 2026; release budget 150 KB restored at the pre-live pass under D-0048), each task island 40 KB. Stage C's room-making (17 October 2026) added plasmid-analysis-worker (the worker, 31.9 KB with the .dna reader, then 26.1 KB once SEQUENCE_CAP moved to its leaf; the row is 30.1 KB), plasmid-view-explainers 9.1 KB and plasmid-view-numbers 1.3 KB, each measured plus 15 percent, and took the workspace chunk from 195.1 to 184.7 KB. The gate cannot measure plasmid-workspace-chunk while its marker is reached by several chunk lists (one of 18 unmatched rows on 000a4206); docs/progress/stage-c-room-measure.mjs measures it by hand (docs/progress/stage-c-room.md).
Licences, excluded sources and GPL tools
Facts only; whether any licence permits sale is held for the General Counsel (D-0010). See Data sources and Dependencies and licences.
| Source (registry id) | Licence id, read on | Use |
|---|---|---|
NCBI GenBank (ncbi-genbank) |
NCBI-no-restrictions-depositor-terms, 2026-10-05 |
Library parts, examples, primer checks, opt-in fetch |
UniProt (uniprot) |
CC-BY-4.0, 2026-10-04 |
Cross-references and canonical sequences shipped in the library file |
McClelland 1994 Table I (mcclelland-1994) |
cited-data, 2026-10-06 |
Methylation entries, transcribed by hand |
Enzyme table (enzyme-table) |
own-content, 2026-10-06 |
Recognition sites and cuts as facts |
Smith-Waterman / Gotoh (smith-waterman-gotoh), Geier and Modrich 1979 |
cited-method, 2026-10-06 |
Alignment; DpnI cut |
Vector makers' manuals (primer-manuals) |
cited-data, 2026-10-06 |
Primer names |
Plasmid fixtures (plasmid-fixtures) |
MIT, 2026-10-06 |
Published fixtures |
Excluded sources (registry excludedSources): Addgene content (non-commercial terms), FPbase (CC BY-SA 4.0; shipping it as a separate share-alike file is the CEO's decision), the iGEM Registry (terms not read), SnapGene's feature database and SnapGene-annotated records, GPL plasmid-annotation tools and their databases, bulk restriction-enzyme downloads, NEB data, SeqViz and Open Vector Editor.
Tools that must not be ported or shipped, with the reasons recorded in the brief and docs/research.md:
- pLannotate: GPL-3.0 code with a SnapGene-derived database (brief §10.1). Neither code nor data may be used; it may be run offline only as a behavioural comparison, and no fixture is taken from it.
- CRISPOR: recorded as academic-only; a commercial organisation needs a licence even to run it, so it is not available even as an oracle (
docs/design/brief-sequence-toolkit.md§3.4,docs/research.md). The guide finder is an independent enumeration with no scores. - Primer3: GPL-2. The fixed-length Gibson primer construction does not use Primer3; Primer3 may only be an offline comparison oracle (
docs/design/pre-release-app-depth/molecular.md). - EMBOSS (
water,restrict) and SMS2: GPL; behaviour references for fragment sizes only. EMBOSS's enzyme data are REBASE-derived and are not copied. - SnapGene's own code: never used. Permissively licensed
.dnareaders (Biopython BSD, teselagen MIT) may be read under their licences; the shipped reader copies none.
Open items held for the General Counsel (from docs/progress/app-plasmid.md): REBASE's terms and citation request; the vendor manuals as primer-name sources; the UniProt canonical sequences shipped in the library file; and several copy lines.
Scientific audit follow-up, 11 October 2026
The main-branch audit advanced double-stranded end and assembly methods to 1.2.0. Wave 6 integrates those repairs and advances ends, restriction cloning and Golden Gate to 1.3.0 for coincident/overlapping-cut refusal and exact paired-core accounting; Gibson remains 1.2.0. Circular cuts retain the signed bottom-minus-top displacement before coordinates wrap. Fragment extents are extracted in one unwrapped coordinate frame. Thus moving the origin through an EcoRI, BamHI or KpnI overhang preserves its physical length, polarity and paired core. Restriction-cloning and Golden Gate fixture inputs are checked at every circular vector origin against their existing independently specified products.
Golden Gate recognition scans convert protruding bottom strands into the top-strand frame, removing a false site created by concatenating incompatible strand orientations. The complete final circle is also scanned, including all junctions and the origin; creation of a recognition site withholds the product. This is a sequence-planning refusal, not a prediction of reaction yield. Gibson refuses plans where the two terminal overlaps consume or overlap a fragment's whole sequence. Retaining a non-overlapping core per fragment is this planner's conservative scope: it does not model bridge-oligo assemblies. The earlier zero-length successful circle is therefore refused.
Plasmid's exact-match primer detail now states its fixed Tm conditions (250 nM primer, 50 mM Na⁺, no Mg²⁺/dNTPs) and the Owczarzy 2004 extrapolation qualification. Mismatched sites still withhold perfect-match Tm. See Sequence's dated source review for unresolved paper/range evidence. No experimental Tm accuracy is newly claimed.
Adversarial regression groups are in src/lib/seq/scientific-audit-regressions.test.ts. Original fixture bytes, historical research records, saved results and published audit evidence are preserved. These changes protect the shared assembly engines. Wave 6 subsequently adds the own-sequence Assembly section described below, with these same refusals.
Known gaps, defects and divergences
Defects found by reading and exercising the code on 10 October 2026:
- "Inspect sequence" does not move to the feature's bases: fixed 17 October 2026 (MNT-21). The card's key now passes the pick's first base, and
panels/sequence-panel.tsxscrolls the line holding it to the top of its region; it selects nothing. The read-only SequenceView of Stage B replaces the panel later (C2.5). - Non-library features cannot be selected: fixed on
mainsince 13 October 2026. The 10 October reading held:pickKeydrops the colon fromrec:3,file:2oradd:<id>, and the lookup'sendsWithtest failed. REL-27's commit887e9c6cadded the match in the written form (selectionValue("f", f.key, f.start) === selectionValue("f", selection.id, selection.at)), so such features are picked and focused; no browser test checked it untilmap-defects.spec.ts(17 October 2026), which picks a GenBank record's own feature from the map and from the Features table and finds the focus ring andselin the address. - Origin-straddling cuts: repaired in Wave 6. The end primitive now retains signed enzyme strand separation on an unwrapped circle. Single and multiple cuts share that representation;
openCirclewas removed. The independent 0.9.0 boundary fixtures cover pBR322 EcoRI, 3′ and blunt cuts, both Type IIS orientations, rotation and reverse-complement invariance, and the six-base EcoRI circle whose four-base overhang is longer than half the circle. The 0.10.0 supplement refuses distinct bottom cuts sharing one top cut (BsaI/AciI); overlapping staggered cleavage intervals refuse rather than producing negative cores. Exact duplicate cuts are collapsed deterministically: known cleavage suffices, otherwise all uncertainties remain. The affected end/restriction/Golden Gate methods are 1.3.0; the integrated audit advances Gibson to 1.2.0. - GenBank re-read joins COMMENT into DEFINITION. The reader appends any 12-space-indented line to DEFINITION while no accession has been read (
src/lib/seq/features/genbank.ts:185). The app's exports writeACCESSION .(read as none) and wrap COMMENT, so re-reading an export yields a DEFINITION with the COMMENT continuation lines appended. Plasmid names pastes from LOCUS, so the visible effect here is small; the reader is shared with Sequence. - Linear map drops the second piece of an origin-crossing primer site: fixed 17 October 2026.
plasmid-svg.tsxdrewstart..nonly; it now draws both pieces, as it does for a feature (map-defects.spec.ts, pUC19 turned so the M13 forward (−20) site runs 2,679 to 8). - Band grouping chains: closed 17 October 2026.
resolvedBandscompared each size with the smallest member of the current group, so 1,000, 960 and 922 bp formed one band although 1,000 and 922 differ by 7.8 percent. Gel rules 2 compare with the band's largest fragment; the oracle was changed first and independently (reference.py resolved_bands, hand cases ingel-bands-v1.json), and trio v2 (trio-v2.json) records that 43 of 4,157 judged digests change their bands and no picker verdict changes against trio v1, which is kept. - Palette "Reset" left the zoom: fixed 17 October 2026.
resetMapnow also raises a count the map panel reads (resetKey), which returns its zoom to 1; the definition chunk is unchanged (map-defects.spec.ts, on a linear molecule, where Reset keeps the layout).
Divergences between copy, records and code:
- The assembly and CRISPR task pages say "then cut and ligate here" and "then assemble here" (
content/apps/plasmid-map.tasks.ts:85,105,119), but the pages only show build-time worked examples. Comments inexamples.server.tsandislands/restriction-cloning.tsxsay "the same engine the workspace runs"; the workspace does not run these engines.molecular.mdM5 asks for user-chosen fragments, which do not exist. - The first limit line says the app does "not ... simulate cloning" (
content/tools/plasmid-map.ts:103), while the registry lists restriction cloning, Golden Gate and Gibson task pages. The brief (§3.4) also put cloning simulation out of v1; Wave 2 added it. content/tools/plasmid-map.ts:39says "25 alignments"; the fixture andFIXTURE_COUNTS.alignmentssay 26.content/apps/plasmid-map.verification.ts:7sayssrc/apps/plasmid-map/verification.test.tschecksFIXTURE_COUNTSagainst the fixtures. That file does not exist, and no test reads the plasmidFIXTURE_COUNTS, so the published counts can drift unnoticed.- The fixtures source link points at
/verification/plasmid-map/0.1.0/manifest.json(content/tools/plasmid-map.ts:96); the current version is 0.7.0. flags.sizeNotegives the library as "about 54 KB"; the file is 60,437 bytes.methylation.csv's header says "81 of 222" enzymes; table 1 has 223 rows with 82 entries or not-recorded classifications.- Brief items not built:
dna-concentrationmeasurements,compoundandfileobjects, H15/H16/H18/H19 hand-offs, Rfam features, 200 to 250 library entries, NEBcutter 3 cross-check values, verify-against-reference (v1.1). - The 10 October audit (
docs/science/2026-10-10-scientific-audit-of-the-7-to-10-october-changes.md, "Sequence and Plasmid") records that/translationis written for 5′-partial CDSs that Biopythoncds=Truerefuses with no partial fixture, that a label-only CDS edit can drop the paired sourcegenefeature from the export, and (M4) that Plasmid's primer Tm lacks the Tm-conditions limit line.
Known limits stated in the product: 50,000 bp cap; 50-part library and seeding limit; heuristic thresholds; dam/dcm only; schematic gel; one-mismatch primer sites with no priming claim; .dna checked on synthetic files only; NCBI fetch sends accession and IP; no offline use. Collected suite-wide on Known issues.
Extending safely
- Enzymes. Type the row in
enzymes.csvfrom a published source, never from Biopython or REBASE files. Runreference.py enzymes(Biopython 1.88 in an isolated environment), settle any disagreement by reading the REBASE page by hand, add Table I entries tomethylation.csvif the site can carry a mark, thenbuild-enzymes.mts --bump. A changed table is a new version; never edit a published file. - Library parts. Add a row to
features.manifest.csvnaming a submitter-annotated NCBI record (no SnapGene-annotated records, nothing from Addgene, FPbase or iGEM), save the record withfetch-records.mts, remove the part fromlibrary-gaps.csv, thenbuild-features.mts --bump. Re-runreference.py featuresandexamples; a change that moves a golden example's output needs a new example version, not an edited fixture. - Primers. Ship a primer only with a dated source that names it and an NCBI binding site where it occurs exactly.
- Thresholds, scoring, gel rules, picker pool. These are in
methods-plasmid.tsand are mirrored inreference.py. Change both, raise the method record's version, regenerate fixtures, and publish a new verification directory with the app version. RaisePLASMID_ENGINE_VERSIONonly for a change that moves an output the golden examples pin (calls, UniProt checks, primer sites, cut counts), with new example versions for the examples whose outputs moved; never a version for an example whose output is byte-identical. The gel rules' version is read byreference.pyfromGEL_RULES.version, which a test holds equal toGEL_METHOD.version; the gel and picker are pinned bygel-bands-v1.jsonand the trio fixtures. Trio v1 (trio.json) is never rewritten, nor is its manifest entry;trio-v2.json's own manifest entry names it undersupersedes(the write-once check allows no other change to a published entry without astatefield). Change note, 17 October 2026: 43 of 4,157 digests of the EGFP-in-pUC19 trio change their bands under gel rule 2.0.0; no picker verdict on that trio changes, and the 1,710 digests telling all three apart are the same set under both rules. - New fixtures. Write them with an independent script that never imports the TypeScript engine, record them in
fixtures/manifest.json, publish withpublish.py, and updateFIXTURE_COUNTSand the copy by hand (nothing checks them; see above). - Assembly in the workspace. Keep the independent boundary and refusal fixtures, declared limits and verbatim standing statements when extending the Assembly view. Do not transfer parent features to product coordinates without an independently tested mapping.
- Annotation scale. The design assumes small libraries. Re-measure annotation time in the worker before growing the library, since there is no timing test.
.dna. Add a packet type only with real-file evidence and a refusal-preserving test; never write.dna.- Address keys. New keys go through
plasmidSchemaand the shared dictionary; no derived result or feature edit may enter a link. - Licences. Never ship or port GPL or academic-only code or data (pLannotate, CRISPOR, Primer3, EMBOSS, SMS2). A GPL dependency or a feature built on a non-commercial source would need a rewrite at the pre-live pass.
- Before finishing, run
npm run lint,npm test,npm run build,npx tsc --noEmitandnpm run test:e2e(see Build, test and CI).
Wave 6 boundary references (fixture-first, 11 October 2026)
pipeline/plasmid-map/assembly-boundaries.py publishes 67 independent
Biopython 1.88 cut/digest and Golden Gate transformation cases as
assembly-boundaries.json. Signed enzyme offsets and unwrapped two-strand
drawings establish polarity across the circular origin; top cuts are
independently checked by Bio.Restriction. pBR322 EcoRI yields a four-base
5′ AATT overhang and a 4,357-base paired core. Rotations and both strands
cover EcoRI, KpnI, SmaI, BsaI and BsmBI, single and multiple cuts.
assembly-record-v2.json contains ten reader-contract cases, adding parent
references and method-specific Gibson accounting (fragmentBases - overlaps = product). The historical malformed additive Gibson block is refused.
Published 0.8.0 and every earlier fixture remain unchanged. This 0.9.0
publication precedes the origin-cut repair; it does not claim the current
engine passes the new boundary cases.
Own-sequence Assembly and record admission (Wave 6)
Assembly is the seventh section, loaded on first visit by one top-level
import in plasmid-workspace.tsx. No new static workspace import was added.
Its marker is drybench-island:plasmid-assembly, budget 40 KB; the three
engines and method copy remain in that on-demand dependency graph. Visited
sections preserve their drafts, and each method retains its own inputs.
Restriction takes the longest enzyme-ended fragments and requires a choice
when both insert orientations are possible. Golden Gate offers BsaI/BsmBI;
Gibson accepts 2–6 ordered linear fragments, refuses overlaps below 15 bp or
repeated homology on either strand, and caps product size at 50,000 bp.
The assembly limit is registered before the view and full method standing
statements appear before Design product. Inputs changing makes old outputs
stale and prevents opening or exporting them.
A design uses the existing construct v1, source.kind: assembly, and an
optional assembly block: original inputs and hashes, parent refs when
available, producing method/version, data release, host, topology, exact
product hash, chosen orientation and alternative count, junctions,
method-specific material accounting, selected-fragment descriptions,
qualifications and the exact standing statement. The additive Gibson shape
published in 0.8.0 is malformed and refused; it is never silently migrated.
Older records without the new descriptive field show that it was not saved.
readConstruct performs bounded structural checks synchronously and returns
candidate assembly metadata separately, never on its own construct.
construct-admission.ts loads only when required, hashes exact residues and
inputs with Web Crypto and either attaches admitted provenance, refuses a
malformed input, or drops stale provenance with an explicit product/topology
reason. Its independent marker has a 10 KB budget. Record opening checks
mine request identity and account scope across admission. The recorded-result
adapter also admits before keeping metadata. Object previews and the
Sequence/molecular input readers omit unchecked assembly metadata.
Product FASTA identifies a designed sequence. GenBank COMMENT carries the method/version, parents, exact hashes and qualifications. Assembly inputs JSON includes a construct and provenance; the labelled import reopens its admitted original inputs and saved design without recomputing history. Open the product in the map uses the existing replacement/unsaved-work guards; ordinary Keep and autosave preserve admitted assembly metadata. No parent feature coordinates are carried onto the new product.
Plasmid 0.11.0 additionally publishes assembly-record-refusals-v1.json: 19
independent controls/refusals committed in ab707c1b before the reader repair.
Gibson input lengths and recorded overlap strings must agree with claimed
accounting; restriction and Golden Gate overhang strings must agree with their
total. Method-specific parent roles, junction counts and single-product rules
are checked before exact hash admission. A balanced but fabricated pair of
totals is refused. The prepared 0.10.0 publication remains byte-for-byte intact.
Sequence receipt admission (Wave 6 item 3, prepared pending checks)
The existing construct kind reads supported Sequence v1/v2 records structurally, then dynamically admits the selected bounded payload. A one-job worker recomputes supported PCR receipts and checks retained primer sets. Product features use linear product coordinates including tails. Template features use only the saved footprints when exact hash, length and topology agree; there is no re-search. Whole mismatched pairs are counted and omitted; contradictory matching footprints refuse the handoff. Received annotations remain intact.
Generated PCR annotations deduplicate only by their complete generated signature. Feature provenance includes source record/app, primers, witness or prediction and retained qualifications. Long receipt JSON is percent-encoded in ordered 60,000-character qualifier chunks; stripping GenBank folding whitespace, sorting by the four-digit prefix, joining payloads then percent-decoding recovers it. The admitted receipt also remains in the source binding. The loader has a 10KB budget and the dedicated worker 20KB; both load only on relevant record arrival. Scope epochs, request/ref/draft identity and unmount cancellation guard every asynchronous application boundary. These changes await the item 3 check chain.
Wave 6 guided cloning walks (prepared, not yet validated)
The assembly@1 definition loads independently through the suite walk registry and owns its Python-derived example inputs. The workspace registers generic actions through its existing host; the Assembly panel retains the engines and edited form state. The walk selects the actual forward product, opens it in Map and sets the ordinary ScaI/BsrGI digest. Its comparison control is the original input vector, not the picker's hypothetical deletion product. Exports retain the existing designed-product statements.
primer-pair@1 reaches Plasmid with the run-owned Sequence PCR record, admitted through the existing worker. The receiving steps inspect primer features and export provenance. Run identifiers and record refs remain in fragments; the payload remains in the record. Raw panel checkpoints use optional independently versioned walkDrafts on the original template/vector. A scoped atomic objects transaction can merge one permitted raw panel block only; results are recomputed after reload. This source preparation awaits full checks and browser evidence; no verified-clone claim is made.
Gibson overlap primers (16 October 2026)
The own-sequence Assembly view retains Check supplied overlaps and adds Design overlap primers. It accepts two to six linear raw templates in user order, with an explicit as-supplied or reverse-complement orientation for each. Circular raw input is refused; the researcher must linearise it and choose its origin explicitly. The final linear or circular product starts at the first base of the first oriented template.
src/lib/seq/gibson-primers.ts implements gibson-primer-tails 1.0.0. For chosen
annealing length N (8–40 nt) and overlap length H (15–40 bp), the forward annealing
region is the first N oriented bases, and the reverse is the reverse complement
of the final N. The forward oligo has no tail. The reverse oligo's 5′ tail is the
reverse complement of the next oriented template's first H bases; the last linear
fragment has no tail. These are user-chosen fixed lengths, not recommended or
optimised lengths. predictPcr requires unique, non-overlapping exact sites and
one full-template product in the requested orientation. The existing
gibsonAssemble checks each resulting PCR product, including repeated homology
on both strands and wholly consumed fragments. A longest terminal overlap other
than the requested prefix withholds the design: no raw template bases may be
silently removed. Final sequence must equal oriented raw-template concatenation.
The existing oligoMelting and retained-Tm review calculate only each annealing
region, with entered oligo, sodium, magnesium and dNTP concentrations; potassium
and Tris remain explicitly zero and the UI uses primer-in-excess concentration.
Method, effective conditions, salt correction, applicability limits and notes
are retained. Refusals retain a reason and no temperature. No full-oligo Tm, PCR
annealing temperature, structure screening, genome specificity or amplification
success is claimed. The full oligos require separate review before ordering.
New designs use assembly v2 inside the existing construct v1 record. Raw
inputs and their hashes stay separate from the versioned primerDesign receipt,
which contains orientations, exact oligos/annealing parts/tails, PCR products and
five separate totals: raw template bases, added tails, PCR product bases,
collapsed overlaps and final product bases. Final bases equal raw bases.
gibson-primer-admission.ts is lazy; it validates these relationships and retained
Tm evidence without recomputing historical numbers. The admission path also
checks raw hashes and the exact expected final product hash. Edits to the product
remove stale assembly/primer provenance under the existing rule. Legacy assembly
v1 records and supplied-overlap designs remain unchanged.
The result shows primers, temperature qualifications, junctions and accounting, then opens the existing map. Assembly inputs JSON retains the complete editable receipt, including its original design timestamp. The existing Keep action and reopen route preserve it without running primer design again. Input/condition changes use the existing run-key and revision guard: stale results cannot be opened or exported. Primer controls/results, computation and v2 admission are separate optional chunks; an ordinary map opening does not load them.
Independent fixture commits precede implementation: v1 contains five positive,
eight design-refusal and three supplied-checker cases; v2 adds three positive
and four refusal cases, including a literal four-oligo cross-check and a
six-fragment 50,000 bp product. See Oracles and verification.
The exact unit/admission and browser evidence is recorded in the builder handoff,
docs/progress/cloning-primers-2026-10-16.md. Existing Golden Gate and diagnostic
schematic-gel paths are reused; gel spacing remains schematic, not predicted
migration or evidence of a performed reaction.
Public reference workflows (16 October 2026)
Public workflows load native bundled maps, digest and Golden Gate examples. The sequence panel also exposes the existing SpCas9 NGG guide finder/table for current windows up to 2,000 bases. It is not a genome-wide off-target search or TTTV/Cas12a support. See Public reference workflows for the shared route, state, source and export contracts.