Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DNAgent

An agent-friendly DNA design and cloning workbench: a desktop app and a CLI on one Rust engine.

Running a typical cloning design task with DNAgent in Claude Code.

DNAgent is an open replacement for the plasmid viewing and cloning work usually done in SnapGene, built so that a person at the desktop and an AI agent in a terminal work on the same constructs with the same engine.

Research software from the Hochegger Lab. It predicts sequences and products; it does not validate experiments. Check designs before ordering.

Start here

  • Installing — macOS and Windows, and how to connect an agent.
  • Agent handoff — how the desktop app and an agent share work.
  • Restriction sites — the first biological operation, and the model the rest of the engine follows.
  • Gibson cloning — the full insert-into-vector workflow.

How to read these pages

Each one is a contract, not a tutorial: what the command does, what it refuses to do, how its output is validated and against what. Where a page says a result is checked against Biopython, the NCBI genetic codes or a brute-force scan, that check runs in CI.

Two conventions hold everywhere:

  • Coordinates are zero-based and half-open; end < start wraps through the origin on a circular molecule.
  • Source files are never modified in place. Saving always writes a new path, losslessly for everything DNAgent models, or it reports what it could not keep.

The repository README covers installation and the command list; the API documentation is generated from the Rust crates.

Installing DNAgent

Current builds are macOS on Apple Silicon (M1 and later) and Windows x64. Intel Macs and Linux are not built yet; on those, build from source.

DNAgent comes in two parts, and they are useful separately:

  • the desktop app, for looking at and editing constructs;
  • the dnagent command-line tool, which does the biology and is what an AI agent drives.

Install both if you want the agent workflows; the app alone is enough to open plasmids.

Install on macOS

Paste these into Terminal. Use the commands rather than downloading in a browser — the reason is below.

# Desktop app
curl -fsSL https://github.com/HocheggerLab/DNAgent/releases/latest/download/DNAgent-macos-arm64.tar.gz | tar xz -C /Applications

# Command-line tool
curl -fsSL https://github.com/HocheggerLab/DNAgent/releases/latest/download/dnagent-cli-macos-arm64.tar.gz | tar xz -C /usr/local/bin

Check it worked:

open -a DNAgent
dnagent --version

If dnagent: command not found, /usr/local/bin is not on your PATH. Add it:

echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc && exec zsh

Why the curl command

DNAgent is not signed with an Apple Developer certificate, because that costs £79 a year and this is free research software. macOS treats unsigned apps harshly: since macOS 15 it refuses to open one downloaded from the web, calling it "damaged", and the old Control-click→Open workaround no longer works.

The quarantine flag that triggers this is set by the downloading application — Safari, Chrome, Mail. curl does not set it. So an app fetched with the command above is not quarantined and simply opens.

"Unsigned" means Apple has not checked who published it. It does not mean the app is modified or unsafe; the build is produced by a public GitHub Actions workflow from the commit its tag names, and each release lists SHA-256 checksums.

If you did download the .dmg in a browser, clear the flag once:

xattr -dr com.apple.quarantine /Applications/DNAgent.app

Install on Windows

Download from the latest release:

  • DNAgent-windows-x64-setup.exe — the app. Run it; it installs to your user account, no administrator rights needed.

  • dnagent-cli-windows-x64.zip — the command-line tool. Unzip dnagent.exe into a folder on your PATH, or make one:

    mkdir "$env:LOCALAPPDATA\Programs\DNAgent"
    # move dnagent.exe there, then add it to PATH for future terminals:
    [Environment]::SetEnvironmentVariable(
        "Path", "$env:Path;$env:LOCALAPPDATA\Programs\DNAgent", "User")
    

    Open a new terminal and check with dnagent --version.

The SmartScreen warning

DNAgent is not code-signed — a Windows certificate is a yearly cost this project does not carry. The first time you run the installer, SmartScreen says the publisher is unknown: click More info, then Run anyway. You should only do that because you can see where the file came from: each release is built by a public workflow from the commit its tag names, and lists SHA-256 checksums. Verify one if you like:

Get-FileHash .\DNAgent-windows-x64-setup.exe -Algorithm SHA256

Unlike macOS, the curl trick does not help here: Windows marks a file by the zone it came from, not by what downloaded it.

What is missing on Windows

The live agent connection is macOS and Linux only. It runs over a unix socket, which Windows does not have, and the portable alternatives are network ports that any process on the machine could reach — that needs an access token before it is safe to serve your constructs over one.

Everything else is the same: the app, every CLI command, reading and writing SnapGene .dna and GenBank. For agent work on Windows, use the file handoff — the agent reads and writes constructs in your workspace folder, and the app picks them up. See agent handoff.

Connect an agent (macOS and Linux)

This is what makes DNAgent different from a viewer: an agent in your terminal can see what you have open and put results on your screen. With Claude Code installed:

claude mcp add dnagent -- dnagent mcp

That registers dnagent mcp, a small relay between the agent and the running app. Start DNAgent, open a construct, then ask the agent something like "what am I looking at?" — it should answer with the construct name and your selection.

The agent sees only DNAgent: the constructs you have open, your selection, and the files it writes into your workspace folder (~/DNAgent by default). The connection is a user-only socket on your own machine; nothing is sent anywhere.

dnagent mcp --help explains the relay and its socket. What the agent can do through it, and the file-based alternative when no app is running, are in agent handoff.

For Claude Desktop (Cowork), other MCP clients, and why ChatGPT cannot connect, see Connect an agent in the README.

Updating

Re-run the install command — tar overwrites in place. Quit DNAgent first if it is running.

Uninstalling

# macOS
rm -rf /Applications/DNAgent.app /usr/local/bin/dnagent
rm -rf ~/Library/Application\ Support/DNAgent   # feature library and enzyme catalogue

On Windows, uninstall the app from Settings → Apps, delete dnagent.exe, and remove %APPDATA%\DNAgent for the feature library and enzyme catalogue.

Your constructs are your own files and are never stored inside the app. DNAgent never modifies a source file in place; it always writes to a new path.

Build from source

Needed for Intel Macs, Windows, Linux, or to develop DNAgent. Requires Rust 1.92+, and for the app also Node 22+ and the Tauri prerequisites for your platform.

git clone https://github.com/HocheggerLab/DNAgent.git
cd DNAgent

# Command-line tool (headless, no GUI dependencies)
cargo build -p dnagent-cli --release --locked
./target/release/dnagent inspect fixtures/formats/snapgene/pUC19_M77789.dna

# Desktop app, run from source
cd desktop && npm ci && npm run tauri dev

# Desktop app, packaged for your platform
npm run tauri build

A locally built app has no Gatekeeper problem at all: nothing was downloaded, so nothing is quarantined.

npm run tauri build can exit 0 even when bundling failed — check that desktop/src-tauri/target/release/bundle/ actually contains what you expect.

Something went wrong

SymptomCauseFix
"DNAgent is damaged and can't be opened"downloaded in a browser, so quarantinedxattr -dr com.apple.quarantine /Applications/DNAgent.app
"command not found: dnagent"/usr/local/bin not on PATHsee Install
tar: could not chdir to '/usr/local/bin'the directory does not existsudo mkdir -p /usr/local/bin then re-run
The agent says no window is connectedthe app is not running, or was started before the CLI was installedstart DNAgent, then retry
"bad CPU type in executable"Intel Macbuild from source
SmartScreen blocks the installerunsigned on WindowsMore info → Run anyway
dnagent mcp says it needs UnixWindows has no unix socketuse the file handoff

Still stuck, or something behaved unscientifically? Open an issue — include the construct if you can share it, and the exact command.

Agent handoff and the DNAgent workspace

DNAgent does not run an agent or a shell. You run the agent (e.g. Claude in a terminal beside the app), and the app shares context with it through a workspace folder (default ~/DNAgent, changeable with Workspace…). While the app runs, an agent can also read what is open and selected live through MCP (below), without a handoff.

Live connection (MCP)

The running app serves agents on a user-only unix socket (~/.dnagent/agent.sock, or $DNAGENT_AGENT_SOCKET). dnagent mcp relays an agent's stdio to it:

claude mcp add dnagent -- dnagent mcp          # Claude Code

For MCPorter (Pi), add a dnagent server that runs dnagent mcp, like other stdio servers. If the app is not running, dnagent mcp exits with a message; start the app and reconnect.

Tools (the contract is schemas/agent-tools.json):

ToolDoes
statusapp version, workspace, number of open constructs
list_documentsopen constructs in tab order: id, name, length, topology, active, unsaved changes, edit_counter
get_viewthe active construct, view tab, selected range and selected feature (a dnagent features row)
get_featuresall features of an open construct, unsaved edits included, as dnagent features rows
export_snapshotwrites the construct as it is open now to <workspace>/.dnagent/snapshots/ and returns the path, for CLI work
open_fileopens a file as a new tab (or switches to its tab) and returns its document_id
select_range, select_featureselect a region or feature and make its construct the active tab
notifyshows a message in the app's Agent panel
presentthe end of autonomous work: opens a result file, shows a summary in the Agent panel and lists highlights (junctions, primer sites, new features); the first is selected and the user clicks the others

The view tools reply only once the app shows the change ("applied": true, plus the resulting view); if the window does not respond within 3 s they reply "applied": false with a note. Invalid targets (a range outside the construct, a wrapping range on a linear construct, an unknown feature, an unreadable file) are rejected before anything changes, with codes such as invalid_range or no_such_feature. Agents cannot edit or save through MCP yet.

Two ways to work with an agent:

  • Autonomous: the agent works from start to finish with the CLI on files, writes the result as GenBank and calls present. Without the app it still works; the result is then offered through the watched workspace.
  • Supervised: the agent works step by step on what is open, reads your selection with get_view and shows you regions with select_range/select_feature.

Coordinates are zero-based and half-open; ranges with end < start wrap through the origin. edit_counter increases with every change (edit, undo, redo) and never repeats.

GUI → agent: Hand off to agent (⇧⌘C)

For every open tab, DNAgent writes into <workspace>/handoff/:

  • <name>.gb: a DNAgent GenBank snapshot of what is open, including unsaved edits. The original file is not touched.
  • context.json: which constructs are open and what you are pointing at.

It also shows a short prompt and copies it to the clipboard, for example:

DNAgent handoff: 2 open constructs (snapshots include unsaved edits) — context: ~/DNAgent/handoff/context.json
Selections mark the regions of interest. Use the dnagent CLI and write results as GenBank into ~/DNAgent (they will open in DNAgent automatically).

Each handoff replaces the previous one's files in handoff/.

context.json (format dnagent-handoff, version 1)

{
  "format": "dnagent-handoff", "version": 1, "created": "2026-09-29T18:04:05Z",
  "workspace": "/Users/me/DNAgent",
  "coordinates": "zero-based, half-open; selections with end < start wrap through the origin",
  "instructions": ["…"],
  "constructs": [{
    "document_id": 1, "name": "pEXAMPLE", "active": true,
    "snapshot": "/Users/me/DNAgent/handoff/pEXAMPLE.gb",   // read this with the dnagent CLI
    "source": "/Users/me/constructs/pEXAMPLE.dna", "unsaved_changes": true,
    "length": 9175, "topology": "circular",
    "selection": {"start": 1250, "end": 6188, "length": 4938, "wraps_origin": false},  // or null
    "selected_feature": { …same shape as `dnagent features` rows… },                  // or null
    "added_feature_ids": ["feature-0021"],
    "features": [ …exactly the `dnagent features --output json` result for the snapshot… ]
  }]
}

Keys appear in alphabetical order in the file. Feature rows and locations use the CLI's JSON contract.

Guidance for agents

  • Work from the snapshots, not the source paths: sources may lack unsaved edits.
  • Use the dnagent CLI (features, translate, orfs, gibson, annotate, convert, …). Put plan files and results in the workspace, not in handoff/.
  • Write sequence results as GenBank (--output genbank, annotate --out, convert --out), so DNAgent can open them with all metadata.
  • Selections and selected features are what the user means by "this", "these parts" or "the region".

Agent → GUI: the watched workspace

DNAgent checks the workspace every two seconds (sequence files up to one folder deep, ignoring hidden folders and handoff/), along with every file open in a tab.

  • New file (e.g. product.gb): a notice offers Open (it opens in a new tab) or Dismiss.
  • Changed file that is not open: the same notice, as "Changed".
  • Changed file open in a clean tab: the tab reloads, keeping the selection if the feature still exists.
  • Changed file open in a tab with unsaved edits: a notice offers Reload (discard my edits) or Keep mine. Nothing is thrown away without asking.

The app's own saves don't trigger notices.

Tabs

Several constructs can be open at once. Each tab has its own selection, undo history and unsaved state (● on the tab). ⌘1–⌘9 switch tabs. The × closes a tab, asking first if it has unsaved edits. Opening a file that is already open switches to its tab.

Security

The app still exposes no shell or process execution to its web view. It writes only to the chosen workspace (handoff and agent snapshots) and to paths you save to, and it only reads the workspace to notice changes. The agent socket lives in a folder only your user can open (0700); it serves the tools above and nothing else.

Tests

The desktop e2e scenarios tabs-isolation, agent-handoff-product and workspace-reload play the agent with real dnagent CLI commands in a test workspace. They check the snapshots and context.json against the GUI and the CLI, and check the notices and reloads. A Rust test checks every desktop command is served by the e2e server. agent-mcp-live, agent-points-back and agent-presents-product play a live agent through dnagent mcp and the e2e server's per-session agent socket, checking the agent's view and what it shows against the GUI and the CLI.

SnapGene .dna: reading and writing

DNAgent reads SnapGene files and writes them, so a construct can go out to a lab that works in SnapGene without an import step. Source files are never modified in place: writing always goes to a path you choose.

