dsh-molbio-tools 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +96 -33
- package/align.mjs +28 -0
- package/batch.mjs +185 -0
- package/benchmark/README.md +27 -8
- package/benchmark/sequences.mjs +7 -0
- package/benchmark/tasks/02-primers.json +132 -19
- package/benchmark/tasks/03-restriction.json +113 -29
- package/benchmark/tasks/04-cloning.json +167 -34
- package/benchmark/tasks/05-plasmids.json +172 -29
- package/benchmark/tasks/06-analysis.json +387 -100
- package/benchmark/tasks/09-formats.json +132 -23
- package/benchmark/tools.mjs +28 -2
- package/benchmark/verifications.mjs +17 -1
- package/build/release-preflight.mjs +2 -2
- package/crispr.mjs +392 -78
- package/docs/history.md +10 -0
- package/docs/maintainer.md +8 -6
- package/docs/roadmap.md +22 -8
- package/docs/rules.md +1 -1
- package/docs/workflow.md +7 -7
- package/fastq-qc.mjs +735 -735
- package/icon.svg +8 -0
- package/index.mjs +573 -36
- package/lib/client.js +665 -86
- package/multiplex.mjs +87 -0
- package/package.json +12 -5
- package/packages/molbio-panel/lib/client.js +665 -86
- package/packages/molbio-panel/package.json +1 -1
- package/preset/molbio-lab/agent.cordis.yml +4 -4
- package/preset/molbio-lab/preset.patch.yml +2 -2
- package/preset/molbio-lab/preset.yml +1 -1
- package/silent-site.mjs +286 -0
- package/snapgene.mjs +17 -1
- package/taqman.mjs +163 -7
- package/test/contract.mjs +14 -0
- package/test/release.mjs +23 -3
- package/test/smoke.mjs +377 -3
- package/benchmark/reports/20260923-142935-suite.json +0 -545
- package/benchmark/reports/20260923-142935-suite.md +0 -30
- package/benchmark/reports/20260923-190149-suite.json +0 -2024
- package/benchmark/reports/20260923-190149-suite.md +0 -235
- package/benchmark/reports/20260923-192913-suite.json +0 -1856
- package/benchmark/reports/20260923-192913-suite.md +0 -158
- package/benchmark/reports/20260923-200212-suite.json +0 -3587
- package/benchmark/reports/20260923-200212-suite.md +0 -163
- package/benchmark/reports/20260923-202830-suite.json +0 -3129
- package/benchmark/reports/20260923-202830-suite.md +0 -99
- package/benchmark/reports/20260923-203856-suite.json +0 -71
- package/benchmark/reports/20260923-203856-suite.md +0 -16
package/batch.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-molbio-tools/batch.mjs
|
|
3
|
+
*
|
|
4
|
+
* Batch sequence-file analysis: parse every matching file in a workspace
|
|
5
|
+
* directory, compute the same summary for each, and emit one CSV/TSV table.
|
|
6
|
+
* Pure parsing and computation; file discovery and writes live in index.mjs and
|
|
7
|
+
* go through ctx.fs with the session sandbox policy.
|
|
8
|
+
*
|
|
9
|
+
* The point is the TABLE, not a second implementation of each analysis: GC/N
|
|
10
|
+
* come from lib.mjs's baseCounts, ORFs from translateFrames, enzyme sites from
|
|
11
|
+
* digest, and the GenBank/SnapGene parsers are the same ones the single-file
|
|
12
|
+
* tools use. A file that cannot be parsed becomes an error row instead of
|
|
13
|
+
* aborting the whole batch — one bad .dna in a folder of 60 must not hide the
|
|
14
|
+
* other 59.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { MolbioInputError, baseCounts, digest, normalizeSequence, translateFrames } from './lib.mjs';
|
|
18
|
+
import { parseGenBank } from './genbank.mjs';
|
|
19
|
+
import { parseSnapGeneBytes } from './snapgene.mjs';
|
|
20
|
+
import { entryStats, parseFasta, parseFastq } from './seqio.mjs';
|
|
21
|
+
|
|
22
|
+
export const BATCH_LIMITS = {
|
|
23
|
+
maxFiles: 500,
|
|
24
|
+
maxFileBytes: 50 * 1024 * 1024,
|
|
25
|
+
maxTotalBytes: 500 * 1024 * 1024,
|
|
26
|
+
maxOrfBases: 2_000_000,
|
|
27
|
+
maxEnzymes: 20,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const round2 = (value) => Math.round(value * 100) / 100;
|
|
31
|
+
|
|
32
|
+
/** Detect the format from the extension, then from the content. */
|
|
33
|
+
export function detectBatchFormat(path, text) {
|
|
34
|
+
const lower = String(path).toLowerCase();
|
|
35
|
+
if (lower.endsWith('.dna')) return 'snapgene';
|
|
36
|
+
if (/\.(gb|gbk|genbank)$/.test(lower)) return 'genbank';
|
|
37
|
+
if (/\.(fa|fasta|fna|fas|ffn)$/.test(lower)) return 'fasta';
|
|
38
|
+
if (/\.(fq|fastq)$/.test(lower)) return 'fastq';
|
|
39
|
+
const head = typeof text === 'string' ? text.trimStart() : '';
|
|
40
|
+
if (head.startsWith('>')) return 'fasta';
|
|
41
|
+
if (head.startsWith('@')) return 'fastq';
|
|
42
|
+
if (/^LOCUS\s/m.test(head)) return 'genbank';
|
|
43
|
+
return 'text';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Convert a simple basename glob (*, ?) to a RegExp. */
|
|
47
|
+
export function globToRegExp(pattern) {
|
|
48
|
+
const source = String(pattern)
|
|
49
|
+
.replace(/[.+^$()|[\]{}]/g, '\$&')
|
|
50
|
+
.replaceAll('*', '.*')
|
|
51
|
+
.replaceAll('?', '.');
|
|
52
|
+
return new RegExp('^' + source + '$', 'i');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parse one sequence file into its name/topology/sequence/features. */
|
|
56
|
+
function parseOne({ path, format, text, bytes }) {
|
|
57
|
+
if (format === 'snapgene') {
|
|
58
|
+
if (bytes === undefined) throw new MolbioInputError('a .dna file needs binary bytes');
|
|
59
|
+
const parsed = parseSnapGeneBytes(bytes);
|
|
60
|
+
return {
|
|
61
|
+
name: parsed.name,
|
|
62
|
+
topology: parsed.topology === 'circular' ? 'circular' : 'linear',
|
|
63
|
+
sequence: parsed.sequence,
|
|
64
|
+
features: parsed.features.length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (format === 'genbank') {
|
|
68
|
+
const parsed = parseGenBank(String(text ?? ''));
|
|
69
|
+
return {
|
|
70
|
+
name: parsed.name ?? path,
|
|
71
|
+
topology: parsed.topology === 'circular' ? 'circular' : 'linear',
|
|
72
|
+
sequence: parsed.sequence,
|
|
73
|
+
features: parsed.features.length,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (format === 'fasta') {
|
|
77
|
+
const entries = parseFasta(String(text ?? ''));
|
|
78
|
+
return {
|
|
79
|
+
name: entries[0]?.id ?? path,
|
|
80
|
+
topology: 'unknown',
|
|
81
|
+
sequence: entries.map((entry) => entry.sequence).join(''),
|
|
82
|
+
features: 0,
|
|
83
|
+
entries: entries.length,
|
|
84
|
+
fastaEntries: entries,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (format === 'fastq') {
|
|
88
|
+
const entries = parseFastq(String(text ?? ''));
|
|
89
|
+
return {
|
|
90
|
+
name: entries[0]?.id ?? path,
|
|
91
|
+
topology: 'unknown',
|
|
92
|
+
sequence: entries.map((entry) => entry.sequence).join(''),
|
|
93
|
+
features: 0,
|
|
94
|
+
entries: entries.length,
|
|
95
|
+
fastqEntries: entries,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const sequence = normalizeSequence(String(text ?? ''), 'sequence');
|
|
99
|
+
return { name: path, topology: 'unknown', sequence, features: 0, entries: 1 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Analyze one file.
|
|
104
|
+
*
|
|
105
|
+
* @param {{path: string, format?: string, text?: string, bytes?: Uint8Array,
|
|
106
|
+
* enzymes?: string[], orfMinAa?: number, circular?: boolean, codonTable?: string}} input
|
|
107
|
+
* @returns {object} one table row; throws only for an unrecognizable file.
|
|
108
|
+
*/
|
|
109
|
+
export function analyzeSequenceFile(input) {
|
|
110
|
+
const { path, text, bytes } = input;
|
|
111
|
+
const format = input.format ?? detectBatchFormat(path, text);
|
|
112
|
+
const parsed = parseOne({ path, format, text, bytes });
|
|
113
|
+
const sequence = normalizeSequence(parsed.sequence, path + ' sequence');
|
|
114
|
+
const counts = baseCounts(sequence);
|
|
115
|
+
const totalBases = sequence.length;
|
|
116
|
+
const gcPercent = counts.gc + counts.at === 0 ? 0 : round2((counts.gc / (counts.gc + counts.at)) * 100);
|
|
117
|
+
const nPercent = totalBases === 0 ? 0 : round2((counts.n / totalBases) * 100);
|
|
118
|
+
const row = {
|
|
119
|
+
path,
|
|
120
|
+
format,
|
|
121
|
+
topology: parsed.topology,
|
|
122
|
+
entries: parsed.entries ?? 1,
|
|
123
|
+
total_bases: totalBases,
|
|
124
|
+
gc_percent: gcPercent,
|
|
125
|
+
n_percent: nPercent,
|
|
126
|
+
feature_count: parsed.features,
|
|
127
|
+
enzyme_sites: [],
|
|
128
|
+
error: '',
|
|
129
|
+
};
|
|
130
|
+
const enzymes = input.enzymes ?? [];
|
|
131
|
+
if (enzymes.length > 0) {
|
|
132
|
+
const circular = input.circular === true || parsed.topology === 'circular';
|
|
133
|
+
const result = digest(sequence, enzymes, circular);
|
|
134
|
+
row.enzyme_sites = result.map((entry, index) => ({
|
|
135
|
+
name: enzymes[index],
|
|
136
|
+
sites: entry.cut_positions.length,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
const orfMinAa = input.orfMinAa ?? 0;
|
|
140
|
+
if (orfMinAa > 0) {
|
|
141
|
+
if (totalBases > BATCH_LIMITS.maxOrfBases) {
|
|
142
|
+
row.orf_count = 0;
|
|
143
|
+
row.longest_orf_aa = 0;
|
|
144
|
+
row.orf_skipped = true;
|
|
145
|
+
} else {
|
|
146
|
+
const { orfs } = translateFrames(sequence, 'all', input.codonTable ?? 'standard', orfMinAa);
|
|
147
|
+
row.orf_count = orfs.length;
|
|
148
|
+
row.longest_orf_aa = orfs.length === 0 ? 0 : Math.max(...orfs.map((orf) => orf.length_aa));
|
|
149
|
+
row.orf_skipped = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (format === 'fasta' || format === 'fastq') {
|
|
153
|
+
const stats = entryStats(parsed.fastaEntries ?? parsed.fastqEntries ?? [], format === 'fastq');
|
|
154
|
+
row.mean_length = stats.mean_length;
|
|
155
|
+
if (format === 'fastq' && stats.quality_mean !== undefined) row.quality_mean = stats.quality_mean;
|
|
156
|
+
}
|
|
157
|
+
return row;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Columns for the table, in a stable order. */
|
|
161
|
+
export function batchColumns({ orfs, enzymes }) {
|
|
162
|
+
const columns = ['path', 'format', 'topology', 'entries', 'total_bases', 'gc_percent', 'n_percent', 'feature_count', 'mean_length'];
|
|
163
|
+
if (orfs) columns.push('orf_count', 'longest_orf_aa', 'orf_skipped');
|
|
164
|
+
if (enzymes.length > 0) columns.push(...enzymes.map((name) => name + '_sites'));
|
|
165
|
+
columns.push('error');
|
|
166
|
+
return columns;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function csvField(value, delimiter) {
|
|
170
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
171
|
+
return text.includes(delimiter) || /["\n\r]/.test(text) ? '"' + text.replaceAll('"', '""') + '"' : text;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Render rows as CSV (delimiter = ",") or TSV (delimiter = "\t"). */
|
|
175
|
+
export function batchTable(rows, columns, delimiter = ',') {
|
|
176
|
+
const value = (row, column) => {
|
|
177
|
+
if (column.endsWith('_sites')) {
|
|
178
|
+
const name = column.slice(0, -'_sites'.length);
|
|
179
|
+
return row.enzyme_sites?.find((entry) => entry.name === name)?.sites;
|
|
180
|
+
}
|
|
181
|
+
return row[column];
|
|
182
|
+
};
|
|
183
|
+
const lines = rows.map((row) => columns.map((column) => csvField(value(row, column), delimiter)).join(delimiter));
|
|
184
|
+
return [columns.join(delimiter), ...lines].join('\n') + '\n';
|
|
185
|
+
}
|
package/benchmark/README.md
CHANGED
|
@@ -76,11 +76,11 @@ Measured, and printed so the answer can be audited:
|
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
$ touch crispr.mjs && node benchmark/run.mjs --list --changed
|
|
79
|
-
1 of
|
|
79
|
+
1 of 61 task(s) selected — --tools molbio_grna_design --changed
|
|
80
80
|
(from --changed; 2 module(s) affected: crispr.mjs, index.mjs)
|
|
81
81
|
|
|
82
82
|
$ touch lib.mjs && node benchmark/run.mjs --list --changed
|
|
83
|
-
55 of
|
|
83
|
+
55 of 61 task(s) selected — --changed (27 module(s) affected: align.mjs, cloning.mjs, …)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
`lib.mjs` selecting nearly everything is the **correct** answer, not a failure to
|
|
@@ -102,7 +102,7 @@ you changed, so its task runs even if it is full-only.
|
|
|
102
102
|
| `--tools a,b,c` / `primer_*` | the union | you changed a few |
|
|
103
103
|
| `--changed --tools X` | the INTERSECTION | "of what I changed, test X" |
|
|
104
104
|
| (default) | **core** — 14 tasks | a broad smoke of the catalog |
|
|
105
|
-
| `--tier full` | all
|
|
105
|
+
| `--tier full` | all 61, 60/60 tools | before a release |
|
|
106
106
|
|
|
107
107
|
The two `--tools` operators are stated because confusing them is the difference
|
|
108
108
|
between testing what you changed and testing **less** than that: alone it unions,
|
|
@@ -125,7 +125,7 @@ widens to the full suite, with the reason printed.
|
|
|
125
125
|
| a module **only** imported by other modules (`svgio.mjs`) | partial — its importers' tasks |
|
|
126
126
|
| docs, changelog, `test/`, `benchmark/`, config | **`none`: nothing to run** |
|
|
127
127
|
|
|
128
|
-
`none` is deliberately NOT `full`. A documentation commit that triggers
|
|
128
|
+
`none` is deliberately NOT `full`. A documentation commit that triggers 61 model
|
|
129
129
|
calls is how a `--changed` default gets switched off and the full suite goes back
|
|
130
130
|
to running every time — the exact behaviour this option exists to prevent. It is
|
|
131
131
|
only reachable with an explicit `--changed`, so everything else still fails safe,
|
|
@@ -134,7 +134,7 @@ and `test/benchmark-changed.mjs` asserts both directions.
|
|
|
134
134
|
### The part that still runs in full, on purpose
|
|
135
135
|
|
|
136
136
|
A model run with `--changed` or `--tools` first executes the **whole** offline
|
|
137
|
-
gate: all
|
|
137
|
+
gate: all 61 tasks' `where: "tool"` expectations are recomputed against the shipped
|
|
138
138
|
tools (no model, a few hundred milliseconds). That is what makes partial runs safe
|
|
139
139
|
— the expected values are re-proven for the tasks you skipped, so "skipped because
|
|
140
140
|
unchanged" is a checked claim, not a remembered one.
|
|
@@ -168,7 +168,7 @@ asserting whatever the tool happened to emit.
|
|
|
168
168
|
|
|
169
169
|
Two guards make "someone forgot" fail loudly instead of quietly:
|
|
170
170
|
|
|
171
|
-
- `test/benchmark-coverage.mjs` asserts **
|
|
171
|
+
- `test/benchmark-coverage.mjs` asserts **60/60 registered tools are covered by
|
|
172
172
|
at least one task**. Adding a tool without a task is a red build. It also
|
|
173
173
|
asserts the fixtures' premises (the Golden Gate backbone is BsaI-free, the
|
|
174
174
|
mutagenesis template still admits a primer pair), that a `covers` entry names a
|
|
@@ -209,7 +209,7 @@ recorded tool text, and it says so instead of silently scoring empty text).
|
|
|
209
209
|
|
|
210
210
|
## The headless profile (`benchmark/profile.mjs`)
|
|
211
211
|
|
|
212
|
-
The model-facing way to get the
|
|
212
|
+
The model-facing way to get the 60 tools is the **Molecular Biology Lab** agent
|
|
213
213
|
preset. The benchmark cannot use it, and the reason is structural rather than
|
|
214
214
|
configurational — `dsh-headless/lib/index.js` refuses an adoption whose session
|
|
215
215
|
records a preset:
|
|
@@ -368,6 +368,25 @@ These are recorded rather than scored, because each one is a question about the
|
|
|
368
368
|
plugin or the harness, not about the model. The first is a **measured plugin
|
|
369
369
|
defect**.
|
|
370
370
|
|
|
371
|
+
### `molbio_extract_region` advertised `sequence`, but the resolver only read `vector`
|
|
372
|
+
|
|
373
|
+
The 0.20.0 full run (61 tasks) failed `region-extract` on **arguments**: the model
|
|
374
|
+
called `molbio_extract_region` with the parameter the schema advertises
|
|
375
|
+
(`sequence`), and the tool answered "provide either vector (a sequence) or
|
|
376
|
+
vector_path". The schema/reality split was the defect: `resolveVector` read only
|
|
377
|
+
`args.vector`, so `sequence` was silently ignored. **Fixed in 0.20.0** by
|
|
378
|
+
accepting every declared inline alias (`vector`/`sequence`/`reference`), with a
|
|
379
|
+
smoke regression that calls the tool through the advertised `sequence` argument.
|
|
380
|
+
|
|
381
|
+
The same run also showed four **assertion** failures rather than product defects:
|
|
382
|
+
`plasmid-digest` (the model wrote `2,840`, the assertion `2840`),
|
|
383
|
+
`protein-props` (`3,935` vs `3935`), `codon-optimize` (the model said
|
|
384
|
+
`EcoRI-free`, the assertion demanded `no EcoRI`), and `chart-volcano` (the task
|
|
385
|
+
asked for `de.svg`, the assertion pinned `volcano.svg`). `--replay` turns all four
|
|
386
|
+
to PASS; the region-extract live rerun was blocked by the DSH sandbox approval
|
|
387
|
+
for headless subprocesses, so the last measured score is **60/61**, with the
|
|
388
|
+
underlying bug fixed and pinned by smoke.
|
|
389
|
+
|
|
371
390
|
### `molbio_design_primers` prints the forward/reverse roles reversed
|
|
372
391
|
|
|
373
392
|
**Confirmed by direct inspection, not by reading a model's claim.**
|
|
@@ -548,7 +567,7 @@ implementation happens to return for one set of arguments.**
|
|
|
548
567
|
and `args_ok` both true is a *reporting* finding, not a tool bug.
|
|
549
568
|
- **Multimodal hand-off.** `attach_image` is exercised as an argument at most; no
|
|
550
569
|
assertion inspects a decoded PNG (that is `svgpng.mjs` and `contract.mjs`).
|
|
551
|
-
- **Most of the toolset is now covered, but shallowly.**
|
|
570
|
+
- **Most of the toolset is now covered, but shallowly.** 61 tasks touch all 60
|
|
552
571
|
tools; most tools appear in exactly one task, and several appearances are a
|
|
553
572
|
single argument check rather than a full workflow. Breadth is asserted by
|
|
554
573
|
`test/benchmark-coverage.mjs`; depth is not, and cannot be.
|
package/benchmark/sequences.mjs
CHANGED
|
@@ -165,6 +165,13 @@ export const NAMED_SEQUENCES = {
|
|
|
165
165
|
cds: FIXTURES.CDS,
|
|
166
166
|
mutagenesis_template: FIXTURES.MUTAGENESIS_TEMPLATE,
|
|
167
167
|
sanger_read: FIXTURES.SANGER_READ,
|
|
168
|
+
// v0.20.0: the family-geometry fixtures. The Cas12a target is a 5' TTTV PAM
|
|
169
|
+
// followed by a 23 nt protospacer; the Cas13a target is a 28 nt spacer with a
|
|
170
|
+
// 3' H PFS. Keeping them here means the instruction and the offline grader
|
|
171
|
+
// cannot diverge.
|
|
172
|
+
cas12a_target: 'AAAATTTAACGTACGTACGTACGTACGTACGCCCCTTTCGGGGGGGGGGG',
|
|
173
|
+
cas13_target: 'AAAAACGTACGTACGTACGTACGTACGTACGTACCCCCCCCCC',
|
|
174
|
+
silent_cds: 'ATGGCTGAATTCGGTTAAGCTTAA',
|
|
168
175
|
/**
|
|
169
176
|
* The three FASTQ reads the `fastq-qc` task pastes in.
|
|
170
177
|
*
|
|
@@ -12,26 +12,54 @@
|
|
|
12
12
|
"id": "primer-qc",
|
|
13
13
|
"category": "primers",
|
|
14
14
|
"tier": "full",
|
|
15
|
-
"covers": [
|
|
15
|
+
"covers": [
|
|
16
|
+
"molbio_primer_check",
|
|
17
|
+
"molbio_primer_tm"
|
|
18
|
+
],
|
|
16
19
|
"instruction": "I have these two primers already ordered and I want to know whether they are any good together before I run them. Report the Tm of the forward primer, and flag anything structurally worrying about the pair.\n\nForward: ATTACTCCTGCTCTTCCCATAC\nReverse: CACTACTCCTCTGTACGCAC",
|
|
17
20
|
"timeout_ms": 240000,
|
|
18
|
-
"expect_tools": [
|
|
21
|
+
"expect_tools": [
|
|
22
|
+
"molbio_primer_check"
|
|
23
|
+
],
|
|
19
24
|
"assertions": [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
{
|
|
26
|
+
"where": "tool",
|
|
27
|
+
"pattern": "primer1: 22 bp, GC 45\\.45%",
|
|
28
|
+
"note": "the forward primer's own metrics"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"where": "tool",
|
|
32
|
+
"pattern": "primer2: 20 bp, GC 55%"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"where": "tool",
|
|
36
|
+
"pattern": "self-any 7 \\(≤8\\)",
|
|
37
|
+
"note": "the reverse primer's self-complementarity, right at the Primer3 limit"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"where": "tool",
|
|
41
|
+
"pattern": "pair \\(dimer\\): score 14",
|
|
42
|
+
"note": "the pair's dimer score"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"where": "answer",
|
|
46
|
+
"pattern": "53\\.9|54\\b",
|
|
47
|
+
"note": "the forward primer's Tm, 53.97 °C"
|
|
48
|
+
}
|
|
25
49
|
]
|
|
26
50
|
},
|
|
27
51
|
{
|
|
28
52
|
"id": "taqman-assay",
|
|
29
53
|
"category": "primers",
|
|
30
54
|
"tier": "full",
|
|
31
|
-
"covers": [
|
|
55
|
+
"covers": [
|
|
56
|
+
"molbio_design_taqman"
|
|
57
|
+
],
|
|
32
58
|
"instruction": "Design a TaqMan (5' nuclease) assay on this template. I want primers plus a hydrolysis probe between them, and I want the probe to sit at least a few degrees above the primers in Tm.\n\nTemplate:\n{template:400:7}",
|
|
33
59
|
"timeout_ms": 300000,
|
|
34
|
-
"expect_tools": [
|
|
60
|
+
"expect_tools": [
|
|
61
|
+
"molbio_design_taqman"
|
|
62
|
+
],
|
|
35
63
|
"assertions": [
|
|
36
64
|
{
|
|
37
65
|
"where": "tool",
|
|
@@ -48,21 +76,39 @@
|
|
|
48
76
|
"pattern": "vs the hotter primer",
|
|
49
77
|
"note": "the probe-vs-primer Tm margin, which the tool reports per candidate"
|
|
50
78
|
},
|
|
51
|
-
{
|
|
79
|
+
{
|
|
80
|
+
"where": "answer",
|
|
81
|
+
"pattern": "probe|Probe"
|
|
82
|
+
}
|
|
52
83
|
]
|
|
53
84
|
},
|
|
54
85
|
{
|
|
55
86
|
"id": "intron-qpcr",
|
|
56
87
|
"category": "primers",
|
|
57
88
|
"tier": "full",
|
|
58
|
-
"covers": [
|
|
89
|
+
"covers": [
|
|
90
|
+
"molbio_design_intron_primers"
|
|
91
|
+
],
|
|
59
92
|
"instruction": "I need a qPCR assay that will NOT amplify contaminating genomic DNA. Here is the genomic sequence and the two exon coordinates — design a pair where the forward primer spans the exon-exon junction. Tell me the spliced amplicon size and how far apart the primers sit on the genome.\n\n{seq:intron_genomic}\n\nExons: 1-400 and 701-1100 (1-based).",
|
|
60
93
|
"timeout_ms": 300000,
|
|
61
|
-
"expect_tools": [
|
|
94
|
+
"expect_tools": [
|
|
95
|
+
"molbio_design_intron_primers"
|
|
96
|
+
],
|
|
62
97
|
"assertions": [
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
{
|
|
99
|
+
"where": "tool",
|
|
100
|
+
"pattern": "forward primer spans an exon-exon junction",
|
|
101
|
+
"note": "the property that makes the assay genomic-DNA-proof"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"where": "tool",
|
|
105
|
+
"pattern": "spliced amplicon 320-406 \\(87 bp\\), genomic span 387 bp"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"where": "tool",
|
|
109
|
+
"pattern": "junction 12\\+6 bp",
|
|
110
|
+
"note": "bases contributed on each side of the junction"
|
|
111
|
+
},
|
|
66
112
|
{
|
|
67
113
|
"where": "answer",
|
|
68
114
|
"pattern": "splic|junction|genomic|intron|87|387",
|
|
@@ -74,12 +120,19 @@
|
|
|
74
120
|
"id": "multiplex-panel",
|
|
75
121
|
"category": "primers",
|
|
76
122
|
"tier": "full",
|
|
77
|
-
"covers": [
|
|
78
|
-
|
|
123
|
+
"covers": [
|
|
124
|
+
"molbio_multiplex_check"
|
|
125
|
+
],
|
|
126
|
+
"instruction": "I want to run these two primer pairs in the same multiplex tube. The first pair is on target A (template below), the second on target B. Check them against each other and tell me whether they interfere — dimers, unintended annealing, or bands I will not be able to tell apart. Also give me the recommended annealing temperature for the panel and a starting primer concentration for each pair.\n\nTarget A template:\n{template:400:7}\n\nPair A: forward ATTACTCCTGCTCTTCCCATAC (Tm 58.4), reverse CACTACTCCTCTGTACGCAC (Tm 58.9)\nPair B: forward GGGTTTCCCAAATTTGGGCCCA, reverse TTTGGGCCCAAATTTGGGAAAC\nTemplate B:\n{template:400:8}",
|
|
79
127
|
"timeout_ms": 300000,
|
|
80
|
-
"expect_tools": [
|
|
128
|
+
"expect_tools": [
|
|
129
|
+
"molbio_multiplex_check"
|
|
130
|
+
],
|
|
81
131
|
"assertions": [
|
|
82
|
-
{
|
|
132
|
+
{
|
|
133
|
+
"where": "tool",
|
|
134
|
+
"pattern": "multiplex panel: 2 target\\(s\\) on 2 template\\(s\\), 4 primer\\(s\\)"
|
|
135
|
+
},
|
|
83
136
|
{
|
|
84
137
|
"where": "tool",
|
|
85
138
|
"pattern": "primer-primer interactions above the dimer threshold: [0-9]+",
|
|
@@ -94,6 +147,66 @@
|
|
|
94
147
|
"where": "answer",
|
|
95
148
|
"pattern": "dimer|interfere|conflict|anneal",
|
|
96
149
|
"note": "the panel verdict. Note the measured behaviour this task must tolerate: a model that FIRST checks each pair on its own template often finds — correctly — that a pair as written does not amplify at all, and answers that instead. That is a better answer than 'no dimer conflict', not a worse one."
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"where": "tool",
|
|
153
|
+
"pattern": "annealing temperature: [0-9.]+ °C \\(range [0-9.]+-[0-9.]+ °C\\)",
|
|
154
|
+
"note": "the panel-level annealing recommendation (lowest primer Tm minus the offset)"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"where": "tool",
|
|
158
|
+
"pattern": "primer concentration plan \\(nM\\):",
|
|
159
|
+
"note": "the starting primer-concentration plan"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"where": "answer",
|
|
163
|
+
"pattern": "anneal|temperature|concentration",
|
|
164
|
+
"note": "the model reports the new planning outputs, not only the dimer verdict"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "taqman-mgb",
|
|
170
|
+
"category": "primers",
|
|
171
|
+
"tier": "full",
|
|
172
|
+
"covers": [
|
|
173
|
+
"molbio_design_taqman"
|
|
174
|
+
],
|
|
175
|
+
"instruction": "Design a TaqMan assay with an MGB probe on this template. Use an MGB probe, write the ordering sheet to mgb-order.csv, and tell me the probe length and Tm window the designer used, the top-ranked probe, and the 5'/3' modifications on its ordering row.\n\nTemplate:\n{template:400:7}",
|
|
176
|
+
"timeout_ms": 300000,
|
|
177
|
+
"expect_tools": [
|
|
178
|
+
"molbio_design_taqman"
|
|
179
|
+
],
|
|
180
|
+
"assertions": [
|
|
181
|
+
{
|
|
182
|
+
"where": "tool",
|
|
183
|
+
"pattern": "Probe chemistry: TaqMan MGB",
|
|
184
|
+
"note": "the MGB chemistry is selected, not silently the standard one"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"where": "tool",
|
|
188
|
+
"pattern": "13-20 nt, Tm 48-60",
|
|
189
|
+
"note": "the MGB length/Tm window, on the unmodified NN scale"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"where": "tool",
|
|
193
|
+
"pattern": "P TTGCTACGCCTGT",
|
|
194
|
+
"note": "the top-ranked MGB probe"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"where": "tool",
|
|
198
|
+
"pattern": "ordering CSV \\([0-9]+ oligo row\\(s\\)\\) saved to",
|
|
199
|
+
"note": "the ordering sheet was written"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"where": "tool",
|
|
203
|
+
"pattern": "MGB probes: the reported probe Tm is the unmodified DNA NN estimate",
|
|
204
|
+
"note": "the tool refuses to fabricate a modified Tm"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"where": "answer",
|
|
208
|
+
"pattern": "MGB|unmodified|modif",
|
|
209
|
+
"note": "the model explains the MGB Tm caveat or the modifications"
|
|
97
210
|
}
|
|
98
211
|
]
|
|
99
212
|
}
|