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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-molbio-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-dependency molecular-biology plugin bundle for the DeepSeek Harness: sequence analysis, primer design (incl. cross-intron qPCR primers, TaqMan hydrolysis/MGB probes with ordering CSV and multiplex compatibility), in-silico PCR, cloning simulation, SnapGene .dna parsing and plasmid maps, Sanger verification, protein tools (incl. helical wheel and
|
|
3
|
+
"version": "0.24.0",
|
|
4
|
+
"description": "Zero-dependency molecular-biology plugin bundle for the DeepSeek Harness: sequence analysis, primer design (incl. cross-intron qPCR primers, TaqMan hydrolysis/MGB probes with ordering CSV and multiplex compatibility), in-silico PCR, cloning simulation, SnapGene .dna parsing and plasmid maps, Sanger verification, protein tools (incl. helical wheel, hydropathy and charge-pH plots), methylation-aware restriction analysis with double-digest buffer checks, qPCR analysis and plotting, multiple sequence alignment that conserves every input residue, conservation analysis, sequence logos, codon-usage analysis, phylogenetic trees with bootstrap support, Newick I/O and long-name label wrapping, CpG-island/GC-skew composition analysis, k-mer dotplots, read-level FASTQ QC, vector/adapter contamination screening, FASTQ trimming/filtering, batch file analysis with CSV export, silent restriction-site mutagenesis, family-aware CRISPR gRNA design (SpCas9/SaCas9/Cas12a/Cas13) with off-target search, an opt-in hand-off that shows a generated plot to the model as an attached image, plus a browser-side right-sidebar panel (plasmid maps and sequence logos drawn in the Web GUI) and a literature/protocol/experiment record system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"engines": {
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"sanger.mjs",
|
|
60
60
|
"protein.mjs",
|
|
61
61
|
"protein-structure.mjs",
|
|
62
|
+
"protein-charge.mjs",
|
|
63
|
+
"dotplot.mjs",
|
|
64
|
+
"contamination.mjs",
|
|
65
|
+
"fastq-trim.mjs",
|
|
62
66
|
"taqman.mjs",
|
|
63
67
|
"multiplex.mjs",
|
|
64
68
|
"methylation.mjs",
|
|
@@ -98,7 +102,8 @@
|
|
|
98
102
|
"0.2.1-rc.2": "compatible",
|
|
99
103
|
"0.2.1-rc.3": "compatible",
|
|
100
104
|
"0.2.1-rc.4": "unknown",
|
|
101
|
-
"0.2.1-rc.5": "compatible"
|
|
105
|
+
"0.2.1-rc.5": "compatible",
|
|
106
|
+
"0.2.2-rc.1": "compatible"
|
|
102
107
|
}
|
|
103
108
|
},
|
|
104
109
|
"bundle": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
#
|
|
18
18
|
# Edit THIS file, then run `node build/preset-patch.mjs`.
|
|
19
19
|
#
|
|
20
|
-
# NOTE: the mode is the ONLY place these
|
|
20
|
+
# NOTE: the mode is the ONLY place these 64 tools load. The same bundle patch's
|
|
21
21
|
# other insert (`molbio-client` → `./host.mjs`) deliberately registers nothing —
|
|
22
22
|
# registering the tool at the host level as well would put it in every session
|
|
23
23
|
# and is not what this package is for.
|
|
@@ -48,18 +48,20 @@
|
|
|
48
48
|
# list carries no comments of its own, so none appear below; provenance and the
|
|
49
49
|
# upgrade rules live in this header.
|
|
50
50
|
#
|
|
51
|
-
# Baseline: the `standard` preset of dsh 0.2.
|
|
51
|
+
# Baseline: the `standard` preset of dsh 0.2.2-rc.1 (harness root
|
|
52
52
|
# `%APPDATA%\npm\node_modules\@deepseek-ai\dsh`, file
|
|
53
53
|
# `node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml`, whose
|
|
54
54
|
# rows live under the `preset-standard` row's `config.plugins:`).
|
|
55
55
|
#
|
|
56
|
-
# 0.2.
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# `tool-subagent
|
|
61
|
-
#
|
|
62
|
-
#
|
|
56
|
+
# 0.2.2-rc.1 is the FIRST row change since 0.2.1-rc.2 dropped the optional
|
|
57
|
+
# subagent providers and `tool-ralph`. It folded `list_agents` into
|
|
58
|
+
# `@deepseek-ai/dsh-tool-subagent-control` (the `./list-agents` subpath export is
|
|
59
|
+
# gone, so the `tool-subagent-list-agents` row was dropped here), renamed the
|
|
60
|
+
# `tool-subagent` model-selection settings to `modelSelection` +
|
|
61
|
+
# `modelDiscovery`, and made `applyPatch: false` explicit on `tool-fs`. A row
|
|
62
|
+
# that names a deleted package subpath still PASSES a directory-existence check:
|
|
63
|
+
# `test/preset-health.mjs` now resolves the specifier the way the Loader does.
|
|
64
|
+
# Absorb such a bump verbatim — see workflow.md §4.
|
|
63
65
|
#
|
|
64
66
|
# Keeping the text verbatim is not cosmetic: a preset mounts DSH's OWN
|
|
65
67
|
# packages, so upstream drift here kills the whole preset at mount (the failure
|
|
@@ -100,6 +102,11 @@
|
|
|
100
102
|
disabled: !!js process.platform !== 'win32'
|
|
101
103
|
- id: tool-fs
|
|
102
104
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
105
|
+
config:
|
|
106
|
+
# Set false to drop the apply_patch tool beside edit. (0.2.2-rc.1 makes this
|
|
107
|
+
# explicit in the shipped standard preset; the package default is false, and
|
|
108
|
+
# the row is kept verbatim so the two stay comparable.)
|
|
109
|
+
applyPatch: false
|
|
103
110
|
- id: tool-fs-search
|
|
104
111
|
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
105
112
|
config:
|
|
@@ -162,14 +169,13 @@
|
|
|
162
169
|
config:
|
|
163
170
|
- id: tool-subagent-control
|
|
164
171
|
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
165
|
-
- id: tool-subagent-list-agents
|
|
166
|
-
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
167
172
|
- id: tool-subagent
|
|
168
173
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
169
174
|
config:
|
|
170
175
|
provider: spawn
|
|
171
176
|
toolName: subagent
|
|
172
|
-
|
|
177
|
+
modelSelection: true
|
|
178
|
+
modelDiscovery: true
|
|
173
179
|
- id: tool-subagent-fork
|
|
174
180
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
175
181
|
config:
|
|
@@ -201,7 +207,7 @@
|
|
|
201
207
|
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
202
208
|
disabled: true
|
|
203
209
|
|
|
204
|
-
# dsh-molbio-tools: molecular-biology toolset (
|
|
210
|
+
# dsh-molbio-tools: molecular-biology toolset (64 tools across sequence
|
|
205
211
|
# analysis, primers (incl. TaqMan probes and multiplex compatibility),
|
|
206
212
|
# in-silico PCR, cloning (incl. Golden Gate), enzyme catalog, methylation-aware
|
|
207
213
|
# and double digests, plasmids, Sanger verification, proteins (incl. helical
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
name: Molecular Biology Lab
|
|
2
|
-
description: 分子生物学专属模式:标准编码能力 +
|
|
2
|
+
description: 分子生物学专属模式:标准编码能力 + 64 个 molbio_* 工具(序列分析、引物设计(含跨内含子 qPCR、盐/浓度旋钮与目标位置偏好)、TaqMan 水解探针(含 MGB 与订购 CSV)与多重 PCR 互扰/退火温度/浓度建议、克隆模拟与选酶(含 Golden Gate)、酶目录查询、甲基化敏感位点与双酶切 buffer 兼容检查、多序列比对(每个输入残基都会进比对结果)、保守性分析与比对后处理(共识/缺口修剪/同一性矩阵/逐列覆盖度,含 sequence logo 图)、CRISPR gRNA 设计(SpCas9/SaCas9/Cas12a/Cas13,家族特定 PAM/PFS/种子)与脱靶搜索、SnapGene/GenBank 解析与质粒图谱、Sanger 测序验证、蛋白性质/电荷-pH 曲线/密码子优化/螺旋轮投影与疏水性图、qPCR 与绘图、虚拟凝胶、实验台分析(FASTQ 质控、密码子使用、系统发生树(长叶名自动折行)、in-silico PCR、CpG/GC 组成、序列 dotplot、载体/接头污染筛查、FASTQ 修剪/过滤、批量文件分析/CSV)、静默酶切位点突变设计——生成的 SVG 图片自动用系统默认应用打开,文献/协议/实验记录)。
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-molbio-tools/protein-charge.mjs
|
|
3
|
+
*
|
|
4
|
+
* Net-charge-vs-pH titration for a protein, with the pI found on the same
|
|
5
|
+
* curve. Pure calculation plus an SVG renderer: no external data, no services.
|
|
6
|
+
*
|
|
7
|
+
* The pK tables live in `protein.mjs` (PK_SETS) so `molbio_protein_props` and
|
|
8
|
+
* this tool can never disagree about the default set. `emboss` is offered as an
|
|
9
|
+
* explicitly named alternative; every result carries the set it used, because a
|
|
10
|
+
* pI is only meaningful together with its pK values.
|
|
11
|
+
*/
|
|
12
|
+
import { MolbioInputError } from './lib.mjs';
|
|
13
|
+
import { PK_SETS, countAminoAcids, isoelectricPoint, netCharge, normalizeProtein } from './protein.mjs';
|
|
14
|
+
import { axis, linearScale, niceTicks, panel, rect, series, svgDocument, textRun } from './svgio.mjs';
|
|
15
|
+
|
|
16
|
+
/** Accepted ranges; the schema checks types and the module checks values. */
|
|
17
|
+
export const CHARGE_LIMITS = {
|
|
18
|
+
maxLength: 20_000,
|
|
19
|
+
minPh: 0,
|
|
20
|
+
maxPh: 14,
|
|
21
|
+
minSteps: 2,
|
|
22
|
+
maxSteps: 141,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const round = (value, digits = 4) => Math.round(value * 10 ** digits) / 10 ** digits;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compute the titration curve and the pI for one protein.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} raw protein sequence (one-letter codes).
|
|
31
|
+
* @param {{pkaSet?: string, phMin?: number, phMax?: number, steps?: number}} [options]
|
|
32
|
+
* @returns {{length: number, pka_set: string, pka_label: string, ph_min: number,
|
|
33
|
+
* ph_max: number, steps: number, pI: number, charge_at_7_4: number,
|
|
34
|
+
* charge_min: number, charge_max: number, points: Array<{ph: number, charge: number}>}}
|
|
35
|
+
*/
|
|
36
|
+
export function chargeCurve(raw, options = {}) {
|
|
37
|
+
const seq = normalizeProtein(raw);
|
|
38
|
+
if (seq.length > CHARGE_LIMITS.maxLength) {
|
|
39
|
+
throw new MolbioInputError('the protein is ' + seq.length + ' aa; the titration limit is ' + CHARGE_LIMITS.maxLength);
|
|
40
|
+
}
|
|
41
|
+
const pkaSet = options.pkaSet ?? 'bjellqvist';
|
|
42
|
+
const pk = PK_SETS[pkaSet];
|
|
43
|
+
if (pk === undefined) {
|
|
44
|
+
throw new MolbioInputError('unknown pka_set ' + JSON.stringify(pkaSet) + '; expected one of ' + Object.keys(PK_SETS).join(', '));
|
|
45
|
+
}
|
|
46
|
+
const phMin = options.phMin ?? 0;
|
|
47
|
+
const phMax = options.phMax ?? 14;
|
|
48
|
+
const steps = options.steps ?? 29;
|
|
49
|
+
if (!(typeof phMin === 'number' && Number.isFinite(phMin) && phMin >= CHARGE_LIMITS.minPh && phMin <= CHARGE_LIMITS.maxPh)) {
|
|
50
|
+
throw new MolbioInputError('ph_min must be a number between ' + CHARGE_LIMITS.minPh + ' and ' + CHARGE_LIMITS.maxPh);
|
|
51
|
+
}
|
|
52
|
+
if (!(typeof phMax === 'number' && Number.isFinite(phMax) && phMax >= CHARGE_LIMITS.minPh && phMax <= CHARGE_LIMITS.maxPh)) {
|
|
53
|
+
throw new MolbioInputError('ph_max must be a number between ' + CHARGE_LIMITS.minPh + ' and ' + CHARGE_LIMITS.maxPh);
|
|
54
|
+
}
|
|
55
|
+
if (!(phMax > phMin)) throw new MolbioInputError('ph_max must be greater than ph_min');
|
|
56
|
+
if (!Number.isInteger(steps) || steps < CHARGE_LIMITS.minSteps || steps > CHARGE_LIMITS.maxSteps) {
|
|
57
|
+
throw new MolbioInputError('steps must be an integer between ' + CHARGE_LIMITS.minSteps + ' and ' + CHARGE_LIMITS.maxSteps);
|
|
58
|
+
}
|
|
59
|
+
const counts = countAminoAcids(seq);
|
|
60
|
+
const points = [];
|
|
61
|
+
for (let index = 0; index < steps; index++) {
|
|
62
|
+
const ph = phMin + ((phMax - phMin) * index) / (steps - 1);
|
|
63
|
+
points.push({ ph: round(ph, 3), charge: round(netCharge(seq, counts, ph, pkaSet), 4) });
|
|
64
|
+
}
|
|
65
|
+
const charges = points.map((point) => point.charge);
|
|
66
|
+
return {
|
|
67
|
+
length: seq.length,
|
|
68
|
+
pka_set: pkaSet,
|
|
69
|
+
pka_label: pk.label,
|
|
70
|
+
ph_min: phMin,
|
|
71
|
+
ph_max: phMax,
|
|
72
|
+
steps,
|
|
73
|
+
pI: isoelectricPoint(seq, counts, pkaSet),
|
|
74
|
+
charge_at_7_4: round(netCharge(seq, counts, 7.4, pkaSet), 4),
|
|
75
|
+
charge_min: Math.min(...charges),
|
|
76
|
+
charge_max: Math.max(...charges),
|
|
77
|
+
points,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Render the titration curve as an SVG document: net charge against pH, with
|
|
83
|
+
* the zero line, the pI marker and the charged ends labelled.
|
|
84
|
+
*
|
|
85
|
+
* @param {ReturnType<typeof chargeCurve>} curve
|
|
86
|
+
* @param {{title?: string}} [options]
|
|
87
|
+
* @returns {string} SVG text.
|
|
88
|
+
*/
|
|
89
|
+
export function renderChargeCurve(curve, options = {}) {
|
|
90
|
+
const width = 720;
|
|
91
|
+
const height = 420;
|
|
92
|
+
const frame = panel({
|
|
93
|
+
x: 16,
|
|
94
|
+
y: 16,
|
|
95
|
+
width: width - 32,
|
|
96
|
+
height: height - 32,
|
|
97
|
+
title: options.title ?? 'Net charge vs pH',
|
|
98
|
+
subtitle: curve.length + ' aa · ' + curve.pka_label + ' pK set · pI ' + curve.pI,
|
|
99
|
+
});
|
|
100
|
+
const plot = frame.plot;
|
|
101
|
+
const yMin = Math.min(0, curve.charge_min);
|
|
102
|
+
const yMax = Math.max(0, curve.charge_max);
|
|
103
|
+
const pad = Math.max(0.5, (yMax - yMin) * 0.08);
|
|
104
|
+
const domain = [yMin - pad, yMax + pad];
|
|
105
|
+
const x = linearScale([curve.ph_min, curve.ph_max], [plot.x, plot.x + plot.width]);
|
|
106
|
+
const y = linearScale(domain, [plot.y + plot.height, plot.y]);
|
|
107
|
+
const parts = [frame.markup];
|
|
108
|
+
// Zero-charge line: the sign of the charge is the point of the plot.
|
|
109
|
+
if (domain[0] < 0 && domain[1] > 0) {
|
|
110
|
+
const zero = y.of(0);
|
|
111
|
+
parts.push('<line x1="' + plot.x + '" y1="' + zero + '" x2="' + (plot.x + plot.width) + '" y2="' + zero + '" stroke="#8c959f" stroke-width="1" stroke-dasharray="4 3"/>');
|
|
112
|
+
}
|
|
113
|
+
parts.push(axis({
|
|
114
|
+
from: plot.x,
|
|
115
|
+
to: plot.x + plot.width,
|
|
116
|
+
at: plot.y + plot.height,
|
|
117
|
+
ticks: niceTicks([curve.ph_min, curve.ph_max], 7).map((tick) => x.of(tick)),
|
|
118
|
+
orientation: 'x',
|
|
119
|
+
label: 'pH',
|
|
120
|
+
tickLabel: (pixel) => String(Math.round(x.invert(pixel) * 10) / 10),
|
|
121
|
+
title: 'pH',
|
|
122
|
+
}));
|
|
123
|
+
parts.push(axis({
|
|
124
|
+
from: plot.y,
|
|
125
|
+
to: plot.y + plot.height,
|
|
126
|
+
at: plot.x,
|
|
127
|
+
ticks: niceTicks(domain, 6).map((tick) => y.of(tick)),
|
|
128
|
+
orientation: 'y',
|
|
129
|
+
label: 'net charge',
|
|
130
|
+
tickLabel: (pixel) => String(Math.round(y.invert(pixel) * 10) / 10),
|
|
131
|
+
title: 'net charge (e)',
|
|
132
|
+
}));
|
|
133
|
+
parts.push(series({
|
|
134
|
+
values: curve.points.map((point) => point.charge),
|
|
135
|
+
xOf: (index) => x.of(curve.points[index].ph),
|
|
136
|
+
yOf: (charge) => y.of(charge),
|
|
137
|
+
stroke: '#0969da',
|
|
138
|
+
width: 2,
|
|
139
|
+
}));
|
|
140
|
+
if (curve.pI >= curve.ph_min && curve.pI <= curve.ph_max) {
|
|
141
|
+
const marker = x.of(curve.pI);
|
|
142
|
+
parts.push('<line x1="' + marker + '" y1="' + plot.y + '" x2="' + marker + '" y2="' + (plot.y + plot.height) + '" stroke="#bf3989" stroke-width="1" stroke-dasharray="2 3"/>');
|
|
143
|
+
parts.push(textRun({ x: marker + 4, y: plot.y + 12, text: 'pI ' + curve.pI, size: 9, fill: '#bf3989' }));
|
|
144
|
+
}
|
|
145
|
+
// Label the two ends of the curve with the charges they represent.
|
|
146
|
+
const first = curve.points[0];
|
|
147
|
+
const last = curve.points[curve.points.length - 1];
|
|
148
|
+
parts.push(textRun({ x: x.of(first.ph) + 4, y: y.of(first.charge) - 6, text: 'pH ' + first.ph + ': ' + first.charge, size: 9, fill: '#57606a' }));
|
|
149
|
+
parts.push(textRun({ x: x.of(last.ph) - 4, y: y.of(last.charge) + 14, text: 'pH ' + last.ph + ': ' + last.charge, size: 9, fill: '#57606a', anchor: 'end' }));
|
|
150
|
+
parts.push(rect({ x: plot.x, y: plot.y, width: plot.width, height: plot.height, fill: 'none', stroke: '#d0d7de' }));
|
|
151
|
+
return svgDocument({
|
|
152
|
+
width,
|
|
153
|
+
height,
|
|
154
|
+
title: options.title ?? 'Net charge vs pH',
|
|
155
|
+
description: 'Titration curve for a ' + curve.length + ' aa protein using the ' + curve.pka_label + ' pK set; pI ' + curve.pI + '.',
|
|
156
|
+
body: parts.join('\n'),
|
|
157
|
+
});
|
|
158
|
+
}
|
package/protein.mjs
CHANGED
|
@@ -32,48 +32,70 @@ const RESIDUE_MASS = {
|
|
|
32
32
|
};
|
|
33
33
|
const WATER = 18.015;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Selectable pK sets for the Henderson-Hasselbalch charge model. `bjellqvist`
|
|
37
|
+
* is the set `proteinProperties` has always used for its pI; `emboss` is the
|
|
38
|
+
* Epk.dat table EMBOSS ships with `iep`. They are NOT interchangeable — the
|
|
39
|
+
* reported set name travels with every charge/pI value.
|
|
40
|
+
*/
|
|
41
|
+
export const PK_SETS = {
|
|
42
|
+
bjellqvist: {
|
|
43
|
+
label: 'Bjellqvist 1993',
|
|
44
|
+
Nterm: 8.6, Cterm: 3.6, C: 9.0, D: 4.05, E: 4.45, H: 6.04,
|
|
45
|
+
K: 10.8, R: 12.0, Y: 10.3,
|
|
46
|
+
},
|
|
47
|
+
emboss: {
|
|
48
|
+
label: 'EMBOSS Epk.dat',
|
|
49
|
+
Nterm: 8.6, Cterm: 3.6, C: 8.5, D: 3.9, E: 4.1, H: 6.5,
|
|
50
|
+
K: 10.8, R: 12.5, Y: 10.1,
|
|
51
|
+
},
|
|
39
52
|
};
|
|
40
53
|
|
|
54
|
+
const PK = PK_SETS.bjellqvist;
|
|
55
|
+
|
|
41
56
|
const HYDROPATHY = {
|
|
42
57
|
A: 1.8, R: -4.5, N: -3.5, D: -3.5, C: 2.5, Q: -3.5, E: -3.5,
|
|
43
58
|
G: -0.4, H: -3.2, I: 4.5, L: 3.8, K: -3.9, M: 1.9, F: 2.8,
|
|
44
59
|
P: -1.6, S: -0.8, T: -0.7, W: -0.9, Y: -1.3, V: 4.2,
|
|
45
60
|
};
|
|
46
61
|
|
|
47
|
-
function countAminoAcids(seq) {
|
|
62
|
+
export function countAminoAcids(seq) {
|
|
48
63
|
const counts = {};
|
|
49
64
|
for (const aa of seq) counts[aa] = (counts[aa] ?? 0) + 1;
|
|
50
65
|
return counts;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Net charge at a given pH by Henderson-Hasselbalch summation. The terminal
|
|
70
|
+
* groups count once each; K/R/H are positive, D/E/C/Y negative. `pkaSet` is a
|
|
71
|
+
* key of PK_SETS (or a set object), so callers can name the table they used.
|
|
72
|
+
*/
|
|
73
|
+
export function netCharge(seq, counts, pH, pkaSet = 'bjellqvist') {
|
|
74
|
+
const pk = typeof pkaSet === 'string' ? PK_SETS[pkaSet] : pkaSet;
|
|
75
|
+
if (pk === undefined) throw new MolbioInputError('unknown pK set ' + JSON.stringify(pkaSet) + '; expected one of ' + Object.keys(PK_SETS).join(', '));
|
|
54
76
|
let charge = 0;
|
|
55
77
|
const pos = (pKa) => 1 / (1 + Math.pow(10, pH - pKa));
|
|
56
78
|
const neg = (pKa) => 1 / (1 + Math.pow(10, pKa - pH));
|
|
57
|
-
charge += pos(
|
|
58
|
-
for (const [aa, pKa] of [['K',
|
|
79
|
+
charge += pos(pk.Nterm);
|
|
80
|
+
for (const [aa, pKa] of [['K', pk.K], ['R', pk.R], ['H', pk.H]]) {
|
|
59
81
|
if (counts[aa] !== undefined) charge += counts[aa] * pos(pKa);
|
|
60
82
|
}
|
|
61
|
-
charge -= neg(
|
|
62
|
-
for (const [aa, pKa] of [['D',
|
|
83
|
+
charge -= neg(pk.Cterm);
|
|
84
|
+
for (const [aa, pKa] of [['D', pk.D], ['E', pk.E], ['C', pk.C], ['Y', pk.Y]]) {
|
|
63
85
|
if (counts[aa] !== undefined) charge -= counts[aa] * neg(pKa);
|
|
64
86
|
}
|
|
65
87
|
return charge;
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
/** Isoelectric point by bisection over the net-charge function. */
|
|
69
|
-
export function isoelectricPoint(seq, counts) {
|
|
91
|
+
export function isoelectricPoint(seq, counts, pkaSet = 'bjellqvist') {
|
|
70
92
|
let lo = 0;
|
|
71
93
|
let hi = 14;
|
|
72
|
-
let chargeLo =
|
|
73
|
-
let chargeHi =
|
|
94
|
+
let chargeLo = netCharge(seq, counts, lo, pkaSet);
|
|
95
|
+
let chargeHi = netCharge(seq, counts, hi, pkaSet);
|
|
74
96
|
for (let i = 0; i < 60; i++) {
|
|
75
97
|
const mid = (lo + hi) / 2;
|
|
76
|
-
const chargeMid =
|
|
98
|
+
const chargeMid = netCharge(seq, counts, mid, pkaSet);
|
|
77
99
|
if (Math.abs(chargeMid) < 1e-4) return Math.round(mid * 100) / 100;
|
|
78
100
|
if (Math.sign(chargeMid) === Math.sign(chargeLo)) {
|
|
79
101
|
lo = mid;
|
package/seqio.mjs
CHANGED
|
@@ -84,6 +84,16 @@ export function toFasta(entries) {
|
|
|
84
84
|
return entries.map((entry) => `>${entry.id}${entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}`).join('\n') + '\n';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Render entries as four-line FASTQ text (quality is preserved verbatim). */
|
|
88
|
+
export function toFastq(entries) {
|
|
89
|
+
return entries.map((entry) => {
|
|
90
|
+
if (typeof entry.quality !== 'string' || entry.quality.length !== entry.sequence.length) {
|
|
91
|
+
throw new MolbioInputError(`entry ${entry.id}: sequence and quality lengths differ`);
|
|
92
|
+
}
|
|
93
|
+
return `@${entry.id}${entry.description !== undefined && entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}\n+\n${entry.quality}`;
|
|
94
|
+
}).join('\n') + '\n';
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
/** Summary statistics over parsed entries. */
|
|
88
98
|
export function entryStats(entries, qualityMode = false) {
|
|
89
99
|
const lengths = entries.map((entry) => entry.sequence.length);
|
package/test/client.mjs
CHANGED
|
@@ -144,6 +144,8 @@ assert.deepEqual(effects, [
|
|
|
144
144
|
'molbio panel: papers tab title',
|
|
145
145
|
'molbio panel: molbio_plasmid_map card',
|
|
146
146
|
'molbio panel: molbio_plasmid_map_file card',
|
|
147
|
+
'molbio panel: molbio_sequence_logo card',
|
|
148
|
+
'molbio panel: molbio_grna_design card',
|
|
147
149
|
'molbio panel: NCBI key section gate',
|
|
148
150
|
'molbio panel: NCBI key section',
|
|
149
151
|
]);
|
|
@@ -186,9 +188,9 @@ assert.deepEqual(
|
|
|
186
188
|
assert.equal(slots.registrations.length, 4, 'both tab bodies and titles land as soon as their seat exists');
|
|
187
189
|
slots.declare('tool.call.toolview');
|
|
188
190
|
|
|
189
|
-
// Body + title register in the keyed seats under each type's id
|
|
190
|
-
//
|
|
191
|
-
// back to the generic tool row, so claiming ours is additive).
|
|
191
|
+
// Body + title register in the keyed seats under each type's id; two map tools
|
|
192
|
+
// and two figure tools claim their own `tool.call.toolview` keys (an unclaimed
|
|
193
|
+
// key falls back to the generic tool row, so claiming ours is additive).
|
|
192
194
|
assert.deepEqual(
|
|
193
195
|
slots.registrations.map((entry) => [entry.registration.name, entry.registration.key]),
|
|
194
196
|
[
|
|
@@ -198,6 +200,8 @@ assert.deepEqual(
|
|
|
198
200
|
['sidebar.right.pane.tab.title', 'dsh-molbio-tools/papers'],
|
|
199
201
|
['tool.call.toolview', 'molbio_plasmid_map'],
|
|
200
202
|
['tool.call.toolview', 'molbio_plasmid_map_file'],
|
|
203
|
+
['tool.call.toolview', 'molbio_sequence_logo'],
|
|
204
|
+
['tool.call.toolview', 'molbio_grna_design'],
|
|
201
205
|
],
|
|
202
206
|
);
|
|
203
207
|
const claim = (name, key) => slots.registrations.find(
|
|
@@ -206,6 +210,9 @@ const claim = (name, key) => slots.registrations.find(
|
|
|
206
210
|
assert.equal(typeof claim('sidebar.right.pane.tab', 'dsh-molbio-tools').component, 'function', 'the plasmid body is a component');
|
|
207
211
|
assert.equal(typeof claim('sidebar.right.pane.tab', 'dsh-molbio-tools/papers').component, 'function', 'the papers body is a component');
|
|
208
212
|
assert.equal(typeof claim('tool.call.toolview', 'molbio_plasmid_map').component, 'function', 'the map card is a component');
|
|
213
|
+
assert.equal(typeof claim('tool.call.toolview', 'molbio_sequence_logo').component, 'function', 'the logo card is a component');
|
|
214
|
+
assert.equal(claim('tool.call.toolview', 'molbio_sequence_logo').component, claim('tool.call.toolview', 'molbio_grna_design').component, 'the two figure tools share one card component');
|
|
215
|
+
assert.equal(claim('tool.call.toolview', 'molbio_grna_design').registration.inject, undefined, 'the figure card declares no injection');
|
|
209
216
|
for (const key of ['dsh-molbio-tools', 'dsh-molbio-tools/papers']) {
|
|
210
217
|
const face = claim('sidebar.right.pane.tab', key).registration.inject('session-1', {});
|
|
211
218
|
assert.equal(typeof face.remote.workspaceFiles.list, 'function', 'the inject factory hands the body the workspace Remote');
|
|
@@ -220,13 +227,13 @@ assert.equal(claim('tool.call.toolview', 'molbio_plasmid_map').registration.inje
|
|
|
220
227
|
// A re-declaration of the seat (the shell's declaration-epoch rule) re-runs the
|
|
221
228
|
// wait: the previous entries are disposed first, so claims never accumulate.
|
|
222
229
|
slots.declare('tool.call.toolview');
|
|
223
|
-
assert.equal(slots.registrations.length,
|
|
230
|
+
assert.equal(slots.registrations.length, 8, 're-declaring the seat re-installs all four cards instead of duplicating them');
|
|
224
231
|
|
|
225
232
|
// The Plugins page's detail seat belongs to the plugin manager. The section
|
|
226
233
|
// lands when that seat is declared, and the component itself decides whether
|
|
227
234
|
// the open subject is this package.
|
|
228
235
|
slots.declare('plugins.detail.section');
|
|
229
|
-
assert.equal(slots.registrations.length,
|
|
236
|
+
assert.equal(slots.registrations.length, 9, 'the key section lands when the plugin manager declares its seat');
|
|
230
237
|
const keyClaim = slots.registrations.find((entry) => entry.registration.name === 'plugins.detail.section');
|
|
231
238
|
assert.ok(keyClaim, 'the key section is claimed in plugins.detail.section');
|
|
232
239
|
assert.equal(keyClaim.registration.id, 'molbio-ncbi-key', 'the list seat is addressed by an id of our own');
|
package/test/contract.mjs
CHANGED
|
@@ -228,7 +228,14 @@ check('the shell derives hook props as use<Capitalised(Source)>', () => {
|
|
|
228
228
|
check('a session-scope tab body receives sessionId + useSessions (official precedent)', () => {
|
|
229
229
|
const files = clientBundle('@deepseek-ai/dsh-client-ui-sidebar-files');
|
|
230
230
|
assert.ok(files !== undefined, 'ui-sidebar-files is installed');
|
|
231
|
-
|
|
231
|
+
// 0.2.2-rc.1 renamed the tab body (`FilesBody` -> `FileNavigator`) without
|
|
232
|
+
// changing what the runtime injects into it. The CONTRACT is the props, not
|
|
233
|
+
// the function's name, so match the destructuring rather than one identifier.
|
|
234
|
+
assert.match(
|
|
235
|
+
files.text,
|
|
236
|
+
/function [A-Za-z_$][\w$]*\(\{(?=[^}]*\buseTabInfo\b)(?=[^}]*\bsessionId\b)(?=[^}]*\buseSessions\b)/,
|
|
237
|
+
'the official tab body receives useTabInfo + sessionId + useSessions from its props',
|
|
238
|
+
);
|
|
232
239
|
assert.ok(files.text.includes('useSessions((sessions) => sessions.byId[sessionId]?.cwd)'), 'and reads the workspace root exactly as the panel does');
|
|
233
240
|
assert.ok(files.text.includes('"slots"') && files.text.includes('"sidebarRightTabs"'), 'and injects the same registries');
|
|
234
241
|
});
|
|
@@ -432,7 +439,7 @@ check('the published tarball carries the client half and exactly one plugin mani
|
|
|
432
439
|
// SUBMISSION_PACKAGE_AMBIGUOUS, plus a duplicate 427 KB client bundle in every
|
|
433
440
|
// download. The panel does not disappear for npm users: the root package's own
|
|
434
441
|
// `lib/client.js` already carries it (the panel-only package exists for people
|
|
435
|
-
// who want the tab without the
|
|
442
|
+
// who want the tab without the 64 tools).
|
|
436
443
|
assert.ok(
|
|
437
444
|
!files.includes('packages'),
|
|
438
445
|
'the root tarball must not embed packages/molbio-panel: a second `dsh` manifest inside one package is an ambiguous submission, and the panel is published separately',
|
|
@@ -468,8 +475,16 @@ check('the toolview card contract still holds (meta path + keyed seat)', () => {
|
|
|
468
475
|
// path that still carries the value — consulted, snapshotted, attached as
|
|
469
476
|
// `meta` — rather than a literal upstream is free to drop (the panel does not
|
|
470
477
|
// depend on nested calls being excluded, only on the root call carrying it).
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
// The projection lives in the assembly pipeline. 0.2.2-rc.1 moved that code
|
|
479
|
+
// out of `lib/index.js` into `lib/pipeline.js` (same call, same snapshot, same
|
|
480
|
+
// attachment), so read whichever files the installed package ships instead of
|
|
481
|
+
// pinning the one upstream happened to use before.
|
|
482
|
+
const toolsDir = packageDir('@deepseek-ai/dsh-tools');
|
|
483
|
+
const toolsFiles = ['lib/index.js', 'lib/pipeline.js']
|
|
484
|
+
.map((rel) => join(toolsDir, rel))
|
|
485
|
+
.filter((path) => existsSync(path));
|
|
486
|
+
assert.ok(toolsFiles.length > 0, 'dsh-tools is installed');
|
|
487
|
+
const tools = { text: toolsFiles.map((path) => readFileSync(path, 'utf8')).join('\n') };
|
|
473
488
|
assert.ok(/output\.presentationMeta\s*!==\s*void 0/.test(tools.text), 'the tool layer still consults output.presentationMeta');
|
|
474
489
|
assert.ok(tools.text.includes('snapshotProjection(tool.name, "presentationMeta"'), 'and records the projection on the result');
|
|
475
490
|
assert.ok(
|
|
@@ -493,16 +508,25 @@ check('the toolview card contract still holds (meta path + keyed seat)', () => {
|
|
|
493
508
|
// key whose tool forgot `presentationMeta`.
|
|
494
509
|
const source = readFileSync(join(packageRoot, 'index.mjs'), 'utf8');
|
|
495
510
|
const bundle = readFileSync(join(packageRoot, 'lib', 'client.js'), 'utf8');
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
511
|
+
const keyList = (name) => {
|
|
512
|
+
const match = new RegExp('const ' + name + ' = \\[([^\\]]*)\\]').exec(bundle);
|
|
513
|
+
assert.ok(match !== null, 'the bundle declares ' + name + ', the tool keys whose cards it draws');
|
|
514
|
+
return match[1].split(',').map((entry) => entry.trim().replace(/^['"]|['"]$/g, '')).filter((entry) => entry !== '');
|
|
515
|
+
};
|
|
516
|
+
// Two card shapes share the seat: the plasmid-map card (MAP_TOOL_KEYS) and the
|
|
517
|
+
// figure card (FIGURE_TOOL_KEYS). Both lists are read from the bundle, so a
|
|
518
|
+
// third key whose tool forgets its projection fails here either way.
|
|
519
|
+
const claimed = [...keyList('MAP_TOOL_KEYS'), ...keyList('FIGURE_TOOL_KEYS')];
|
|
499
520
|
assert.ok(claimed.length > 0, 'the bundle claims at least one card key');
|
|
500
521
|
assert.equal(
|
|
501
|
-
|
|
522
|
+
// Anchored: the file-header comment also mentions the signature, and a
|
|
523
|
+
// substring count would treat that prose as a fourth implementation.
|
|
524
|
+
(source.match(/^ presentationMeta\((?:args|_args), value\)/gm) ?? []).length,
|
|
502
525
|
claimed.length,
|
|
503
526
|
'every claimed card key belongs to a tool that declares the projection',
|
|
504
527
|
);
|
|
505
|
-
assert.ok(source.includes('mapCardMeta'), 'and build it through the shared projection');
|
|
528
|
+
assert.ok(source.includes('mapCardMeta'), 'and build it through the shared map projection');
|
|
529
|
+
assert.ok(source.includes('figureCardMeta'), 'and the shared figure projection');
|
|
506
530
|
for (const key of claimed) {
|
|
507
531
|
// The host half registers a tool under that exact name.
|
|
508
532
|
assert.ok(source.includes(`name: '${key}'`), `the host registers the tool ${key} whose card the bundle claims`);
|
|
@@ -710,8 +734,8 @@ check('a tool may show the model a picture the way read_image does', () => {
|
|
|
710
734
|
// The count is deliberate: it is what catches a NEW picture tool that forgets
|
|
711
735
|
// the hand-off (or a report tool that quietly grows it).
|
|
712
736
|
const index = readFileSync(join(packageRoot, 'index.mjs'), 'utf8');
|
|
713
|
-
assert.equal((index.match(/image: ATTACHED_IMAGE_SCHEMA/g) ?? []).length,
|
|
714
|
-
assert.equal((index.match(/\.\.\.ATTACH_IMAGE_PARAM/g) ?? []).length,
|
|
737
|
+
assert.equal((index.match(/image: ATTACHED_IMAGE_SCHEMA/g) ?? []).length, 17, 'all 17 picture tools declare the image output field');
|
|
738
|
+
assert.equal((index.match(/\.\.\.ATTACH_IMAGE_PARAM/g) ?? []).length, 17, 'and offer the opt-in attach_image parameter');
|
|
715
739
|
assert.ok(/output\.render/.test(index) || index.includes('imageBlockFor(value)'), 'the block builder is wired into define()');
|
|
716
740
|
});
|
|
717
741
|
|
|
@@ -724,7 +748,15 @@ check('the workspace still cannot hold a binary file (the reason the picture rid
|
|
|
724
748
|
// the local backend (the base is consulted last, only as a presence fallback).
|
|
725
749
|
const fsTypes = declarationEvidence('@deepseek-ai/dsh-fs', ['lib/types/types.d.ts', '../dsh-fs-local/lib/index.js', 'lib/index.js']).text;
|
|
726
750
|
const fsReadme = readFileSync(join(packageDir('@deepseek-ai/dsh-fs'), 'README.md'), 'utf8');
|
|
727
|
-
|
|
751
|
+
// 0.2.2-rc.1 reworded this: the old "binary-safe mutations remain deferred"
|
|
752
|
+
// note became the precise rule under "Known Limitations and Deferred Work"
|
|
753
|
+
// ("Literal edits, patches, and moves require UTF-8 text"). Accept either
|
|
754
|
+
// spelling of the SAME policy — the seam assertions below are the real gate.
|
|
755
|
+
assert.match(
|
|
756
|
+
fsReadme,
|
|
757
|
+
/binary-safe mutations remain deferred|Literal edits, patches, and moves require UTF-8 text/i,
|
|
758
|
+
'and still documents the text-only mutation rule the picture hand-off depends on',
|
|
759
|
+
);
|
|
728
760
|
const fsIndex = readFileSync(join(packageDir('@deepseek-ai/dsh-fs'), 'lib', 'index.js'), 'utf8');
|
|
729
761
|
assertFsSeamDeclared(fsTypes, fsIndex);
|
|
730
762
|
});
|
package/test/drift-probe.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { existsSync } from 'node:fs';
|
|
|
29
29
|
import { dirname, join, resolve } from 'node:path';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
|
|
32
|
-
import { compositionDrift, toolCountDrift, presetPlugins, asPresetPatch, relativeSpecifiers, presetOrderDeclared, orderCollisions } from './preset-health.mjs';
|
|
32
|
+
import { compositionDrift, toolCountDrift, presetPlugins, asPresetPatch, relativeSpecifiers, presetOrderDeclared, orderCollisions, specifierProblem } from './preset-health.mjs';
|
|
33
33
|
// The three helpers this probe used to carry its own copies of. Duplicating
|
|
34
34
|
// `findHarnessRoot` was the worst of them: the probe could resolve a DIFFERENT
|
|
35
35
|
// harness than `preset-health` validated against, so a mutation experiment could
|
|
@@ -215,6 +215,20 @@ check('the specifier guard is quiet on a package name and catches a relative pat
|
|
|
215
215
|
);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
check('a row naming a removed package subpath is caught (the 0.2.2-rc.1 list-agents trap)', () => {
|
|
219
|
+
// 0.2.2-rc.1 folded `list_agents` into `@deepseek-ai/dsh-tool-subagent-control`
|
|
220
|
+
// and dropped its `./list-agents` export. `packageInstalled` still finds the
|
|
221
|
+
// package ROOT, so only a real specifier resolution notices — and this probe
|
|
222
|
+
// must not depend on one version's removals to prove the guard works. A
|
|
223
|
+
// subpath no release can export is the version-independent mutation.
|
|
224
|
+
const base = '@deepseek-ai/dsh-tool-subagent-control';
|
|
225
|
+
assert.equal(specifierProblem(base, harnessRoot), undefined, 'the package root resolves');
|
|
226
|
+
assert.equal(specifierProblem(`${base}/package.json`, harnessRoot), undefined, 'an exported subpath resolves');
|
|
227
|
+
const problem = specifierProblem(`${base}/no-such-subpath`, harnessRoot);
|
|
228
|
+
assert.ok(problem !== undefined, 'an unexported subpath is rejected');
|
|
229
|
+
assert.match(problem, /does not resolve/, problem);
|
|
230
|
+
});
|
|
231
|
+
|
|
218
232
|
check('the order guard reads the order under config, not the declaration row id', () => {
|
|
219
233
|
const layer = [
|
|
220
234
|
' - id: preset-mine',
|