Two ways a file is written

Verbatim. A record that DNAgent read from .dna and has not edited since is written back byte for byte — the same cookie version fields, the same sequence flag bits, the same packet order, and the annotation packets as their original XML. Nothing SnapGene stores is lost by passing a file through DNAgent, including the packets DNAgent does not interpret.

Before copying those packets, DNAgent re-reads them and compares them with the record. If they no longer agree, it does not copy them: a file whose annotation packets contradict its own model would be worse than no file.

Generated. Anything else — an edited record, a Gibson product, a construct read from GenBank or FASTA — has its Features and Primers packets built from DNAgent's model. Such a file holds exactly what DNAgent models and is reported as generated. It does not carry:

  • the uninterpreted packets of a source file (0x03, 0x08, 0x0d, 0x0e, 0x11, 0x23, 0x1c, notes 0x06), because their meaning is unknown and they may describe annotations the record no longer has; each is named in a warning;
  • SnapGene's display and detection attributes (translationMW, maxRunOn, detectionMode, allowSegmentOverlaps, …), which DNAgent does not model.

Sequence flag bits other than topology (methylation and similar settings) are carried over when the record came from a SnapGene file that set them, and reported.

Writing one

dnagent convert construct.gb --out construct.dna      # the extension chooses the format
dnagent annotate construct.dna --out annotated.dna --add --range 100..160 --label probe
dnagent gibson-optimise plan.json --out product.dna   # hand the product to a SnapGene lab

In the desktop app, Save as… offers SnapGene alongside GenBank; the status line says which format was written and repeats any warning.

What is checked

  • Every valid public fixture, and (opt-in, via DNAGENT_PRIVATE_DNA) real SnapGene files, are read and written back byte for byte.
  • Taken the long way round — .dna → DNAgent GenBank → .dna, which forces regeneration — Biopython 1.85, which never sees DNAgent's own reader, finds the same sequence, topology and features in the rebuilt file as DNAgent models in the original (scripts/check_snapgene_write.py).
  • A record whose annotations no longer match its source packets is refused the verbatim path, and the desktop save reports the regeneration.

Opening a generated file in SnapGene itself is the acceptance test these cannot replace.

GenBank records (read and write)

DNAgent opens .gb, .gbk and .genbank files and saves records as GenBank. It also writes SnapGene .dna (see SnapGene reading and writing); either way a source file is never modified in place. GenBank remains the working format: it is the one that keeps everything DNAgent read, including SnapGene-only data, in a form any tool can open.

dnagent convert construct.dna --out construct.gb
dnagent annotate construct.dna --out edited.gb --add --range 1250..6188 --label "fusion ORF" --translate
dnagent annotate edited.gb --out edited.gb --remove feature-0021
dnagent features edited.gb --output json

The desktop app saves through the same code (Save / Save as…). Writes go to a temporary sibling file and are then renamed over the target.

What is written

The body is ordinary GenBank that Biopython, NCBI tools and other editors read normally.

  • LOCUS: the record name (non-printable characters and spaces become _), length, topology and the save date. DEFINITION through COMMENT headers read from a GenBank input are written back verbatim. Otherwise a minimal synthetic-construct header is written.
  • Features: in source order.
    • Key: the feature kind.
    • Location: standard notation. A reverse join is complement(join(a,b)), in DNAgent source order. An origin-spanning part is split into join(x..end,1..y).
    • Qualifiers: every original qualifier, in order. Repeated and valueless ones are kept.
  • Sequence: lowercase, 60 bases per line.

GenBank has no standard form for some things DNAgent models. These additions carry them:

AdditionHolds
/label (always first)the feature label; an imported label qualifier survives as the second one
/dnagent_colorthe display colour
/dnagent_location="v1;<operator>;<strand>;<parts>"the exact location when the standard one cannot express it: origin-spanning single parts (A<start>+<length>) or unknown strand
/dnagent_ida feature id that differs from its position-based default
COMMENT block BEGIN-DNAGENT-DATA … END-DNAGENT-DATA (files with the pre-rename …DNAAGENT… markers still open)JSON: the original record name, unplaced primers, uninterpreted SnapGene packets (base64, marked opaque or original interpreted source) and the source's import warnings

