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/cordis.patch.yml
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
id: molbio-lab
|
|
48
48
|
order: 5
|
|
49
49
|
name: Molecular Biology Lab
|
|
50
|
-
description: "分子生物学专属模式:标准编码能力 +
|
|
50
|
+
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 图片自动用系统默认应用打开,文献/协议/实验记录)。"
|
|
51
51
|
plugins:
|
|
52
52
|
- id: persona
|
|
53
53
|
name: '@deepseek-ai/dsh-persona'
|
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
disabled: !!js process.platform !== 'win32'
|
|
68
68
|
- id: tool-fs
|
|
69
69
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
70
|
+
config:
|
|
71
|
+
# Set false to drop the apply_patch tool beside edit. (0.2.2-rc.1 makes this
|
|
72
|
+
# explicit in the shipped standard preset; the package default is false, and
|
|
73
|
+
# the row is kept verbatim so the two stay comparable.)
|
|
74
|
+
applyPatch: false
|
|
70
75
|
- id: tool-fs-search
|
|
71
76
|
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
72
77
|
config:
|
|
@@ -129,14 +134,13 @@
|
|
|
129
134
|
config:
|
|
130
135
|
- id: tool-subagent-control
|
|
131
136
|
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
132
|
-
- id: tool-subagent-list-agents
|
|
133
|
-
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
134
137
|
- id: tool-subagent
|
|
135
138
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
136
139
|
config:
|
|
137
140
|
provider: spawn
|
|
138
141
|
toolName: subagent
|
|
139
|
-
|
|
142
|
+
modelSelection: true
|
|
143
|
+
modelDiscovery: true
|
|
140
144
|
- id: tool-subagent-fork
|
|
141
145
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
142
146
|
config:
|
|
@@ -168,7 +172,7 @@
|
|
|
168
172
|
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
169
173
|
disabled: true
|
|
170
174
|
|
|
171
|
-
# dsh-molbio-tools: molecular-biology toolset (
|
|
175
|
+
# dsh-molbio-tools: molecular-biology toolset (64 tools across sequence
|
|
172
176
|
# analysis, primers (incl. TaqMan probes and multiplex compatibility),
|
|
173
177
|
# in-silico PCR, cloning (incl. Golden Gate), enzyme catalog, methylation-aware
|
|
174
178
|
# and double digests, plasmids, Sanger verification, proteins (incl. helical
|
package/design.mjs
CHANGED
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
* from `anchor` and not from the RC-scan `start`. It is the REVERSE primer,
|
|
37
37
|
* and the amplicon ends at `anchor + length - 1`.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
39
|
+
* `designIntronSpanningPrimers` (tool: `molbio_design_intron_primers`)
|
|
40
|
+
* follows the SAME convention since 0.23.0. The junction is carried by the
|
|
41
|
+
* REVERSE primer (the assay sits 5' of the junction), so the reverse candidate
|
|
42
|
+
* is scanned on the reverse complement of the spliced transcript: every 3'-end
|
|
43
|
+
* constraint is then evaluated on the molecule that is ordered, and the hit is
|
|
44
|
+
* mapped back to sense coordinates through `anchor` (its 3' end, the LEFT edge
|
|
45
|
+
* of its site). `forward` is upstream of `reverse`, the spliced amplicon runs
|
|
46
|
+
* from the forward 5' end to the reverse 5' end, and the reported reverse
|
|
47
|
+
* sequence is what goes on the order form. Before 0.23.0 the tool reported both
|
|
48
|
+
* primers as sense substrings, so `forward` sat downstream of `reverse` and the
|
|
49
|
+
* reported reverse was the reverse complement of the molecule to order.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import {
|
|
@@ -738,37 +738,60 @@ export function designPrimers(rawTemplate, rawOptions) {
|
|
|
738
738
|
// ── cross-intron primer design ──────────────────────────────────────────────
|
|
739
739
|
|
|
740
740
|
/**
|
|
741
|
-
* Mismatch report for cross-intron candidates.
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* both
|
|
741
|
+
* Mismatch report for cross-intron candidates. The forward primer is the
|
|
742
|
+
* sense-strand substring at its binding site; the reverse primer is the REVERSE
|
|
743
|
+
* COMPLEMENT of the sense-strand substring at its site (the molecule that is
|
|
744
|
+
* ordered). `anchorStart` is the 0-based sense-strand position of the primer's
|
|
745
|
+
* 3' end — for the reverse primer that is the LEFT edge of its site — and
|
|
746
|
+
* `isReverse` flips both the perfect-match base and how a primer offset maps
|
|
747
|
+
* onto the transcript.
|
|
747
748
|
*/
|
|
748
|
-
function intronMismatchReport(candidate, spliced, splicedToGenomic) {
|
|
749
|
+
function intronMismatchReport(candidate, spliced, splicedToGenomic, anchorStart, isReverse) {
|
|
749
750
|
const len = candidate.length;
|
|
750
|
-
return candidate.mismatches.map(({ p, base }) =>
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
751
|
+
return candidate.mismatches.map(({ p, base }) => {
|
|
752
|
+
const offset = isReverse ? len - 1 - p : p;
|
|
753
|
+
const index = anchorStart + offset;
|
|
754
|
+
return {
|
|
755
|
+
position: p + 1,
|
|
756
|
+
template_base: isReverse ? complement(spliced[index]) : spliced[index],
|
|
757
|
+
primer_base: base,
|
|
758
|
+
spliced_position: index + 1,
|
|
759
|
+
genomic_position: splicedToGenomic[index] + 1,
|
|
760
|
+
distance_from_3prime: len - 1 - p,
|
|
761
|
+
};
|
|
762
|
+
});
|
|
758
763
|
}
|
|
759
764
|
|
|
760
765
|
/**
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
766
|
+
* The same weights as `mismatchPenalty`, over the snake_case report fields.
|
|
767
|
+
* The old intron path handed snake_case reports to the camelCase helper, so the
|
|
768
|
+
* 3'-zone surcharge never applied there; this keeps the two conventions apart on
|
|
769
|
+
* purpose instead of loosening the shared helper.
|
|
770
|
+
*/
|
|
771
|
+
function intronMismatchPenalty(report, zone) {
|
|
772
|
+
return report.reduce((sum, mismatch) => sum + 8 + (mismatch.distance_from_3prime <= zone ? 4 : 0), 0);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Design qPCR primer pairs across an exon-exon junction so genomic DNA cannot
|
|
777
|
+
* be amplified: the REVERSE primer straddles the junction (>= minJunctionBases
|
|
778
|
+
* on each side), the FORWARD primer sits upstream inside a single exon, and the
|
|
779
|
+
* spliced amplicon crosses the junction inside the reverse primer's site.
|
|
780
|
+
*
|
|
781
|
+
* Coordinates are reported both on the spliced transcript and on the genomic
|
|
782
|
+
* sequence. Orientation follows `designPrimerPairs` (see the file header):
|
|
783
|
+
* `forward.sequence` is the sense-strand substring at its site and
|
|
784
|
+
* `reverse.sequence` is the reverse complement of the sense-strand substring at
|
|
785
|
+
* its site. The reverse candidate is scanned on the reverse complement of the
|
|
786
|
+
* spliced transcript, so every 3'-end constraint (GC clamp, end stability, end
|
|
787
|
+
* GC, self-end, hairpin) applies to the molecule that is ordered.
|
|
765
788
|
*/
|
|
766
789
|
export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
767
790
|
if (!Array.isArray(exons) || exons.length < 2) throw new MolbioInputError('exons must be an array of at least two {start, end} spans');
|
|
768
791
|
const sorted = [...exons].sort((a, b) => a.start - b.start);
|
|
769
792
|
for (const exon of sorted) {
|
|
770
793
|
if (!Number.isInteger(exon.start) || !Number.isInteger(exon.end) || exon.start < 1 || exon.end > genomic.length || exon.start > exon.end) {
|
|
771
|
-
throw new MolbioInputError(
|
|
794
|
+
throw new MolbioInputError('exon ' + JSON.stringify(exon) + ' is outside the genomic sequence (length ' + genomic.length + ')');
|
|
772
795
|
}
|
|
773
796
|
}
|
|
774
797
|
for (let i = 1; i < sorted.length; i++) {
|
|
@@ -791,33 +814,39 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
791
814
|
}
|
|
792
815
|
});
|
|
793
816
|
if (spliced.length < 24) throw new MolbioInputError('the spliced transcript is too short for primer design');
|
|
817
|
+
const splicedLength = spliced.length;
|
|
818
|
+
const rcSpliced = reverseComplement(spliced);
|
|
794
819
|
|
|
795
820
|
// v13 target position preference (coordinates on the spliced transcript).
|
|
796
821
|
const targetPosition = opts.targetPosition;
|
|
797
|
-
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition >
|
|
798
|
-
throw new MolbioInputError(
|
|
822
|
+
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition > splicedLength)) {
|
|
823
|
+
throw new MolbioInputError('target_position ' + targetPosition + ' is outside the spliced transcript (length ' + splicedLength + ')');
|
|
799
824
|
}
|
|
800
825
|
const targetWeight = opts.targetPenalty ?? DESIGN_DEFAULTS.targetPenalty;
|
|
801
826
|
|
|
802
|
-
// Forward candidates
|
|
803
|
-
const fwd =
|
|
804
|
-
|
|
827
|
+
// Forward candidates: entirely inside one exon on the sense strand.
|
|
828
|
+
const fwd = scanCandidates(spliced, 0, splicedLength - opts.lenMin, opts)
|
|
829
|
+
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
830
|
+
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
831
|
+
.sort((a, b) => a.start - b.start);
|
|
832
|
+
|
|
833
|
+
// Reverse candidates: scanned on the reverse complement (the ordered
|
|
834
|
+
// molecule), then mapped back to the sense-strand 3' end `anchor`.
|
|
835
|
+
const rev = [];
|
|
836
|
+
for (const candidate of scanCandidates(rcSpliced, 0, splicedLength - opts.lenMin, opts)) {
|
|
837
|
+
const anchor = splicedLength - (candidate.start + candidate.length);
|
|
838
|
+
const topEnd = anchor + candidate.length - 1;
|
|
805
839
|
for (const junction of junctionPositions) {
|
|
806
|
-
const left = junction.splicedPos -
|
|
807
|
-
const right =
|
|
840
|
+
const left = junction.splicedPos - anchor;
|
|
841
|
+
const right = topEnd + 1 - junction.splicedPos;
|
|
808
842
|
if (left >= minSide && right >= minSide) {
|
|
809
|
-
|
|
843
|
+
rev.push({ ...candidate, anchor, junction, junction_left: left, junction_right: right });
|
|
810
844
|
break;
|
|
811
845
|
}
|
|
812
846
|
}
|
|
813
847
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const rev = scanCandidates(spliced, 0, spliced.length - opts.lenMin, opts)
|
|
817
|
-
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
818
|
-
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
819
|
-
.sort((a, b) => a.start - b.start);
|
|
820
|
-
const revStarts = rev.map((candidate) => candidate.start);
|
|
848
|
+
rev.sort((a, b) => a.anchor - b.anchor);
|
|
849
|
+
const revAnchors = rev.map((candidate) => candidate.anchor);
|
|
821
850
|
|
|
822
851
|
const tmCenter = (opts.tmMin + opts.tmMax) / 2;
|
|
823
852
|
const misprimingIndex = opts.checkMispriming
|
|
@@ -826,48 +855,52 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
826
855
|
const maxSites = opts.misprimingMaxSites ?? DESIGN_DEFAULTS.misprimingMaxSites;
|
|
827
856
|
const pairs = [];
|
|
828
857
|
for (const f of fwd) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
858
|
+
// The reverse site must start after the forward 3' end, and the amplicon
|
|
859
|
+
// (forward 5' -> reverse 5') must fit the window. The bounds are widened by
|
|
860
|
+
// the primer-length range and the exact length is checked below, because a
|
|
861
|
+
// long reverse primer can start earlier and still close inside the window.
|
|
862
|
+
const aMin = Math.max(f.start + f.length, f.start + opts.ampliconMin - opts.lenMax);
|
|
863
|
+
const aMax = f.start + opts.ampliconMax - opts.lenMin;
|
|
864
|
+
if (aMax < aMin) continue;
|
|
832
865
|
let lo = 0;
|
|
833
|
-
let hi =
|
|
866
|
+
let hi = revAnchors.length;
|
|
834
867
|
while (lo < hi) {
|
|
835
868
|
const mid = (lo + hi) >> 1;
|
|
836
|
-
if (
|
|
869
|
+
if (revAnchors[mid] < aMin) lo = mid + 1;
|
|
837
870
|
else hi = mid;
|
|
838
871
|
}
|
|
839
872
|
for (let i = lo; i < rev.length; i++) {
|
|
840
873
|
const r = rev[i];
|
|
841
|
-
if (r.
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
const
|
|
874
|
+
if (r.anchor > aMax) break;
|
|
875
|
+
const ampliconStart = f.start;
|
|
876
|
+
const ampliconEnd = r.anchor + r.length - 1;
|
|
877
|
+
const ampliconLength = ampliconEnd - ampliconStart + 1;
|
|
878
|
+
if (ampliconLength < opts.ampliconMin || ampliconLength > opts.ampliconMax) continue;
|
|
879
|
+
const gStart = splicedToGenomic[ampliconStart];
|
|
880
|
+
const gEnd = splicedToGenomic[ampliconEnd];
|
|
845
881
|
if (gEnd - gStart + 1 < minGenomicSpan) continue;
|
|
846
882
|
if (Math.abs(f.tm - r.tm) > opts.maxTmDelta) continue;
|
|
847
883
|
const dimer = dimerThermo(f.sequence, r.sequence, opts.ctMolar ?? DEFAULT_CT_MOLAR);
|
|
848
884
|
if (dimer.any_tm > opts.maxDimerTm || dimer.end_tm > opts.maxDimerEndTm) continue;
|
|
885
|
+
const misK = opts.mispriming3PrimeBases ?? DESIGN_DEFAULTS.mispriming3PrimeBases;
|
|
849
886
|
let fMispriming = { count: 0, sites: [] };
|
|
850
887
|
let rMispriming = { count: 0, sites: [] };
|
|
851
888
|
if (misprimingIndex !== undefined) {
|
|
852
|
-
|
|
853
|
-
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start);
|
|
889
|
+
// Exclude each primer's own 3'-tail window (not its 5' end).
|
|
890
|
+
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start + f.length - misK);
|
|
854
891
|
if (fMispriming.count > maxSites) continue;
|
|
855
|
-
|
|
856
|
-
// the real oligo is its reverse complement, so its 3' tail is the RC
|
|
857
|
-
// of the FIRST k bases and the intended site is r.start
|
|
858
|
-
const rTail = reverseComplement(r.sequence.slice(0, misK));
|
|
859
|
-
rMispriming = misprimingForCandidate(r, rTail, misprimingIndex, opts, r.start);
|
|
892
|
+
rMispriming = misprimingForCandidate(r, r.sequence.slice(-misK), misprimingIndex, opts, r.anchor);
|
|
860
893
|
if (rMispriming.count > maxSites) continue;
|
|
861
894
|
}
|
|
862
|
-
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic);
|
|
863
|
-
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic);
|
|
895
|
+
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic, f.start, false);
|
|
896
|
+
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic, r.anchor, true);
|
|
864
897
|
const zone = opts.mismatch3PrimeZone ?? DESIGN_DEFAULTS.mismatch3PrimeZone;
|
|
865
898
|
let fTargetDistance;
|
|
866
899
|
let rTargetDistance;
|
|
867
900
|
let targetDistance;
|
|
868
901
|
if (targetPosition !== undefined) {
|
|
869
|
-
fTargetDistance = Math.abs(f.start + f.length - targetPosition); //
|
|
870
|
-
rTargetDistance = Math.abs(r.
|
|
902
|
+
fTargetDistance = Math.abs(f.start + f.length - targetPosition); // forward 3' end (1-based)
|
|
903
|
+
rTargetDistance = Math.abs(r.anchor + 1 - targetPosition); // reverse 3' end (1-based)
|
|
871
904
|
targetDistance = Math.min(fTargetDistance, rTargetDistance);
|
|
872
905
|
}
|
|
873
906
|
const penalty = 0.6 * Math.abs(f.tm - r.tm) + Math.abs(f.tm - tmCenter) + Math.abs(r.tm - tmCenter)
|
|
@@ -875,8 +908,8 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
875
908
|
+ 1.0 * Math.max(0, f.self_end - 1) + 1.0 * Math.max(0, r.self_end - 1)
|
|
876
909
|
+ 0.2 * Math.max(0, f.hairpin_tm - 40) + 0.2 * Math.max(0, r.hairpin_tm - 40)
|
|
877
910
|
+ 0.2 * Math.max(0, dimer.any_tm - 40) + 0.2 * Math.max(0, dimer.end_tm - 40)
|
|
878
|
-
+
|
|
879
|
-
+
|
|
911
|
+
+ intronMismatchPenalty(fMismatches, zone)
|
|
912
|
+
+ intronMismatchPenalty(rMismatches, zone)
|
|
880
913
|
+ 8 * fMispriming.count + 8 * rMispriming.count
|
|
881
914
|
+ (targetDistance !== undefined ? targetWeight * targetDistance : 0);
|
|
882
915
|
pairs.push({
|
|
@@ -894,9 +927,7 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
894
927
|
spliced_end: f.start + f.length,
|
|
895
928
|
genomic_start: splicedToGenomic[f.start] + 1,
|
|
896
929
|
genomic_end: splicedToGenomic[f.start + f.length - 1] + 1,
|
|
897
|
-
|
|
898
|
-
junction_left: f.junction_left,
|
|
899
|
-
junction_right: f.junction_right,
|
|
930
|
+
exon: f.exon + 1,
|
|
900
931
|
mismatch_count: fMismatches.length,
|
|
901
932
|
mismatches: fMismatches,
|
|
902
933
|
mispriming_count: fMispriming.count,
|
|
@@ -918,11 +949,13 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
918
949
|
hairpin_tm: r.hairpin_tm,
|
|
919
950
|
end_stability_kcal: r.end_stability_kcal,
|
|
920
951
|
end_gc_count: r.end_gc_count,
|
|
921
|
-
spliced_start: r.
|
|
922
|
-
spliced_end: r.
|
|
923
|
-
genomic_start: splicedToGenomic[r.
|
|
924
|
-
genomic_end: splicedToGenomic[r.
|
|
925
|
-
|
|
952
|
+
spliced_start: r.anchor + 1,
|
|
953
|
+
spliced_end: r.anchor + r.length,
|
|
954
|
+
genomic_start: splicedToGenomic[r.anchor] + 1,
|
|
955
|
+
genomic_end: splicedToGenomic[r.anchor + r.length - 1] + 1,
|
|
956
|
+
exons: [String(r.junction.up + 1), String(r.junction.down + 1)],
|
|
957
|
+
junction_left: r.junction_left,
|
|
958
|
+
junction_right: r.junction_right,
|
|
926
959
|
mismatch_count: rMismatches.length,
|
|
927
960
|
mismatches: rMismatches,
|
|
928
961
|
mispriming_count: rMispriming.count,
|
|
@@ -934,7 +967,7 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
934
967
|
})),
|
|
935
968
|
...(rTargetDistance !== undefined ? { target_distance: rTargetDistance } : {}),
|
|
936
969
|
},
|
|
937
|
-
spliced_amplicon: { start:
|
|
970
|
+
spliced_amplicon: { start: ampliconStart + 1, end: ampliconEnd + 1, length: ampliconLength },
|
|
938
971
|
genomic_amplicon_length: gEnd - gStart + 1,
|
|
939
972
|
...(targetDistance !== undefined ? { target_distance: targetDistance } : {}),
|
|
940
973
|
penalty: Math.round(penalty * 100) / 100,
|
|
@@ -945,7 +978,7 @@ export function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
|
945
978
|
const seen = new Set();
|
|
946
979
|
const unique = [];
|
|
947
980
|
for (const pair of pairs) {
|
|
948
|
-
const key =
|
|
981
|
+
const key = pair.spliced_amplicon.start + ':' + pair.spliced_amplicon.end;
|
|
949
982
|
if (seen.has(key)) continue;
|
|
950
983
|
seen.add(key);
|
|
951
984
|
unique.push(pair);
|
|
@@ -6,6 +6,11 @@ mainstream bioinformatics landscape offer that this toolset does **not**, and wh
|
|
|
6
6
|
deterministic algorithms in a few hundred lines of dependency-free JS with **no external data files,
|
|
7
7
|
binaries, network, or reference databases**?
|
|
8
8
|
|
|
9
|
+
> **Status note (0.24.0).** The baseline below is the 52-tool package this survey was written
|
|
10
|
+
> against. Since then rows 1–7, 11, 12, 23, 30, 34, 40 and the Cas-family half of row 20 have shipped
|
|
11
|
+
> (see [history.md](history.md) and the roadmap); the remaining rows are still candidates unless
|
|
12
|
+
> §3 names a blocker. The rankings are left as written so the original judgement can be audited.
|
|
13
|
+
|
|
9
14
|
**Method.** Surveyed nine source families (commercial plasmid/cloning suites; bench web calculators;
|
|
10
15
|
Biopython/BioPerl/SeqKit/EMBOSS command-line toolkits; standard sequence/statistics analyses;
|
|
11
16
|
microbiome; population genetics; protein prediction; alignment post-processing; sequencing QC),
|
package/docs/client-panel.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| **Molbio**(右栏 tab) | 列出会话工作区里的序列文件,选中即画:`.dna`/`.gb`/`.gbk` → 质粒图谱 + 特征表;`.fa`/`.fasta`(≥2 条)→ 序列标识图 | `workspaceFiles.readAll`(二进制,含 SnapGene) |
|
|
14
14
|
| **Papers**(右栏 tab) | 把 `molbio_paper_*` 工具维护的 `papers.json` 渲染成可搜索的阅读列表 + 详情面板(标题/作者/期刊/年份/PMID/URL/标签/笔记,标题链接到 PubMed 或原 URL) | `workspaceFiles.read`(文本) |
|
|
15
15
|
| **图谱调用卡**(`tool.call.toolview`) | `molbio_plasmid_map` / `molbio_plasmid_map_file` 的调用卡:在对话里直接画出这次调用产出的图谱(摘要行 + 写入路径),失败时显示错误文本与 Inspect 入口 | 工具自己声明的 `output.presentationMeta`(见下) |
|
|
16
|
+
| **图形调用卡**(`tool.call.toolview`) | `molbio_sequence_logo` / `molbio_grna_design` 的调用卡:logo 直接画图;gRNA 画 `map_path` 并列出前 5 条 guide(无 map 时仍有摘要与表格) | 同上,kind = `molbio-figure` |
|
|
16
17
|
| **PubMed API key**(`plugins.detail.section`) | 插件 → 已安装插件 → `dsh-molbio-tools` 详情页里的一段表单:password 输入 + Save/Remove,状态只报「已配置 / 来源 / 未配置」,永不回显值 | `ctx.remote.credentials.describe` / `set` / `unset`(host 的凭据 seam) |
|
|
17
18
|
|
|
18
19
|
**解析与渲染全部在浏览器里跑本仓库自己的模块**(`lib/genbank/snapgene/plasmid/msa/logo`),
|
|
@@ -47,11 +48,11 @@ SVG 随 meta 传输有上限(256 KB):真实载体的图谱 20-60 KB,但
|
|
|
47
48
|
| `build/client-bundle-core.mjs` | 打包器的**生成逻辑**(可 import):`createGenerator({entry, baseDir})` → 模块图 + `renderBundle(packageName)`;`test/contract.mjs` 在进程内用它重算产物做新鲜度检查(不 spawn,受限沙箱下也能验证) |
|
|
48
49
|
| `build/browser-api.mjs` | 浏览器安全面:从**包根的 `.mjs` 源文件**再导出面板可用的一切(单一事实来源) |
|
|
49
50
|
| `build/panel-core.mjs` | 面板的数据通路(分类/解码/解析/渲染/文献库投影),不含 React,可在 Node 里单测 |
|
|
50
|
-
| `build/client-entry.mjs` | 浏览器半本体:两个 tab 类型的注册、正文、标题 chip,两个 map 工具的调用卡,以及插件详情页的 key 配置段 |
|
|
51
|
+
| `build/client-entry.mjs` | 浏览器半本体:两个 tab 类型的注册、正文、标题 chip,两个 map 工具与两个 figure 工具的调用卡,以及插件详情页的 key 配置段 |
|
|
51
52
|
| `lib/client.js` | **构建产物**(`exports["./client"]` 指向它;由 `npm run build:client` 生成) |
|
|
52
|
-
| `packages/molbio-panel/` | 面板专用包(宿主半边空实现),面板与
|
|
53
|
+
| `packages/molbio-panel/` | 面板专用包(宿主半边空实现),面板与 64 个工具解耦的交付通道 |
|
|
53
54
|
| `test/client.mjs` | 按加载器的方式执行产物 + 驱动两条数据通路(含真实 pUC118 夹具) |
|
|
54
|
-
| `test/map-card.mjs` |
|
|
55
|
+
| `test/map-card.mjs` | 调用卡的**跨界**检查:地图卡与图形卡(logo/gRNA)的真实投影 → 卡片读取 → 渲染出 SVG(含降级、超限、坏 meta 路径) |
|
|
55
56
|
| `test/client-mount.mjs` | 复刻宿主侧图扫描,证明两个包都能挂上、依赖可解析 |
|
|
56
57
|
|
|
57
58
|
命令:`npm run build:client`(构建)、`npm test`(全部测试)、
|
|
@@ -250,10 +251,11 @@ shadow(无冲突),面板的 `remote`/`slot` 服务不受影响。
|
|
|
250
251
|
- **插件详情页的 key 配置**:✅ 已落地(0.22.1,见第 1 与第 4 节)。后续可把同一个
|
|
251
252
|
`plugins.detail.section` 模式用于其它凭据(例如文献库的机构代理),前提是它们也有一个
|
|
252
253
|
稳定的环境变量名。
|
|
253
|
-
- **`tool.call.toolview`**:✅ 已落地(见第 1 与第 3 节),覆盖两个 map
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
0.7.1 的卡片因为裸 `register()` 抢座位而让 GUI
|
|
254
|
+
- **`tool.call.toolview`**:✅ 已落地(见第 1 与第 3 节),覆盖两个 map 工具与
|
|
255
|
+
`molbio_sequence_logo` / `molbio_grna_design`(0.23.0,`molbio-figure` 投影)。
|
|
256
|
+
后续可考虑把同样的卡片给 `molbio_dotplot` 等新画图工具。**任何新卡片上线前先跑
|
|
257
|
+
`test/client.mjs` 的抢座位顺序那一层**:0.7.1 的卡片因为裸 `register()` 抢座位而让 GUI
|
|
258
|
+
起不来(0.7.2 修复,见 §4 第一条);`contract` 会从产物里的 key 表反推投影数。
|
|
257
259
|
- **文献库写回**(可选):若要面板内编辑笔记/标签,需要先定义与 `molbio_paper_*` 工具
|
|
258
260
|
一致的并发契约(乐观版本号或串行化队列),或让工具走同一条 RPC。
|
|
259
261
|
- **上游提案(路径 B)**:preset 渠道挂 client 仍是真实生态需求,可以在社区反馈时
|
package/docs/history.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
| 包版本 | preset 版本 | 内容 | 工具数 |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
|
+
| 0.24.0 | — | **数据卫生版**:新增 `molbio_contamination_screen`(内置载体/接头短签名,双链、0–2 错配)与 `molbio_fastq_trim`(质量/接头/引物/poly/N/长度,写 FASTQ/FASTA);修 FASTQ QC 接头表 Small RNA 5' 的 RNA→DNA 静默错 | 62 → 64 |
|
|
20
|
+
| 0.23.0 | — | **2.0 遗留修复 + 2.9a 调用卡 + 两个候选池工具**:跨内含子引物报告真实反向分子(F 在 R 上游)、logo/gRNA 调用卡、`molbio_protein_charge`、`molbio_dotplot`;同版完成 **DSH 0.2.2-rc.1 适配**(删掉上游已移除的 `list-agents` 子路径行、subagent 模型选择改用新键、守卫补上 specifier 解析) | 60 → 62 |
|
|
19
21
|
| 0.22.2 | — | **DSH 0.2.1-rc.5 适配**:基线 rc.3 → rc.5(上游 standard 逐字节未变);contract 声明守卫兼容桌面版裁掉 `lib/types` 的打包形态(+突变自检);engines.node 与 CI 降到 24 | 60 |
|
|
20
22
|
| 0.22.1 | — | **PubMed key 改走 DSH 凭据库**:解析顺序 `api_key` → 凭据库 → 环境变量;无凭据库时行为同 0.22.0 | 60 |
|
|
21
23
|
| 0.22.0 | — | **DSH 0.2.1-rc.3 适配 + PubMed 官方 API**:`molbio_pubmed_search`/`_abstract` 支持 NCBI API key(E-utilities),无 key 行为不变 | 60 |
|
|
@@ -47,6 +49,66 @@
|
|
|
47
49
|
|
|
48
50
|
---
|
|
49
51
|
|
|
52
|
+
## v0.24.0(2026-11-05,包 0.24.0)
|
|
53
|
+
|
|
54
|
+
**三条教训,都关于“表/边界看起来对、实际不对”。**
|
|
55
|
+
|
|
56
|
+
**(1) 接头表是 RNA,reads 是 DNA。** `fastq-qc.mjs` 的 Illumina Small RNA 5' 条目一直是
|
|
57
|
+
`GUUCAG…`(RNA,含 U),而 `adapterContent` 直接拿它去 DNA reads 里做 `indexOf`。正向分子
|
|
58
|
+
永远匹配不上,只有它的反向互补能命中,所以旧测试/使用中会看到“接头只报一半”。这不是算法错,
|
|
59
|
+
是**数据表示**错:现在表放在 `contamination.mjs` 并以 DNA 形式定义(U→T),`fastq-qc` 只是
|
|
60
|
+
导入/再导出;smoke 用正向与反向互补两条 read 断言两者都命中。
|
|
61
|
+
|
|
62
|
+
**(2) 短签名筛查必须把“不是数据库”写进输出。** 内置表能回答“有没有 pUC/pET/attB/Illumina
|
|
63
|
+
接头”,但不能回答“有没有未知载体”。描述、README 与 render 都写明:命中是线索,**未命中
|
|
64
|
+
不等于干净**;默认 exact,`max_mismatches` 才启用 seed-and-verify(不扫描每个窗口)。
|
|
65
|
+
|
|
66
|
+
**(3) 修剪工具的价值在“顺序与边界”,不在旋钮数量。** `molbio_fastq_trim` 固定 5' 引物 →
|
|
67
|
+
3' 接头 → 3' 质量尾 → poly 尾 → 长度/N 的顺序,并把每个阶段的计数与条件回显在 `criteria` 里。
|
|
68
|
+
不做 paired-end merge、semiglobal adapter alignment、UMI;这些另开工具比塞进一个“全能”工具
|
|
69
|
+
更诚实。
|
|
70
|
+
|
|
71
|
+
**顺带记录**:`seqio.mjs` 新增 `toFastq` 进了浏览器半的模块图,所以 0.24.0 虽然不改面板行为,
|
|
72
|
+
仍重建了两个 `lib/client.js` 并把面板包 `0.1.8 → 0.1.9`;不 bump 会在发布时用同版本号发不同字节。
|
|
73
|
+
定向模型跑分覆盖 `contamination-screen` 与 `fastq-trim`,两题 2/2(409,576 tokens,70 s;详见 CHANGELOG)。
|
|
74
|
+
|
|
75
|
+
## v0.23.0(2026-10-31,包 0.23.0)
|
|
76
|
+
|
|
77
|
+
三条教训,都关于"一个约定改了,哪些地方必须跟着改"。
|
|
78
|
+
|
|
79
|
+
**(1) 方向不是标签问题,是约束落在哪个分子上的问题。** 跨内含子工具把两条引物都报成 sense
|
|
80
|
+
子串,看起来只是"名字反了";真正的代价是 GC clamp、末端稳定性、末端 GC、self-end 与发夹
|
|
81
|
+
**全都检查在反向引物的 5' 端**(因为扫描方向反了),而报告的那条分子还不是要订购的。
|
|
82
|
+
修法不是交换两个字段:反向候选要改到剪接转录本的**反向互补**上扫描,才让 3' 约束落到
|
|
83
|
+
真正下单的分子上,再由 `anchor` 映回 sense 坐标。这也解释了为什么 smoke 夹具必须换 seed:
|
|
84
|
+
旧夹具能在错误的约束下通过,改正后同一段序列不再产生合法候选。
|
|
85
|
+
|
|
86
|
+
**(2) 两份字段命名(snake_case/camelCase)之间"看起来能跑"的调用,是静默失效的温床。**
|
|
87
|
+
旧代码把 `mismatchReport` 的 `distance_from_3prime` 报告直接喂给读 `distanceFrom3Prime` 的
|
|
88
|
+
`mismatchPenalty`,于是 3' 关键区加罚在跨内含子模式里**从未生效**——没有报错、没有测试红,
|
|
89
|
+
只是排序少了一项。现在用专门的 `intronMismatchPenalty`,两种命名不再混用。
|
|
90
|
+
|
|
91
|
+
**(3) 把"一个 key 列表"写成常量时,第二个列表不会自己出现。** 加调用卡时最容易漏的是
|
|
92
|
+
守卫:`contract` 原本只读 `MAP_TOOL_KEYS`,新卡片的工具即使没有 `presentationMeta` 也照样绿。
|
|
93
|
+
现在它从产物里的 `MAP_TOOL_KEYS` 与 `FIGURE_TOOL_KEYS` **两处**读 key,并用行首锚定的正则
|
|
94
|
+
数投影——文件头注释里也提到 `presentationMeta(args, value)`,子串计数会把散文当成第四个实现。
|
|
95
|
+
这类"守卫自己的样本过期"在 0.21.0 的 drift-probe 上已经付过一次账。
|
|
96
|
+
|
|
97
|
+
**(4) 包还在,不等于行能挂载。** 0.2.2-rc.1 把 `list_agents` 并入了
|
|
98
|
+
`@deepseek-ai/dsh-tool-subagent-control` 主入口,删掉了 `./list-agents` 子路径导出。preset 仍抄着
|
|
99
|
+
rc.5 的那一行,而 `preset-health` 的 `packageInstalled` 只证明**包目录存在**,随后 import 的还是
|
|
100
|
+
包根的 `exports["."]`——于是"包在、子路径没了"的行一路绿。真正的解析发生在 Loader 里
|
|
101
|
+
(`import.meta.resolve`,受 `exports` 映射约束),所以现在的守卫先用 `createRequire(...).resolve`
|
|
102
|
+
解析行名,再用 `no-such-subpath` 做突变自检。**组合树正确不代表行能挂载**,这和规则 1 记录的是
|
|
103
|
+
同一类失败:静态检查看不见,只有真正解析/启动一次才会暴露。
|
|
104
|
+
|
|
105
|
+
**顺带记录**:新工具的 pK 表与电荷函数从 `protein.mjs` 导出(`PK_SETS`/`netCharge`),
|
|
106
|
+
而不是在 `protein-charge.mjs` 里复制一份;两个工具各自只加一条 benchmark 任务,
|
|
107
|
+
`benchmark-coverage` 的 62/62 与 `benchmark-profile` 的 62 工具是同一批断言的两种读法。
|
|
108
|
+
`dotplot` 的版式在收尾时从 720×720 改为 720×760(绘图区仍是 628 px 正方形),让 x 轴标题与
|
|
109
|
+
图例各占一行;`test/smoke.mjs` 用几何断言盯住「标题不贴底、图例在画布内且在标题下方」。
|
|
110
|
+
定向真实模型跑分(2026-11-05)覆盖 `intron-qpcr` / `protein-charge` / `dotplot`,三题 3/3。
|
|
111
|
+
|
|
50
112
|
## v0.22.1(2026-10-25,包 0.22.1)
|
|
51
113
|
|
|
52
114
|
**环境变量不是唯一选择。** 0.22.0 只从 `api_key` 参数与 `process.env` 读 key;这一版把 DSH 的
|
package/docs/maintainer.md
CHANGED
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
|
|
31
31
|
## 60 秒速览
|
|
32
32
|
|
|
33
|
-
- **包**:`dsh-molbio-tools`,零依赖 DSH 插件包,**
|
|
33
|
+
- **包**:`dsh-molbio-tools`,零依赖 DSH 插件包,**64 个 `molbio_*` 工具**(其中
|
|
34
34
|
`molbio_plot` 一个工具承载 8 种图表)。
|
|
35
|
-
- **基线**:DSH **0.2.
|
|
35
|
+
- **基线**:DSH **0.2.2-rc.1**;`package.json` 版本 0.24.0(DSH 桌面版内嵌 Node 24.21.0)。
|
|
36
36
|
- **两条分发渠道**:根包(工具 + 面板)与 `packages/molbio-panel`(只面板),
|
|
37
37
|
**两个独立的 npm 条目、两条独立的版本线**,必须分别发布。
|
|
38
38
|
- **工具只在专属模式里出现**(设计如此):bundle 的 `cordis.patch.yml` 只插一行**惰性宿主锚点**
|
|
39
|
-
(`molbio-client` → `./host.mjs`),
|
|
39
|
+
(`molbio-client` → `./host.mjs`),64 个工具由同文件里的 preset insert 承载,
|
|
40
40
|
只会挂进 "Molecular Biology Lab" 模式。
|
|
41
41
|
- **测试**:`npm test` = 插件(`smoke`)+ 画面(`svgpng`/`svgio`/`charts`)+ 客户端四套件 +
|
|
42
42
|
DSH 契约(`contract`)+ 组合(`preset-health`)+ 漂移守卫(`drift-probe`)+
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
`build/release-preflight.mjs`,然后用 **OIDC 可信发布**(仓库里没有任何长期 token)分两步发
|
|
46
46
|
两个 npm 条目,最后用 CHANGELOG 该版本段落建 GitHub Release。**首次发布必须手动做一次**
|
|
47
47
|
(可信发布要求包已存在),完整步骤见 [workflow.md](workflow.md) 第 4 节的 CD 段。
|
|
48
|
-
- **benchmark**:`npm run bench` 用真实模型跑 core 档 14 题(`bench:full` 全部
|
|
48
|
+
- **benchmark**:`npm run bench` 用真实模型跑 core 档 14 题(`bench:full` 全部 65 题),分
|
|
49
49
|
`tools_ok` / `args_ok` / `answer_ok` 三项判分;`npm run bench:offline` 零成本校验期望值。
|
|
50
50
|
|
|
51
51
|
包内目录结构:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
dsh-molbio-tools/
|
|
55
|
-
├── index.mjs # 插件入口:export { name, inject, apply },注册
|
|
55
|
+
├── index.mjs # 插件入口:export { name, inject, apply },注册 64 个工具
|
|
56
56
|
├── lib.mjs # 基础库:IUPAC、翻译、酶表、NN 热力学、qPCR、lab math、甲基化/buffer 参考表
|
|
57
57
|
├── design.mjs # 引物自动设计(含跨内含子 qPCR)
|
|
58
58
|
├── taqman.mjs # TaqMan 水解探针设计(复用 design.mjs 的引物引擎)
|
|
@@ -60,6 +60,9 @@ dsh-molbio-tools/
|
|
|
60
60
|
├── methylation.mjs # 甲基化敏感位点检查 + 双酶切 buffer 兼容
|
|
61
61
|
├── protein.mjs # 蛋白性质 / 肽段酶切 / 密码子优化
|
|
62
62
|
├── protein-structure.mjs # 螺旋轮与疏水性图
|
|
63
|
+
├── protein-charge.mjs # 电荷-pH 滴定曲线 + pI(两套 pK 表)
|
|
64
|
+
├── contamination.mjs # 内置载体/接头短签名 + 双链污染筛查
|
|
65
|
+
├── fastq-trim.mjs # FASTQ 修剪/过滤(质量/接头/引物/poly/N/长度)
|
|
63
66
|
├── genbank.mjs # GenBank flatfile 解析器
|
|
64
67
|
├── snapgene.mjs # SnapGene .dna 二进制解析器
|
|
65
68
|
├── plasmid.mjs # SVG 质粒图谱渲染器
|