dsh-molbio-tools 0.22.2 → 0.24.0
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 +173 -0
- package/README.md +51 -19
- package/benchmark/README.md +7 -7
- package/benchmark/fixtures.mjs +32 -4
- package/benchmark/run.mjs +1 -0
- package/benchmark/sequences.mjs +1 -0
- package/benchmark/tasks/02-primers.json +3 -3
- package/benchmark/tasks/06-analysis.json +111 -1
- package/benchmark/tasks/09-formats.json +44 -1
- package/benchmark/verifications.mjs +13 -0
- package/build/client-entry.mjs +80 -1
- package/build/panel-core.mjs +85 -0
- package/build/release-preflight.mjs +1 -1
- package/contamination.mjs +229 -0
- package/cordis.patch.yml +9 -5
- package/design.mjs +111 -78
- package/docs/capability-gap-survey.md +5 -0
- package/docs/client-panel.md +9 -7
- package/docs/history.md +62 -0
- package/docs/maintainer.md +8 -5
- package/docs/roadmap.md +33 -15
- package/docs/rules.md +5 -2
- package/docs/store-evidence.md +30 -23
- package/docs/store-preset-exemption-issue.md +1 -1
- package/docs/store-preset-exemption.md +1 -1
- package/docs/v24-plan.md +178 -0
- package/docs/workflow.md +11 -10
- package/dotplot.mjs +257 -0
- package/fastq-qc.mjs +6 -9
- package/fastq-trim.mjs +238 -0
- package/index.mjs +470 -24
- package/lib/client.js +327 -93
- package/package.json +8 -3
- package/preset/molbio-lab/agent.cordis.yml +19 -13
- package/preset/molbio-lab/preset.yml +1 -1
- package/protein-charge.mjs +158 -0
- package/protein.mjs +36 -14
- package/seqio.mjs +10 -0
- package/test/client.mjs +12 -5
- package/test/contract.mjs +44 -12
- package/test/drift-probe.mjs +15 -1
- package/test/map-card.mjs +139 -1
- package/test/preset-health.mjs +39 -0
- package/test/smoke.mjs +288 -11
package/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* dsh-molbio-tools/index.mjs
|
|
3
3
|
*
|
|
4
4
|
* DeepSeek Harness Cordis plugin: molecular-biology computation tools for the
|
|
5
|
-
* model. Zero runtime dependencies —
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* model. Zero runtime dependencies — every import is a sibling module of this
|
|
6
|
+
* package — so the toolset travels with an agent preset and can be referenced
|
|
7
|
+
* by a package-name row (see README.md and rules.md rule 1).
|
|
8
8
|
*
|
|
9
9
|
* Every tool is pure computation: deterministic, no network, no filesystem.
|
|
10
10
|
* The plugin publishes no service, so the preset row needs no isolate realm.
|
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
} from './cloning.mjs';
|
|
51
51
|
import { readTraceFromBytes, verifySanger } from './sanger.mjs';
|
|
52
52
|
import { CODON_HOSTS, codonOptimize, peptideDigest, proteinProperties } from './protein.mjs';
|
|
53
|
+
import { chargeCurve, renderChargeCurve } from './protein-charge.mjs';
|
|
54
|
+
import { dotplot, renderDotplot } from './dotplot.mjs';
|
|
53
55
|
import { linearFit, renderBarChart, renderGel, renderScatterChart } from './plot.mjs';
|
|
54
56
|
import {
|
|
55
57
|
SERIES_COLORS,
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
renderVolcanoPlot,
|
|
66
68
|
resolveTable,
|
|
67
69
|
} from './charts.mjs';
|
|
68
|
-
import { entryStats, parseFasta, parseFastq, toFasta } from './seqio.mjs';
|
|
70
|
+
import { entryStats, parseFasta, parseFastq, toFasta, toFastq } from './seqio.mjs';
|
|
69
71
|
import { addExperiment, addProtocol, loadRecords, recordPath, saveRecords, updateProtocol } from './records.mjs';
|
|
70
72
|
import { toBibtex } from './papers.mjs';
|
|
71
73
|
import { parseXml } from './snapgene.mjs';
|
|
@@ -123,6 +125,8 @@ import {
|
|
|
123
125
|
import { openDefaultViewer } from './view.mjs';
|
|
124
126
|
import { renderSvgToPng } from './svgpng.mjs';
|
|
125
127
|
import { fastqQcReport, renderFastqQcReport } from './fastq-qc.mjs';
|
|
128
|
+
import { screenContamination } from './contamination.mjs';
|
|
129
|
+
import { trimFastq } from './fastq-trim.mjs';
|
|
126
130
|
import { CODON_FREQUENCIES, codonUsageAnalysis } from './codon.mjs';
|
|
127
131
|
import {
|
|
128
132
|
DISTANCE_MODELS,
|
|
@@ -2140,7 +2144,7 @@ const plasmidMapTool = (ctx) => define({
|
|
|
2140
2144
|
},
|
|
2141
2145
|
// The map card in the transcript/panel reads this meta (see mapCardMeta).
|
|
2142
2146
|
presentationMeta(_args, value) {
|
|
2143
|
-
return mapCardMeta(value,
|
|
2147
|
+
return mapCardMeta(value, cardSvgFor(value.svg_path));
|
|
2144
2148
|
},
|
|
2145
2149
|
async execute(args, exec) {
|
|
2146
2150
|
const sequence = normalizeSequence(args.sequence, 'plasmid sequence');
|
|
@@ -2182,7 +2186,7 @@ const plasmidMapTool = (ctx) => define({
|
|
|
2182
2186
|
gc_skew: args.gc_skew === true,
|
|
2183
2187
|
});
|
|
2184
2188
|
const { file, viewed, attached } = await writeSvgFile(ctx, exec, args, svg, name);
|
|
2185
|
-
|
|
2189
|
+
rememberCardSvg(file, svg);
|
|
2186
2190
|
return mergeAttachedImage({
|
|
2187
2191
|
svg_path: file,
|
|
2188
2192
|
name,
|
|
@@ -2334,7 +2338,7 @@ const plasmidMapFileTool = (ctx) => define({
|
|
|
2334
2338
|
},
|
|
2335
2339
|
// The map card in the transcript/panel reads this meta (see mapCardMeta).
|
|
2336
2340
|
presentationMeta(_args, value) {
|
|
2337
|
-
return mapCardMeta(value,
|
|
2341
|
+
return mapCardMeta(value, cardSvgFor(value.svg_path));
|
|
2338
2342
|
},
|
|
2339
2343
|
async execute(args, exec) {
|
|
2340
2344
|
const lower = args.path.toLowerCase();
|
|
@@ -2374,7 +2378,7 @@ const plasmidMapFileTool = (ctx) => define({
|
|
|
2374
2378
|
gc_skew: args.gc_skew === true,
|
|
2375
2379
|
});
|
|
2376
2380
|
const { file, viewed, attached } = await writeSvgFile(ctx, exec, args, svg, name);
|
|
2377
|
-
|
|
2381
|
+
rememberCardSvg(file, svg);
|
|
2378
2382
|
return mergeAttachedImage({
|
|
2379
2383
|
svg_path: file,
|
|
2380
2384
|
name,
|
|
@@ -2423,22 +2427,40 @@ function mapCardMeta(value, svg) {
|
|
|
2423
2427
|
}
|
|
2424
2428
|
|
|
2425
2429
|
/**
|
|
2426
|
-
*
|
|
2430
|
+
* Build the meta a figure card reads (sequence logo, gRNA map). Shared by both
|
|
2431
|
+
* tools so the browser half has one shape to validate: `summary` is the header
|
|
2432
|
+
* line, `extra` carries bounded presentation rows, and the markup travels only
|
|
2433
|
+
* when it fits `CARD_SVG_MAX_BYTES`.
|
|
2434
|
+
*/
|
|
2435
|
+
function figureCardMeta({ figure, summary, svgPath, extra = {} }, svg) {
|
|
2436
|
+
return {
|
|
2437
|
+
kind: 'molbio-figure',
|
|
2438
|
+
figure,
|
|
2439
|
+
summary,
|
|
2440
|
+
svg_path: svgPath,
|
|
2441
|
+
svg_bytes: svg.length,
|
|
2442
|
+
...extra,
|
|
2443
|
+
...svg.length === 0 ? { svg_missing: true } : svg.length <= CARD_SVG_MAX_BYTES ? { svg } : { svg_omitted: true },
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* Markup of the most recent written figures, keyed by path, so a card's
|
|
2427
2449
|
* projection never has to read a file back. Bounded on both axes — a handful of
|
|
2428
|
-
*
|
|
2450
|
+
* figures, each at most `CARD_SVG_MAX_BYTES` — because this is a module-level
|
|
2429
2451
|
* cache in a long-lived host process, not a store.
|
|
2430
2452
|
*/
|
|
2431
|
-
const
|
|
2453
|
+
const recentCardSvg = new Map();
|
|
2432
2454
|
|
|
2433
|
-
function
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2436
|
-
while (
|
|
2455
|
+
function rememberCardSvg(svgPath, svg) {
|
|
2456
|
+
if (recentCardSvg.has(svgPath)) recentCardSvg.delete(svgPath);
|
|
2457
|
+
recentCardSvg.set(svgPath, svg.length <= CARD_SVG_MAX_BYTES ? svg : '');
|
|
2458
|
+
while (recentCardSvg.size > 8) recentCardSvg.delete(recentCardSvg.keys().next().value);
|
|
2437
2459
|
}
|
|
2438
2460
|
|
|
2439
2461
|
/** The markup remembered for one written map ('' when it was too large or is gone). */
|
|
2440
|
-
function
|
|
2441
|
-
return
|
|
2462
|
+
function cardSvgFor(svgPath) {
|
|
2463
|
+
return recentCardSvg.get(svgPath) ?? '';
|
|
2442
2464
|
}
|
|
2443
2465
|
|
|
2444
2466
|
// ── cross-intron primer design ──────────────────────────────────────────────
|
|
@@ -2449,7 +2471,7 @@ const INTRON_MISMATCH_SCHEMA = {
|
|
|
2449
2471
|
required: ['position', 'template_base', 'primer_base', 'spliced_position', 'genomic_position', 'distance_from_3prime'],
|
|
2450
2472
|
properties: {
|
|
2451
2473
|
position: { type: 'integer', description: '1-based position of the mismatch in the primer (5\'→3\').' },
|
|
2452
|
-
template_base: { type: 'string', description: 'The base a perfectly matching primer would carry at this position
|
|
2474
|
+
template_base: { type: 'string', description: 'The base a perfectly matching primer would carry at this position: the spliced-transcript base for the forward primer, its complement for the reverse primer (which is reported as the molecule to order).' },
|
|
2453
2475
|
primer_base: { type: 'string', description: 'The base the reported primer actually carries (primer_base !== template_base marks a real mismatch).' },
|
|
2454
2476
|
spliced_position: { type: 'integer', description: '1-based position on the spliced transcript.' },
|
|
2455
2477
|
genomic_position: { type: 'integer', description: '1-based position on the genomic sequence.' },
|
|
@@ -2501,7 +2523,7 @@ const INTRON_PRIMER_SCHEMA = {
|
|
|
2501
2523
|
|
|
2502
2524
|
const intronPrimersTool = (ctx) => define({
|
|
2503
2525
|
name: 'molbio_design_intron_primers',
|
|
2504
|
-
description: 'Design qPCR primer pairs where
|
|
2526
|
+
description: 'Design qPCR primer pairs where ONE primer spans an exon-exon junction (>= min_junction_bases on each side) so genomic DNA cannot be amplified. In the returned pair the junction is carried by the REVERSE primer (the assay sits 5\' of the junction): the forward primer is upstream inside a single exon, and the reverse sequence is the molecule to order (the reverse complement of its spliced site), so F is upstream of R and the pair amplifies as printed. Provide the genomic sequence and the exon spans (1-based, inclusive). min_genomic_span enforces a minimal genomic distance between the primers (making gDNA amplification impossible or easily detectable). Coordinates come back both on the spliced transcript and on the genomic sequence. v12: Primer3-style structural filters (self-complementarity alignment scores, hairpin/dimer folding Tm at 47 °C defaults, end stability/end GC, GC clamp levels); pass max_mismatches > 0 to allow mismatched primers (never on the 3\'-terminal base) with spliced+genomic mismatch reports; pass check_mispriming: true to reject/penalize non-specific 3\'-tail annealing on the transcript.',
|
|
2505
2527
|
parameters: {
|
|
2506
2528
|
type: 'object',
|
|
2507
2529
|
required: ['exons'],
|
|
@@ -2621,7 +2643,7 @@ const intronPrimersTool = (ctx) => define({
|
|
|
2621
2643
|
}
|
|
2622
2644
|
const lines = [
|
|
2623
2645
|
`Tm model conditions: ${value.conditions.na_mm} mM Na+ / ${value.conditions.mg_mm} mM Mg2+ / ${value.conditions.dntp_mm} mM dNTP / ${value.conditions.primer_nm} nM primer (SantaLucia 1998 NN — an estimate).`,
|
|
2624
|
-
`${value.pairs.length} candidate pair(s); the
|
|
2646
|
+
`${value.pairs.length} candidate pair(s); the REVERSE primer spans an exon-exon junction, so genomic DNA will not amplify (forward is upstream of it):`,
|
|
2625
2647
|
];
|
|
2626
2648
|
const mismatchLine = (label, primer) => {
|
|
2627
2649
|
if (primer.mismatch_count > 0) {
|
|
@@ -2641,8 +2663,8 @@ const intronPrimersTool = (ctx) => define({
|
|
|
2641
2663
|
};
|
|
2642
2664
|
for (const [index, pair] of value.pairs.entries()) {
|
|
2643
2665
|
lines.push(`#${index + 1} spliced amplicon ${pair.spliced_amplicon.start}-${pair.spliced_amplicon.end} (${pair.spliced_amplicon.length} bp), genomic span ${pair.genomic_amplicon_length} bp${pair.target_distance !== undefined ? `, nearer 3' end ${pair.target_distance} spliced bp from target_position` : ''}, penalty ${pair.penalty}`);
|
|
2644
|
-
lines.push(` F ${pair.forward.sequence} (spliced ${pair.forward.spliced_start}-${pair.forward.spliced_end}; genomic ${pair.forward.genomic_start}-${pair.forward.genomic_end};
|
|
2645
|
-
lines.push(` R ${pair.reverse.sequence} (spliced ${pair.reverse.spliced_start}-${pair.reverse.spliced_end}; genomic ${pair.reverse.genomic_start}-${pair.reverse.genomic_end};
|
|
2666
|
+
lines.push(` F ${pair.forward.sequence} (spliced ${pair.forward.spliced_start}-${pair.forward.spliced_end}; genomic ${pair.forward.genomic_start}-${pair.forward.genomic_end}; exon ${pair.forward.exon}; Tm ${pair.forward.tm} °C, self ${pair.forward.self_any}/${pair.forward.self_end}, hairpin ${pair.forward.hairpin_tm} °C)`);
|
|
2667
|
+
lines.push(` R ${pair.reverse.sequence} (spliced ${pair.reverse.spliced_start}-${pair.reverse.spliced_end}; genomic ${pair.reverse.genomic_start}-${pair.reverse.genomic_end}; exons ${pair.reverse.exons.join('/')}, junction ${pair.reverse.junction_left}+${pair.reverse.junction_right} bp; Tm ${pair.reverse.tm} °C, self ${pair.reverse.self_any}/${pair.reverse.self_end}, hairpin ${pair.reverse.hairpin_tm} °C)`);
|
|
2646
2668
|
mismatchLine('F', pair.forward);
|
|
2647
2669
|
mismatchLine('R', pair.reverse);
|
|
2648
2670
|
misprimingLine('F', pair.forward);
|
|
@@ -3524,6 +3546,141 @@ const proteinPropsTool = define({
|
|
|
3524
3546
|
},
|
|
3525
3547
|
});
|
|
3526
3548
|
|
|
3549
|
+
const proteinChargeTool = (ctx) => define({
|
|
3550
|
+
name: 'molbio_protein_charge',
|
|
3551
|
+
description: 'Plot a protein\'s net charge across a pH range and report the isoelectric point on the same curve (Henderson-Hasselbalch summation over the terminal groups, K/R/H positive and D/E/C/Y negative). pka_set selects the pK table: bjellqvist (the set molbio_protein_props uses, so the pI matches) or emboss (the Epk.dat values). The SVG is written to the workspace and opened with the OS default viewer (auto_view, default true); the sampled curve and the charge at pH 7.4 are returned too. pI is an estimate and is only meaningful together with the named pK set.',
|
|
3552
|
+
parameters: {
|
|
3553
|
+
type: 'object',
|
|
3554
|
+
required: ['sequence'],
|
|
3555
|
+
properties: {
|
|
3556
|
+
sequence: requiredString('Protein sequence (one-letter amino acid codes).'),
|
|
3557
|
+
pka_set: { type: 'string', enum: ['bjellqvist', 'emboss'], description: 'pK table: bjellqvist (default, matches molbio_protein_props) or emboss (Epk.dat).' },
|
|
3558
|
+
ph_min: { type: 'number', description: 'Lowest pH sampled (0-14, default 0).' },
|
|
3559
|
+
ph_max: { type: 'number', description: 'Highest pH sampled (0-14, default 14).' },
|
|
3560
|
+
steps: { type: 'integer', description: 'Number of sampled points including both ends (2-141, default 29 = 0.5 pH).' },
|
|
3561
|
+
title: { type: 'string', description: 'Title drawn on the SVG (default "Net charge vs pH").' },
|
|
3562
|
+
output_path: { type: 'string', description: 'Optional SVG file path; default protein-charge.svg in the session workspace.' },
|
|
3563
|
+
auto_view: { type: 'boolean', description: 'Open the SVG automatically with the OS default application (default true).' },
|
|
3564
|
+
...ATTACH_IMAGE_PARAM,
|
|
3565
|
+
},
|
|
3566
|
+
},
|
|
3567
|
+
outputSchema: {
|
|
3568
|
+
type: 'object',
|
|
3569
|
+
additionalProperties: false,
|
|
3570
|
+
required: ['length', 'pka_set', 'pka_label', 'pI', 'charge_at_7_4', 'charge_min', 'charge_max', 'ph_min', 'ph_max', 'steps', 'points', 'svg_path', 'auto_viewed'],
|
|
3571
|
+
properties: {
|
|
3572
|
+
length: { type: 'integer' },
|
|
3573
|
+
pka_set: { type: 'string', enum: ['bjellqvist', 'emboss'] },
|
|
3574
|
+
pka_label: { type: 'string' },
|
|
3575
|
+
pI: { type: 'number', description: 'Isoelectric point on the sampled pK set.' },
|
|
3576
|
+
charge_at_7_4: { type: 'number', description: 'Net charge at pH 7.4 (e).' },
|
|
3577
|
+
charge_min: { type: 'number' },
|
|
3578
|
+
charge_max: { type: 'number' },
|
|
3579
|
+
ph_min: { type: 'number' },
|
|
3580
|
+
ph_max: { type: 'number' },
|
|
3581
|
+
steps: { type: 'integer' },
|
|
3582
|
+
points: {
|
|
3583
|
+
type: 'array',
|
|
3584
|
+
items: {
|
|
3585
|
+
type: 'object',
|
|
3586
|
+
additionalProperties: false,
|
|
3587
|
+
required: ['ph', 'charge'],
|
|
3588
|
+
properties: {
|
|
3589
|
+
ph: { type: 'number' },
|
|
3590
|
+
charge: { type: 'number' },
|
|
3591
|
+
},
|
|
3592
|
+
},
|
|
3593
|
+
},
|
|
3594
|
+
svg_path: { type: 'string' },
|
|
3595
|
+
auto_viewed: { type: 'boolean' },
|
|
3596
|
+
image: ATTACHED_IMAGE_SCHEMA,
|
|
3597
|
+
image_note: { type: 'string' },
|
|
3598
|
+
},
|
|
3599
|
+
},
|
|
3600
|
+
render(value) {
|
|
3601
|
+
const direction = value.charge_at_7_4 > 0 ? 'net positive' : value.charge_at_7_4 < 0 ? 'net negative' : 'neutral';
|
|
3602
|
+
return [
|
|
3603
|
+
value.length + ' aa · ' + value.pka_label + ' pK set · pI ' + value.pI + ' · charge at pH 7.4 ' + value.charge_at_7_4 + ' e (' + direction + ')',
|
|
3604
|
+
'Curve over pH ' + value.ph_min + '-' + value.ph_max + ' (' + value.steps + ' points; range ' + value.charge_min + ' .. ' + value.charge_max + ' e), SVG written to ' + value.svg_path + (value.auto_viewed === true ? ' and opened automatically' : '') + '.',
|
|
3605
|
+
'Henderson-Hasselbalch estimate: the pI belongs to the named pK set and is not interchangeable with another calculator.',
|
|
3606
|
+
imageNote(value).trim(),
|
|
3607
|
+
].filter((line) => line !== '').join('\n');
|
|
3608
|
+
},
|
|
3609
|
+
async execute(args, exec) {
|
|
3610
|
+
const curve = chargeCurve(args.sequence, {
|
|
3611
|
+
pkaSet: args.pka_set ?? 'bjellqvist',
|
|
3612
|
+
phMin: args.ph_min,
|
|
3613
|
+
phMax: args.ph_max,
|
|
3614
|
+
steps: args.steps,
|
|
3615
|
+
});
|
|
3616
|
+
const svg = renderChargeCurve(curve, { title: args.title });
|
|
3617
|
+
const { file, viewed, attached } = await writeSvgFile(ctx, exec, args, svg, 'protein-charge.svg');
|
|
3618
|
+
return mergeAttachedImage({ ...curve, svg_path: file, auto_viewed: viewed }, attached);
|
|
3619
|
+
},
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
const dotplotTool = (ctx) => define({
|
|
3623
|
+
name: 'molbio_dotplot',
|
|
3624
|
+
description: 'Compute and draw a self-comparison dotplot of a DNA sequence by exact word matching: a black cell at (i,j) means the k-mer at i equals the k-mer at j (direct repeat); a red cell means it equals the REVERSE COMPLEMENT of the k-mer at j (inverted repeat). word defaults to 8 (4-12); lower words find shorter repeats and produce more dots. The plot is binned to a fixed grid so a repetitive sequence cannot produce an enormous file; direct/inverted match counts and any scan/plot caps are reported. The SVG is written and opened with the OS default viewer (auto_view, default true). Exact k-mer view — not a windowed identity/scoring dotplot.',
|
|
3625
|
+
parameters: {
|
|
3626
|
+
type: 'object',
|
|
3627
|
+
required: ['sequence'],
|
|
3628
|
+
properties: {
|
|
3629
|
+
sequence: requiredString('DNA sequence (IUPAC; ambiguous k-mers are skipped and counted).'),
|
|
3630
|
+
word: { type: 'integer', description: 'Word (k-mer) length, 4-12 (default 8).' },
|
|
3631
|
+
mode: { type: 'string', enum: ['both', 'direct', 'inverted'], description: 'Which repeats to plot: both (default), direct only, or inverted only.' },
|
|
3632
|
+
title: { type: 'string', description: 'Title drawn on the SVG (default "Sequence dotplot").' },
|
|
3633
|
+
output_path: { type: 'string', description: 'Optional SVG file path; default dotplot.svg in the session workspace.' },
|
|
3634
|
+
auto_view: { type: 'boolean', description: 'Open the SVG automatically with the OS default application (default true).' },
|
|
3635
|
+
...ATTACH_IMAGE_PARAM,
|
|
3636
|
+
},
|
|
3637
|
+
},
|
|
3638
|
+
outputSchema: {
|
|
3639
|
+
type: 'object',
|
|
3640
|
+
additionalProperties: false,
|
|
3641
|
+
required: ['length', 'word', 'mode', 'direct_matches', 'inverted_matches', 'direct_cells', 'inverted_cells', 'ambiguous_bases', 'truncated', 'plot_truncated', 'svg_path', 'auto_viewed'],
|
|
3642
|
+
properties: {
|
|
3643
|
+
length: { type: 'integer' },
|
|
3644
|
+
word: { type: 'integer' },
|
|
3645
|
+
mode: { type: 'string', enum: ['both', 'direct', 'inverted'] },
|
|
3646
|
+
direct_matches: { type: 'integer' },
|
|
3647
|
+
inverted_matches: { type: 'integer' },
|
|
3648
|
+
direct_cells: { type: 'integer', description: 'Occupied grid cells for direct repeats (the plotted count, not the match count).' },
|
|
3649
|
+
inverted_cells: { type: 'integer' },
|
|
3650
|
+
ambiguous_bases: { type: 'integer', description: 'Ambiguous k-mers skipped (IUPAC bases other than A/C/G/T).' },
|
|
3651
|
+
truncated: { type: 'boolean', description: 'The match scan hit the work cap; counts are lower bounds.' },
|
|
3652
|
+
plot_truncated: { type: 'boolean', description: 'The grid hit the cell cap; some dots are not drawn.' },
|
|
3653
|
+
grid: { type: 'integer', description: 'Grid resolution used for binning the plot (cells per axis).' },
|
|
3654
|
+
svg_path: { type: 'string' },
|
|
3655
|
+
auto_viewed: { type: 'boolean' },
|
|
3656
|
+
image: ATTACHED_IMAGE_SCHEMA,
|
|
3657
|
+
image_note: { type: 'string' },
|
|
3658
|
+
},
|
|
3659
|
+
},
|
|
3660
|
+
render(value) {
|
|
3661
|
+
const lines = [
|
|
3662
|
+
value.length + ' bp · word ' + value.word + ' · ' + value.direct_matches + ' direct / ' + value.inverted_matches + ' inverted match(es); ' + value.direct_cells + ' + ' + value.inverted_cells + ' occupied cell(s)',
|
|
3663
|
+
'SVG written to ' + value.svg_path + (value.auto_viewed === true ? ' and opened automatically' : '') + '.',
|
|
3664
|
+
];
|
|
3665
|
+
if (value.ambiguous_bases > 0) lines.push(value.ambiguous_bases + ' ambiguous k-mer(s) skipped (IUPAC bases other than A/C/G/T).');
|
|
3666
|
+
if (value.truncated) lines.push('The match scan hit its work cap: the counts are lower bounds. Raise word to focus on longer, rarer repeats.');
|
|
3667
|
+
if (value.plot_truncated) lines.push('The plotted grid hit its cell cap: some dots are not drawn.');
|
|
3668
|
+
lines.push('Exact k-mer view (black = direct, red = inverted); the diagonal is the sequence against itself.');
|
|
3669
|
+
if (imageNote(value) !== '') lines.push(imageNote(value).trim());
|
|
3670
|
+
return lines.join('\n');
|
|
3671
|
+
},
|
|
3672
|
+
async execute(args, exec) {
|
|
3673
|
+
const data = dotplot(args.sequence, {
|
|
3674
|
+
word: args.word ?? 8,
|
|
3675
|
+
mode: args.mode ?? 'both',
|
|
3676
|
+
});
|
|
3677
|
+
const svg = renderDotplot(data, { title: args.title });
|
|
3678
|
+
const { file, viewed, attached } = await writeSvgFile(ctx, exec, args, svg, 'dotplot.svg');
|
|
3679
|
+
const { directGrid, invertedGrid, ...stats } = data;
|
|
3680
|
+
return mergeAttachedImage({ ...stats, svg_path: file, auto_viewed: viewed }, attached);
|
|
3681
|
+
},
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3527
3684
|
const peptideDigestTool = define({
|
|
3528
3685
|
name: 'molbio_peptide_digest',
|
|
3529
3686
|
description: 'In silico protease digestion for mass spectrometry: trypsin, chymotrypsin, LysC or GluC cleavage rules (no cleavage before proline where applicable), optional missed cleavages (0-3), monoisotopic or average [M+H]+ peptide masses, and optional mass-range filtering.',
|
|
@@ -4907,6 +5064,7 @@ const sequenceLogoTool = (ctx) => define({
|
|
|
4907
5064
|
smallSample: args.small_sample !== false,
|
|
4908
5065
|
});
|
|
4909
5066
|
const { file, viewed, attached } = await writeSvgFile(ctx, exec, args, svg, 'logo.svg');
|
|
5067
|
+
rememberCardSvg(file, svg);
|
|
4910
5068
|
const total = compositions.reduce((sum, column) => sum + column.bits, 0);
|
|
4911
5069
|
const best = compositions.reduce((top, column) => (column.bits > top.bits ? column : top), compositions[0]);
|
|
4912
5070
|
const round2 = (x) => Math.round(x * 100) / 100;
|
|
@@ -4924,6 +5082,17 @@ const sequenceLogoTool = (ctx) => define({
|
|
|
4924
5082
|
auto_viewed: viewed,
|
|
4925
5083
|
}, attached);
|
|
4926
5084
|
},
|
|
5085
|
+
// The transcript card draws the logo instead of asking the reader to open the
|
|
5086
|
+
// file. The meta is a projection: cheap, total, and bounded (the markup is
|
|
5087
|
+
// dropped when it exceeds CARD_SVG_MAX_BYTES).
|
|
5088
|
+
presentationMeta(args, value) {
|
|
5089
|
+
return figureCardMeta({
|
|
5090
|
+
figure: 'logo',
|
|
5091
|
+
summary: `Sequence logo · ${value.sequence_count} sequences × ${value.columns} columns · ${value.total_bits} bits (${value.score_type})`,
|
|
5092
|
+
svgPath: value.svg_path,
|
|
5093
|
+
extra: { title: args?.title ?? 'Sequence logo' },
|
|
5094
|
+
}, cardSvgFor(value.svg_path));
|
|
5095
|
+
},
|
|
4927
5096
|
});
|
|
4928
5097
|
|
|
4929
5098
|
/**
|
|
@@ -5199,6 +5368,7 @@ const grnaDesignTool = (ctx) => define({
|
|
|
5199
5368
|
mapPath = written.file;
|
|
5200
5369
|
viewed = written.viewed;
|
|
5201
5370
|
attached = written.attached;
|
|
5371
|
+
rememberCardSvg(written.file, svg);
|
|
5202
5372
|
}
|
|
5203
5373
|
return mergeAttachedImage({
|
|
5204
5374
|
target_name: name,
|
|
@@ -5224,6 +5394,36 @@ const grnaDesignTool = (ctx) => define({
|
|
|
5224
5394
|
auto_viewed: viewed,
|
|
5225
5395
|
}, attached);
|
|
5226
5396
|
},
|
|
5397
|
+
// The transcript card shows the ranked guides (top five) and, when the caller
|
|
5398
|
+
// asked for map_path, the map itself. The summary is built here so the card
|
|
5399
|
+
// stays a pure function of this projection.
|
|
5400
|
+
presentationMeta(_args, value) {
|
|
5401
|
+
const guides = value.guides.slice(0, 5).map((guide) => ({
|
|
5402
|
+
rank: guide.rank,
|
|
5403
|
+
sequence: guide.sequence,
|
|
5404
|
+
pam: guide.pam,
|
|
5405
|
+
strand: guide.strand,
|
|
5406
|
+
start: guide.start,
|
|
5407
|
+
end: guide.end,
|
|
5408
|
+
score: guide.score,
|
|
5409
|
+
off_target_count: guide.off_target_count ?? 0,
|
|
5410
|
+
}));
|
|
5411
|
+
const recognition = value.pam === '' ? 'no PAM (Cas13)' : value.pam + ' PAM';
|
|
5412
|
+
return figureCardMeta({
|
|
5413
|
+
figure: 'grna',
|
|
5414
|
+
summary: `${value.cas_label} · ${value.guides.length} of ${value.candidate_count} guide(s) · ${recognition} · ${value.target_name} (${value.target_length} bp)`,
|
|
5415
|
+
svgPath: value.map_path,
|
|
5416
|
+
extra: {
|
|
5417
|
+
guides,
|
|
5418
|
+
guide_count: value.guides.length,
|
|
5419
|
+
candidate_count: value.candidate_count,
|
|
5420
|
+
csv_path: value.saved_to,
|
|
5421
|
+
map_path: value.map_path,
|
|
5422
|
+
cas_family: value.cas_family,
|
|
5423
|
+
pam: value.pam,
|
|
5424
|
+
},
|
|
5425
|
+
}, value.map_path === '' ? '' : cardSvgFor(value.map_path));
|
|
5426
|
+
},
|
|
5227
5427
|
});
|
|
5228
5428
|
|
|
5229
5429
|
/** CSV of the returned guides, for pasting into an ordering sheet. */
|
|
@@ -6481,6 +6681,248 @@ const fastqQcTool = (ctx) => define({
|
|
|
6481
6681
|
},
|
|
6482
6682
|
});
|
|
6483
6683
|
|
|
6684
|
+
|
|
6685
|
+
const contaminationScreenTool = (ctx) => define({
|
|
6686
|
+
name: 'molbio_contamination_screen',
|
|
6687
|
+
description: 'Screen a DNA sequence, or every entry in a FASTA/FASTQ file, for common vector-backbone/promoter/origin and Illumina/Nextera adapter signatures. The signature table is SHORT and embedded (no reference database, no network): a hit is evidence worth following up, and a negative result does NOT prove the sample is clean. Matching is both-strand and substitution-only (no gaps); max_mismatches adds a seed-and-verify search (0-2). Reports per-signature read counts, the best example position/strand, and the criteria that produced them. Use it for "is this assembly contaminated with vector backbone or adapter" — not as a BLAST replacement.',
|
|
6688
|
+
parameters: {
|
|
6689
|
+
type: 'object',
|
|
6690
|
+
properties: {
|
|
6691
|
+
sequence: { type: 'string', description: 'Inline DNA sequence to screen. Provide exactly one of sequence or path.' },
|
|
6692
|
+
path: { type: 'string', description: 'Path to a FASTA/FASTQ file, or a plain sequence file. Provide exactly one of sequence or path.' },
|
|
6693
|
+
max_mismatches: { type: 'integer', description: 'Substitutions allowed per signature (0-2, default 0). A hit still needs one exact seed chunk.' },
|
|
6694
|
+
max_reads: { type: 'integer', description: 'Scan at most this many entries (default 0 = all, capped at 200000).' },
|
|
6695
|
+
kinds: { type: 'string', enum: ['both', 'adapter', 'vector'], description: 'Restrict the table to adapters, vectors, or both (default both).' },
|
|
6696
|
+
},
|
|
6697
|
+
},
|
|
6698
|
+
outputSchema: {
|
|
6699
|
+
type: 'object',
|
|
6700
|
+
additionalProperties: false,
|
|
6701
|
+
required: ['input_type', 'total_entries', 'scanned_entries', 'bases', 'ambiguous_bases', 'hits', 'hit_count', 'reads_with_hit', 'truncated', 'criteria'],
|
|
6702
|
+
properties: {
|
|
6703
|
+
input_type: { type: 'string', enum: ['sequence', 'fasta', 'fastq'] },
|
|
6704
|
+
total_entries: { type: 'integer' },
|
|
6705
|
+
scanned_entries: { type: 'integer' },
|
|
6706
|
+
bases: { type: 'integer' },
|
|
6707
|
+
ambiguous_bases: { type: 'integer' },
|
|
6708
|
+
hits: {
|
|
6709
|
+
type: 'array',
|
|
6710
|
+
items: {
|
|
6711
|
+
type: 'object',
|
|
6712
|
+
additionalProperties: false,
|
|
6713
|
+
required: ['name', 'kind', 'category', 'signature', 'reads', 'percent', 'best_mismatches', 'example'],
|
|
6714
|
+
properties: {
|
|
6715
|
+
name: { type: 'string' },
|
|
6716
|
+
kind: { type: 'string', enum: ['adapter', 'vector'] },
|
|
6717
|
+
category: { type: 'string' },
|
|
6718
|
+
signature: { type: 'string' },
|
|
6719
|
+
reads: { type: 'integer' },
|
|
6720
|
+
percent: { type: 'number' },
|
|
6721
|
+
best_mismatches: { type: 'integer' },
|
|
6722
|
+
example: {
|
|
6723
|
+
type: 'object',
|
|
6724
|
+
additionalProperties: false,
|
|
6725
|
+
required: ['id', 'start', 'end', 'strand', 'mismatches'],
|
|
6726
|
+
properties: {
|
|
6727
|
+
id: { type: 'string' },
|
|
6728
|
+
start: { type: 'integer' },
|
|
6729
|
+
end: { type: 'integer' },
|
|
6730
|
+
strand: { type: 'string', enum: ['+', '-'] },
|
|
6731
|
+
mismatches: { type: 'integer' },
|
|
6732
|
+
},
|
|
6733
|
+
},
|
|
6734
|
+
},
|
|
6735
|
+
},
|
|
6736
|
+
},
|
|
6737
|
+
hit_count: { type: 'integer' },
|
|
6738
|
+
reads_with_hit: { type: 'integer' },
|
|
6739
|
+
truncated: { type: 'boolean' },
|
|
6740
|
+
criteria: {
|
|
6741
|
+
type: 'object',
|
|
6742
|
+
additionalProperties: false,
|
|
6743
|
+
required: ['max_mismatches', 'kinds', 'signature_count', 'max_reads', 'match_basis'],
|
|
6744
|
+
properties: {
|
|
6745
|
+
max_mismatches: { type: 'integer' },
|
|
6746
|
+
kinds: { type: 'string', enum: ['both', 'adapter', 'vector'] },
|
|
6747
|
+
signature_count: { type: 'integer' },
|
|
6748
|
+
max_reads: { type: 'integer' },
|
|
6749
|
+
match_basis: { type: 'string' },
|
|
6750
|
+
},
|
|
6751
|
+
},
|
|
6752
|
+
},
|
|
6753
|
+
},
|
|
6754
|
+
render(value) {
|
|
6755
|
+
const lines = [
|
|
6756
|
+
value.input_type + ' input: ' + value.scanned_entries + ' of ' + value.total_entries + ' entr' + (value.total_entries === 1 ? 'y' : 'ies') + ', ' + value.bases + ' bp; ' + value.hit_count + ' signature(s) hit, ' + value.reads_with_hit + ' entr' + (value.total_entries === 1 ? 'y' : 'ies') + ' with a hit.',
|
|
6757
|
+
];
|
|
6758
|
+
if (value.hits.length === 0) {
|
|
6759
|
+
lines.push('No embedded vector/adapter signature matched.');
|
|
6760
|
+
} else {
|
|
6761
|
+
for (const hit of value.hits) {
|
|
6762
|
+
lines.push(' ' + hit.name + ' [' + hit.kind + '/' + hit.category + '] ' + hit.reads + '/' + value.scanned_entries + ' (' + hit.percent + '%), best ' + hit.best_mismatches + ' mismatch(es); e.g. ' + hit.example.id + ' ' + hit.example.start + '-' + hit.example.end + ' (' + hit.example.strand + ')');
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
if (value.ambiguous_bases > 0) lines.push(value.ambiguous_bases + ' ambiguous base(s) were scanned literally (they can only match a signature at an unambiguous position).');
|
|
6766
|
+
if (value.truncated) lines.push('Only the first ' + value.scanned_entries + ' entr' + (value.scanned_entries === 1 ? 'y was' : 'ies were') + ' scanned; max_reads capped the input.');
|
|
6767
|
+
lines.push('Short embedded signatures, both strands, substitutions only (no gaps). A negative result does not prove the sample is clean.');
|
|
6768
|
+
return lines.join('\n');
|
|
6769
|
+
},
|
|
6770
|
+
async execute(args, exec) {
|
|
6771
|
+
const hasSequence = typeof args.sequence === 'string' && args.sequence !== '';
|
|
6772
|
+
const hasPath = typeof args.path === 'string' && args.path !== '';
|
|
6773
|
+
if (hasSequence === hasPath) throw new MolbioInputError('provide exactly one of sequence or path');
|
|
6774
|
+
let entries;
|
|
6775
|
+
let inputType;
|
|
6776
|
+
if (hasSequence) {
|
|
6777
|
+
entries = [{ id: 'input', sequence: normalizeSequence(args.sequence, 'sequence') }];
|
|
6778
|
+
inputType = 'sequence';
|
|
6779
|
+
} else {
|
|
6780
|
+
const bytes = await readFileBytes(ctx, exec, args.path);
|
|
6781
|
+
const text = new TextDecoder().decode(bytes).replace(/^\uFEFF/, '');
|
|
6782
|
+
const first = text.trimStart();
|
|
6783
|
+
if (first.startsWith('@')) {
|
|
6784
|
+
entries = parseFastq(text);
|
|
6785
|
+
inputType = 'fastq';
|
|
6786
|
+
} else if (first.startsWith('>')) {
|
|
6787
|
+
entries = parseFasta(text);
|
|
6788
|
+
inputType = 'fasta';
|
|
6789
|
+
} else {
|
|
6790
|
+
entries = [{ id: 'input', sequence: normalizeSequence(text, 'sequence') }];
|
|
6791
|
+
inputType = 'sequence';
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
const result = screenContamination(entries, {
|
|
6795
|
+
maxMismatches: args.max_mismatches,
|
|
6796
|
+
maxReads: args.max_reads,
|
|
6797
|
+
kinds: args.kinds,
|
|
6798
|
+
});
|
|
6799
|
+
return { input_type: inputType, ...result };
|
|
6800
|
+
},
|
|
6801
|
+
});
|
|
6802
|
+
|
|
6803
|
+
const FASTQ_TRIM_TOOL_SCHEMA = {
|
|
6804
|
+
type: 'object',
|
|
6805
|
+
additionalProperties: false,
|
|
6806
|
+
required: ['reads_in', 'total_reads', 'reads_out', 'bases_in', 'bases_out', 'trimmed_reads', 'dropped_reads', 'trim_counts', 'drop_counts', 'truncated', 'criteria', 'saved_to', 'output_format'],
|
|
6807
|
+
properties: {
|
|
6808
|
+
reads_in: { type: 'integer' },
|
|
6809
|
+
total_reads: { type: 'integer' },
|
|
6810
|
+
reads_out: { type: 'integer' },
|
|
6811
|
+
bases_in: { type: 'integer' },
|
|
6812
|
+
bases_out: { type: 'integer' },
|
|
6813
|
+
trimmed_reads: { type: 'integer' },
|
|
6814
|
+
dropped_reads: { type: 'integer' },
|
|
6815
|
+
trim_counts: {
|
|
6816
|
+
type: 'object',
|
|
6817
|
+
additionalProperties: false,
|
|
6818
|
+
required: ['quality', 'adapter', 'poly_g', 'poly_a', 'primer'],
|
|
6819
|
+
properties: {
|
|
6820
|
+
quality: { type: 'integer' },
|
|
6821
|
+
adapter: { type: 'integer' },
|
|
6822
|
+
poly_g: { type: 'integer' },
|
|
6823
|
+
poly_a: { type: 'integer' },
|
|
6824
|
+
primer: { type: 'integer' },
|
|
6825
|
+
},
|
|
6826
|
+
},
|
|
6827
|
+
drop_counts: {
|
|
6828
|
+
type: 'object',
|
|
6829
|
+
additionalProperties: false,
|
|
6830
|
+
required: ['length', 'n'],
|
|
6831
|
+
properties: {
|
|
6832
|
+
length: { type: 'integer' },
|
|
6833
|
+
n: { type: 'integer' },
|
|
6834
|
+
},
|
|
6835
|
+
},
|
|
6836
|
+
truncated: { type: 'boolean' },
|
|
6837
|
+
criteria: {
|
|
6838
|
+
type: 'object',
|
|
6839
|
+
additionalProperties: false,
|
|
6840
|
+
required: ['min_quality', 'quality_window', 'poly_min', 'min_length', 'max_n_percent', 'phred', 'max_reads', 'adapter', 'primer', 'poly_g', 'poly_a'],
|
|
6841
|
+
properties: {
|
|
6842
|
+
min_quality: { type: 'integer' },
|
|
6843
|
+
quality_window: { type: 'integer' },
|
|
6844
|
+
poly_min: { type: 'integer' },
|
|
6845
|
+
min_length: { type: 'integer' },
|
|
6846
|
+
max_n_percent: { type: 'number' },
|
|
6847
|
+
phred: { type: 'integer', enum: [33, 64] },
|
|
6848
|
+
max_reads: { type: 'integer' },
|
|
6849
|
+
adapter: { type: 'string' },
|
|
6850
|
+
primer: { type: 'string' },
|
|
6851
|
+
poly_g: { type: 'boolean' },
|
|
6852
|
+
poly_a: { type: 'boolean' },
|
|
6853
|
+
},
|
|
6854
|
+
},
|
|
6855
|
+
saved_to: { type: 'string' },
|
|
6856
|
+
output_format: { type: 'string', enum: ['fastq', 'fasta'] },
|
|
6857
|
+
},
|
|
6858
|
+
};
|
|
6859
|
+
|
|
6860
|
+
const fastqTrimTool = (ctx) => define({
|
|
6861
|
+
safe: false,
|
|
6862
|
+
name: 'molbio_fastq_trim',
|
|
6863
|
+
description: 'Trim and filter a FASTQ file, then WRITE the surviving reads as FASTQ or FASTA. Supported operations, applied in this order: remove an exact 5\' primer, remove an exact 3\' adapter and everything after it, trim a low-quality 3\' tail with a sliding mean-quality window, trim a trailing poly-G or poly-A run, then drop reads below min_length or above max_n_percent. Phred+33 and Phred+64 are supported (quality characters are preserved). This is not a paired-end merger, not a semiglobal adapter aligner, and not a UMI pipeline; those are deliberately out of scope. Returns before/after counts, per-stage trim counts, and the written path.',
|
|
6864
|
+
parameters: {
|
|
6865
|
+
type: 'object',
|
|
6866
|
+
required: ['path', 'output_path'],
|
|
6867
|
+
properties: {
|
|
6868
|
+
path: { type: 'string', description: 'Path to the input .fastq/.fq file.' },
|
|
6869
|
+
output_path: { type: 'string', description: 'Path to write the trimmed reads (FASTQ or FASTA, per output_format).' },
|
|
6870
|
+
output_format: { type: 'string', enum: ['fastq', 'fasta'], description: 'Output format (default fastq).' },
|
|
6871
|
+
min_quality: { type: 'integer', description: 'Minimum mean Phred in the sliding window (0-60, default 20). Set 0 to disable quality trimming.' },
|
|
6872
|
+
quality_window: { type: 'integer', description: 'Sliding-window width in bases (1-100, default 4).' },
|
|
6873
|
+
poly_g: { type: 'boolean', description: 'Trim a trailing poly-G run (default false; useful for NovaSeq/NextSeq).' },
|
|
6874
|
+
poly_a: { type: 'boolean', description: 'Trim a trailing poly-A run (default false).' },
|
|
6875
|
+
poly_min: { type: 'integer', description: 'Minimum trailing run length to trim (2-100, default 8).' },
|
|
6876
|
+
adapter: { type: 'string', description: 'Exact 3\' adapter sequence; it and everything after it are removed. Empty = no adapter trim.' },
|
|
6877
|
+
primer: { type: 'string', description: 'Exact 5\' primer sequence; it is removed when the read starts with it. Empty = no primer trim.' },
|
|
6878
|
+
min_length: { type: 'integer', description: 'Drop reads shorter than this after trimming (default 0).' },
|
|
6879
|
+
max_n_percent: { type: 'number', description: 'Drop reads whose trimmed sequence has more than this percent of non-ACGT bases (0-100, default 100 = no filter).' },
|
|
6880
|
+
phred: { type: 'integer', enum: [33, 64], description: 'Quality encoding of the input (33 = Sanger/Illumina 1.8+, 64 = old Illumina). Output keeps the same encoding.' },
|
|
6881
|
+
max_reads: { type: 'integer', description: 'Process at most this many reads (default 0 = all, capped at 200000).' },
|
|
6882
|
+
},
|
|
6883
|
+
},
|
|
6884
|
+
outputSchema: FASTQ_TRIM_TOOL_SCHEMA,
|
|
6885
|
+
render(value) {
|
|
6886
|
+
const counts = value.trim_counts;
|
|
6887
|
+
const lines = [
|
|
6888
|
+
'FASTQ trim: ' + value.reads_in + ' of ' + value.total_reads + ' read(s) scanned (' + value.bases_in + ' bases) -> ' + value.reads_out + ' read(s) (' + value.bases_out + ' bases); ' + value.trimmed_reads + ' trimmed, ' + value.dropped_reads + ' dropped.',
|
|
6889
|
+
' quality ' + counts.quality + ' · adapter ' + counts.adapter + ' · poly-G ' + counts.poly_g + ' · poly-A ' + counts.poly_a + ' · primer ' + counts.primer + '; dropped by length ' + value.drop_counts.length + ', by N ' + value.drop_counts.n,
|
|
6890
|
+
'Saved ' + value.output_format.toUpperCase() + ' to ' + value.saved_to + '.',
|
|
6891
|
+
];
|
|
6892
|
+
if (value.truncated) lines.push('Only the first ' + value.reads_in + ' read(s) were processed; max_reads capped the input.');
|
|
6893
|
+
lines.push('Settings: Phred+' + value.criteria.phred + ', min Q' + value.criteria.min_quality + ' over ' + value.criteria.quality_window + ' base(s), poly >= ' + value.criteria.poly_min + ', min length ' + value.criteria.min_length + ', max N ' + value.criteria.max_n_percent + '%.');
|
|
6894
|
+
if (value.criteria.adapter !== '') lines.push('Adapter: ' + value.criteria.adapter + '.');
|
|
6895
|
+
if (value.criteria.primer !== '') lines.push('Primer: ' + value.criteria.primer + '.');
|
|
6896
|
+
return lines.join('\n');
|
|
6897
|
+
},
|
|
6898
|
+
async execute(args, exec) {
|
|
6899
|
+
const bytes = await readFileBytes(ctx, exec, args.path);
|
|
6900
|
+
const text = new TextDecoder().decode(bytes).replace(/^\uFEFF/, '');
|
|
6901
|
+
if (text.trimStart().startsWith('>')) throw new MolbioInputError('this looks like FASTA, not FASTQ: molbio_fastq_trim needs per-base quality strings');
|
|
6902
|
+
const entries = parseFastq(text);
|
|
6903
|
+
const trimmed = trimFastq(entries, {
|
|
6904
|
+
minQuality: args.min_quality,
|
|
6905
|
+
qualityWindow: args.quality_window,
|
|
6906
|
+
polyG: args.poly_g,
|
|
6907
|
+
polyA: args.poly_a,
|
|
6908
|
+
polyMin: args.poly_min,
|
|
6909
|
+
adapter: args.adapter,
|
|
6910
|
+
primer: args.primer,
|
|
6911
|
+
minLength: args.min_length,
|
|
6912
|
+
maxNPercent: args.max_n_percent,
|
|
6913
|
+
phred: args.phred,
|
|
6914
|
+
maxReads: args.max_reads,
|
|
6915
|
+
});
|
|
6916
|
+
const outputFormat = args.output_format ?? 'fastq';
|
|
6917
|
+
const fs = fsService(ctx);
|
|
6918
|
+
const sandboxPolicyService = ctx.get('sandboxPolicy');
|
|
6919
|
+
const policy = sandboxPolicyService?.resolve({ ...exec?.agent !== undefined ? { session: exec.agent.session } : {} });
|
|
6920
|
+
const file = workspaceFilePath(args.output_path, exec, policy?.workspaceRoot);
|
|
6921
|
+
await writeWorkspaceFile(fs, file, outputFormat === 'fasta' ? toFasta(trimmed.entries) : toFastq(trimmed.entries), policy);
|
|
6922
|
+
return { ...trimmed.stats, saved_to: file, output_format: outputFormat };
|
|
6923
|
+
},
|
|
6924
|
+
});
|
|
6925
|
+
|
|
6484
6926
|
const RARE_CODON_SCHEMA = {
|
|
6485
6927
|
type: 'object',
|
|
6486
6928
|
additionalProperties: false,
|
|
@@ -7602,11 +8044,11 @@ const PROMPT_SECTION = `Molecular-biology tools (dsh-molbio-tools) are available
|
|
|
7602
8044
|
- Proteins: molbio_protein_props (MW/pI/A280/GRAVY — estimates), molbio_peptide_digest (trypsin etc. for MS), molbio_codon_optimize (E. coli/yeast/human, can avoid restriction sites), molbio_helical_wheel (Schiffer-Edmundson wheel + Eisenberg hydrophobic moment, written as an SVG), molbio_hydropathy_plot (Kyte-Doolittle sliding-window profile with the 1.6 threshold peaks, written as an SVG).
|
|
7603
8045
|
- Quantitation: molbio_qpcr_efficiency (standard curve + plot), molbio_plot (experiment-data charts written to files), molbio_virtual_gel (expected band pattern as an SVG gel image with a size ladder).
|
|
7604
8046
|
- molbio_plot kinds: bar (labelled values + optional SD error bars) and scatter (x/y + optional least-squares line) take their arrays directly in the call; the table kinds read a workspace CSV/TSV — pass data_path plus columns naming which column plays which role, or pass data with the table text inline. Use a table kind whenever the numbers are already in a file: do NOT retype a column into the call. line = one series per group_column; histogram = distribution of one column (bins default to Freedman-Diaconis unless you pass bins); box = Q1-Q3 box, median, 1.5xIQR whiskers, outliers as dots; violin = the same samples as a kernel-density silhouette with the box summary inside (use it when the SHAPE matters, not just the five numbers); volcano = effect size against significance from a log2-fold-change column and a p column, with dashed thresholds — it computes no statistics of its own, so the p-values must already be in the table; heatmap = a row x column matrix, one cell per value plus a colour bar, accepting either a wide table (first column = row labels, the rest = samples) or the long form (columns.row/column/value), with scale row_zscore|log2 as an explicitly-reported DISPLAY transform. The result carries the computed statistics (medians, quartiles, outliers, bin counts, axis ranges, significance counts, matrix extent), so quote THOSE instead of estimating values off the picture.
|
|
7605
|
-
- Sequences & files: molbio_align (local alignment with a readable match line), molbio_msa_align (progressive multiple sequence alignment — pass an array of sequences or a workspace FASTA), molbio_conservation (consensus + per-column identity/conservation + variable positions; accepts aligned rows or raw sequences it aligns first), molbio_sequence_logo (draws the same alignment as a sequence logo SVG — pass the molbio_msa_align output rows as the alignment argument; letters are sized by per-position information content), molbio_fasta_fastq (FASTA/FASTQ stats/extract/convert/QC on workspace files), molbio_batch_analyze (run one summary/ORF/enzyme analysis over every matching FASTA/FASTQ/GenBank/.dna file in a directory and write one CSV/TSV table), molbio_extract_region (pull a CDS/promoter/region from a plasmid file by feature label or coordinates).
|
|
8047
|
+
- Sequences & files: molbio_align (local alignment with a readable match line), molbio_msa_align (progressive multiple sequence alignment — pass an array of sequences or a workspace FASTA), molbio_conservation (consensus + per-column identity/conservation + variable positions; accepts aligned rows or raw sequences it aligns first), molbio_sequence_logo (draws the same alignment as a sequence logo SVG — pass the molbio_msa_align output rows as the alignment argument; letters are sized by per-position information content), molbio_fasta_fastq (FASTA/FASTQ stats/extract/convert/QC on workspace files), molbio_contamination_screen (embedded short vector/adapter signatures, both strands, exact or 0-2 mismatches — a short-signature screen, not BLAST), molbio_batch_analyze (run one summary/ORF/enzyme analysis over every matching FASTA/FASTQ/GenBank/.dna file in a directory and write one CSV/TSV table), molbio_extract_region (pull a CDS/promoter/region from a plasmid file by feature label or coordinates).
|
|
7606
8048
|
- CRISPR: molbio_grna_design (family-aware guide RNAs over a sequence or plasmid file: cas_family spcas9/sacas9/cas12a/cas13a/cas13b/cas13d selects the PAM side, crRNA length, seed and target type — Cas12a uses a 5' TTTV PAM and 23 nt crRNA; Cas13 targets RNA and reports a PFS preference rather than a PAM. Both strands are scanned for DNA families, the sense strand by default for Cas13; guides are filtered on GC/poly-T/self-complementarity and ranked, with a mismatch-tolerant off-target search over the same sequence — reaching a whole genome simply means passing it as the target). Pass the top candidates on for ordering with save_path (CSV) or map_path (SVG map); the ranking is a heuristic and says so, so present it as candidate ranking, never as a validated efficiency prediction.
|
|
7607
8049
|
- Map extras: pass gc_skew: true for the GC skew ring, show_unique_cutters: true to mark single-cutting enzymes on the map.
|
|
7608
8050
|
- Literature & records: molbio_pubmed_abstract (fetch abstracts by PMID — needs web fetch; an NCBI key comes from the api_key argument, the credential store's NCBI_API_KEY, or the environment), molbio_paper_export_bibtex (papers.json → .bib), molbio_protocol_add / molbio_protocol_list / molbio_protocol_update (protocols.json), molbio_experiment_log / molbio_experiment_list (experiments.json).
|
|
7609
|
-
- Bench analysis (v19): molbio_fastq_qc (read-level QC for a FASTQ file: per-base quality with quartiles, per-read quality, base content, GC distribution, length, exact duplication, over-represented sequences, adapter screen, plus a report SVG — use it for "is this run good"), molbio_codon_usage (CAI/RSCU/Nc/GC3, rare-codon list and CDS CpG against E. coli/yeast/human — use it for "will this gene express"; it explains, molbio_codon_optimize rewrites), molbio_phylogenetic_tree (distance method: p-distance/JC/K2P/Tamura-Nei, UPGMA or neighbour-joining, bootstrap support, consensus, Newick file and an SVG tree — it is NOT a maximum-likelihood or Bayesian analysis, and bootstrap percentages are not p-values), molbio_pcr_simulate (where a primer pair amplifies: product sizes and coordinates on both strands, mismatch detail, the 3'-end exact-match requirement, circular templates, screening other templates, and an expected gel — use it for "what band will I see"), molbio_gc_composition (sliding-window GC, CpG islands under the Gardiner-Garden or Takai criteria, GC/AT skew with the cumulative-skew origin/terminus indication, dinucleotide obs/exp, entropy, complexity, N50/L50, plus a figure — use it for "is this a promoter or an origin").
|
|
8051
|
+
- Bench analysis (v19): molbio_fastq_qc (read-level QC for a FASTQ file: per-base quality with quartiles, per-read quality, base content, GC distribution, length, exact duplication, over-represented sequences, adapter screen, plus a report SVG — use it for "is this run good"), molbio_codon_usage (CAI/RSCU/Nc/GC3, rare-codon list and CDS CpG against E. coli/yeast/human — use it for "will this gene express"; it explains, molbio_codon_optimize rewrites), molbio_phylogenetic_tree (distance method: p-distance/JC/K2P/Tamura-Nei, UPGMA or neighbour-joining, bootstrap support, consensus, Newick file and an SVG tree — it is NOT a maximum-likelihood or Bayesian analysis, and bootstrap percentages are not p-values), molbio_pcr_simulate (where a primer pair amplifies: product sizes and coordinates on both strands, mismatch detail, the 3'-end exact-match requirement, circular templates, screening other templates, and an expected gel — use it for "what band will I see"), molbio_gc_composition (sliding-window GC, CpG islands under the Gardiner-Garden or Takai criteria, GC/AT skew with the cumulative-skew origin/terminus indication, dinucleotide obs/exp, entropy, complexity, N50/L50, plus a figure — use it for "is this a promoter or an origin"), molbio_fastq_trim (trim low-quality 3' tails, exact adapters/primers, poly-G/poly-A, then filter by length/N and write FASTQ/FASTA — not a paired-end merger).
|
|
7610
8052
|
- Literature: molbio_pubmed_search — with an NCBI API key (api_key argument, the credential store under NCBI_API_KEY / MOLBIO_NCBI_API_KEY, or the environment) it uses the official PubMed E-utilities API and returns PMIDs, titles, journal/year, authors and DOI; without a key it falls back to the harness web search. The reading library is molbio_paper_add / molbio_paper_list / molbio_paper_update / molbio_paper_remove (papers.json in the workspace).
|
|
7611
8053
|
|
|
7612
8054
|
Cloning workflow rules (follow them to avoid slow re-derivation):
|
|
@@ -7650,6 +8092,7 @@ export function apply(ctx) {
|
|
|
7650
8092
|
silentSiteTool,
|
|
7651
8093
|
verifySangerTool(ctx),
|
|
7652
8094
|
proteinPropsTool,
|
|
8095
|
+
proteinChargeTool(ctx),
|
|
7653
8096
|
peptideDigestTool,
|
|
7654
8097
|
codonOptimizeTool,
|
|
7655
8098
|
helicalWheelTool(ctx),
|
|
@@ -7666,10 +8109,13 @@ export function apply(ctx) {
|
|
|
7666
8109
|
fastaFastqTool(ctx),
|
|
7667
8110
|
batchAnalyzeTool(ctx),
|
|
7668
8111
|
fastqQcTool(ctx),
|
|
8112
|
+
contaminationScreenTool(ctx),
|
|
8113
|
+
fastqTrimTool(ctx),
|
|
7669
8114
|
codonUsageTool,
|
|
7670
8115
|
phylogeneticTreeTool(ctx),
|
|
7671
8116
|
pcrSimulateTool(ctx),
|
|
7672
8117
|
gcCompositionTool(ctx),
|
|
8118
|
+
dotplotTool(ctx),
|
|
7673
8119
|
extractRegionTool(ctx),
|
|
7674
8120
|
pubmedAbstractTool(ctx),
|
|
7675
8121
|
bibtexTool(ctx),
|