The comment block is chunked JSON, with spaces escaped as , so line trimming by editors cannot change it. It deliberately avoids NCBI structured-comment syntax (##…##), which other parsers would try to read. On reading, the block restores the name, primers and packets. The original import warnings are re-reported, so a record that lost something at its first import still says so.

Translated features added in DNAgent are CDSs with /codon_start, /transl_table and a computed /translation (without the terminal stop, following GenBank convention).

What is reported, not silently lost

  • When saving: control characters (e.g. line breaks) in labels and qualifiers cannot be stored in GenBank. They are replaced by spaces and reported as genbank_control_characters_flattened. --strict refuses to write in that case, and nothing is written.
  • When reading third-party GenBank:
    • </> partial-end markers are dropped (genbank_partial_location).
    • Remote (J00194.1:100..202), between-base (a^b) and mixed-strand locations skip the feature (genbank_feature_skipped).
    • A /dnagent_location that disagrees with the standard location is ignored (genbank_dnagent_location_ignored).
    • A missing LOCUS topology imports as linear (genbank_topology_missing).
  • Individually complemented joins: join(complement(B),complement(A)) is read as complement(join(A,B)) (the same sequence) and written back in the second form.
  • Labels: features without /label take their label from /gene, /product, /locus_tag, /standard_name or /note (the qualifier stays), else the key.

Validation

  • Unit tests in crates/dnagent-formats/src/genbank_record_tests.rs:
    • a lossless round trip of a hand-built record, covering reverse joins, origin arcs, unknown strand, order, custom ids, quotes, repeated/valueless qualifiers, long translations, primers, both packet roles and warnings;
    • byte-identical rewrites;
    • hand-written third-party GenBank;
    • whitespace trimming at chunk boundaries;
    • malformed input.
  • CLI tests (crates/dnagent-cli/tests/editing.rs): every public SnapGene fixture converts to GenBank and reopens with identical inspect, features, primers and translations. A second save is byte-identical.
  • uv run scripts/check_genbank.py --binary target/debug/dnagent: Biopython 1.85 parses every written file. Its sequence, topology, keys, labels, strands, part-by-part locations and qualifiers must equal the CLI's view of the original. DNAgent must also read NCBI's own pUC19_M77789.gb, and a Biopython-written copy of it, exactly as Biopython does.
  • The private construct used during development round-trips identically (warnings included; ten SnapGene packets carried along). It is not committed.

Conservative GenBank strand-view export

# Keep the corresponding full provenance/duplex report alongside the export.
dnagent fragments construct.dna --enzymes EcoRI,BamHI --output json > fragments.json
dnagent fragments construct.dna --enzymes EcoRI,BamHI \
  --output genbank --strand top > top.gb
dnagent fragments construct.dna --enzymes EcoRI,BamHI \
  --output genbank --strand bottom > bottom.gb

--strand is mandatory for GenBank and rejected for JSON/FASTA. Each export contains one GenBank record per fragment, in digest order. It is a selected 5′→3′ strand view, not a fully paired duplex product. Unequal strand lengths, cohesive ends and paired cores remain in the JSON report. No sequences are padded or silently circularised. An uncut circle remains circular; cut products are linear.

Deliberately conservative annotation policy

Every projected piece becomes a separate misc_feature. Its zero-based half-open coordinates are converted to one-based inclusive GenBank coordinates. Reverse mapped orientation uses complement(...). Unknown orientation uses an ordinary location plus an explicit source strand unknown note: GenBank consumers may parse that ordinary location as forward, so consult the note/JSON rather than infer biological orientation.

Pieces are not combined into an inferred join or order, nor promoted into functional CDS/gene annotations. Each carries notes recording:

  • Zero-based source-feature index into JSON source_features.
  • Zero-based original part index and forward offset within that part.
  • Complete or clipped source-base coverage (not inferred biological function).
  • A split-source-part flag when applicable to linear product ends.

Original types, IDs, ordered qualifiers, location operators, primers and display metadata retained by the domain remain in JSON. Opaque SnapGene packets and raw XML are not exported in either format; import warnings and report assumptions remain relevant. In particular, stale translation, codon_start and protein_id qualifiers are not attached to derived regions. Complete coverage alone is insufficient to assert an intact CDS or safe qualifier propagation.

Short, safe ASCII labels (at most 40 characters; letters, digits, spaces, dots, underscores and hyphens, without leading/trailing whitespace) are copied as display labels. Other labels receive a generated display label and a note pointing to the unaltered original in JSON. Quotes, newlines, Unicode and long labels cannot inject GenBank syntax. This is an explicit display fallback, not lossless metadata export.

Determinism and scope

  • Record names are generated (frag1_top, frag1_bottom, etc.); imported filenames and source names are not interpolated into native GenBank record headers.
  • LOCUS dates use 01-JAN-1980 as a documented fixed placeholder, not an experiment or source-file date. Accessions and organisms are not invented.
  • Coordinates, source forward-axis interval start and limitations are inspectable. Keep the JSON produced from the same input and enzyme selection for full linkage.
  • Global --strict retains its import-warning meaning. It can reject the operation before any output, but it does not certify a lossless or submission-ready export.
  • The CLI renders the complete text before writing stdout, so a digest/format error does not leave partial records on stdout. Shell redirection may still create an empty destination file before the program runs.
  • A fidelity advisory goes to stderr. This is not a native SnapGene round trip, a GenBank import implementation, a database submission file or experimental QC.

The format serializer lives in dnagent-formats, behind a typed application operation. Coordinate biology remains in the domain's existing projection logic; CLI and format adapters do not recompute biological intersections. JSON payloads were unchanged by GenBank export (introduced alongside 0.6.0); current 0.9.0 envelopes additionally support ligation products and Gibson PCR-tail candidates.

Validation

check_fragment_annotations.py parses both exports with pinned Biopython 1.85, treating GenBank parser warnings as failures. It compares sequence, topology, feature bounds/orientation, source-link notes, split/unknown flags and extracted feature sequences against independently checked projections. The suite covers 41 synthetic cases and the optional eight-file private corpus. Rust/CLI tests also cover hostile labels, malformed projection rejection, deterministic output, missing/inapplicable strand flags, strict mode and empty stdout on failed exports.

Remaining work: richer biological product-feature reconstruction can follow validated assembly semantics. This initial export intentionally favours explicit provenance over false biological precision. Restriction/ligation product simulation is now implemented with component annotation placements. A first Gibson PCR-tail design slice is available; its scope review and hardening precede GUI work.

Translation and ORFs

dnagent translate and dnagent orfs translate DNA with pinned NCBI genetic codes. The same engine (dnagent-domain::translation) feeds the desktop Sequence view and map. Results are computational: they do not establish genes, expression, splicing or experimental validity.

dnagent translate construct.dna --feature feature-0001 --output json
dnagent translate construct.dna --all-cds --output json
dnagent translate construct.dna --range 100..400 --strand reverse --frame 1 --table 11
dnagent orfs construct.dna --min-codons 75 --starts atg --table 1 --output json

Genetic codes

All 27 tables of NCBI gc.prt version 4.6 (1–6, 9–16, 21–33), committed as references/ncbi/gc.prt with its SHA-256 and generated into Rust by scripts/generate_genetic_codes.py. Every result reports genetic_code_source. Table 1 is the default; a feature's transl_table qualifier is honoured, and --table overrides both. In tables 27, 28 and 31 the context-dependent stop codons are translated as amino acids (the gc.prt amino-acid row), never as stops.

Translation rules

  • Coordinates: each codon reports the zero-based forward-reference positions of its three coding bases, in coding order. They decrease on the reverse strand and can jump at a join.
  • Features: parts are spliced in source order. A reverse-strand feature is the reverse complement of that splice, the GenBank complement(join(a,b)) reading. Circular parts may cross the origin. codon_start (1–3) skips bases at the 5′ end. A feature with unknown strand is not translated; --all-cds lists it under skipped with a warning.
  • Ranges: half-open START..END on the forward reference. END < START wraps through the origin on circular records only. --strand reverse reads the reverse complement from END − 1. --frame skips 0–2 bases at the 5′ end of the coding strand.
  • Ambiguity: a codon translates to an amino acid only when every IUPAC expansion agrees (CTN → L, AAN → X). A stop requires every expansion to be a stop. Starts must be exact. Biopython prints B/Z/J for some ambiguous pairs; DNAgent prints X.
  • Initiator: a complete feature CDS (codon_start 1, a table start codon, a terminal stop) whose first codon is an alternative start (e.g. GTG in table 11) is translated with M, and initiator_as_methionine is true. This is the NCBI and Biopython cds=True convention. It is never applied to ranges.
  • Reporting: protein keeps every stop as *. The result also lists internal_stops, terminal_stop, trailing_bases (0–2), ambiguous_codons and starts_with_atg.
  • Imported translations: a feature's translation qualifier is compared after removing whitespace, SnapGene's per-part commas and one terminal *. A mismatch is the structured warning translation_imported_mismatch, giving the first differing residue. Nothing is corrected silently.

Feature translation warnings are translation_internal_stop, translation_incomplete_codon, translation_ambiguous_codons, translation_imported_mismatch and translation_skipped. --strict rejects any of them, like import warnings. Failures use the error code translation_failed.

ORFs

An ORF is a complete start-to-stop reading frame on either strand.

  • Starts: ATG only by default; --starts table accepts every start codon of the table.
  • One ORF per stop: the longest, starting at the first start after the previous in-frame stop. Nested starts sharing that stop are not reported separately.
  • Minimum length: at least --min-codons amino acids, excluding the stop (default 75).
  • Circular molecules: an ORF may cross the origin (wraps_origin) but never exceeds one molecule length. A frame with no stop at all yields no ORF.
  • Linear molecules: an ORF must end with a stop inside the sequence.
  • Order and ids: ORFs are sorted by start, then forward before reverse, and numbered orf-0001 onwards. Ids depend on the search parameters.

start/length give the lowest covered forward coordinate and the length including the stop. frame is the 5′ codon base's offset from the 5′ end of its strand, modulo 3. It matches the desktop's six-frame offsets.

Desktop

The desktop document carries, all computed by the engine:

  • translations of every CDS (with flattened codon positions and warnings);
  • six whole-molecule frame translations (table 1; codons crossing the origin omitted);
  • ORFs of at least 30 codons (table 1, ATG).

The GUI only places them:

  • Amino acids sit under each codon's middle base.
  • The ORF length control filters from 30 codons upwards.
  • A dragged range's translation is a slice of the matching engine frame: forward codons start at the first selected base; reverse codons end at the last. This equals dnagent translate --range for ranges that don't cross the origin.

Validation

  • crates/dnagent-domain/src/translation_tests.rs: hand-derived cases for tables, ambiguity, joins with a split codon, origin wrap, codon_start, initiator M, ORFs and six frames.
  • crates/dnagent-cli/tests/translation.rs: JSON contract, warnings, strict mode, errors.
  • uv run scripts/check_translation.py --binary target/debug/dnagent: Biopython 1.85 re-derives every CDS translation from the CLI's locations and sequence, 768 ranges (both strands, three frames, tables 1 and 11, wrapping), all 27 tables codon by codon, and ORFs from an independent implementation of the definition above.
  • fixtures/formats/snapgene/synthetic_translation.dna covers the edge cases. The private construct used during development matched SnapGene's embedded translations for all seven CDSs, including a two-part AmpR; that file is not committed.
  • Desktop e2e scenarios check the displayed amino acids, frames, ORFs and range translations against the CLI.

Limits

  • No protein properties (mass, pI), codon usage, optimisation or splicing prediction.
  • No reading-frame repair across assembly junctions.
  • ORFs are not genes.
  • Six-frame display omits origin-crossing codons, and range selection by dragging doesn't cross the origin (the CLI --range does).

Feature library

DNAgent keeps a feature library: a SQLite database of annotated parts collected from your sequence files. dnagent detect-features finds those parts in any construct.

dnagent library import ~/constructs            # folders are searched recursively
dnagent library info
dnagent library list --kind CDS --limit 20    # one row per variant family; --all for every feature
dnagent library search puro
dnagent library show 245                       # sequence, qualifiers, every file it was seen in
dnagent library edit 245 --name "PuroR (codon-optimised)"
dnagent library edit 312 --hide                # e.g. a partial copy
dnagent library edit 88 --standalone           # keep out of a variant family
dnagent library import --rescan ~/constructs   # re-read unchanged files after rule changes
dnagent detect-features construct.gb --new-only --output json

All commands take --output json (envelope 0.9.0). The library lives at ~/Library/Application Support/DNAgent/features.sqlite on macOS ($XDG_DATA_HOME/dnagent/features.sqlite elsewhere). Use --db PATH or DNAGENT_FEATURE_DB to choose another file.

The library is private data. It is built from your constructs, so it is never committed and never bundled. Tests use the public fixtures only.

What is collected

Each annotated feature is collected unless it is:

Skip reasonRule
source_featurea GenBank source feature (it describes the record)
whole_moleculeas long as the molecule
too_shortshorter than 12 bp (--min-length); short motifs match by chance
generic_nameunnamed, named after its type, a placeholder ("Feature 3", "New feature", "untitled", "region", …), without any letter or digit (e.g. "), or starting with "(null)"
sequence_as_namenamed by a DNA sequence of 8+ letters (an unnamed primer)
ambiguous_basescontains a base other than A, C, G or T
incomplete_locationsome segments could not be imported (the file's import warnings say which), so its bases are not the feature it names

Imported primers (SnapGene's primer list) are not features and are not collected.

A feature's sequence is its bases 5′→3′ in its own direction: parts joined in order, through the origin if they wrap, reverse-complemented for reverse features.

Identity, names and provenance

  • Identity is the exact sequence, on either strand. The same part on the reverse strand, or under another name, is the same library feature. Point variants and codon-optimised versions are separate features (e.g. several 600 bp "PuroR").

  • Name, type and colour are the most common among the feature's occurrences (ties: first seen). Other names are aliases, and search matches them too.

  • Occurrences record every file (path and SHA-256), the label and type used there, and where it starts.

  • Re-importing is safe. Unchanged files are skipped. A file whose content changed replaces its earlier occurrences. A copy of an imported file is reported as a duplicate. Features no file carries any more are removed.

  • Variant families: the same part is often stored in slightly different versions: different ends (ori 589 bp, pBR322_origin 620 bp, ColE1 origin 629 and 683 bp), a few point differences (five 600 bp PuroR), or another codon usage. A shorter-or-equal feature (≥ 80 % of the head's length) joins a family when

    • its sequence lies inside the head's, on either strand (contained); or
    • they share a name and it aligns fully within the head (either strand) at ≥ 97 % identity, for features of ≥ 100 bp (similar_dna: point variants); or
    • they share a name, both are CDSs of ≥ 100 codons, and their proteins (standard code, from the first base) are ≥ 98 % identical (same_protein: codon variants).

    "Sharing a name" means any name or label either was seen under, ignoring case and punctuation ("Hyg(R)" = "HygR", "f1 ori" = "F1 ori"). Similarity alone is not enough: engineered point mutants (Plk1-K82R, OsTIR1 F74G, eSpCas9, rtTA vs tTA), other fluorescent proteins (mEmerald vs GFP) and other att or lox sites are 97–99.8 % identical to their parents but are different parts, and they have different names. Nested parts much shorter than their container (tet operator in a TRE promoter) stay separate.

    Features are taken longest first and each joins the longest existing head it relates to, otherwise it heads its own family (no chains). library list shows one row per family, ranked by the family's total occurrences, with a variant count; library show lists the family with each variant's relation and identity. Every variant stays in the library and is detected on its own. library edit --standalone keeps a feature out of families; --grouped undoes it. Families are recomputed after imports and edits, and when a new DNAgent version changes the rule.

  • Curation: library edit renames, retypes, hides or unhides a feature. Edited features keep your changes on later imports, and hidden ones are not detected.

  • Strand: if a feature is usually annotated without a strand, detections report strand unknown.

Every file is reported: imported, unchanged, duplicate or failed with a reason (for example "is not a sequence file: it contains a PDF document").

Detection

detect-features reports every exact occurrence of every visible library feature (12 bp or longer by default) on both strands, through the origin of circular molecules. Results are ordered by start, longer first. Each match carries its library id, name, type, colour, qualifiers, a standard location (linear or circular_arc) and annotated_as: existing features with exactly that span and a compatible strand. --new-only drops those. family_id names the match's family; superseded_by is the index of the longest match of the same family whose span contains it (a shorter variant at the same place), else null. All matches are listed; the desktop folds superseded ones into their longer match. Nested and overlapping matches are all reported (e.g. "lac promoter" and "lac"). Sequences containing N can't match across the N.

In the desktop app

Detect (next to the feature list heading) runs the same detection on the active tab and opens the Detected panel:

  • one row per match, with type, length, position, strand, and "already annotated" or "inside ". Shorter variants of the same family at the same place are folded into the longer match ("+2 shorter variants"); a short variant on its own still gets its own row;
  • rows that are new and not inside a longer match start ticked. Tick or untick freely;
  • clicking a row selects its span. New matches are drawn on the map as dashed arrows (solid when ticked);
  • Add N features adds the ticked ones as a single undoable edit. Each gets the library name, type and colour, a translation for CDSs, and a /note naming the library entry. The panel then refreshes, so added matches show as annotated.

Without a library the panel says how to build one. The app reads the library at the default location (or DNAGENT_FEATURE_DB).

Opening files: format by content

Sequence files are read by what they contain, not only by their extension:

  • GenBank text named .dna is read as GenBank, with a content_format_mismatch warning.
  • Office documents, PDFs and images named like sequence files fail with a clear message.
  • A .dna file without the SnapGene header is reported as an older or different format.

GenBank reading also accepts two things other editors write:

  • ApE's unquoted qualifier values that continue onto the next line.
  • * placeholders in ORIGIN. These are read as N, so feature coordinates after them stay right, with a genbank_sequence_placeholder warning. A LOCUS length that disagrees with ORIGIN gives genbank_length_mismatch.

Validation

cargo test -p dnagent-library -p dnagent-app -p dnagent-cli
uv run scripts/check_feature_library.py --binary target/debug/dnagent
uv run scripts/check_feature_library.py --binary target/debug/dnagent --collection ~/constructs  # optional, private

check_feature_library.py builds a library and compares it with an independent implementation. Biopython 1.85 parses every file and applies the rules above, and a brute-force scan checks every detection. Three Biopython differences are handled explicitly:

  • It turns SnapGene primers into primer_bind features, so the primers packet is removed first.
  • It lets a label qualifier replace a SnapGene feature's name, where SnapGene and DNAgent show the name. Names are read from the feature XML.
  • It orders the pieces of a reverse feature that wraps the origin incorrectly. A feature whose parts tile one contiguous span is read from that span.

The script also recomputes variant families (with Biopython's PairwiseAligner for the identity rules) and superseded_by independently. On the lab collection (2026-09-30): 281 files, 716 features from 4,466 occurrences, 530 families (94 contained, 89 point variants, 3 codon variants); 281 detection scans (13,096 matches, 4,950 of them folded variants) agreed.

Not yet

  • Grouping differently named copies of the same part (needs curation; use library edit).
  • Integration with the lab inventory database.

Isoform viewer

A gene locus opens as an ordinary DNAgent document: the genomic slice (plus strand, with flanks) is the sequence, and every transcript is an mRNA feature (misc_RNA if non-coding) joining its exons, plus a CDS feature that includes the stop codon. The desktop app adds an Isoforms tab for such documents; dnagent isoforms reports the same view for agents.

Getting a locus

degron-db exports one gene at a time (Python → Rust handover through a file; the database never enters DNAgent):

# in ~/code/degron-db
uv run degron-db locus HAPSTR1            # → data/export/HAPSTR1.locus.json

Open the .locus.json with Browse…, the path field, or by saving it into the workspace (~/DNAgent), where it is offered as a new tab. .locus.json is the only JSON the workspace watcher offers.

What the view shows

  • One row per transcript: exons as boxes (thick = CDS, thin = UTR), introns as lines, start and stop codons as red marks (split codons are drawn on both exons).
  • Colour is the long-read evidence for the chosen quantifier:
    • orange: quantified (matched to a long-read transcript model) and expressed;
    • blue: quantified but zero across the panel, or discoverable (it has a novel junction, so long reads could have found it) but not found;
    • grey, hatched: not distinguishable in long reads (no matching model and no junction that sets it apart), or no values from this quantifier.
  • Rows are ordered by the mean expression across the cell-line panel for the quantifier chosen in Order by (default bambu_lr), then by evidence, MANE Select first, then id. Dense quantifiers (which report zeros) average the cell lines they measured; sparse ones (NanoCount) count a missing cell line as 0. A transcript with no rows at all has no data (null mean), not zero.
  • True scale by default, with genomic coordinates on the axis; Compress introns draws every stretch not covered by any exon as a fixed short gap (marked ⫽). Loci over 100 kb start compressed (at true scale an exon would be narrower than a pixel).
  • Zoom with + / − / Fit, ⌘ + scroll, or Zoom to selection; scroll sideways to pan.

Interaction

  • Click an isoform: selects its mRNA feature, highlights its exons as bands across all rows (to compare structures), and opens the expression panel below the drawing: a bar per panel cell line (mean, with the min–max range), library counts, the panel mean, the evidence in words and the long-read model it matched.
  • Drag across the exons to mark a region; the ends snap to exon and CDS edges within a few pixels. Double-click an exon to mark exactly that exon. Show in Sequence opens the marked region (or the selected isoform) in the Sequence view.
  • Export SVG… (here and on the Map) writes the drawing as a standalone SVG with the theme's colours resolved.

CLI

dnagent isoforms GENE.locus.json                       # text table
dnagent isoforms GENE.locus.json --quantifier NanoCount_lr --output json

The JSON result (isoforms in schemas/cli-envelope-0.9.0.schema.json) lists the quantifiers with their cell lines and order, and per isoform its exons, CDS, codon marks, evidence, long-read mappings and per-quantifier expression with the panel mean and display state. Other commands work as for any record (features, translate --feature, sequence, …).

Bundle format (dnagent-locus v1)

format: "dnagent-locus", version: 1
gene {symbol, gene_id, chrom, strand "+"|"-", gene_type}
annotation_version, genomic_start (1-based genomic coordinate of position 0), genomic_end, flank
sequence                       plus strand of chrom
transcripts[] {
  transcript_id, transcript_type, is_mane_select, tsl, appris, cds_start_nf, cds_end_nf,
  protein_id, aa_len, protein,
  exons [[start, end]], cds [[start, end]]     zero-based, half-open on `sequence`
  cds_includes_stop, codon_start (1-3)
  start_codon / stop_codon {position, split}   first base in transcription direction
  evidence {state quantified|discoverable|invisible, mappings[], junctions, novel_junctions}
  expression {quantifier: [{cell_line, n_libraries, mean, min, max}]}
}
panel {quantifier: {cell_lines [{cell_line, libraries}], reports_zeros}}
expression_units, novel_transcripts, provenance[]

Coordinates are converted from degron-db's 1-based inclusive intervals once, in the exporter. Cell-line names are passed through as degron-db stores them (not normalised).

Saving

Saving writes GenBank as for any document. The locus metadata (evidence, expression, codon marks; not a second copy of the sequence) is carried in the DNAgent data block, so a saved locus reopens with the same Isoforms view. Deleting an isoform's mRNA feature removes its row; the view reports it as missing rather than drawing it from the metadata.

Validation

  • cargo test covers the reader, ordering and panel means, the GenBank round trip and the CLI.
  • uv run scripts/check_locus.py --binary target/debug/dnagent [--bundle FILE ...] recomputes everything above from the bundle: feature parts, spliced CDS translation (Biopython), codon positions, panel means, display states, order, and the GenBank round trip.
  • e2e scenarios isoforms-order-structure and isoforms-select-zoom-sequence check the drawing against dnagent isoforms on both strands and with compressed introns.

Large loci

The desktop app opens ordinary records up to 100 kb, and gene loci up to 3 Mb (the longest human genes, with flanks). Over 100 kb:

  • the Isoforms tab starts with introns compressed;
  • the Sequence view shows a window of at most 50 kb around the selection (a marked region, or the selected feature/isoform, plus 60 bases either side) instead of the whole record; with nothing selected it explains how to choose a region. Show in Sequence from the Isoforms tab is the intended route;
  • ORFs, six-frame translation and range translation are not computed (New feature… still previews a translation; dnagent orfs/translate --range work on any size);
  • restriction sites are scanned only for enzymes you Choose…; the unique-cutter sets need counts over the whole catalogue, which take seconds per megabase.

Larger loci open in the CLI. The e2e suite generates a 1.2 Mb locus (generate_synthetic_locus.py --large) rather than committing it.

Limits

  • One gene per bundle; novel (unannotated) long-read transcripts are counted (novel_transcripts) but not drawn.

Restriction sites: first biological operation

Scope

This slice detects exact recognition sites and reports nominal cut boundaries. It does not simulate a digest, produce fragment sequences, design cloning strategies or predict experimental cleavage efficiency.

cargo build -p dnagent-cli --locked
target/debug/dnagent enzymes --output json
target/debug/dnagent sites construct.dna --enzymes EcoRI,BamHI,BsaI --output json

Selection is explicit, case-insensitive and deduplicated. Catalogue output has a fixed order; sites are ordered by recognition start, enzyme name, then strand. enzymes and sites were introduced in the 0.3.0 envelope/schema. Current responses use 0.9.0; 0.8.0 added Gibson candidates after 0.7.0's ligation products and 0.6.0's annotation projections after 0.5.0's end compatibility and the separate digest command introduced in 0.4.0.

Enzyme catalogues and provenance

DNAgent always has a built-in set of 99 commercially available enzymes (BUILTIN in crates/dnagent-domain/src/restriction.rs): common cloning and diagnostic enzymes, Type IIS enzymes for Golden Gate, degenerate sites (e.g. AccI GTMKAC, ApoI RAATTY), interrupted sites (e.g. BglI GCCNNNNNGGC) and enzymes that cut outside their site. The names, sites and cut offsets are factual definitions that were hand-listed and checked against Biopython 1.85 Bio.Restriction (its REBASE-derived dictionary). No database file or implementation was copied.

A complete REBASE catalogue is optional and local. REBASE is "Copyright (c) Dr. Richard J. Roberts … All rights reserved" and states no redistribution licence, so DNAgent never commits or bundles it. scripts/manage_enzymes.py downloads one pinned release (609, EMBOSS format), verifies the pinned SHA-256 values and writes a manifest:

python3 scripts/manage_enzymes.py plan      # what would be installed, and where
python3 scripts/manage_enzymes.py install   # download, verify, publish
python3 scripts/manage_enzymes.py verify
dnagent enzyme-catalogue                    # which catalogue is active

Files go to ~/Library/Application Support/DNAgent/enzymes on macOS ($XDG_DATA_HOME/dnagent/enzymes elsewhere, or DNAGENT_ENZYME_DIR). When a verified installation exists, the CLI and the desktop app use its commercially available enzymes. Two-sided cutters and enzymes with unknown cuts are listed as unsupported rather than approximated. An installation that fails verification falls back to the built-in set with an enzyme_catalogue_fallback warning. DNAGENT_ENZYMES=builtin forces the built-in set, which the validation scripts and desktop e2e tests do so results don't depend on the machine. Please cite REBASE (Roberts et al., Nucleic Acids Res. 2023; 51:D629–D630) for results that depend on it.

Offsets are base-boundary distances from the recognition-oriented sequence start; negative values cut upstream of the site. Biopython uses fst5 for the top offset and len(site) + fst3 for the bottom. EMBOSS cut numbers are converted with offset = c + 1 for c < 0, else c (EMBOSS has no position 0).

Coordinate semantics

  • Recognition locations use the existing zero-based region representation.
  • All reported cut positions are boundaries along the stored forward sequence, including cuts on the complementary strand. The input sequence is not rotated or reverse-complemented in the artifact.
  • For a forward site at s, cuts are s + top_offset, s + bottom_offset.
  • For a reverse site of recognition length m, cuts are s + m - bottom_offset, s + m - top_offset respectively.
  • A linear cut boundary lies in 0..=length. Out-of-bounds cuts are null; the recognition site is still reported, with cleavage_available: false and a restriction_cut_out_of_bounds warning. Strict mode rejects that result.
  • Circular cuts are normalised modulo molecule length. Origin-spanning recognition sites are retained as one circular arc, not split or duplicated.
  • Palindromic sites appear once, canonically on the forward strand.
  • overhang_polarity and overhang_length describe the enzyme's nominal geometry, not an experimentally demonstrated end or a fragment-end sequence. cleavage_available only says both cut coordinates exist on the substrate.

For example, reverse-oriented BsaI at position 5 cuts at forward-coordinate boundaries 0 and 4. It must not be modelled by simply adding 7 and 11 to position 5.

Deliberate limitations

  • Input is assumed to represent double-stranded DNA.
  • Any ambiguous base in the input causes an explicit error; we do not report an uncertain sequence as having no sites. Degenerate and interrupted enzyme motifs are matched by IUPAC code on both strands.
  • Very short circular molecules are rejected if shorter than a selected enzyme's recognition/cleavage span. Multi-turn recognition or cleavage is not modelled.
  • Methylation, star activity, accessibility, buffer/temperature dependence and required flanking DNA are not modelled. In-bounds cuts at linear ends are geometric positions, not proof that an enzyme can cleave that substrate.
  • Imported metadata warnings remain visible. --strict rejects both import warnings and out-of-bounds cleavage warnings before emitting a result.
  • Unsupported enzymes/ambiguous input/short circles return JSON error code restriction_scan_failed. CLI syntax errors still use Clap diagnostics.
  • Two-sided cutters (e.g. BaeI, CspCI) are not modelled yet; REBASE lists them as unsupported.

Validation

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_restriction.py --binary target/debug/dnagent
uv run scripts/check_enzymes.py --binary target/debug/dnagent
# Optional: append --manifest "$PRIVATE_CORPUS/manifest.json" to check_restriction.py

check_enzymes.py compares every built-in enzyme (and, if installed, every REBASE enzyme Biopython also knows with the same site) with Biopython 1.85: overhang length and polarity, and top-strand cuts on a synthetic sequence carrying every site in both orientations, linear and circular. DNAgent's cuts must equal an independent brute-force scan exactly. Differences from Biopython's search are allowed only where they are explained and confirmed by brute force: Biopython omits a nominal cut exactly at a linear end, and it misses a degenerate site's reverse match at a start that also matches forward (regex alternation, e.g. MspJI CNNR at CAAG). For enzymes that cut upstream of their site, Biopython's ovhg is one too large (TspRI leaves a 9-nt 3′ overhang per REBASE, Biopython says 10), so only their cut positions are compared.

Rust tests use hand-checked cut geometry, both Type IIS orientations, circular origin wrapping, missing flanks, selection errors and ambiguous input. Three synthetic .dna fixtures are generated by the existing public fixture script.

check_restriction.py verifies all built-in catalogue definitions and forward-coordinate top-strand cut positions across both orientations, linear substrates and every rotation of synthetic circles (566 synthetic scans). Ample linear flanks avoid conflating geometric boundaries with library-specific terminal-cut conventions. Both-strand cut positions are covered by the Rust expectations; Biopython's public search comparison checks top-strand cuts only. Every comparison response is also JSON-Schema validated. Optional private scans check source hashes before processing and print no sequences.

Digest simulation

The separate digest command now partitions supported substrates at validated cut pairs, reports both strand sequences and ends, and checks against independent reference products. sites remains a recognition/geometry projection: it never silently runs a digest or assumes that reported cuts work experimentally.

Desktop display

The desktop app shows sites for an enzyme set chosen in the toolbar (remembered between sessions):

SetEnzymes shown
Unique 6+ cutters (default)exactly one recognition site, and a site of at least 6 specified bases (N not counted; BglI GCCNNNNNGGC counts 6)
Unique & dual 6+ cuttersone or two sites, 6+ specified bases
All unique cuttersexactly one site, any length
Common cloning enzymesa fixed list of 32 everyday enzymes (COMMON_ENZYMES in desktop/src/enzymes.ts) that have a site
Chosen enzymesticked in Choose… (filterable, with site counts), with a site

Counts, sites and fragments come from the engine (enzyme_counts, find_sites and digest desktop commands over the active catalogue). The frontend only picks names. An enzyme whose span is longer than a very short circle counts zero sites; any other scan failure (e.g. an N in the sequence) is shown in the status line instead of sites.

  • Map: a tick at each top-strand cut, labelled "EcoRI, ApoI (396)" with every enzyme cutting there (cut positions are zero-based boundaries, as in dnagent sites). Site labels share placement with feature labels but rank below them. Labels that don't fit are counted in the map notice (the ticks are still drawn; hover the notice for their positions).
  • Sequence: a sites row with one box per recognition site (stacked when they overlap, split across the origin) and red cut marks before the base after each cut, on both strands. A cut at a linear molecule's end has no base after it and is not marked.
  • Click a site label or box to select its recognition sequence. The selection works like a dragged range: translate it, or add a feature with New feature….
  • Digest (below the feature list) digests with the enzymes shown, lists fragments longest first with their ends (enzyme, blunt or 5′/3′ overhang, or the molecule end), and selects a fragment when you click it. The list stays, labelled with its enzymes, until you digest again.

The e2e scenarios enzymes-unique-sites, enzymes-digest-linear and enzymes-origin-site check all of this against dnagent enzymes, sites and digest.

Complete restriction digest simulation

cargo build -p dnagent-cli --locked
target/debug/dnagent digest construct.dna --enzymes EcoRI,BamHI --output json
# Public demonstration (not a lab construct):
target/debug/dnagent digest fixtures/formats/snapgene/synthetic_restriction_linear.dna \
  --enzymes EcoRI,BamHI,EcoRV,KpnI,BsaI,BsmBI --output json

The digest command calls the typed application layer and the filesystem-free dnagent_domain::digest::simulate_digest operation. It uses the same six-enzyme catalogue and recognition/cut rules as sites.

This is a complete, sequence-only cleavage model, not experimental validation. Methylation, star activity, incomplete digestion, reaction conditions, flanking DNA requirements and gel migration are not modelled. Imported files are not modified. Fragment features, primers and retained metadata are not propagated; this limitation is included in every result's assumptions. This operation returns fragment descriptions, not newly annotated SequenceRecord objects.

Contract (envelope 0.4.0)

Digest simulation was introduced in schemas/cli-envelope-0.4.0.schema.json. Current envelopes use 0.9.0; 0.8.0 added Gibson candidates after 0.7.0's ligation products and 0.6.0's annotation projections after 0.5.0's end compatibility. The digest payload shape is unchanged. Historical schemas remain available for archived responses only.

A successful digest contains:

  • input_length, input_topology, selected enzymes and model assumptions.
  • cuts: unique physical top/bottom boundary pairs, grouped with their enzymes.
  • fragments: deterministic IDs and input-forward-coordinate order, not size order. Each has topology, top, bottom, paired_length, left_end, right_end.
  • Each strand has source_start, length, sequence_5to3. Both sequences are written 5′→3′ on their own strand. source_start is the lower boundary of the source interval on the stored forward coordinate axis, including for the bottom strand; it is not the bottom oligo's 5′ end. A circular source interval can wrap and is unambiguously described by start plus length.
  • Ends report polarity (blunt, five_prime, three_prime), protruding_strand (forward = top, reverse = bottom, or null), the exposed overhang_sequence written 5′→3′, enzymes and original_terminus.

There is deliberately no single ambiguous fragment length: the two strand lengths can differ. paired_length counts the duplex core, excluding exposed bases. Text output labels top bases, bottom bases and paired bases separately. It is not a gel-band prediction.

Uncut circles retain circular topology and null ends. Uncut linear molecules retain their assumed-blunt original ends. A circle with one unique cut becomes one linear fragment; linear molecules with k accepted cuts produce k + 1 fragments, circles with k > 0 produce k. Selection order and duplicate enzyme names do not change the result. Identical cut pairs are merged, including convergent Type IIS sites that produce the same physical cuts.

Strand geometry

For adjacent cut pairs (t₁, b₁) and (t₂, b₂), a fragment comprises the top source interval [t₁, t₂) and the reverse complement of the bottom source interval [b₁, b₂). The paired core is [max(t₁, b₁), min(t₂, b₂)). Circular calculations use unwrapped boundaries; only reported coordinates are reduced modulo input length. This avoids confusing an origin-crossing 4-base overhang with an almost-full-molecule overhang.

At a cut with bottom > top, the left fragment exposes a bottom-strand 5′ overhang and the right fragment exposes the complementary top-strand 5′ overhang. For bottom < top, those are top- and bottom-strand 3′ overhangs respectively. Blunt ends have no protruding strand and an empty overhang sequence.

For example, EcoRI cleavage of AAAGAATTCTTT gives:

ProductTop, 5′→3′Bottom, 5′→3′Paired bases
LeftAAAGAATTCTTT4
RightAATTCTTTAAAG4

The top strands sum to 12 bases, and the bottom strands independently sum to 12. The exposed ends are complementary AATT 5′ overhangs. Top-only slicing would miss the asymmetric strand lengths and the actual duplex core.

Conservative rejection policy

Errors return digest_failed with no partial fragment result:

  • Ambiguous sequence, unknown enzyme, empty selection or unsupported short circle.
  • Any selected site with an unavailable cut. Unlike sites, digest cannot merely warn and omit it while calling the remaining products a complete digest.
  • Any cut at a linear terminus on either strand. Single-stranded terminal products are outside this initial model.
  • Overlapping or touching cleavage regions that leave no positive paired core between adjacent cuts. This includes the closing interval of a circle. These cases need a richer strand-product model, not invented duplex fragments.

Import warnings remain in the envelope. --strict still rejects them before analysis with import_warnings. No fragment files are written by this command.

Validation and provenance

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_digest.py --binary target/debug/dnagent
# Optional private regression corpus; never place it in the repository:
# append --manifest "$PRIVATE_CORPUS/manifest.json"

Hand-checked Rust examples cover blunt/5′/3′ ends, non-palindromic Type IIS oligos, reverse recognition, circular origins, no/single/multiple cuts, coincident cut merging and rejected geometries.

The independent checker uses Biopython 1.85, Bio.Restriction.catalyse, on both the input strand and its reverse complement for single-enzyme products. Multi-enzyme reference partitions use the union of Biopython's public search positions. All circle rotations are exercised, including mixed-polarity digests. The comparison matches the domain's documented uppercase intake convention; source files are unchanged. Additional assertions check each strand's base conservation and source-coordinate reconstruction, paired core plus exposed bases, protruding oligo position and complementary adjacent ends. JSON responses are schema-validated, including unsupported cases.

Synthetic substrates are deterministic originals, generated inside temporary directories; committed examples reuse the public synthetic fixture generator. Optional private EcoRI/BamHI digests are hash-checked and output no sequences. No reference source code or enzyme database was copied.

The separate end-compatibility operation now evaluates these ends. The separate fragments operation now projects source annotations onto both strands and exports JSON/FASTA plus conservative GenBank strand views. Ligation products now include explicit component placements. A first Gibson PCR-tail design slice is available; further hardening and scope review precede GUI integration; see the roadmap. Terminal/overlapping strand-product support remains a follow-up limitation.

Restriction-end compatibility

cargo build -p dnagent-cli --locked
# Compare all distinct ends within a digest:
target/debug/dnagent compatible-ends construct.dna --enzymes EcoRI,BamHI --output json
# Include a second, independently selected digest:
target/debug/dnagent compatible-ends vector.dna --enzymes BsaI \
  --other insert.dna --other-enzymes BsmBI --output json

The application layer digests each record using the complete-digest model and passes those digests to a filesystem-free domain operation. No source sequence is changed, and no ligation product is constructed. The existing digest rejection policy remains in force: ambiguous DNA, unavailable/terminal cuts and unsupported overlapping cleavage regions fail before compatibility is assessed.

What compatible means

Sequence compatibility only, not a prediction of experimental ligation. Phosphorylation, damage, ligase, reaction conditions, molecule concentration and circularisation geometry are not modelled. Original linear ends are assumed blunt; their chemistry is unknown. Pairwise matches do not establish a complete assembly.

Rules, in priority order:

ReasonCompatible?Meaning
polarity_mismatchNoBlunt, 5′ and 3′ ends cannot be interchanged
blunt_endsYesNo protruding oligos to match
overhang_length_mismatchNoSame polarity, unequal exposed lengths
complementary_overhangsYesEqual-length oligos are reverse complements
overhang_sequence_mismatchNoSame polarity/length, non-complementary oligos

Overhangs are compared in their own 5′→3′ orientation. Thus ACGA pairs with TCGT, not another ACGA; palindromic AATT pairs with AATT. Enzyme names are provenance, not a matching criterion: different Type IIS enzymes can generate compatible ends, while the same enzyme can produce incompatible variable ends. Five- and three-prime ends remain incompatible even if their oligos complement.

Orientation and placement

The first fragment stays in its stored orientation. The report proposes:

  • Opposite-side ends: second_fragment_orientation: forward.
  • Same-side ends: second_fragment_orientation: reverse.
  • First end on the left: second_placement: before_first.
  • First end on the right: second_placement: after_first.

Reversing a double-stranded fragment exchanges its top/bottom strands and left/right sides; it does not rewrite an exposed physical oligo's own 5′→3′ sequence for the compatibility comparison. These fields describe the local alignment considered, even for incompatible pairs. They do not apply a transformation or create a product. For same-fragment pairs they describe local end alignment, not a second molecule copy; a sequence-compatible pair is only a potential closure candidate.

The public domain comparator validates side/polarity/protruding-strand consistency, uppercase ACGT oligos and blunt-end shape. Malformed public FragmentEnd values produce an error, not a plausible compatibility assessment.

JSON contract 0.5.0

schemas/cli-envelope-0.5.0.schema.json adds compatible-ends; historical schemas remain unchanged. Existing command payload shapes are unchanged, with current envelopes now labelled 0.9.0; 0.8.0 added Gibson candidates after 0.7.0's ligation products and 0.6.0's annotation projections. The 0.5.0 schema is retired and retained for archived responses only.

  • inputs: one or two named, complete digest results, in command-line input order. This retains source intervals, strand sequences, enzyme/cut provenance and model assumptions so endpoint references can be independently checked.
  • analysis.endpoints: deterministic IDs such as input-1:fragment-0001:left, one-based input number, local fragment ID, side and full end geometry. Uncut circles contribute no endpoints.
  • analysis.pairs: every unordered pair of distinct endpoints, including within-input and cross-input comparisons. Each has endpoint IDs, same_fragment and an assessment with compatibility, reason, orientation and placement.
  • analysis.assumptions: explicit scope and experimental limitations.

Endpoints are ordered by input, digest fragment order, then left/right side. Pairs follow that order. No endpoint is paired with itself; identical molecule copies are not inferred. An uncut linear molecule still contributes its two assumed-blunt natural ends. Two uncut circles produce an empty endpoint/pair report.

The matrix is limited to 128 endpoints (8,128 pairs). Larger requests fail with compatibility_failed; output is never silently truncated. Select fewer enzymes to reduce fragment count. Endpoint structure errors use the same code; upstream digest failures retain digest_failed.

Both imports' warnings are retained in input order, including when loading the second file fails. Strict mode rejects warnings before producing an analysis. CLI argument errors (for example --other without --other-enzymes) still use Clap diagnostics, not the runtime JSON envelope.

Validation

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_compatibility.py --binary target/debug/dnagent

Domain and CLI tests cover all reasons, both cohesive polarities, same/opposite sides, cross-enzyme matches, malformed end values, closure candidates, uncut circles, deterministic bounded matrices and two-input warning retention.

The independent checker creates all 256 four-base BsaI oligos and a second BsmBI substrate in temporary synthetic files. It verifies the designed oligos against the extracted ends, uses Biopython 1.85 to calculate reference reverse complements, then checks every pair's reason, orientation, placement and provenance. Mixed-polarity fixtures and explicit oversized-request rejection bring the total to 260 cases. All responses are schema-validated. These are software/model checks, not experimental ligation measurements; no private inputs are needed.

Source-linked strand projections and JSON/FASTA exports and conservative GenBank views are now implemented. Explicit restriction/ligation products are now implemented; a first Gibson PCR-tail design slice is also available. Gibson hardening and scope review precede GUI integration.

Annotation-aware digest strands and exports

# Default output is JSON; includes duplex geometry and annotation provenance.
dnagent fragments construct.dna --enzymes EcoRI,BamHI --output json > fragments.json
# Two explicitly labelled 5′→3′ strand records per fragment, not one duplex record.
dnagent fragments construct.dna --enzymes EcoRI,BamHI --output fasta > strands.fasta

fragments uses the same conservative complete-digest simulation as digest. It does not skip unavailable, terminal or overlapping cuts. Failed calculations return no partial products. Global --strict rejects import warnings before emitting either export. Runtime annotation/digest failures use annotation_failed; import failures retain the usual import error codes. Current JSON envelopes are 0.9.0 (fragment projections were introduced in 0.6.0).

Why separate strand projections?

A cohesive-ended fragment does not have a single sequence length or identical strand boundaries. An EcoRI digest can retain two annotated bases on the top strand but six on the bottom strand of the same product. Applying top-strand coordinates to both would silently lose or invent annotation coverage.

Each output has:

  • source_name, full unmodified source_features, and source_primers.
  • digest: the existing sequence-only duplex report, including topology, both strand sequences, paired length, cohesive ends and source intervals. Its sequence-only assumptions describe this nested payload, not the wrapper.
  • annotations: one item per digest fragment with separate top and bottom mapping arrays. Features with no retained bases on a strand are omitted from that array, but remain in source_features.
  • Explicit assumptions and limitations; ordinary import warnings remain in the top-level envelope.

Each mapping references a source feature ID. fragment_strand is relative to that output strand's own 5′→3′ sequence: bottom projections flip forward and reverse, retaining unknown strand as unknown. Original strand and location operator remain in the source feature. This is an explicit coordinate transform, not a claim that a feature resides biologically on both physical strands.

parts preserve original source-part order, then forward offset within each part:

  • source_part: zero-based index into the original ordered location parts.
  • source_offset: offset along that original part's forward-coordinate traversal.
  • source_region: retained region on the original forward source axis.
  • fragment_region: linear, half-open region on this exported strand's sequence.

Circular regions are intersected using unwrapped coordinates. A source part may be represented by multiple pieces; its identity and ordering are not flattened. Do not sort the mapping parts by local coordinate or interpret their count as a new join/order operator. A bottom-frame transform does not change original source-part order.

Coverage is not biological integrity

source_bases and retained_bases count annotation bases with part multiplicity (overlapping original parts remain distinct). complete means all those bases are represented, not that a gene is contiguous, in frame, expressed or functional. False indicates clipping. split_source_parts identifies original contiguous parts represented across opposite ends of a linear fragment; an uncut circular display seam is not marked as a physical disruption.

A single-cut circle can retain every annotated base while splitting a contiguous feature between the product ends: complete: true and a nonempty split_source_parts deliberately coexist. Disruption between multipart regions, splicing/frame changes and functional integrity are not inferred. Source qualifiers, including any translation or position-valued text, are unchanged provenance and must not be reused as validated product annotations.

Export boundaries

FASTA contains both strands in their physical 5′→3′ orientation, with distinct fragment-0001|top and fragment-0001|bottom identifiers. Its source_start header field is the start of the strand interval on the source forward axis, not necessarily the coordinate of the first emitted base on a bottom strand. Sequences wrap at 80 characters. Names and imported labels are not inserted into headers. An advisory goes to stderr; stdout contains only FASTA.

FASTA does not preserve feature mappings, complete provenance, topology or duplex end geometry; keep the JSON alongside it. JSON preserves source metadata and projections but is not a lossless SnapGene round-trip: opaque packets/XML and uninterpreted metadata are not exported. Source primers are retained without inventing binding-site mappings.

Conservative GenBank export is now available with --output genbank --strand top (or bottom). It explicitly selects one strand and represents each mapped piece as misc_feature, with source-link and clipping notes rather than stale CDS qualifiers. It is not a duplex product or lossless annotation round trip; keep the JSON report alongside it.

Validation

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_fragment_annotations.py --binary target/debug/dnagent
# Optional private corpus; inputs remain external and hash-checked:
uv run scripts/check_fragment_annotations.py --binary target/debug/dnagent \
  --manifest "$PRIVATE_CORPUS/manifest.json"

The independent oracle enumerates per-base source/local associations from source XML rather than reusing the Rust interval-intersection algorithm. It checks all nonempty intervals/arcs and all three feature strands across every rotation of synthetic EcoRI/KpnI/EcoRV circles, linear substrates, an uncut circle and a multipart/origin fixture. It validates retained coverage, split markers, strand orientation, source-region reconstruction, schema, FASTA content and both GenBank strand views (including parsed feature extraction and topology). Additional Rust tests cover order, unknown strand and qualifier preservation; CLI tests cover strict mode and failure-without-partial-output.

Biopython 1.85 is used only for sequence and FASTA verification: its annotation parser treats some full-circle arcs (e.g. one-based 2-1) as empty. For sequence verification the checker builds an in-memory header/DNA-packet view; original annotation XML and files are unchanged. This workaround is explicit and does not remove full-circle arc cases from annotation validation.

Explicit restriction/ligation simulation

dnagent ligate plan.json --output json
# Public synthetic examples:
dnagent ligate fixtures/plans/synthetic-religation.json
dnagent ligate fixtures/plans/synthetic-closure.json

JSON is the default; --output text gives a human-readable sequence/assumption summary. No product files are written by the program. Plans use ligation-plan-1.schema.json; output envelopes are 0.9.0 (ligation was introduced in 0.7.0). Plan version 1 is independent of envelope versioning.

Explicit selection, never inferred copies

{
  "schema_version": 1,
  "inputs": [
    {"path": "vector.dna", "enzymes": ["EcoRI", "BamHI"]},
    {"path": "insert.dna", "enzymes": ["EcoRI", "BamHI"]}
  ],
  "fragments": [
    {"input": 1, "fragment_id": "fragment-0002", "orientation": "forward"},
    {"input": 2, "fragment_id": "fragment-0001", "orientation": "reverse"}
  ],
  "topology": "circular"
}

This is a plan-shape example, not a verified construct design. Run digest or fragments on the intended sources to inspect IDs, ends and annotations; select suitable fragments deliberately. Relative input paths resolve against the plan directory, not the caller's working directory. Unknown plan fields and orientation values fail rather than being ignored.

  • inputs declares distinct complete-digest instances, numbered from 1.
  • fragments declares product left-to-right component order and explicit forward/reverse orientation. Nothing searches for a favourable ordering.
  • A particular (input, fragment_id) can appear only once. To model a second molecule copy, explicitly declare another input instance (the same path is allowed). This is a declared model copy, not an inferred experimental quantity.
  • Each internal junction uses the previous component's right end and the next component's left end. Circular topology also joins the final right end to the first left end. Ends are never reused.
  • Uncut circles have no free ends and cannot be selected, including as pass-through products. A linear single-component plan is an explicit zero-junction pass-through.
  • Unselected fragments remain in unused_fragments, linked to full input reports.

All sources use the conservative complete-digest rules: ambiguous bases and unavailable/terminal/overlapping cuts fail. A plan does not silently skip cuts in unused inputs or select only a convenient subset of a failed digestion.

Product geometry and conservation

Reversing a duplex exchanges its original top/bottom 5′→3′ sequences and left/right ends; protruding-strand labels flip. The physical overhang oligos do not themselves get reverse-complemented during that orientation change.

The product top sequence concatenates oriented component tops in plan order. The bottom 5′→3′ sequence concatenates oriented component bottoms in reverse plan order. No overlap sequence is deleted: cohesive overhangs are complementary physical strands, not duplicated same-strand bases as in a homology assembly.

Every selected base on both strands is retained exactly once. Each proposed junction must pass the existing end-compatibility rules. Product-wide duplex checks additionally require:

  • Linear paired cores to be complementary and both terminal staggers to agree with the retained free ends. Unequal strand lengths remain explicit.
  • Circular strand lengths to be equal and complementary at the reported phase.

bottom_forward_start locates the start of bottom's forward-coordinate interval relative to top base zero. It can be negative for a linear 3′ stagger; on a circle it is normalised modulo product length. The bottom sequence itself is not rotated to force it to equal the direct reverse complement of the top string.

components[].top and .bottom give zero-based start/length on each product strand's own 5′→3′ sequence. source_strand specifies which original digest strand supplied that span (forward = original top, reverse = original bottom). junctions give one-based adjacent component numbers, a closure flag, compatibility assessment and each strand's own-axis nick boundary. The assessment compares already oriented components: its relative forward result does not override the explicit selection orientation or instruct a second transformation. Circular closure boundaries are zero in those respective frames, which can have different forward-axis origins.

Annotation provenance, not inferred gene repair

inputs retains each complete annotated digest, including original features, qualifiers, operators, primers and component-local projections. Match a product component by input number and fragment ID, choose its source_strand projection, then add the component's product-span start to its local coordinates. Local 5′→3′ sequences are used unchanged, so no additional per-piece reversal is needed.

This preserves an inspectable source-to-product mapping. It does not merge clipped features across junctions, infer repaired/fused genes, update translations, or validate functional integrity. Existing clipping/split flags describe source components, not a reconstructed biological feature in the final product. Opaque SnapGene metadata is still outside these exports; import warnings remain relevant.

Failures, bounds and biological assumptions

Plan and ligation-analysis failures use ligation_failed, with no partial product. Incompatible junction errors identify the adjoining component numbers and reason. Source-read failures retain command_failed; strict warning failures retain import_warnings. Top-level warnings accumulate in source-loading order, including when a later source fails. --strict rejects any import warning before simulation; it is not experimental validation or a statement of complete export fidelity.

The operation bounds plans to 16 inputs, 128 selected components and 10,000,000 bases per product strand. These are explicit resource limits, not biological rules. It does not construct a quadratic all-end matrix.

The prediction assumes compatible selected junctions seal completely. It does not model phosphorylation, unsealed nicks, reaction conditions, yields, competing products, gel isolation, methylation, star activity or experimental sequence QC. There is no trimming, fill-in, polishing or implicit end repair. Gibson PCR-tail design uses separate homology rules.

Validation

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_ligation.py --binary target/debug/dnagent
# Optional existing authorised private corpus (kept outside Git):
uv run scripts/check_ligation.py --binary target/debug/dnagent \
  --manifest "$PRIVATE_CORPUS/manifest.json"

The checker verifies literal designed Type IIS fusion sequences for all 256 four-base oligos, including reversed asymmetric components (512 fusions), complete source recovery after linear re-ligation, every rotation of three circular enzyme substrates, placement/annotation offsets, nick boundaries, duplex phase and unused fragment accounting. Private checks recover each of eight real plasmids in both orientations against Biopython 1.85 source sequences. Source hashes are checked before/after; no private inputs or reports are committed.

Rust/CLI checks also cover plan parsing, cwd-independent relative paths, copy/reuse rules, no-end circles, malformed/missing plans, resource limits, sticky-ended pass-through products, warning retention and failure without partial products. This is an independently checked software model, not an independent experimental ligation oracle. Native product annotation reconstruction and product-specific FASTA/GenBank export remain follow-up work; current product sequences are in JSON or text. A first Gibson PCR-tail design slice is now implemented; Gibson hardening and scope review remain ahead of GUI work.

Gibson cloning: explicit PCR-tail candidates

dnagent gibson fixtures/plans/synthetic-gibson.json --output json
# Human-readable product and primer candidates:
dnagent gibson fixtures/plans/synthetic-gibson.json --output text

This first Gibson operation designs PCR-tailed fragments and predicts their ideal overlap-directed product. It is not an assembler for supplied fragments with pre-existing overlaps, and does not optimise primer thermodynamics. Generated oligos are inspectable candidates, not ordering-ready or experimentally validated recommendations. Separate commands now provide bounded primer optimisation and existing-overlap assembly; neither changes this fixed-length plan contract. No GUI integration is included in this milestone.

Plan and coordinate contract

{
  "schema_version": 1,
  "inputs": [{"path": "template.dna"}],
  "cores": [
    {"input": 1, "start": 270, "length": 90, "orientation": "forward"},
    {"input": 1, "start": 90, "length": 100, "orientation": "reverse"}
  ],
  "topology": "circular",
  "overlap_length": 25,
  "annealing_length": 22
}

This is the shape of the synthetic example, not a design for an unspecified real 300-base template. The version-2 plan schema is independent of the current 0.10.0 CLI output-envelope schema. Relative paths resolve against the plan directory. Unknown fields, missing orientations and unsupported versions fail explicitly.

  • Inputs may be SnapGene .dna, single-record FASTA, literal sequences, selected digest strands or ideal PCR-product projections. SnapGene records retain source annotations; projected digest/PCR records explicitly warn when source annotation or duplex geometry is not carried into the derived sequence view. See existing-overlap assembly for the source forms.
  • input is one-based. start is zero-based on the original forward axis; length is the count of selected source bases. Linear ranges cannot wrap. Circular ranges may cross the origin, but cannot exceed one full source lap.
  • Extract the interval first, then reverse-complement it only if orientation is reverse. The cores array explicitly fixes final product order and base zero.
  • The predicted product is exactly the concatenation of these oriented cores. If the selected cores already contain shared sequence, that sequence remains in each core: endogenous overlaps are not silently deduplicated.
  • Reusing a template for multiple PCRs is allowed. Product overlaps still must pass uniqueness checks; intended repeat-containing architectures may be refused.
  • Every core states its preparation: pcr (amplified here, so it can be given primer tails) or provided (a restriction fragment, a synthesised piece or stock linear DNA, used exactly as given). This is required, because defaulting it would silently amplify a recipient vector the user intends to digest. A provided fragment's ends are taken as declared; DNAgent does not verify that a digest actually produces them.

Primer, PCR and junction construction

Let oriented core i be C_i, the annealing length be A, and overlap length L. A junction's overlap is one shared stretch, written into one of the two neighbouring fragments by a primer tail. Which one depends on their preparation, because a provided fragment cannot be extended:

Junction i → nextOverlapTail goes on
C_i is pcrC_next[:L]reverse primer of i
C_i is provided, C_next is pcrC_i[-L:]forward primer of next
both provided—refused: no primer can add it
  • Forward primer of i: (forward tail) + C_i[:A], where the tail is empty unless the upstream fragment is provided.
  • Reverse primer of i: RC(reverse tail) + RC(C_i[-A:]), all written 5′→3′.
  • Predicted PCR top strand: (forward tail) + C_i + (reverse tail).

For the final linear component there is no downstream junction; for circular closure its neighbour is the first core. A tail is never divided between two primer pairs. Each primer reports its complete oligo, 3′ annealing segment, 5′ tail and annealing GC base count. GC count is descriptive, not a quality score or melting-temperature estimate. Provided fragments report no primers at all, and their fragment_sequence_5to3 is the core itself.

The usual bench case is a vector cut with one or two enzymes and used as it is: mark it provided and the insert pcr, and both overlaps are written into the insert's two primers — two oligos, with no amplification of the backbone.

The simulator also reassembles the predicted PCR products: verify each suffix/prefix overlap, retain one copy, then remove the duplicated closing overlap for a circle. The result must equal the requested core concatenation exactly. Unlike restriction ligation, same-strand synthetic homology copies are merged. The product is an ideal fully paired duplex; product_sequence_5to3 is its chosen reference strand and the other strand is its reverse complement. Intermediate single strands, gaps, nicks and reaction kinetics are not represented.

junctions report one-based neighbouring component numbers, the closure flag, overlap sequence, and its zero-based start in the final product. Closing overlap starts at product zero. Product topology is always explicit; single-core linear plans predict PCR only, while single-core circular plans model PCR-assisted reclosure.

Product GenBank (--out)

dnagent gibson plan.json --out product.gb --output json
dnagent gibson-optimise plan.json --out product.gb [--name "pUC19-ORF"]

Writes the predicted product as DNAgent GenBank (the path must end in .gb, .gbk or .genbank), which opens in the desktop app with everything annotated:

  • carried features: every source feature lying wholly inside a core, at its product position, strand-flipped for reverse cores, with its type, name, colour and qualifiers, plus a /note naming its source. A feature only partly inside a core is left out with a gibson_feature_clipped warning (per core), never clipped silently;
  • primer sites: primer_bind "Gibson F" / "Gibson R" over the whole oligo as it reads on the product (a reverse primer's tail runs into the next core; the last one may cross the origin). gibson-optimise uses the optimiser's chosen primers. The file's primer list holds the full oligos (candidates, not validated). A primer that does not read as one stretch is listed but not annotated (gibson_primer_not_placed);
  • junctions: "Gibson overlap a-b" over each overlap.

The JSON result gains product_genbank (path, length, feature, primer and left-out counts); the JSON remains the design record. --strict refuses before writing if the product would carry any warning. scripts/check_gibson_product.py checks the written records independently (Biopython: sequence = oriented cores, carried and left-out features from the core intervals, primer and overlap bases). The desktop scenario gibson-agent-cdna-into-vector plays the agent after a handoff and opens the product.

Exact-match checks and conservative limits

  • Every primer annealing sequence must have exactly one exact binding-site locus across both orientations of its full source template, including circular seam matches. Self-reverse-complementary annealing sequences are rejected.
  • Every junction overlap must occur exactly once across both orientations of the whole intended product, including component junctions and circular seams. Repeated or self-reverse-complementary homology is rejected, not silently selected.
  • Templates must be unambiguous uppercase ACGT after normal import canonicalisation. Ambiguity anywhere on a template prevents an exact-site uniqueness claim.
  • Bounds: 1–16 templates; 1–128 cores; 20–60-base overlaps; 18–40-base annealing segments; each core at least max(L, 2*A); at most 1,000,000 bases per source template and final product. These are implementation limits, not universal experimental thresholds.

Uniqueness here is an exact-match sequence check, not proof of specific PCR or exclusive assembly. Partial/mismatched homology, genomic/off-template targets, melting temperatures, salt/concentration effects, hairpins, primer dimers, polymerase behaviour and reaction conditions are not evaluated. Separate template-specific PCR reactions and sequence-faithful products are assumed; multiplex PCR is not modelled. Single-fragment closure can create interacting primer sequences, which especially requires secondary-structure review. No particular reaction yield is predicted, and primer ordering requires further review.

Annotation and import provenance

Each component retains core-local source feature mappings. Match the selection's input number, then the mapping's source feature ID. Add product_start to its local coordinates to obtain final reference-strand positions. Reverse-oriented cores already have reversed local coordinates and flipped known feature strands; do not reverse them a second time. Multipart ordering, coverage, source offsets and split flags use the shared annotation-projection rules.

Synthetic tails are copies of downstream core prefixes, not new functional features. The final product retains that downstream core's annotation association once. No feature reunion, gene fusion, coding-frame repair or translation update is inferred; clipping/split flags describe the individual cores, not a reconstructed gene. Product-specific FASTA/GenBank export and biological feature reconstruction remain follow-ups. JSON is the authoritative design/provenance report for now.

Imported primers are historical metadata, not reused primer-design predictions. Opaque packets and raw XML are not exported. Warnings accumulate in input-loading order, even if a later input fails; --strict rejects any import warning before simulation. Design/plan failures use gibson_failed, source I/O uses command_failed, and strict rejection uses import_warnings. There is no partial product on failure. The program does not write product or primer-order files.

Reproducible validation and next steps

cargo test --workspace --locked
uv run scripts/check_cli_schema.py --binary target/debug/dnagent
uv run scripts/check_gibson.py --binary target/debug/dnagent

The independent synthetic checker uses Biopython 1.85 to recover source sequences and reverse complements, reconstructs PCR products from the whole primer oligos and template interiors, then merges overlaps to recover the expected core product. It covers both orientations of each component, linear/circular products, circular source arcs, minimum/maximum configured lengths, single-core cases, GC accounting, and per-base source-feature associations. Negative controls cover repeats, opposite- strand overlap ambiguity, invalid plans/ranges/limits, ambiguous templates and strict warning retention. These are software-model checks, not wet-lab validation or an independent thermodynamic/experimental assembly oracle. The new Gibson checker uses synthetic originals only; private-template design coverage remains a gap.

Bounded NN-Tm primer optimisation and explicit existing-overlap intake are now available as separate operations. Next work: full folding-energy assessment, richer product annotation/export, real-template designs and a separate assembly-engine comparison. GUI adapters remain deferred while these scope boundaries are reviewed.

Bounded Gibson primer optimisation

dnagent gibson-optimise fixtures/plans/synthetic-gibson-optimisation.json
# US spelling is an alias; envelopes retain the canonical command name:
dnagent gibson-optimize fixtures/plans/synthetic-gibson-optimisation.json

Output is JSON only, envelope 0.9.0. The input uses a separate version-2 optimisation plan. See the public synthetic example for all required fields. There are no implicit reaction-condition or screening defaults. Its values are a software example, not a recommended PCR protocol.

What changes, and what does not

The optimiser keeps the declared source intervals, orientations, product order, overlap lengths and one-sided PCR-tail scheme unchanged. It enumerates forward and reverse annealing lengths within the explicitly requested 18–40-base bounds. A longer annealing segment can resolve an exact-site ambiguity at a shorter length; the operation does not prematurely reject the design at its minimum length.

Candidates must pass:

  • Exact annealing-site uniqueness on the whole source, both orientations, including circular seams. Self-reverse-complementary annealing segments are refused.
  • Inclusive annealing-Tm and GC-fraction bounds.
  • Inclusive full-oligo hairpin/self-dimer sequence-screen limits.
  • Pair Tm-difference and full-oligo heterodimer limits, and non-overlapping annealing intervals on the selected core.

Among feasible pairs, minimise abs(Tm_forward-target) + abs(Tm_reverse-target). Ties prefer shorter total annealing length, then shorter forward, then reverse length. Each component is optimised independently for separate PCR reactions. There is no multiplex cross-component primer screen. At most 23 candidates per orientation and 529 pairs per component are considered before filtering.

Failure to find a feasible pair returns gibson_failed, identifying the component, with no relaxed-constraint fallback or partial design. In particular, the fixed one-sided tail scheme can make single-core circularisation infeasible because the primer pair contains complementary sequence. No alternative tail scheme or source boundary shift is silently substituted.

Tm model and explicit units

Tm applies to the template-matching annealing segment, not the full tailed primer. It is not a recommended PCR annealing temperature.

  • DNA nearest-neighbour parameters: Allawi & SantaLucia (1997), Biochemistry 36, 10581–10594; equivalent to Biopython's DNA_NN3 table.
  • Entropy salt correction: SantaLucia (1998), PNAS 95, 1460–1465: ΔS += 0.368 × (length−1) × ln([Na_equivalent] in M).
  • Sodium equivalence: von Ahsen et al. (2001), Clinical Chemistry 47, 1956–1961: concentrations in mM, Na_equivalent = Na + K + Tris/2 + 120 × sqrt(max(Mg−total_dNTP, 0)).
  • Tm = 1000×ΔH / (ΔS + 1.987×ln(primer concentration in M)) − 273.15. The primer is assumed in excess; complementary template concentration is neglected. Self-complementarity symmetry corrections are unnecessary because such annealing sequences are rejected before scoring.

Plan concentrations sodium_mm, potassium_mm, tris_mm, magnesium_mm and dntp_mm are millimolar; dNTP means total, not per nucleotide. primer_nm is nanomolar. All must be finite. Accepted bounds are Na/K 0–500 mM, Tris 0–200 mM, Mg/total-dNTP 0–10 mM, primer 1–5000 nM, with positive sodium-equivalent concentration. These are input guards, not empirical calibration guarantees. The Mg/dNTP treatment is an approximation, not a chemical-equilibrium solver.

The Rust equations and numeric lookup are independently implemented from the referenced model; pinned Biopython 1.85 is the numerical comparison, not a required runtime engine. No Primer3 or other folding engine is silently invoked.

Structure screens are not structure thermodynamics

Screens examine the complete oligos, including tails:

  • Hairpin: longest contiguous Watson–Crick stem with at least three unpaired loop bases. max_hairpin_stem is an inclusive allowed stem length.
  • Dimer: longest contiguous complementary run over all antiparallel alignments.
  • 3′ dimer: longest such run reaching either oligo's 3′ terminus.

max_dimer_run and max_three_prime_run apply to both self- and within-pair heterodimers. Stem/run lengths are not ΔG or structure Tm. Bulges, mismatches, non-contiguous stems, temperature-dependent folding and kinetic effects are not modelled. A passing screen is not proof that primers will work or are ordering-ready.

Report, provenance and validation

The result includes unchanged constraints/solution values, selected full oligos, annealing Tm, full-oligo screens, score, and candidate-pair counts after single-primer filtering. design is the shared PCR-tail product/provenance report. Its annealing_length is null when selected lengths vary; the actual annealing sequences are always explicit. Fixed-length gibson results retain their integer length. Original templates, annotations and import warnings remain available.

The independent checker enumerates the whole candidate space, compares Tm with Bio.SeqUtils.MeltingTemp.Tm_NN(DNA_NN3, saltcorr=5, dnac2=0), uses separate brute-force sequence screens, verifies the winning pair and feasible counts, and reconstructs PCR products from the selected oligos. It covers different salts, Mg below/equal/ above total dNTP, orientations, varied length bounds, and impossible constraints. This is software-model validation, not wet-lab validation or a second empirical primer-design engine. Real-template optimisation coverage and full folding-energy assessment remain follow-ups.

uv run scripts/check_gibson_extensions.py --binary target/debug/dnagent

Assembly of declared existing overlaps

dnagent gibson-assemble fixtures/plans/synthetic-gibson-existing.json
dnagent gibson-assemble plan.json --output fasta > product.fasta
dnagent gibson-assemble plan.json --output genbank > product.gb

JSON output uses envelope 0.9.0. FASTA is sequence-only; conservative GenBank uses component misc_feature records and does not infer genes, CDSs or translations. The input is a separate version-1 existing-overlap plan. It uses fragments, not PCR cores, and an overlaps array, not a single PCR-tail length. Wrong plan kinds and unknown fields are rejected.

Input meaning

Each selection identifies input (1-based), start (zero-based source forward axis), length and explicit orientation. Extraction precedes reverse complementation. Paths resolve relative to the plan. A supplied full fragment is a selection starting at zero with its full sequence length.

Every Gibson plan now accepts four source forms:

{"path": "fragment.fasta"}
{"name": "ordered_insert", "sequence": "ACGT...", "topology": "linear"}
{"path": "vector.dna", "enzymes": ["EcoRI"], "fragment_id": "fragment-0002", "strand": "top"}
{"path": "template.dna", "start": 5535, "length": 654,
 "orientation": "forward", "left_tail": "ACGT...", "right_tail": "TGCA...",
 "forward_annealing_length": 26, "reverse_annealing_length": 18}
  • Plain paths accept SnapGene .dna and single-record .fa, .fasta or .fna. FASTA is imported as sequence-only linear DNA and emits an explicit warning because topology/features are not represented by the format.
  • Literals require a name, validated IUPAC DNA and explicit topology. Exact Gibson operations subsequently require unambiguous ACGT.
  • Digest sources run a complete digest and expose the selected top or bottom 5′→3′ strand as a linear source. Its selection can trim terminal restriction-site bases when the declared assembly overlap lies inside the physical digest fragment.
  • PCR sources extract an oriented source interval, add product-oriented left/right tails, check exact annealing-site uniqueness and expose the resulting ideal PCR product. Use either one shared annealing_length or both asymmetric lengths. Designed 5′→3′ primer candidates are retained in that derived input record's primers array. Thermodynamic optimisation is not performed by this source form.

Digest/PCR projections emit warnings because their JSON source record is a selected assembly strand/product rather than the complete original duplex/annotation model. Use the separate digest/fragment output for end geometry and source annotations. --strict therefore rejects these deliberately lossy projections.

Selections declare existing linear fragment views, even when the source record is circular. Source topology controls interval extraction only; preparation by PCR, digestion or synthesis is not inferred. This operation neither designs primers nor pretends to open an uncut circular molecule. The user must establish that the physical fragments and termini match the declared views.

  • Explicit order and orientations; no order/orientation search.
  • One 20–60-base overlap for each adjacent junction. Circular products also require the last-to-first overlap. Lengths may differ at different junctions.
  • Each oriented suffix must exactly equal the following oriented prefix.
  • Each fragment must have a positive interior after incoming and outgoing overlap lengths are subtracted. Nested/overlap-only fragments are refused.
  • At most 16 sources, 128 fragments and 1,000,000 total supplied fragment bases; each source is also limited to 1,000,000 unambiguous ACGT bases.
  • Circular source intervals may cross the origin but cannot exceed one lap; linear sources cannot wrap.

A linear one-fragment plan has overlaps: [] and is a pass-through. A circular one-fragment plan has one overlap and requires matching duplicated terminal homology already present in its supplied linear sequence. It is not implicit circularisation of a source circle.

Product and ambiguity policy

The predicted top-strand sequence merges only the declared overlap copies. The first selected fragment defines product origin; a circular closing duplicate is removed from the final suffix. Length is exactly sum(fragment lengths) − sum(declared overlap lengths).

Every overlap must occur exactly once in the merged product when both orientations are considered, with circular seam matches included. Repeated or self-reverse- complementary homology is rejected conservatively. There is no mismatched/partial homology, best-effort trimming, length search or ambiguous alternative output. All source-fragment bases are checked against their reported product positions.

This is an ideal exact-overlap sequence prediction, not digestion-end geometry, assembly kinetics or experimental validation. Existing fragment overlaps are not assessed for overlap Tm or folding; the separate primer optimiser evaluates PCR annealing segments, not Gibson-reaction conditions.

Annotation provenance in shared homology

Components retain source selections, their entire supplied sequence and source- linked component-local annotation projections. Overlapping source associations are both retained, even though the product carries one copy of the sequence. No gene reunion, fusion, translation or functional repair is inferred.

For component-local coordinate x:

  • Linear product coordinate: component.product_start + x.
  • Circular: (component.product_start + x) % product_length.

Orientation is already applied to component-local annotations; do not reverse them again. wraps_origin reports whether this component's span exceeds the stored product end. It is particularly important for the closing component and single- fragment reclosure. In shared homology this mapping need not be one-to-one across components (or across the duplicated termini of a reclosed fragment).

An annotation's complete flag concerns retained source-base associations in the component view, not a distinct-base count or biological integrity of a product feature. JSON remains the source of truth. Direct FASTA and conservative GenBank exports are available, but they intentionally carry less provenance: FASTA is sequence-only and GenBank records component associations as misc_feature entries.

Validation

Domain/CLI tests and scripts/check_gibson_extensions.py independently extract source intervals with Biopython, compare exact merged products and lengths, and verify per-base source-to-product associations including both homology copies. Cases cover both orientations/topologies, variable overlap lengths, origin arcs, single-fragment reclosure, mismatches, exhausted interiors, repeats, malformed plans, strict imports and warning retention on later failures. Inputs are synthetic originals; a separate assembly-engine and experimental comparison remain pending.

Offline amplification / primer design, version 1

This is the first implementation slice motivated by the analytical-primer/qPCR run reported on 2026-09-22. It is not a complete qPCR assay designer, Primer3 replacement, or validation of the private primers from that run. It leaves the GUI map work intact.

Run

cargo build -p dnagent-cli --locked
target/debug/dnagent primer-design fixtures/plans/synthetic-primer-design.json

JSON-only command, using the existing 0.9.0 envelope and a named object result. Existing features array and circular-arc contracts are unchanged. --strict rejects import warnings; successful/failing JSON preserves warnings gathered before failure. No files are written and no remote services or external executables are called.

Plan

See schemas/primer-design-plan-1.schema.json and the committed synthetic example.

  • schema_version: 1.
  • inputs: 1–8 local SnapGene/single-record FASTA paths, relative to the plan file. FASTA is linear; circular templates need an explicitly circular supported source.
  • feature_id: optional exact source feature ID. Currently requires a single-part feature whose start/length exactly equal the declared window. No label ambiguity, implicit feature expansion, strand reversal or multipart flattening. Larger features require an explicit subwindow without feature_id.
  • request.reference_input: one-based input index.
  • window_start, window_length: zero-based start and length in the forward reference, length 36–500. Circular windows may wrap; linear windows must fit. Primer pairs are selected within this window, not necessarily flanking or covering the whole target.
  • junction_offset: optional boundary in the unrolled window, not a whole-record index. At least one primer must span it with junction_min_bases on each side. This is a primer-over-junction constraint, not just a product-containing-junction constraint.
  • positive_inputs / negative_inputs: partition every input exactly once; reference must be positive. One pair must produce exactly one exact heteroprimer product of the requested size on each positive, with no additional screened products; none on negatives. Products need not be byte-identical across positives. Each template interval is reported.
  • min_product_length, max_product_length: allowed positive product lengths. Primer binding sites must be nonoverlapping; minimum product size is 36.
  • screen_max_product_length: screen for alternative products up to this length, at least the design maximum, at most 5000. Products longer than the template are excluded.
  • max_mismatches: 0–2 full-oligo Hamming mismatches; exact_three_prime_bases: 4 up to minimum primer length. Anchor mismatches are excluded, not merely penalised.
  • max_results: 1–20 ranked pairs.
  • constraints: existing explicit primer length/Tm/GC/structure/solution constraints. See primer-optimisation.md for numerical provenance and concentration units.

All templates must be unambiguous ACGT and at most 100,000 bases. No guessing for N or other ambiguous symbols. Cross-field validation in Rust supplements JSON Schema.

Computation and screening

  1. Enumerate forward and reverse oligos of each allowed length at every legal window position. Assess Tm/GC, contiguous hairpin stems and self-dimer runs.
  2. For each surviving oligo, search every supplied template in both orientations, including origin-spanning binding. Require an exact occurrence on every positive.
  3. Enumerate pairs, requiring product size, pair Tm difference, junction (if supplied), and heterodimer constraints.
  4. Enumerate inward-facing, nonoverlapping binding-site combinations on every template. Include products involving two copies of the same oligo and reversed F/R placement. Reject additional screened products on positives and any screened product on negatives.
  5. Rank feasible pairs by summed absolute deviations from target Tm, then total primer length, forward reference start, reverse reference start and forward primer length. Return top N, plus counts of examined and feasible pairs. No stochastic search.

The search has explicit hard budgets: 200 million base comparisons, 200 sites per oligo per template, one million candidate-pair combinations and 20 million product-site combinations. Exceeding any budget returns an error, never a silently truncated search or partially screened candidates. Narrow the window or length range instead.

Thermodynamic model

dnagent-nn-v1 reuses the existing Rust Allawi/SantaLucia 1997 nearest-neighbour model, SantaLucia 1998 entropy salt correction and von Ahsen 2001 sodium-equivalent Mg/dNTP approximation, with primer in excess. Results preserve all solution concentrations. Self-complementary oligos unsupported by this model are excluded. This is not Primer3, and structure screens are contiguous Watson–Crick runs, not folding free energies, bulges or temperature-dependent secondary-structure predictions. No PCR annealing temperature is inferred from the reported Tm.

Evidence and identity

Application intake imports the same byte snapshot it hashes. Each source record has file SHA-256, exact uppercase forward-sequence SHA-256, name, path, length, topology, input index and source-associated warnings. The result also has a SHA-256 of the exact plan bytes, crate version, model identifier and effective request. Sequence hashes are not rotation-invariant; origin and orientation remain explicit.

Each primer has its 5′→3′ sequence and forward-reference start/length/orientation. Every selected pair carries its binding sites (primer 1 = forward candidate, 2 = reverse candidate) and product template intervals. template_sequence_forward is the source interval, not an inferred mismatch-corrected molecular product. Accepted positive products have exact matches; their forward-reference sequences may differ in orientation between sources. Features/translations are not inferred for these intervals.

Architecture

  • dnagent-domain::amplification: typed validation, design, site and product logic.
  • dnagent-app::amplification: versioned plan intake, immutable byte snapshots, hashes, feature checks, source-associated warnings and shared service.
  • CLI: thin JSON adapter. Future GUI can call the same service; it must not reimplement design or specificity calculations in TypeScript.

A small import_path_bytes application entry point allows hashed callers to import exactly the bytes they read. Existing open_path behaviour and callers are retained. Existing Gibson structure screens are reused internally; no duplication of their model.

Explicitly deferred

  • Automatic shared/unique block discovery across whole constructs: candidates here originate from one explicit reference window and are screened against all sources.
  • Primer3 integration / energetic secondary-structure evaluation.
  • Seed-only searches, indels, mismatches within the declared exact 3′ anchor, and genome/transcriptome specificity. No products is a statement about this bounded model, not proof of biological specificity.
  • Local BLAST: optional future adapter requiring declared database identity/version and parameters. Remote searches must remain opt-in; private sequence must never be sent automatically.
  • GUI primer placement, amplicon inspection and agent-to-live-GUI handoff.
  • Assay-type inference, DNA-versus-RNA specificity, experimental validity or oligo ordering.

Validation

cargo test -p dnagent-domain amplification
uv run scripts/check_amplification.py --binary target/debug/dnagent

Rust tests exercise rotation/reverse positives, negative identity rejection, junction coverage, orientation-aware mismatch anchoring, circular sites/products, self-primer products and invalid input/budgets. Independent Python checks use public synthetic SnapGene fixtures, enumerate sites/products separately and compare NN Tms to pinned Biopython 1.85. They validate file/sequence/plan hashes, deterministic output, circular windows, schemas, refusal cases, strict mode and warning retention after later failures. No private construct or previously reported real primer pair is claimed revalidated.

Roadmap

DNAgent is developed in small, validated slices. The CLI and the desktop app call the same Rust application layer; neither reimplements biology. Native Rust runs all calculations; WASM is deferred.

Done

  • Read-only SnapGene import; GenBank and FASTA read by content; lossless GenBank saving.
  • Restriction sites, complete digests, end compatibility and explicit ligation plans.
  • Gibson assembly: PCR-tail primer design (fixed or Tm-optimised), existing overlaps, mixed sources (SnapGene, FASTA, literal DNA, digest strands, PCR products) and annotated product GenBank.
  • Offline amplification primer design with positive/negative template screening.
  • Translation, six-frame ORFs and a private feature library with detection.
  • Tauri desktop app: map, sequence, enzymes, feature editing with undo, tabs, agent handoff, isoform viewer for gene loci.

Next

  • Continuous integration running the AGENTS.md validation list.
  • Gibson hardening: folding-energy assessment and broader real-template coverage. Candidate oligos are model predictions, not ordering-ready designs.
  • A more direct agent–desktop channel than the file handoff.
  • Consolidated provenance and diagnostics across operations.

Unsupported biology is reported, never silently approximated.

Local reference store (provisioning v1)

Use compressed public FASTA originals plus BLAST's native version-5 indexes, not a SQL database for sequence searching. Reference data stays outside Git, shared between projects. This installer provisions data; it does not yet connect BLAST results to primer-design or claim genome-wide specificity.

Default profile

references/human-refseq-grch38.json pins:

  • Homo sapiens, taxid 9606; assembly GCF_000001405.40 / GRCh38.p14.
  • Associated RefSeq annotation release GCF_000001405.40-RS_2025_08.
  • Genome FASTA (~928 MB compressed) and associated RefSeq RNA FASTA (~129 MB).
  • Assembly report and annotation README.
  • Exact compressed-file MD5 values from NCBI's HTTPS checksum listing, retrieved 2026-09-22. Downloaded file/index SHA-256 values are recorded at installation.

These URLs can change in place. A pinned-checksum mismatch fails rather than adopting new data under the old release ID. Future profile updates must explicitly select new content/release identity. Existing installed versions are never overwritten.

Coverage caveat: the full assembly includes primary chromosomes, alternate loci, patches and non-nuclear sequence. Multiple hits on alternate representations must not be naively counted as independent off-target loci. The assembly report is retained for future interpretation. Transcript data is assembly-associated RefSeq RNA, not the much larger all-species refseq_rna database. Predicted and curated records retain accession identifiers; future evidence displays should distinguish them.

Source: NCBI RefSeq / Genome Reference Consortium. Original FASTA headers and provider metadata are retained. The committed profile contains URLs and hashes, no private data.

Install

Requires Python 3.11+ and NCBI BLAST+ (makeblastdb, blastdbcmd, blastn) on PATH. On macOS the user can install BLAST+ with brew install blast. The agent does not request administrator credentials or automatically install system software.

python3 scripts/manage_references.py plan
python3 scripts/manage_references.py install
python3 scripts/manage_references.py verify
python3 scripts/manage_references.py list

On macOS the default store is ~/Library/Application Support/DNAgent/references. Elsewhere it is $XDG_DATA_HOME/dnagent/references, falling back to ~/.local/share/dnagent/references. Set DNAGENT_REFERENCE_DIR or use --root PATH for another volume. Installing within the source repository is refused.

Allow approximately 10 GB final storage and at least 20 GiB free during installation. The latter is an enforced conservative preflight minimum, not a measured guarantee of final index size. The installer checks all tools and space before downloading. BLAST v5's LMDB can require a large virtual address space; that is not equivalent to physical RAM or on-disk reference size.

Layout and publication

<root>/human-refseq-grch38/GCF_000001405.40-RS_2025_08/
  manifest.json
  source/       # original FASTA.gz, assembly report and annotation README
  blast/        # genome.* and transcripts.*; keep each database's files together
  logs/         # makeblastdb output and blastdbcmd -info

The manifest records the complete pinned profile, source URLs/checksum provenance, file sizes and SHA-256s, tool versions/paths, actual build arguments, database prefixes, uncompressed FASTA sizes/hashes, database information and creation time. Paths to installed content are relative to the release directory. Machine-specific tool paths occur only in local manifests, never committed configuration.

Files are downloaded and checksummed in a unique .staging/ directory. Only the current FASTA is decompressed; it is removed after successful indexing/inspection. Indexing uses -dbtype nucl -parse_seqids -blastdb_version 5 -taxid 9606. Both source and index hashes are verified before an atomic same-filesystem directory rename publishes the release. There is no implicit latest alias or automatic update.

An exclusive .install-lock prevents concurrent installers. Failures leave staging files and logs for diagnosis, never a ready release. Ordinary errors release the lock; a killed process can leave a stale lock. Inspect before manually removing stale locks or failed staging directories. This v1 has no automatic cleanup, resume or retry.

verify checks all manifest-listed file sizes and SHA-256s; it does not rerun a biological search. list explicitly reports that integrity has not been checked. blastdbcmd -db <release>/blast/genome -entry <accession> can retrieve sequence without keeping the decompressed FASTA. The entire reference store is local: only fixed public reference URLs are requested; no construct or primer queries are uploaded.

Validation and next step

python3 scripts/test_manage_references.py

Offline tests exercise successful publication, corrupt-file detection, pinned-checksum mismatch, failed builds, missing prerequisites/space, concurrent-install locks, stream size limits and manifest traversal rejection. They mock BLAST and downloads; they are not evidence that a real human index has been built successfully.

After BLAST+ is installed, run the real installer and record its actual disk footprint. Then implement the optional local specificity adapter: short-primer queries, paired hit/product interpretation, alternate-locus handling, and reference manifest identity in the design evidence. Until then, primer-design remains supplied-template-only.

Private SnapGene import regression corpus

The opt-in scripts/check_private_corpus.py harness checks real .dna files, not only synthetic recognition motifs. It is a read-only import regression test, not complete SnapGene fidelity or experimental validation.

Independent checks

For each manifest entry, the checker verifies the SHA-256 before invoking the CLI, then compares:

  • Complete sequence against both the raw DNA packet and Biopython 1.85. Uppercase normalisation follows the domain intake contract; source files stay unchanged. The raw topology flag independently checks imported topology.
  • Feature counts, labels, kinds, directionality, multipart/origin-spanning locations, colours and ordered qualifiers against source XML with explicit coordinate conversion.
  • Primer counts, names, sequences and descriptions against source XML.
  • Import-warning codes and packet types against uninterpreted source packets; warning equality across inspect/features/primers/sequence projections.
  • Half-open sequence ranges at termini, midpoint and boundaries from the first eight annotations, including the empty terminal interval.
  • Strict acceptance for warning-free imports and strict rejection when warnings exist.
  • Full current JSON Schema conformance for every response and no unexpected stderr.
  • File hash again after checking to detect changes during the run.

Source XML is an independent coordinate/field reference, not an assertion that all SnapGene metadata is understood. Biopython agrees on decoded sequence/topology; it cannot certify every native SnapGene feature or interpretation.

Reproducible use

cargo build -p dnagent-cli --locked
# Test the harness using public fixtures and intentionally injected faults:
uv run scripts/test_private_corpus.py
# Private paths are supplied by the operator; never put inputs/reports in Git:
umask 077
uv run scripts/check_private_corpus.py "$PRIVATE_CORPUS/manifest.json" \
  --binary target/debug/dnagent > "$PRIVATE_CORPUS/report.json"

The external manifest uses schema_version: 1 and a nonempty records list; each record needs a relative file and its sha256. Resolved paths must remain inside the manifest directory. Do not silently replace manifest hashes when a check fails. The checker exits nonzero for any failing record.

Reports contain per-record failure labels and coverage, packet counts, range-probe counts, UTC generation time, plus manifest/checker/binary/CLI-schema hashes. They contain private filenames but no full sequences. Schema failures are sanitised so validation error messages do not echo private instance values. Reports belong outside the repository and should retain restrictive permissions.

Current verified coverage — 2026-09-15

Eight authorised real plasmids passed the strengthened harness, with 163 range probes, 10 multipart features, 3 origin-spanning parts and 30 reverse features. All eight inputs are circular. Private sequence files and reports remain outside Git; no construct sequences or names are included here.

The public harness tests verify successful controls and intentional failures: changed source hashes, altered decoded sequences/features/ranges, malformed CLI schema output, path escapes, nonzero failed-report exit and truncated packets. These tests ensure that the checker detects errors rather than only replaying known passing inputs.

Remaining evidence gaps

  • More authorised real-file variants, especially linear DNA, are needed before broadening fidelity claims. Synthetic linear fixtures do not fill that gap.
  • Native SnapGene application comparison remains separate; there is no native round-trip/export test and no comprehensive preserved-packet semantic validation.
  • GUI interpretation, primer binding-site prediction and experimental sequence confirmation are outside this harness.
  • Malformed or partially interpreted real annotations intentionally fail rather than being silently dropped from the reference comparison.

Restriction/digest correctness is checked separately by check_restriction.py and check_digest.py, which can use the same private manifest. This separation makes it clear whether a regression is in import fidelity or downstream biology.