dsh-molbio-tools 0.22.2 → 0.24.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 +221 -0
- package/README.md +51 -19
- package/batch.mjs +5 -1
- package/benchmark/README.md +7 -7
- package/benchmark/changed.mjs +4 -3
- package/benchmark/fixtures.mjs +32 -4
- package/benchmark/profile.mjs +1 -1
- 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-bundle.mjs +1 -1
- package/build/client-entry.mjs +80 -1
- package/build/panel-core.mjs +85 -0
- package/build/preset-patch.mjs +1 -1
- package/build/release-preflight.mjs +1 -1
- package/charts.mjs +35 -16
- package/contamination.mjs +229 -0
- package/cordis.patch.yml +10 -6
- package/design.mjs +111 -78
- package/docs/capability-gap-survey.md +5 -0
- package/docs/client-panel.md +10 -8
- package/docs/history.md +86 -0
- package/docs/maintainer.md +8 -5
- package/docs/roadmap.md +37 -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 +31 -16
- package/fastq-trim.mjs +238 -0
- package/host.mjs +2 -2
- package/index.mjs +521 -34
- package/lib/client.js +376 -109
- package/package.json +8 -3
- package/papers.mjs +10 -0
- package/plot.mjs +24 -8
- package/preset/install.mjs +8 -0
- 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-structure.mjs +10 -2
- package/protein.mjs +36 -14
- package/records.mjs +17 -2
- package/seqio.mjs +49 -14
- package/test/charts.mjs +11 -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 +378 -11
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"covers": [
|
|
90
90
|
"molbio_design_intron_primers"
|
|
91
91
|
],
|
|
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
|
|
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 one primer spans the exon-exon junction (report which one it is). 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).",
|
|
93
93
|
"timeout_ms": 300000,
|
|
94
94
|
"expect_tools": [
|
|
95
95
|
"molbio_design_intron_primers"
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
"assertions": [
|
|
98
98
|
{
|
|
99
99
|
"where": "tool",
|
|
100
|
-
"pattern": "
|
|
101
|
-
"note": "the property that makes the assay genomic-DNA-proof"
|
|
100
|
+
"pattern": "REVERSE primer spans an exon-exon junction",
|
|
101
|
+
"note": "the property that makes the assay genomic-DNA-proof; since 0.23.0 the junction is carried by the reverse primer (the assay sits 5' of the junction), and the forward primer is upstream"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"where": "tool",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": [
|
|
3
|
-
"Protein analysis, bench analysis and plotting
|
|
3
|
+
"Protein analysis, bench analysis and plotting, plus the embedded-signature",
|
|
4
|
+
"contamination screen (short signatures, both strands, negative != clean).",
|
|
4
5
|
"",
|
|
5
6
|
"`reading-frames` is the one task whose point is a TOOL CHOICE rather than a",
|
|
6
7
|
"value: the model must call the translator in all-frames mode with an ORF floor,",
|
|
@@ -164,6 +165,115 @@
|
|
|
164
165
|
}
|
|
165
166
|
]
|
|
166
167
|
},
|
|
168
|
+
{
|
|
169
|
+
"id": "protein-charge",
|
|
170
|
+
"category": "protein",
|
|
171
|
+
"tier": "full",
|
|
172
|
+
"covers": [
|
|
173
|
+
"molbio_protein_charge"
|
|
174
|
+
],
|
|
175
|
+
"instruction": "I need to know how this peptide's charge changes with pH. Plot the net charge from pH 0 to 14, report the isoelectric point and the net charge at pH 7.4, and tell me which pK set the numbers come from. Write the figure to charge.svg.\n\nMKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ",
|
|
176
|
+
"timeout_ms": 240000,
|
|
177
|
+
"expect_tools": [
|
|
178
|
+
"molbio_protein_charge"
|
|
179
|
+
],
|
|
180
|
+
"assertions": [
|
|
181
|
+
{
|
|
182
|
+
"where": "tool",
|
|
183
|
+
"pattern": "33 aa · Bjellqvist 1993 pK set · pI 10\\.63 · charge at pH 7\\.4 2\\.9835 e \\(net positive\\)"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"where": "tool",
|
|
187
|
+
"pattern": "Curve over pH 0-14 \\(29 points; range -4\\.9682 \\.\\. 7\\.9996 e\\)"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"where": "answer",
|
|
191
|
+
"pattern": "10\\.6"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"where": "answer",
|
|
195
|
+
"pattern": "2\\.98|3\\.0|net positive"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"where": "answer",
|
|
199
|
+
"pattern": "Bjellqvist|pK"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "dotplot",
|
|
205
|
+
"category": "bench-analysis",
|
|
206
|
+
"tier": "full",
|
|
207
|
+
"covers": [
|
|
208
|
+
"molbio_dotplot"
|
|
209
|
+
],
|
|
210
|
+
"instruction": "I want to see the repeat structure of this 46 bp sequence. Draw a self dotplot with a 5-mer word, covering both direct and inverted repeats, and tell me how many direct and how many inverted matches it finds. Write the figure to dot.svg.\n\nTTAGGCACGTAGCTAGGCTAACGTTAGGCACGTAGCTAGGCTAACG",
|
|
211
|
+
"timeout_ms": 240000,
|
|
212
|
+
"expect_tools": [
|
|
213
|
+
"molbio_dotplot"
|
|
214
|
+
],
|
|
215
|
+
"assertions": [
|
|
216
|
+
{
|
|
217
|
+
"where": "tool",
|
|
218
|
+
"pattern": "46 bp · word 5 · 88 direct / 18 inverted match\\(es\\); 88 \\+ 18 occupied cell\\(s\\)"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"where": "tool",
|
|
222
|
+
"pattern": "Exact k-mer view \\(black = direct, red = inverted\\)"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"where": "answer",
|
|
226
|
+
"pattern": "88"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"where": "answer",
|
|
230
|
+
"pattern": "18"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"where": "answer",
|
|
234
|
+
"pattern": "direct|inverted|repeat"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"id": "contamination-screen",
|
|
240
|
+
"category": "bench-analysis",
|
|
241
|
+
"tier": "full",
|
|
242
|
+
"covers": [
|
|
243
|
+
"molbio_contamination_screen"
|
|
244
|
+
],
|
|
245
|
+
"instruction": "I have this 122 bp sequence from a miniprep and I am worried it is contaminated with vector backbone or Illumina/Nextera adapter. Screen it against common vector and adapter signatures, tell me which signatures hit and their positions, and say whether this is proof of contamination.\n\n{seq:contamination}",
|
|
246
|
+
"timeout_ms": 240000,
|
|
247
|
+
"expect_tools": [
|
|
248
|
+
"molbio_contamination_screen"
|
|
249
|
+
],
|
|
250
|
+
"assertions": [
|
|
251
|
+
{
|
|
252
|
+
"where": "tool",
|
|
253
|
+
"pattern": "sequence input: 1 of 1 entry, 122 bp; 2 signature\\(s\\) hit, 1 entry with a hit\\."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"where": "tool",
|
|
257
|
+
"pattern": "Illumina Universal \\[adapter/adapter\\] 1/1 \\(100%\\), best 0 mismatch\\(es\\); e\\.g\\. input 111-122 \\(\\+\\)"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"where": "tool",
|
|
261
|
+
"pattern": "pUC/pBR322 origin \\[vector/origin\\] 1/1 \\(100%\\), best 0 mismatch\\(es\\); e\\.g\\. input 1-30 \\(\\+\\)"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"where": "answer",
|
|
265
|
+
"pattern": "pUC|pBR322|vector"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"where": "answer",
|
|
269
|
+
"pattern": "Illumina|adapter"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"where": "answer",
|
|
273
|
+
"pattern": "not proof|short signature|does not prove|suggest|consistent"
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
},
|
|
167
277
|
{
|
|
168
278
|
"id": "codon-optimize",
|
|
169
279
|
"category": "bench-analysis",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": [
|
|
3
|
-
"Codon usage, phylogeny, and the two file-parsing tools.",
|
|
3
|
+
"Codon usage, phylogeny, FASTQ trimming, and the two file-parsing tools.",
|
|
4
|
+
"",
|
|
5
|
+
"`fastq-trim` belongs here rather than under visualisation: it reads a FASTQ,",
|
|
6
|
+
"transforms every record, and writes a FASTQ/FASTA back. The task pins the",
|
|
7
|
+
"before/after counts and the per-stage trim reasons, not just 'it ran'.",
|
|
4
8
|
"",
|
|
5
9
|
"The parsing tools appear here as a CHAIN rather than alone: a model that",
|
|
6
10
|
"answers a question about a GenBank record without parsing it is doing",
|
|
@@ -128,6 +132,45 @@
|
|
|
128
132
|
}
|
|
129
133
|
]
|
|
130
134
|
},
|
|
135
|
+
{
|
|
136
|
+
"id": "fastq-trim",
|
|
137
|
+
"category": "sequence-io",
|
|
138
|
+
"tier": "full",
|
|
139
|
+
"covers": [
|
|
140
|
+
"molbio_fastq_trim"
|
|
141
|
+
],
|
|
142
|
+
"instruction": "These four reads are from an old Illumina run: r1 has a low-quality 3' tail, r2 carries the Illumina Universal adapter, r3 has a poly-G tail, and r4 is very short. Trim the low-quality tail with a 3-base window at mean Q30, remove the adapter and everything after it, trim poly-G runs of 8 or more, drop reads shorter than 20 bp, and write the survivors to trimmed.fq. Tell me how many reads and bases went in and out, how many were trimmed or dropped, and why.\n",
|
|
143
|
+
"timeout_ms": 240000,
|
|
144
|
+
"expect_tools": [
|
|
145
|
+
"molbio_fastq_trim"
|
|
146
|
+
],
|
|
147
|
+
"assertions": [
|
|
148
|
+
{
|
|
149
|
+
"where": "tool",
|
|
150
|
+
"pattern": "FASTQ trim: 4 of 4 read\\(s\\) scanned \\(127 bases\\) -> 3 read\\(s\\) \\(80 bases\\); 3 trimmed, 1 dropped\\."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"where": "tool",
|
|
154
|
+
"pattern": "quality 1 · adapter 1 · poly-G 1 · poly-A 0 · primer 0; dropped by length 1, by N 0"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"where": "tool",
|
|
158
|
+
"pattern": "Saved FASTQ to .*trimmed\\.fq\\."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"where": "answer",
|
|
162
|
+
"pattern": "3 .{0,20}(read|kept|surviv)|(kept|surviv).{0,20}3"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"where": "answer",
|
|
166
|
+
"pattern": "80"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"where": "answer",
|
|
170
|
+
"pattern": "adapter|poly-G|quality|drop"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
},
|
|
131
174
|
{
|
|
132
175
|
"id": "batch-analysis",
|
|
133
176
|
"category": "formats",
|
|
@@ -74,6 +74,7 @@ const FILE_SETUPS = {
|
|
|
74
74
|
'chart-volcano': { 'de.csv': FIXTURES.DE_CSV },
|
|
75
75
|
'chart-heatmap': { 'expression.tsv': FIXTURES.HEATMAP_TSV },
|
|
76
76
|
'batch-analysis': { 'pUC118.gb': GENBANK_TEXT },
|
|
77
|
+
'fastq-trim': { 'reads.fq': FIXTURES.TRIM_FASTQ_TEXT },
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
/**
|
|
@@ -252,6 +253,18 @@ export function invocationsFor(task) {
|
|
|
252
253
|
'peptide-digest': () => [{ tool: 'molbio_peptide_digest', args: { sequence: FIXTURES.DIGEST_PEPTIDE, enzyme: 'trypsin', missed: 0 } }],
|
|
253
254
|
'helical-wheel': () => [{ tool: 'molbio_helical_wheel', args: { sequence: FIXTURES.AMPHIPATHIC_PEPTIDE, title: 'amphipathic test' } }],
|
|
254
255
|
hydropathy: () => [{ tool: 'molbio_hydropathy_plot', args: { sequence: FIXTURES.DIGEST_PEPTIDE } }],
|
|
256
|
+
'protein-charge': () => [
|
|
257
|
+
{ tool: 'molbio_protein_charge', args: { sequence: FIXTURES.DIGEST_PEPTIDE, output_path: at('charge.svg') } },
|
|
258
|
+
],
|
|
259
|
+
dotplot: () => [
|
|
260
|
+
{ tool: 'molbio_dotplot', args: { sequence: FIXTURES.DOTPLOT_SEQUENCE, word: 5, output_path: at('dot.svg') } },
|
|
261
|
+
],
|
|
262
|
+
'contamination-screen': () => [
|
|
263
|
+
{ tool: 'molbio_contamination_screen', args: { sequence: FIXTURES.CONTAMINATION_SEQUENCE } },
|
|
264
|
+
],
|
|
265
|
+
'fastq-trim': () => [
|
|
266
|
+
{ tool: 'molbio_fastq_trim', args: { path: at('reads.fq'), output_path: at('trimmed.fq'), min_quality: 30, quality_window: 3, poly_g: true, adapter: 'AGATCGGAAGAG', min_length: 20 } },
|
|
267
|
+
],
|
|
255
268
|
|
|
256
269
|
// bench analysis
|
|
257
270
|
'codon-optimize': () => [{ tool: 'molbio_codon_optimize', args: { sequence: FIXTURES.CDS, host: 'e_coli' } }],
|
package/build/client-bundle.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const checkOnly = process.argv.slice(2).includes('--check');
|
|
|
40
40
|
/**
|
|
41
41
|
* The bundles to emit. The SAME browser half ships through two channels:
|
|
42
42
|
*
|
|
43
|
-
* - `dsh-molbio-tools` — the plugin package: one install gives the
|
|
43
|
+
* - `dsh-molbio-tools` — the plugin package: one install gives the 64 tools
|
|
44
44
|
* plus the panel (its host half also registers the tools).
|
|
45
45
|
* - `dsh-molbio-panel` — a panel-only package: installing it adds the tab to a
|
|
46
46
|
* profile WITHOUT dragging the tools into every session of that profile,
|
package/build/client-entry.mjs
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
childPath,
|
|
37
37
|
classifyEntry,
|
|
38
38
|
credentialInfo,
|
|
39
|
+
figureCardSummary,
|
|
40
|
+
figureCardView,
|
|
39
41
|
isMolbioDetailSubject,
|
|
40
42
|
remoteFailure,
|
|
41
43
|
decodeWorkspaceBytes,
|
|
@@ -294,8 +296,10 @@ function papersTitle() {
|
|
|
294
296
|
return 'Papers';
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
/** The tool names whose transcript card this package draws. */
|
|
299
|
+
/** The tool names whose transcript card this package draws (plasmid maps). */
|
|
298
300
|
const MAP_TOOL_KEYS = ['molbio_plasmid_map', 'molbio_plasmid_map_file'];
|
|
301
|
+
/** Figure tools: a drawn logo / gRNA map plus (for gRNA) the ranked list. */
|
|
302
|
+
const FIGURE_TOOL_KEYS = ['molbio_sequence_logo', 'molbio_grna_design'];
|
|
299
303
|
|
|
300
304
|
/**
|
|
301
305
|
* The transcript card for the map tools (`tool.call.toolview`, keyed by tool
|
|
@@ -364,6 +368,72 @@ function MolbioMapCard({ block, inspect }) {
|
|
|
364
368
|
return h('div', { style: styles.card }, ...content);
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
/**
|
|
372
|
+
* The transcript card for the sequence-logo and gRNA tools. The logo card draws
|
|
373
|
+
* the logo; the gRNA card draws the map when `map_path` was requested AND lists
|
|
374
|
+
* the top-ranked guides (a gRNA call without a map still gets the table). Like
|
|
375
|
+
* the map card it is a pure function of the block, and everything it reads is
|
|
376
|
+
* validated and bounded by `figureCardView`.
|
|
377
|
+
*/
|
|
378
|
+
function MolbioFigureCard({ block, inspect }) {
|
|
379
|
+
const settled = block !== null && typeof block === 'object' && block.kind === 'tool-result';
|
|
380
|
+
const view = figureCardView(settled ? block.meta : undefined);
|
|
381
|
+
const host = useRef(null);
|
|
382
|
+
const svg = view.kind === 'figure' ? view.svg : '';
|
|
383
|
+
const [drawError, setDrawError] = useState(null);
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
const node = host.current;
|
|
387
|
+
if (node === null) return;
|
|
388
|
+
node.replaceChildren();
|
|
389
|
+
if (svg === '') return;
|
|
390
|
+
const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
391
|
+
if (parsed.querySelector('parsererror') !== null) {
|
|
392
|
+
setDrawError('the figure markup could not be parsed as SVG');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
setDrawError(null);
|
|
396
|
+
const element = parsed.documentElement;
|
|
397
|
+
element.setAttribute('style', 'width:100%;height:auto;max-width:640px');
|
|
398
|
+
node.append(document.importNode(element, true));
|
|
399
|
+
}, [svg]);
|
|
400
|
+
|
|
401
|
+
const content = [h('div', { key: 'head', style: styles.cardHead }, settled ? figureCardSummary(view) : 'Drawing the figure…')];
|
|
402
|
+
if (view.kind === 'figure') {
|
|
403
|
+
if (drawError !== null) content.push(h('div', { key: 'drawerr', style: styles.cardNote }, drawError));
|
|
404
|
+
content.push(h('div', { key: 'svg', ref: host, style: styles.cardSvg }));
|
|
405
|
+
} else {
|
|
406
|
+
content.push(h('div', { key: 'note', style: styles.cardNote }, view.message));
|
|
407
|
+
}
|
|
408
|
+
if (view.guides.length > 0) {
|
|
409
|
+
const head = ['#', 'Guide', 'PAM', 'Position', 'Score', 'Off-targets'];
|
|
410
|
+
const rows = view.guides.map((guide) => h('tr', { key: guide.rank },
|
|
411
|
+
h('td', { style: styles.td }, String(guide.rank)),
|
|
412
|
+
h('td', { style: styles.td }, guide.sequence),
|
|
413
|
+
h('td', { style: styles.td }, guide.pam === '' ? '—' : guide.pam),
|
|
414
|
+
h('td', { style: styles.td }, `${guide.strand === 'reverse' ? '−' : '+'}${guide.start}-${guide.end}`),
|
|
415
|
+
h('td', { style: styles.td }, String(guide.score)),
|
|
416
|
+
h('td', { style: styles.td }, String(guide.offTargets)),
|
|
417
|
+
));
|
|
418
|
+
content.push(h('table', { key: 'guides', style: styles.table },
|
|
419
|
+
h('thead', null, h('tr', null, ...head.map((label) => h('th', { key: label, style: styles.th }, label)))),
|
|
420
|
+
h('tbody', null, ...rows)));
|
|
421
|
+
}
|
|
422
|
+
if (view.svgPath !== '') content.push(h('div', { key: 'path', style: styles.cardPath }, view.svgPath));
|
|
423
|
+
if (view.csvPath !== '') content.push(h('div', { key: 'csv', style: styles.cardPath }, view.csvPath));
|
|
424
|
+
if (settled && block.isError === true) {
|
|
425
|
+
const text = (Array.isArray(block.content) ? block.content : [])
|
|
426
|
+
.map((item) => (item !== null && typeof item === 'object' && item.type === 'text' ? String(item.text ?? '') : ''))
|
|
427
|
+
.join('\n')
|
|
428
|
+
.trim();
|
|
429
|
+
content.push(h('div', { key: 'err', style: styles.error }, text === '' ? 'The call failed.' : text));
|
|
430
|
+
if (typeof inspect === 'function') {
|
|
431
|
+
content.push(h('button', { key: 'inspect', style: styles.cardButton, onClick: () => inspect() }, 'Inspect'));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return h('div', { style: styles.card }, ...content);
|
|
435
|
+
}
|
|
436
|
+
|
|
367
437
|
/**
|
|
368
438
|
* The literature panel: the workspace `papers.json` the molbio_paper_* tools
|
|
369
439
|
* maintain, rendered as a searchable reading list with a detail pane.
|
|
@@ -712,6 +782,15 @@ function apply(ctx) {
|
|
|
712
782
|
key: toolName,
|
|
713
783
|
}, MolbioMapCard)), `molbio panel: ${toolName} card`);
|
|
714
784
|
}
|
|
785
|
+
// Figure cards: the sequence-logo and gRNA tools declare a `molbio-figure`
|
|
786
|
+
// projection; the same component draws a logo or a gRNA map and, for gRNA,
|
|
787
|
+
// lists the top-ranked guides even when no map was requested.
|
|
788
|
+
for (const toolName of FIGURE_TOOL_KEYS) {
|
|
789
|
+
ctx.effect(() => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
790
|
+
name: 'tool.call.toolview',
|
|
791
|
+
key: toolName,
|
|
792
|
+
}, MolbioFigureCard)), `molbio panel: ${toolName} card`);
|
|
793
|
+
}
|
|
715
794
|
// Plugins -> the installed plugin's detail page: the one place a user can
|
|
716
795
|
// store the NCBI key without editing a YAML file or exporting a variable.
|
|
717
796
|
//
|
package/build/panel-core.mjs
CHANGED
|
@@ -268,6 +268,91 @@ export function mapCardSummary(view) {
|
|
|
268
268
|
return parts.join(' · ');
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// ── figure cards (the same `tool.call.toolview` seat) ───────────────────────
|
|
272
|
+
|
|
273
|
+
/** The meta kind the sequence-logo and gRNA tools attach to their results. */
|
|
274
|
+
export const FIGURE_CARD_KIND = 'molbio-figure';
|
|
275
|
+
|
|
276
|
+
const CARD_SUMMARY_MAX = 300;
|
|
277
|
+
const CARD_PATH_MAX = 500;
|
|
278
|
+
const CARD_GUIDES_MAX = 5;
|
|
279
|
+
|
|
280
|
+
/** A bounded string, or '' when the value is not a string. */
|
|
281
|
+
function cardText(value, max) {
|
|
282
|
+
return typeof value === 'string' ? value.slice(0, max) : '';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** One guide row, validated field by field; malformed rows are dropped. */
|
|
286
|
+
function cardGuide(row) {
|
|
287
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return undefined;
|
|
288
|
+
const sequence = cardText(row.sequence, 60);
|
|
289
|
+
if (sequence === '') return undefined;
|
|
290
|
+
return {
|
|
291
|
+
rank: Number.isInteger(row.rank) ? row.rank : 0,
|
|
292
|
+
sequence,
|
|
293
|
+
pam: cardText(row.pam, 12),
|
|
294
|
+
strand: row.strand === 'reverse' ? 'reverse' : 'forward',
|
|
295
|
+
start: Number.isInteger(row.start) ? row.start : 0,
|
|
296
|
+
end: Number.isInteger(row.end) ? row.end : 0,
|
|
297
|
+
score: typeof row.score === 'number' && Number.isFinite(row.score) ? row.score : 0,
|
|
298
|
+
offTargets: Number.isInteger(row.off_target_count) ? row.off_target_count : 0,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Read a tool-result block's meta into what the figure card draws.
|
|
304
|
+
*
|
|
305
|
+
* Like `mapCardView`, the value is a projection this package produced itself,
|
|
306
|
+
* but the card renders inside the conversation, so every field is checked and
|
|
307
|
+
* bounded rather than trusted: a block from an older version, another tool, a
|
|
308
|
+
* running call or a failed run must degrade to a notice, never throw.
|
|
309
|
+
*
|
|
310
|
+
* @param {unknown} meta the block's meta, as the client received it.
|
|
311
|
+
* @returns {{kind: 'notice', message: string, ...} | {kind: 'figure', figure:
|
|
312
|
+
* 'logo'|'grna', summary: string, svg: string, svgPath: string,
|
|
313
|
+
* csvPath: string, mapPath: string, guides: Array}}
|
|
314
|
+
*/
|
|
315
|
+
export function figureCardView(meta) {
|
|
316
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
317
|
+
return { kind: 'notice', message: 'No figure data was attached to this call.' };
|
|
318
|
+
}
|
|
319
|
+
if (meta.kind !== FIGURE_CARD_KIND) {
|
|
320
|
+
return { kind: 'notice', message: 'This call carries no figure.' };
|
|
321
|
+
}
|
|
322
|
+
const figure = meta.figure === 'grna' ? 'grna' : meta.figure === 'logo' ? 'logo' : '';
|
|
323
|
+
if (figure === '') {
|
|
324
|
+
return { kind: 'notice', message: 'This call carries an unknown figure type.' };
|
|
325
|
+
}
|
|
326
|
+
const base = {
|
|
327
|
+
figure,
|
|
328
|
+
summary: cardText(meta.summary, CARD_SUMMARY_MAX),
|
|
329
|
+
svgPath: cardText(meta.svg_path, CARD_PATH_MAX),
|
|
330
|
+
csvPath: cardText(meta.csv_path, CARD_PATH_MAX),
|
|
331
|
+
mapPath: cardText(meta.map_path, CARD_PATH_MAX),
|
|
332
|
+
guides: Array.isArray(meta.guides)
|
|
333
|
+
// Bound the work before validating, then keep at most five VALID rows:
|
|
334
|
+
// malformed entries must not consume the budget and hide good ones.
|
|
335
|
+
? meta.guides.slice(0, 50).map(cardGuide).filter((guide) => guide !== undefined).slice(0, CARD_GUIDES_MAX)
|
|
336
|
+
: [],
|
|
337
|
+
svg: '',
|
|
338
|
+
};
|
|
339
|
+
if (!isSvgMarkup(meta.svg)) {
|
|
340
|
+
const message = meta.svg_missing === true
|
|
341
|
+
? 'No picture was requested for this call; the numbers are in the tool result.'
|
|
342
|
+
: meta.svg_omitted === true
|
|
343
|
+
? `The figure is too large to draw inline (${Math.round((Number.isInteger(meta.svg_bytes) ? meta.svg_bytes : 0) / 1024)} KB); the written file is named below.`
|
|
344
|
+
: 'The figure markup did not travel with this call.';
|
|
345
|
+
return { kind: 'notice', message, ...base };
|
|
346
|
+
}
|
|
347
|
+
return { kind: 'figure', ...base, svg: meta.svg };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The header line for a figure card, with a stable fallback. */
|
|
351
|
+
export function figureCardSummary(view) {
|
|
352
|
+
if (view.summary !== '') return view.summary;
|
|
353
|
+
return view.figure === 'logo' ? 'Sequence logo' : 'gRNA design';
|
|
354
|
+
}
|
|
355
|
+
|
|
271
356
|
// ── literature library (papers.json) ────────────────────────────────────────
|
|
272
357
|
|
|
273
358
|
/** The library file the molbio_paper_* tools write, relative to the workspace. */
|
package/build/preset-patch.mjs
CHANGED
|
@@ -101,7 +101,7 @@ const ANCHOR = [
|
|
|
101
101
|
'#',
|
|
102
102
|
'# Keep the tool in the PRESET and only the anchor here. An earlier version of',
|
|
103
103
|
"# this file inserted a host row for the tool itself (`name: dsh-molbio-tools`),",
|
|
104
|
-
"# which both registered
|
|
104
|
+
"# which both registered every tool globally and collided with the preset's own",
|
|
105
105
|
'# copy of that row:',
|
|
106
106
|
'#',
|
|
107
107
|
'# broken: "tool-molbio (...): never started"',
|
|
@@ -41,7 +41,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
41
41
|
* The two npm entries this repository publishes, in publish order.
|
|
42
42
|
*
|
|
43
43
|
* THEY DO NOT SHARE A VERSION LINE, and that is deliberate rather than an
|
|
44
|
-
* oversight to be "fixed" here. The root package carries the
|
|
44
|
+
* oversight to be "fixed" here. The root package carries the 64 tools and moves
|
|
45
45
|
* with every toolset release; `dsh-molbio-panel` carries only the browser panel
|
|
46
46
|
* and moves when the panel's own artifact moves. The tag names the ROOT version
|
|
47
47
|
* (`v0.19.0`), and each entry is checked against its own declared version —
|
package/charts.mjs
CHANGED
|
@@ -60,6 +60,25 @@ import {
|
|
|
60
60
|
textRun,
|
|
61
61
|
} from './svgio.mjs';
|
|
62
62
|
|
|
63
|
+
// ── data-sized min/max ──────────────────────────────────────────────────────
|
|
64
|
+
//
|
|
65
|
+
// A table this module accepts can hold MAX_ROWS (200 000) values. Spreading an
|
|
66
|
+
// array that large into Math.min/Math.max overflows the engine's argument limit
|
|
67
|
+
// with a RangeError, so the data-sized extrema are computed with loops. Fixed-
|
|
68
|
+
// size arrays (bins, colour stops, density grids) still use the spread form.
|
|
69
|
+
|
|
70
|
+
function minData(values) {
|
|
71
|
+
let min = Infinity;
|
|
72
|
+
for (const value of values) if (value < min) min = value;
|
|
73
|
+
return min;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function maxData(values) {
|
|
77
|
+
let max = -Infinity;
|
|
78
|
+
for (const value of values) if (value > max) max = value;
|
|
79
|
+
return max;
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
// ── canvas ──────────────────────────────────────────────────────────────────
|
|
64
83
|
|
|
65
84
|
export const CHART_WIDTH = 720;
|
|
@@ -130,8 +149,8 @@ export function histogramBins(values, requestedBins) {
|
|
|
130
149
|
const finite = values.filter((value) => Number.isFinite(value));
|
|
131
150
|
if (finite.length === 0) throw new MolbioInputError('a histogram needs at least one finite value');
|
|
132
151
|
const n = finite.length;
|
|
133
|
-
const min =
|
|
134
|
-
const max =
|
|
152
|
+
const min = minData(finite);
|
|
153
|
+
const max = maxData(finite);
|
|
135
154
|
let binCount;
|
|
136
155
|
let rule;
|
|
137
156
|
if (requestedBins !== undefined) {
|
|
@@ -634,9 +653,9 @@ export function renderBoxPlot({ title, x_label, y_label, series }) {
|
|
|
634
653
|
const stats = boxStats(validateFinite(entry.values, `series ${index + 1} values`));
|
|
635
654
|
return { label, stats, color: entry.color ?? SERIES_COLORS[index % SERIES_COLORS.length] };
|
|
636
655
|
});
|
|
637
|
-
const lows = prepared.map((entry) => Math.min(entry.stats.whisker_low,
|
|
638
|
-
const highs = prepared.map((entry) => Math.max(entry.stats.whisker_high,
|
|
639
|
-
const [domainMin, domainMax] = padDomain(
|
|
656
|
+
const lows = prepared.map((entry) => Math.min(entry.stats.whisker_low, minData(entry.stats.outliers), entry.stats.min));
|
|
657
|
+
const highs = prepared.map((entry) => Math.max(entry.stats.whisker_high, maxData(entry.stats.outliers), entry.stats.max));
|
|
658
|
+
const [domainMin, domainMax] = padDomain(minData(lows), maxData(highs));
|
|
640
659
|
const { parts, plot } = frame(title);
|
|
641
660
|
const y = yAxis(parts, plot, domainMin, domainMax, y_label);
|
|
642
661
|
const slot = plot.width / prepared.length;
|
|
@@ -695,8 +714,8 @@ export function renderLineChart({ title, x_label, y_label, series, markers = tru
|
|
|
695
714
|
});
|
|
696
715
|
const allX = prepared.flatMap((entry) => entry.xs);
|
|
697
716
|
const allY = prepared.flatMap((entry) => entry.ys);
|
|
698
|
-
const [xMin, xMax] = padDomain(
|
|
699
|
-
const [yMin, yMax] = padDomain(
|
|
717
|
+
const [xMin, xMax] = padDomain(minData(allX), maxData(allX));
|
|
718
|
+
const [yMin, yMax] = padDomain(minData(allY), maxData(allY));
|
|
700
719
|
const { parts, plot } = frame(title);
|
|
701
720
|
const y = yAxis(parts, plot, yMin, yMax, y_label);
|
|
702
721
|
const x = linearScale([xMin, xMax], [plot.x, plot.x + plot.width]);
|
|
@@ -719,11 +738,11 @@ export function renderLineChart({ title, x_label, y_label, series, markers = tru
|
|
|
719
738
|
return {
|
|
720
739
|
svg,
|
|
721
740
|
stats: {
|
|
722
|
-
series: prepared.map((entry) => ({ label: entry.label, points: entry.xs.length, x_min:
|
|
741
|
+
series: prepared.map((entry) => ({ label: entry.label, points: entry.xs.length, x_min: minData(entry.xs), x_max: maxData(entry.xs), y_min: minData(entry.ys), y_max: maxData(entry.ys) })),
|
|
723
742
|
series_count: prepared.length,
|
|
724
743
|
point_count: prepared.reduce((sum, entry) => sum + entry.xs.length, 0),
|
|
725
|
-
x_range: [
|
|
726
|
-
y_range: [
|
|
744
|
+
x_range: [minData(allX), maxData(allX)],
|
|
745
|
+
y_range: [minData(allY), maxData(allY)],
|
|
727
746
|
},
|
|
728
747
|
marks: prepared.reduce((sum, entry) => sum + entry.xs.length, 0),
|
|
729
748
|
plot,
|
|
@@ -815,7 +834,7 @@ export function renderViolinPlot({ title, x_label, y_label, series, bandwidth, s
|
|
|
815
834
|
});
|
|
816
835
|
const lows = prepared.map((entry) => entry.stats.min);
|
|
817
836
|
const highs = prepared.map((entry) => entry.stats.max);
|
|
818
|
-
const [domainMin, domainMax] = padDomain(
|
|
837
|
+
const [domainMin, domainMax] = padDomain(minData(lows), maxData(highs));
|
|
819
838
|
const { parts, plot } = frame(title);
|
|
820
839
|
const y = yAxis(parts, plot, domainMin, domainMax, y_label);
|
|
821
840
|
const slot = plot.width / prepared.length;
|
|
@@ -890,8 +909,8 @@ export function renderVolcanoPlot({ title, x_label, y_label, points, fold_change
|
|
|
890
909
|
const significant = (point) => Math.abs(point.fold) >= fold_change_threshold && point.p <= p_threshold;
|
|
891
910
|
const xs = prepared.map((point) => point.fold);
|
|
892
911
|
const ys = prepared.map((point) => point.significance);
|
|
893
|
-
const [xMin, xMax] = padDomain(Math.min(0,
|
|
894
|
-
const [yMin, yMax] = padDomain(0, Math.max(
|
|
912
|
+
const [xMin, xMax] = padDomain(Math.min(0, minData(xs)), Math.max(0, maxData(xs)));
|
|
913
|
+
const [yMin, yMax] = padDomain(0, Math.max(maxData(ys), -Math.log10(p_threshold)));
|
|
895
914
|
const { parts, plot } = frame(title);
|
|
896
915
|
const y = yAxis(parts, plot, yMin, yMax, y_label ?? '-log10 p');
|
|
897
916
|
const x = linearScale([xMin, xMax], [plot.x, plot.x + plot.width]);
|
|
@@ -936,7 +955,7 @@ export function renderVolcanoPlot({ title, x_label, y_label, points, fold_change
|
|
|
936
955
|
down_count: prepared.filter((point) => significant(point) && point.fold < 0).length,
|
|
937
956
|
fold_change_threshold,
|
|
938
957
|
p_threshold,
|
|
939
|
-
max_significance:
|
|
958
|
+
max_significance: maxData(ys),
|
|
940
959
|
labelled: [...labelled],
|
|
941
960
|
},
|
|
942
961
|
marks: prepared.length,
|
|
@@ -1000,8 +1019,8 @@ export function renderHeatmap({ title, x_label, y_label, rows, columns, matrix,
|
|
|
1000
1019
|
}
|
|
1001
1020
|
const flat = values.flat().filter((value) => value !== null);
|
|
1002
1021
|
if (flat.length === 0) throw new MolbioInputError('the matrix has no finite values to draw');
|
|
1003
|
-
const min =
|
|
1004
|
-
const max =
|
|
1022
|
+
const min = minData(flat);
|
|
1023
|
+
const max = maxData(flat);
|
|
1005
1024
|
const ramp = color_scale ?? (min < 0 ? 'divergingRedBlue' : 'viridis');
|
|
1006
1025
|
if (COLOR_RAMPS[ramp] === undefined) {
|
|
1007
1026
|
throw new MolbioInputError(`unknown color_scale ${JSON.stringify(color_scale)}; use ${Object.keys(COLOR_RAMPS).join(', ')}`);
|