dsh-molbio-tools 0.19.0 → 0.20.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +65 -21
  3. package/align.mjs +28 -0
  4. package/batch.mjs +185 -0
  5. package/benchmark/README.md +27 -8
  6. package/benchmark/sequences.mjs +7 -0
  7. package/benchmark/tasks/02-primers.json +132 -19
  8. package/benchmark/tasks/03-restriction.json +113 -29
  9. package/benchmark/tasks/04-cloning.json +167 -34
  10. package/benchmark/tasks/05-plasmids.json +172 -29
  11. package/benchmark/tasks/06-analysis.json +387 -100
  12. package/benchmark/tasks/09-formats.json +132 -23
  13. package/benchmark/tools.mjs +28 -2
  14. package/benchmark/verifications.mjs +17 -1
  15. package/build/release-preflight.mjs +2 -2
  16. package/crispr.mjs +392 -78
  17. package/docs/history.md +10 -0
  18. package/docs/maintainer.md +8 -6
  19. package/docs/roadmap.md +22 -8
  20. package/docs/rules.md +1 -1
  21. package/docs/workflow.md +7 -7
  22. package/fastq-qc.mjs +735 -735
  23. package/index.mjs +573 -36
  24. package/lib/client.js +665 -86
  25. package/multiplex.mjs +87 -0
  26. package/package.json +9 -4
  27. package/packages/molbio-panel/lib/client.js +665 -86
  28. package/packages/molbio-panel/package.json +1 -1
  29. package/preset/molbio-lab/agent.cordis.yml +4 -4
  30. package/preset/molbio-lab/preset.patch.yml +2 -2
  31. package/preset/molbio-lab/preset.yml +1 -1
  32. package/silent-site.mjs +286 -0
  33. package/snapgene.mjs +17 -1
  34. package/taqman.mjs +163 -7
  35. package/test/contract.mjs +14 -0
  36. package/test/release.mjs +23 -3
  37. package/test/smoke.mjs +377 -3
  38. package/benchmark/reports/20260923-142935-suite.json +0 -545
  39. package/benchmark/reports/20260923-142935-suite.md +0 -30
  40. package/benchmark/reports/20260923-190149-suite.json +0 -2024
  41. package/benchmark/reports/20260923-190149-suite.md +0 -235
  42. package/benchmark/reports/20260923-192913-suite.json +0 -1856
  43. package/benchmark/reports/20260923-192913-suite.md +0 -158
  44. package/benchmark/reports/20260923-200212-suite.json +0 -3587
  45. package/benchmark/reports/20260923-200212-suite.md +0 -163
  46. package/benchmark/reports/20260923-202830-suite.json +0 -3129
  47. package/benchmark/reports/20260923-202830-suite.md +0 -99
  48. package/benchmark/reports/20260923-203856-suite.json +0 -71
  49. package/benchmark/reports/20260923-203856-suite.md +0 -16
package/CHANGELOG.md CHANGED
@@ -10,6 +10,106 @@
10
10
  改为按**包名**引用后模块缓存问题随之消失——没有拷贝,也就没有需要保持同步的版本目录。
11
11
  下文历史条目里出现的 `vN` 目录保持原样,作为当时的记录。
12
12
 
13
+ ## [0.20.0] — 2026-10-17(Cas 家族几何、qPCR 订购/配平、批量分析与静默位点突变;并入 0.19.1 硬化)
14
+
15
+ **工具数 58 → 60,benchmark 56 → 61 题(60/60 覆盖)。** 这一版把 0.19.1 计划的四项硬化直接并入,
16
+ 并落地 roadmap 2.3 / 2.5 / 2.7 / 2.8 与 survey #8。Cas12a/Cas13 不是"再加一个 PAM 字符串":
17
+ 原来的 `findProtospacers` 把 3' PAM、20 nt 和 3' 种子写死在几何里,对 Cas12a/Cas13 会给出**生物学上错误**的
18
+ 坐标与评分,因此这一版把家族几何做成一张表,并补上了反向链脱靶定位的静默错误。
19
+
20
+ ### 1. 并入的 0.19.1 硬化(原计划单发 patch,现随 0.20.0 一起)
21
+
22
+ - **Smith-Waterman 的 DP 矩阵上限**:`molbio_align` 在分配前拒绝超过 50,000,000 cells 的输入对,
23
+ 而不是让两个几百 kb 的序列把宿主进程的内存吃光;工具描述同步说明"对齐窗口或改用
24
+ `molbio_verify_sanger`"。`test/smoke.mjs` 用 10 kb × 10 kb 的输入证明拒绝发生在分配之前。
25
+ - **SnapGene/PubMed XML 数值实体**:`�` 曾让 `String.fromCodePoint` 抛出 RangeError
26
+ 并逃出整个解析器;现在越界码点与代理项按字面文本保留,合法实体(含星形平面)照常解码。
27
+ - **`molbio_sequence_logo` 的并发声明**:它写共享默认路径 `logo.svg`,之前却默认 `safe`;
28
+ 现与其他画图工具一致声明 `safe: false`。
29
+ - **npm `files` 白名单**:不再整目录带 `benchmark`,改为 `benchmark/*.mjs`、`benchmark/tasks`、
30
+ `benchmark/README.md`,因此 `.gitignore` 里的本机 `benchmark/reports/` 不会再被打进包
31
+ (此前约 1.4 MB)。`test/contract.mjs` 直接禁止裸 `benchmark` 条目。
32
+
33
+ ### 2. roadmap 2.5:Cas12a/Cas13 家族几何(以及反向脱靶定位缺陷)
34
+
35
+ - `crispr.mjs` 新增 `CAS_FAMILIES` 表:SpCas9(3' NGG / 20 nt / 3' 种子)、SaCas9
36
+ (3' NNGRRT / 21 nt)、Cas12a(**5' TTTV / 23 nt / 5' 种子**)、Cas13a(RNA / 3' H PFS / 28 nt)、
37
+ Cas13b(RNA / 5' D + 3' NAN / 30 nt)、Cas13d(RNA / 无 PFS / 22 nt)。PAM 校验现在按
38
+ **PAM 的哪一侧贴近 protospacer** 检查简并位,而不是一律要求首位为 N——`TTTV` 因此不再被误拒。
39
+ - **Cas13 不是 PAM**:它有 PFS **偏好**,工具报告 PFS、按缺失扣分并明确说明"不是硬性 PAM";
40
+ 只有 `require_pfs=true` 才过滤。Cas13 的 `tm_celsius` 返回 `null`,因为对 RNA guide 报 DNA
41
+ NN Tm 是另一个分子;工具描述与渲染都写明这一点。
42
+ - **反向链脱靶定位是真 bug**:索引把每个 k-mer 记在 top-strand 起点,旧 `finalizeSites` 却对
43
+ 反向命中减去 `guideLength-1`,使它们整体偏移 19 nt 并大多越界丢弃——正向 fixture 永远看不见。
44
+ 现在两种链都从 `entry.position` 起算,PAM 按 5'/3' 侧与链向取 top-strand 窗口;新增的
45
+ smoke fixture 在 forward guide 与 reverse perfect copy 之间互指,断言反向脱靶 `start/end/pam`
46
+ 与 0 mismatch。
47
+ - 新 benchmark:`crispr-cas12a`(5' TTTV、23 nt、坐标 9-31)与 `crispr-cas13`(RNA、PFS A、28 nt)。
48
+ `test/smoke.mjs` 另钉住 Cas12a/Cas13a 的几何、PFS 过滤与错误路径。
49
+
50
+ ### 3. roadmap 2.8:TaqMan MGB 与订购 CSV
51
+
52
+ - `probe_chemistry: 'mgb'` 使用 13-20 nt、未修饰 NN Tm 48-60 °C 的窗口;**MGB 修饰的 Tm 不被
53
+ NN 模型预测**,因此报告的是未修饰 Tm,并以 `tm_model` 与 notes 明确要求向供应商复核。
54
+ MGB 不施加"探针 Tm 高于引物"的未修饰差值罚分(那会把每个 MGB 候选都罚掉)。
55
+ - 新增 `taqmanOrderRows` / `taqmanOrderCsv`:每条 assay 三行(F/R/P),列含 5'/3' 修饰、规模、
56
+ 纯化与说明;MGB 默认 `NFQ-MGB`,5' 默认 FAM。`save_path` 写 CSV;不传则只返回结构化 order。
57
+ - 标准 chemistry 的默认值、排序与已发布数值保持不变(smoke 的 9 assays 与 pinned probe 仍绿)。
58
+
59
+ ### 4. roadmap 2.7:多重 PCR 的退火温度与浓度配平建议
60
+
61
+ - `checkMultiplex` 新增 `annealing`:最低引物 Tm − `annealing_offset_c`(默认 3 °C),
62
+ 给出 ±`annealing_window_c` 范围、最低 Tm 引物、Tm 离散度与是否超过警告阈值。
63
+ - 新增 `primer_concentrations` 与 `concentration_note`:默认 200 nM;当扩增子大小比 ≥ 2 时,
64
+ 最短的降到 150 nM、最长的升到 300 nM,并明确这是**起始点**而非实测最优。
65
+ - 这些是清晰标注的规则性建议,notes 写明"不是热循环优化,需跑梯度"。
66
+
67
+ ### 5. roadmap 2.3:批量分析 + CSV/TSV
68
+
69
+ - 新模块 `batch.mjs` 与新工具 `molbio_batch_analyze`:扫描目录(可递归)或显式 paths,复用
70
+ 同一批 FASTA/FASTQ/GenBank/SnapGene 解析器,输出一张 CSV/TSV。
71
+ - 每行:格式、拓扑、条目数、总长、GC%、N%、特征数、平均长度;`analysis: orf` 加 ORF 数/最长
72
+ ORF,`analysis: enzymes` 加指定酶的位点数,`all` 两者都加。单个坏文件变成一行 `error`,
73
+ 其余文件继续;文件/字节上限与 `truncated` 都显式报告。
74
+ - 拓扑按 GenBank/SnapGene 声明优先(环状质粒的位点计数不会被当成线性);无声明时才用
75
+ `circular` 参数。新 benchmark `batch-analysis` 同时读 `pUC118.gb` 与 `pUC118.dna`。
76
+
77
+ ### 6. survey #8:静默酶切位点突变
78
+
79
+ - 新模块 `silent-site.mjs` 与新工具 `molbio_silent_site_mutagenesis`:只在同义密码子内枚举,
80
+ 因此**蛋白序列按构造不变**;输出逐碱基变化、密码子前后与氨基酸、可直接喂给
81
+ `molbio_mutagenesis_primers` 的 mutation texts、以及突变后序列。
82
+ - 支持 remove/add、frame 1-3、可选 `host`(按 e_coli/yeast/human 密码子偏好排序)与区域限制;
83
+ 只接受识别序列无简并碱基的酶(`GGTNACC` 这类直接拒绝而不是猜)。
84
+ - 新 benchmark `silent-site` 钉住 EcoRI 的 `C12T`(TTC→TTT,Phe)与突变后序列。
85
+
86
+ ### 7. 版本、基线与文档
87
+
88
+ - 根包 `0.19.0 → 0.20.0`,面板 `0.1.2 → 0.1.3`(客户端产物因 crispr/taqman/multiplex 改动而变,
89
+ 两个 npm 条目需分别发布)。
90
+ - **DSH 基线 `0.1.7-rc.1 → 0.2.0-rc.2`**:`.github/workflows/test.yml` 的 pin、roadmap/maintainer/
91
+ workflow 与 preset 头注释同步;`preset-health` 在 0.2.0-rc.2 上仍为 30 行全过(行清单无需改动)。
92
+ - README/preset 描述/工具表/benchmark 计数同步到 **60 tools / 61 tasks**;新增模块进入 npm
93
+ `files` 白名单与目录树。
94
+ - **未做**:`benchmark-score` 里 2 条 0.16.0 之前的冻结响应(`methylation-block`、`qpcr-primers`)
95
+ 仍是 suspended;本次没有用真实模型重录,测试输出会继续点名这两条,而不是假装它们已通过。
96
+
97
+ ### 8. 发布前完整 benchmark(2026-10-17,真实模型)
98
+
99
+ - **61 题首跑 55/61**:tool selection **61/61**,arguments **59/61**,final answer **57/61**;
100
+ 12.7M tokens / 1747 s。
101
+ - **新功能任务全部通过**:`taqman-mgb`、`crispr-cas12a`、`crispr-cas13`、`silent-site`、
102
+ `batch-analysis`。`multiplex-panel` 在把"退火温度/浓度"明确写进指令后复跑通过。
103
+ - **4 个失败是断言写死格式,不是产品缺陷**:`plasmid-digest`(模型写 `2,840`)、
104
+ `protein-props`(`3,935`)、`codon-optimize`(`EcoRI-free`)、`chart-volcano`
105
+ (任务要求 `de.svg`,断言却写 `volcano.svg`)。修正后 `--replay` 四题全部转为 PASS。
106
+ - **1 个真实可用性缺陷**:`molbio_extract_region` 的 schema 广告 `sequence`,但
107
+ `resolveVector` 只读 `vector`,于是文档里写的参数被静默忽略、调用报
108
+ "provide either vector or vector_path"。现在 `vector`/`sequence`/`reference` 三个已声明别名
109
+ 都接受,smoke 增加回归断言。该任务的真实模型复跑因 DSH 沙箱对 headless 子进程的审批限制
110
+ 未完成,因此发布时的实测口径是 **60/61**(这一题以 smoke 回归 + 修复代码为准,而不是假装已跑)。
111
+ - benchmark 工具本身未改;全量首跑使用了一次性更宽沙箱来启动 headless DSH 子进程。
112
+
13
113
  ## [0.19.0] — 2026-10-11(环形树的旋转多行标签;比对后处理套件;持续交付)
14
114
 
15
115
  **工具数 57 → 58。** 这一版是 [roadmap](docs/roadmap.md) 2.4 与 2.2 两条候选的落地:
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-molbio-tools
2
2
 
3
- **给 AI 助手装上分子生物学的"手"**:一个零依赖的 DeepSeek Harness 插件包,为它提供 **58 个
3
+ **给 AI 助手装上分子生物学的"手"**:一个零依赖的 DeepSeek Harness 插件包,为它提供 **60 个
4
4
  `molbio_*` 工具**——序列分析、引物/探针自动设计、克隆模拟、质粒图谱、测序验证、蛋白与 CRISPR
5
5
  分析、qPCR 统计、文献与实验记录。装上之后,你可以直接用自然语言让助手完成这些计算,并把结果
6
6
  画成图。
@@ -31,7 +31,7 @@
31
31
  ## 目录
32
32
 
33
33
  - [安装](#安装)(3 步)
34
- - [它会做什么](#它会做什么)(按任务分组的 58 个工具)
34
+ - [它会做什么](#它会做什么)(按任务分组的 60 个工具)
35
35
  - [典型用法示例](#典型用法示例)
36
36
  - [输出文件、自动打开与浏览器面板](#输出文件自动打开与浏览器面板)
37
37
  - [方法学与使用须知](#方法学与使用须知)(各工具的模型、估算范围与边界)
@@ -46,7 +46,7 @@
46
46
 
47
47
  ### 推荐:专属模式 preset(2 步)
48
48
 
49
- 装完后,预设选择器里会出现 **Molecular Biology Lab** 模式;**58 个工具只在这个模式里出现**,
49
+ 装完后,预设选择器里会出现 **Molecular Biology Lab** 模式;**60 个工具只在这个模式里出现**,
50
50
  不会污染你其它会话(这点很重要:工具多了会占提示预算)。
51
51
 
52
52
  ```powershell
@@ -86,9 +86,9 @@ dsh plugin --profile web update dsh-molbio-tools
86
86
  | 层 | 作用 |
87
87
  | --- | --- |
88
88
  | `cordis.patch.yml` | 只插一行**惰性宿主锚点**(`./host.mjs`),不注册任何工具 |
89
- | `preset/molbio-lab/preset.patch.yml` | 声明 **Molecular Biology Lab** 模式,58 个工具在这一层里 |
89
+ | `preset/molbio-lab/preset.patch.yml` | 声明 **Molecular Biology Lab** 模式,60 个工具在这一层里 |
90
90
 
91
- 所以 58 个工具**只在选中该模式的会话里加载**,其它模式(standard / ptc / minimal / cordis)
91
+ 所以 60 个工具**只在选中该模式的会话里加载**,其它模式(standard / ptc / minimal / cordis)
92
92
  不带它们,不占提示预算。bundle 安装 ≠ 工具全局可见:bundle 只是把"模式"和"面板"一起装进来。
93
93
 
94
94
  > **那一行锚点不是多余的。** DSH 的客户端模块扫描只走**宿主 Loader 树**(官方原话:
@@ -104,7 +104,7 @@ dsh plugin --profile web update dsh-molbio-tools
104
104
 
105
105
  ### 只要浏览器面板(可选)
106
106
 
107
- 如果只想要图形面板、不想让该 profile 装 58 个工具(连模式都不装),装面板专用包即可
107
+ 如果只想要图形面板、不想让该 profile 装 60 个工具(连模式都不装),装面板专用包即可
108
108
  (见[浏览器面板](#浏览器面板)):
109
109
 
110
110
  ```powershell
@@ -115,7 +115,7 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
115
115
 
116
116
  ## 它会做什么
117
117
 
118
- 58 个工具按任务分为十组。下表是"你想做什么 → 用哪个工具"的索引,参数细节模型会在调用时自己
118
+ 60 个工具按任务分为十组。下表是"你想做什么 → 用哪个工具"的索引,参数细节模型会在调用时自己
119
119
  填;你只需要把意图说清楚。
120
120
 
121
121
  ### 1. 序列分析与酶切(7)
@@ -136,10 +136,10 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
136
136
  | --- | --- |
137
137
  | `molbio_design_primers` | **自动设计 PCR 引物对**:Tm(SantaLucia 1998 最近邻模型,盐/浓度可调)、GC、GC clamp、run/发夹/自互补/二聚体约束、错配容差、非特异结合检查、3' 目标位置偏好(SNP/定点设计),按罚分排序给出多组候选 |
138
138
  | `molbio_design_intron_primers` | **跨内含子 qPCR 引物**:给定基因组序列 + 外显子坐标,正向引物跨外显子-外显子连接点,基因组 DNA 不会扩增(可设最小基因组间距) |
139
- | `molbio_design_taqman` | **TaqMan(水解探针)测定设计**:引物对 + 探针一次给出(探针 5' 端非 G、Tm 比引物高 5 °C 起、无 run/重复、不与引物二聚化),每条候选附逐项罚分 |
139
+ | `molbio_design_taqman` | **TaqMan(水解探针)测定设计**:引物对 + 探针一次给出(探针 5' 端非 G、Tm 比引物高 5 °C 起、无 run/重复、不与引物二聚化),每条候选附逐项罚分;支持 **MGB 化学**(更短、按未修饰 NN Tm 报告并明确标注修饰未建模)和**订购 CSV**(引物/探针、5'/3' 修饰、规模、纯化) |
140
140
  | `molbio_primer_tm` | 单条引物 Tm 估算(Na⁺/Mg²⁺/dNTP 盐校正) |
141
141
  | `molbio_primer_check` | 引物结构筛查:重复序列、3' 端加权的自互补、发夹、引物对二聚体,以及 Primer3 同款热力学指标 |
142
- | `molbio_multiplex_check` | **多重 PCR 互扰检查**:面板内所有引物对的二聚体、3' 尾的脱靶与跨模板退火、扩增子是否能在胶上分辨,并给出重设计建议 |
142
+ | `molbio_multiplex_check` | **多重 PCR 互扰检查**:面板内所有引物对的二聚体、3' 尾的脱靶与跨模板退火、扩增子是否能在胶上分辨,并给出重设计建议;另给出**退火温度建议**(最低引物 Tm − offset,含范围与 Tm 离散警告)和**引物浓度起始计划**(按扩增子大小比例配平) |
143
143
 
144
144
  ### 3. 质粒与图谱(4)
145
145
 
@@ -150,7 +150,7 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
150
150
  | `molbio_plasmid_map_file` | **一步成图**:直接读 `.dna`/`.gb`/`.gbk` → 把 SVG 图谱**写入工作区**并自动打开 |
151
151
  | `molbio_plasmid_map` | 从序列 + 特征数组渲染环形/线形图谱(可选 GC skew 环、单切酶标记) |
152
152
 
153
- ### 4. 克隆构建(6)
153
+ ### 4. 克隆构建(7)
154
154
 
155
155
  | 工具 | 用途 |
156
156
  | --- | --- |
@@ -159,6 +159,7 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
159
159
  | `molbio_golden_gate` | **Golden Gate 多片段组装**:自动设计 4 bp 突出端、检查酶不切片段、给出可直接下单的片段序列、拼出最终质粒 |
160
160
  | `molbio_clone_primers` | 给插入片段加酶切位点 + 保护碱基(内置推荐表)或 Gibson 同源臂,并复检全长 Tm/GC/二聚体 |
161
161
  | `molbio_mutagenesis_primers` | QuickChange 式定点突变引物(`A123G`/`123A>G`/`123_125del`/`after123insGCT`),报告氨基酸变化 |
162
+ | `molbio_silent_site_mutagenesis` | **静默酶切位点突变**:在同义密码子范围内添加/移除限制性位点,保持蛋白序列不变;输出逐碱基变化、可直接喂给 `molbio_mutagenesis_primers` 的 mutation texts、突变后序列;只支持识别序列无简并碱基的酶 |
162
163
  | `molbio_extract_region` | 从质粒文件按特征名(如 "AmpR"、"MCS")或坐标提取子序列,可选反向互补与存 FASTA |
163
164
 
164
165
  ### 5. 测序与实验数据(6)
@@ -182,7 +183,7 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
182
183
  | `molbio_helical_wheel` | **螺旋轮投影**:残基按 3.6 残基/圈落在圆周上、按性质着色,给出疏水矩 μH(判断是否两亲性),出 SVG |
183
184
  | `molbio_hydropathy_plot` | **Kyte-Doolittle 疏水性图**:滑动窗口曲线 + GRAVY + 达到跨膜阈值的峰,出 SVG |
184
185
 
185
- ### 7. 比对与保守性(6)
186
+ ### 7. 比对与保守性(7)
186
187
 
187
188
  | 工具 | 用途 |
188
189
  | --- | --- |
@@ -192,12 +193,13 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
192
193
  | `molbio_alignment_postprocess` | **比对后处理**:共识(plurality/union/strict 三选一)、按缺口比例修剪(报原始 1-based 列号)、两两同一性矩阵、逐列覆盖度 |
193
194
  | `molbio_sequence_logo` | **序列标识图**:把保守性画成 SVG logo(字母高度 = 信息量 bits) |
194
195
  | `molbio_fasta_fastq` | 工作区 FASTA/FASTQ:条目统计、按 id 提取、FASTQ→FASTA、FASTQ 质量报告 |
196
+ | `molbio_batch_analyze` | **批量文件分析 + CSV/TSV**:对目录(或显式 paths)里所有匹配的 FASTA/FASTQ/GenBank/.dna 跑同一套摘要/ORF/酶切位点分析;单个坏文件是一行 error,不会中断整批 |
195
197
 
196
198
  ### 8. CRISPR(1)
197
199
 
198
200
  | 工具 | 用途 |
199
201
  | --- | --- |
200
- | `molbio_grna_design` | **gRNA 设计**(默认 SpCas9 `NGG` PAM):双链扫描、GC/poly-T/自互补/发夹等质量指标、逐项公开的启发式评分,以及**同序列内的错配容差脱靶搜索**(种子区不许错配);可出订购 CSV 与带标记的图谱 |
202
+ | `molbio_grna_design` | **gRNA 设计**(默认 SpCas9 `NGG` PAM,亦可 SaCas9/Cas12a/Cas13a/Cas13b/Cas13d):按家族选择 PAM/PFS 侧、crRNA 长度和种子定义(Cas12a 是 5' TTTV + 5' 种子,Cas13 是 RNA 靶标 + PFS 偏好而非 PAM);双链扫描、GC/poly-T/自互补/发夹等质量指标、逐项公开的启发式评分,以及**同序列内的错配容差脱靶搜索**;可出订购 CSV 与带标记的图谱 |
201
203
 
202
204
  ### 9. 文献与实验记录(12)
203
205
 
@@ -252,6 +254,11 @@ dsh plugin --profile <profile> add D:\path\to\dsh-molbio-tools\packages\molbio-p
252
254
  | "这对引物除了目标还会在哪扩增?预期几条带" | `molbio_pcr_simulate(template, primer_pairs:[…])` → 产物大小 + 凝胶图 |
253
255
  | "这几条序列谁跟谁最近,这个结论有信心吗" | `molbio_phylogenetic_tree(sequences:[…], bootstrap:100)` → 支持度 + Newick |
254
256
  | "这段是不是启动子/CPG 岛?复制起点在哪" | `molbio_gc_composition(sequence)` → CpG 岛区间 + 累积 GC skew 的 ori/ter 提示 |
257
+ | "把这 60 个质粒/reads 一起做个汇总表" | `molbio_batch_analyze(directory, pattern, analysis:'all', enzymes:[…])` → 一张 CSV/TSV |
258
+ | "把 EcoRI 位点静默去掉,蛋白不能变" | `molbio_silent_site_mutagenesis(sequence, enzyme:'EcoRI', action:'remove')` → mutation texts → `molbio_mutagenesis_primers` |
259
+ | "用 Cas12a 设计 gRNA(5' TTTV PAM)" | `molbio_grna_design(sequence, cas_family:'cas12a')` |
260
+ | "用 Cas13a 敲低这个转录本" | `molbio_grna_design(sequence, cas_family:'cas13a', target_strand:'forward')` → 看 PFS 与 require_pfs |
261
+ | "TaqMan 用 MGB 探针,并出一张订购表" | `molbio_design_taqman(sequence, probe_chemistry:'mgb', save_path:'order.csv')` |
255
262
  | "搜一下 KRAS G12D 抑制剂的最新文献并存进阅读库" | `molbio_pubmed_search` → `molbio_paper_add` |
256
263
 
257
264
  ---
@@ -409,6 +416,12 @@ mutations remain deferred")。所以插件无法在工作区里合法地落一
409
416
  snake_case 键(`region_start`/`tm_min`/`amplicon_max`/`na_mm`…),未知键会明确报错。
410
417
  - `tm_delta_vs_primer`(探针 Tm − 较热引物 Tm)为负时探针仍会给出,但在 `notes` 里点名该
411
418
  扩增子——看到这条就说明这套测定该换个区域。
419
+ - **MGB(0.20.0)**:`probe_chemistry: "mgb"` 用 13–20 nt、未修饰 NN Tm 48–60 °C 的窗口。
420
+ MGB 修饰会把真实 Tm 抬高,但**NN 模型不预测这个增量**,所以报告的是未修饰 Tm,并在
421
+ `tm_model`/notes 里要求向供应商复核;也不会用"探针 Tm 必须高于引物"的未修饰差值去惩罚每个
422
+ MGB 候选。
423
+ - **订购 CSV(0.20.0)**:`save_path` 写出每条 assay 的 F/R/P 三行,含 5'/3' 修饰、合成规模与
424
+ 纯化;不传路径时只返回结构化 `order`。探针默认 5' FAM、3' 按化学(MGB 默认 NFQ-MGB)。
412
425
 
413
426
  ### 多重 PCR 互扰(v17)
414
427
 
@@ -420,6 +433,11 @@ mutations remain deferred")。所以插件无法在工作区里合法地落一
420
433
  - 大小分辨:差 < 20 bp(`min_size_separation_bp`)= 胶上不可分辨,< 40 bp = 偏近。
421
434
  - `compatible` 为 `false` 只由三类阻断项决定:**跨 target 二聚体 / 跨模板退火 / 不可分辨的
422
435
  扩增子大小**。
436
+ - **退火温度(0.20.0)**:`annealing.recommended_c` = 最低引物 Tm − `annealing_offset_c`
437
+ (默认 3 °C),`range_c` 按其 ±window 给范围;`spread_c` 超过阈值时明确说"一个温度不可能对
438
+ 所有引物最优"。这是规则性起点,不是热循环优化。
439
+ - **引物浓度(0.20.0)**:默认 200 nM;当已知扩增子大小比 ≥ 2 时,最短的降到 150 nM、最长的
440
+ 升到 300 nM。`rationale` 逐 target 说明属于哪种情形,notes 写明这是起始点而非实测最优。
423
441
 
424
442
  ### 甲基化与双酶切(v17)
425
443
 
@@ -483,14 +501,38 @@ mutations remain deferred")。所以插件无法在工作区里合法地落一
483
501
 
484
502
  ### CRISPR
485
503
 
486
- - 几何按 SpCas9 约定:protospacer 是紧邻 PAM **5' 侧**的 20 nt;`start`/`end` 是**顶链 1-based
487
- 闭区间**,`sequence` 始终是按 5'→3' 下单的序列,`pam` 始终读在 **guide 靶向的那条链**上。
504
+ - **几何按家族**(0.20.0):SpCas9 是 3' NGG / 20 nt / 3' 种子;SaCas9 是 3' NNGRRT / 21 nt;
505
+ **Cas12a 是 5' TTTV / 23 nt crRNA / 5' 端种子**;**Cas13a/b/d 是 RNA 靶标 + PFS 偏好**(不是 PAM:
506
+ Cas13a 3' H / 28 nt,Cas13b 5' D + 3' NAN / 30 nt,Cas13d 无 PFS / 22 nt)。`start`/`end` 仍是
507
+ 顶链 1-based 闭区间,`sequence` 是按 5'→3' 下单的 DNA 序列(Cas13 另给 `rna_sequence`),
508
+ `pam` 读在 guide 靶向的那条链上(Cas13 为空)。
509
+ - **PFS 不是 PAM**:Cas13 的 PFS 缺失只扣分并标注,只有 `require_pfs: true` 才过滤;Cas13 不报
510
+ DNA NN Tm(返回 null),因为 guide 是 RNA。
511
+ - **反向链脱靶**(0.20.0 修复):旧 `finalizeSites` 对反向命中减了 `guideLength-1`,使反向脱靶
512
+ 偏移 19 nt 并大多被丢弃;现在两种链都从 top-strand 起点算,PAM 按 5'/3' 侧与链向取窗口。
488
513
  - 评分逐项公开(GC 偏差、Tm 偏差、poly-T 与同聚、自互补、种子自互补、发夹 Tm、**每个脱靶
489
514
  8 分**、PAM 前一位为 G 时 +4),起点 100。**这是排序用的启发式,不是切割效率或特异性的
490
515
  预测值。**
491
- - 脱靶搜索只建模**替换**(无 bulge/indel)、要求 PAM 完好、种子末端 2 位不允许错配;默认只搜
492
- 得分最高的若干候选。想在基因组尺度搜索,需要把基因组序列作为 target 传入(当前实现的规模
493
- 上限就是单次调用的序列长度)。
516
+ - 脱靶搜索只建模**替换**(无 bulge/indel)、要求 PAM 完好、**家族对应的种子端**不允许错配
517
+ (SpCas9/SaCas9 3' 末 2 位,Cas12a 5' 起 2 位,Cas13 无硬种子);默认只搜得分最高的若干候选。
518
+ 想在基因组尺度搜索,需要把基因组序列作为 target 传入(当前实现的规模上限就是单次调用的
519
+ 序列长度;Cas13 另有候选窗口上限并提示缩小 region)。
520
+
521
+ ### 批量分析(0.20.0)
522
+
523
+ - 目录扫描默认不递归;`recursive: true` 才进子目录(深度上限 8)。`paths` 与 `directory` 二选一,
524
+ 显式 paths 优先。
525
+ - 每个文件独立解析,**坏文件是一行 `error`**,不会让整批失败;`truncated` 表示触到文件数/字节上限。
526
+ - 拓扑以 GenBank/SnapGene 声明为准(环状质粒的酶切位点计数不会按线性处理),没有声明时才用
527
+ `circular` 参数。ORF 用所选遗传密码与 ATG 起始规则;长 ORF 不等于真基因。
528
+
529
+ ### 静默位点突变(0.20.0)
530
+
531
+ - 只在**同义密码子**内枚举:蛋白序列按构造不变;`changes` 逐碱基给出位置/前后碱基/密码子/氨基酸。
532
+ - `mutation_texts` 是 `A123G` 形式,可直接传给 `molbio_mutagenesis_primers`;`mutated_sequence`
533
+ 是改完的 CDS。
534
+ - 只支持识别序列**无简并碱基**的酶(`GGTNACC` 这类直接拒绝);`host` 只影响同义密码子的排序,
535
+ 不改变"是否静默"这一事实。静默是蛋白层面的,mRNA 结构/剪接/密码子使用仍可能变化。
494
536
 
495
537
  ### 文献与记录
496
538
 
@@ -615,13 +657,13 @@ node <包目录>\preset\install.mjs --profile web --check # 只报告,不组
615
657
 
616
658
  想评估"模型到底会不会用这些工具",看 **[benchmark/README.md](benchmark/README.md)**:
617
659
  `npm run bench:offline` 零成本校验期望值(在 `npm test` 里),`npm run bench` 用真实模型
618
- 跑 **core 档 14 题**,`npm run bench:full` 跑**全部 56 题(覆盖 58/58 个工具)**。
660
+ 跑 **core 档 14 题**,`npm run bench:full` 跑**全部 61 题(覆盖 60/60 个工具)**。
619
661
 
620
662
  > **改动与 benchmark 的关系(硬规则)**:新增或改动任何工具行为,**必须**新增/更新对应的
621
663
  > benchmark 任务并重跑;没有改动就不必重跑。日常只需要一条命令——
622
664
  > `node benchmark/run.mjs --model --changed`——它会从你的改动反推出该跑哪几题
623
665
  > (`lib.mjs` 这类共享库会自动放大到全量),改动只在文档/测试时则直接回答"无事可跑"。
624
- > `test/benchmark-coverage.mjs` 会拦住"新工具没有任务"(断言 58/58 覆盖),
666
+ > `test/benchmark-coverage.mjs` 会拦住"新工具没有任务"(断言 60/60 覆盖),
625
667
  > `bench --offline` 会拦住"期望值过期"。完整对照表见
626
668
  > [docs/workflow.md](docs/workflow.md) 第 2 节。
627
669
 
@@ -647,7 +689,7 @@ node <包目录>\preset\install.mjs --profile web --check # 只报告,不组
647
689
 
648
690
  ```
649
691
  dsh-molbio-tools/
650
- ├── index.mjs # 插件入口:export { name, inject, apply },注册 58 个工具
692
+ ├── index.mjs # 插件入口:export { name, inject, apply },注册 60 个工具
651
693
  ├── lib.mjs # 基础库:IUPAC、翻译、酶表、NN 热力学、qPCR、lab math、甲基化/buffer 参考表
652
694
  ├── design.mjs # 引物自动设计(含跨内含子 qPCR)
653
695
  ├── taqman.mjs # TaqMan 水解探针设计(复用 design.mjs 的引物引擎)
@@ -662,12 +704,14 @@ dsh-molbio-tools/
662
704
  ├── msa.mjs # 多序列渐进式比对与保守性分析
663
705
  ├── align-post.mjs # 比对后处理:共识/缺口修剪/同一性矩阵/逐列覆盖度
664
706
  ├── logo.mjs # 序列标识图(信息量 + SVG)
665
- ├── crispr.mjs # CRISPR gRNA 设计(PAM 扫描、评分、脱靶搜索)
707
+ ├── crispr.mjs # CRISPR gRNA 设计(SpCas9/SaCas9/Cas12a/Cas13 家族几何、评分、脱靶搜索)
666
708
  ├── cloning.mjs # 克隆模拟:选酶/酶切连接/Gibson/Golden Gate/克隆引物/突变引物
709
+ ├── silent-site.mjs # 静默酶切位点突变(同义密码子内添加/移除位点)
667
710
  ├── sanger.mjs # ABIF (.ab1) 解析 + 测序验证
668
711
  ├── plot.mjs # SVG 柱状/散点图 + 虚拟琼脂糖凝胶
669
712
  ├── charts.mjs # 实验数据图表:CSV/TSV 读取 + 折线/直方图/箱型/小提琴/火山/热力图(基于 svgio)
670
713
  ├── seqio.mjs # FASTA/FASTQ 解析与统计
714
+ ├── batch.mjs # 批量文件分析:同一套摘要/ORF/酶切位点 → CSV/TSV
671
715
  ├── records.mjs # 协议库 / 实验日志存储
672
716
  ├── papers.mjs # 文献库存储
673
717
  ├── view.mjs # auto-view:把 SVG 交给系统默认应用打开
package/align.mjs CHANGED
@@ -8,6 +8,27 @@
8
8
 
9
9
  import { MolbioInputError } from './lib.mjs';
10
10
 
11
+ /**
12
+ * Safety limits, exported so the tools, their descriptions and the tests share
13
+ * one source.
14
+ *
15
+ * Local alignment allocates the WHOLE dynamic-programming matrix up front — a
16
+ * scoring array and a direction array of `(len(a)+1) * (len(b)+1)` cells, both
17
+ * filled before a single backtrace step. The cost is quadratic in the input, and
18
+ * the only cap upstream is `normalizeSequence`'s 1,000,000 bases, so without a
19
+ * budget here one call with two unremarkable few-hundred-kb sequences asks for
20
+ * tens of gigabytes and takes the host process down with it. Measured on the
21
+ * development machine: 8,000 x 8,000 costs ~237 MB and 0.5 s, so 50e6 cells is
22
+ * ~250 MB and ~2 s.
23
+ *
24
+ * `msa.mjs`, `phylo.mjs` and `align-post.mjs` each carry a budget of their own;
25
+ * this covers the pairwise path, which had none.
26
+ */
27
+ export const ALIGN_LIMITS = {
28
+ /** DP cells (length(a) * length(b)) one local alignment may allocate. */
29
+ maxCells: 50_000_000,
30
+ };
31
+
11
32
  /**
12
33
  * Smith-Waterman local alignment of `a` against `b`.
13
34
  * @returns {object} aligned strings, start offsets, identity, and a unified
@@ -15,6 +36,13 @@ import { MolbioInputError } from './lib.mjs';
15
36
  * relative to `b`).
16
37
  */
17
38
  export function smithWaterman(a, b, { match = 2, mismatch = -2, gap = -1 } = {}) {
39
+ const cells = a.length * b.length;
40
+ if (cells > ALIGN_LIMITS.maxCells) {
41
+ throw new MolbioInputError(
42
+ `alignment of ${a.length} x ${b.length} bases needs ${cells} matrix cells (limit ${ALIGN_LIMITS.maxCells}); `
43
+ + 'align a window instead — an exact 21-mer anchor keeps a read-vs-reference alignment linear in practice (see molbio_verify_sanger)',
44
+ );
45
+ }
18
46
  const m = a.length + 1;
19
47
  const n = b.length + 1;
20
48
  const H = new Int32Array(m * n);
package/batch.mjs ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * dsh-molbio-tools/batch.mjs
3
+ *
4
+ * Batch sequence-file analysis: parse every matching file in a workspace
5
+ * directory, compute the same summary for each, and emit one CSV/TSV table.
6
+ * Pure parsing and computation; file discovery and writes live in index.mjs and
7
+ * go through ctx.fs with the session sandbox policy.
8
+ *
9
+ * The point is the TABLE, not a second implementation of each analysis: GC/N
10
+ * come from lib.mjs's baseCounts, ORFs from translateFrames, enzyme sites from
11
+ * digest, and the GenBank/SnapGene parsers are the same ones the single-file
12
+ * tools use. A file that cannot be parsed becomes an error row instead of
13
+ * aborting the whole batch — one bad .dna in a folder of 60 must not hide the
14
+ * other 59.
15
+ */
16
+
17
+ import { MolbioInputError, baseCounts, digest, normalizeSequence, translateFrames } from './lib.mjs';
18
+ import { parseGenBank } from './genbank.mjs';
19
+ import { parseSnapGeneBytes } from './snapgene.mjs';
20
+ import { entryStats, parseFasta, parseFastq } from './seqio.mjs';
21
+
22
+ export const BATCH_LIMITS = {
23
+ maxFiles: 500,
24
+ maxFileBytes: 50 * 1024 * 1024,
25
+ maxTotalBytes: 500 * 1024 * 1024,
26
+ maxOrfBases: 2_000_000,
27
+ maxEnzymes: 20,
28
+ };
29
+
30
+ const round2 = (value) => Math.round(value * 100) / 100;
31
+
32
+ /** Detect the format from the extension, then from the content. */
33
+ export function detectBatchFormat(path, text) {
34
+ const lower = String(path).toLowerCase();
35
+ if (lower.endsWith('.dna')) return 'snapgene';
36
+ if (/\.(gb|gbk|genbank)$/.test(lower)) return 'genbank';
37
+ if (/\.(fa|fasta|fna|fas|ffn)$/.test(lower)) return 'fasta';
38
+ if (/\.(fq|fastq)$/.test(lower)) return 'fastq';
39
+ const head = typeof text === 'string' ? text.trimStart() : '';
40
+ if (head.startsWith('>')) return 'fasta';
41
+ if (head.startsWith('@')) return 'fastq';
42
+ if (/^LOCUS\s/m.test(head)) return 'genbank';
43
+ return 'text';
44
+ }
45
+
46
+ /** Convert a simple basename glob (*, ?) to a RegExp. */
47
+ export function globToRegExp(pattern) {
48
+ const source = String(pattern)
49
+ .replace(/[.+^$()|[\]{}]/g, '\$&')
50
+ .replaceAll('*', '.*')
51
+ .replaceAll('?', '.');
52
+ return new RegExp('^' + source + '$', 'i');
53
+ }
54
+
55
+ /** Parse one sequence file into its name/topology/sequence/features. */
56
+ function parseOne({ path, format, text, bytes }) {
57
+ if (format === 'snapgene') {
58
+ if (bytes === undefined) throw new MolbioInputError('a .dna file needs binary bytes');
59
+ const parsed = parseSnapGeneBytes(bytes);
60
+ return {
61
+ name: parsed.name,
62
+ topology: parsed.topology === 'circular' ? 'circular' : 'linear',
63
+ sequence: parsed.sequence,
64
+ features: parsed.features.length,
65
+ };
66
+ }
67
+ if (format === 'genbank') {
68
+ const parsed = parseGenBank(String(text ?? ''));
69
+ return {
70
+ name: parsed.name ?? path,
71
+ topology: parsed.topology === 'circular' ? 'circular' : 'linear',
72
+ sequence: parsed.sequence,
73
+ features: parsed.features.length,
74
+ };
75
+ }
76
+ if (format === 'fasta') {
77
+ const entries = parseFasta(String(text ?? ''));
78
+ return {
79
+ name: entries[0]?.id ?? path,
80
+ topology: 'unknown',
81
+ sequence: entries.map((entry) => entry.sequence).join(''),
82
+ features: 0,
83
+ entries: entries.length,
84
+ fastaEntries: entries,
85
+ };
86
+ }
87
+ if (format === 'fastq') {
88
+ const entries = parseFastq(String(text ?? ''));
89
+ return {
90
+ name: entries[0]?.id ?? path,
91
+ topology: 'unknown',
92
+ sequence: entries.map((entry) => entry.sequence).join(''),
93
+ features: 0,
94
+ entries: entries.length,
95
+ fastqEntries: entries,
96
+ };
97
+ }
98
+ const sequence = normalizeSequence(String(text ?? ''), 'sequence');
99
+ return { name: path, topology: 'unknown', sequence, features: 0, entries: 1 };
100
+ }
101
+
102
+ /**
103
+ * Analyze one file.
104
+ *
105
+ * @param {{path: string, format?: string, text?: string, bytes?: Uint8Array,
106
+ * enzymes?: string[], orfMinAa?: number, circular?: boolean, codonTable?: string}} input
107
+ * @returns {object} one table row; throws only for an unrecognizable file.
108
+ */
109
+ export function analyzeSequenceFile(input) {
110
+ const { path, text, bytes } = input;
111
+ const format = input.format ?? detectBatchFormat(path, text);
112
+ const parsed = parseOne({ path, format, text, bytes });
113
+ const sequence = normalizeSequence(parsed.sequence, path + ' sequence');
114
+ const counts = baseCounts(sequence);
115
+ const totalBases = sequence.length;
116
+ const gcPercent = counts.gc + counts.at === 0 ? 0 : round2((counts.gc / (counts.gc + counts.at)) * 100);
117
+ const nPercent = totalBases === 0 ? 0 : round2((counts.n / totalBases) * 100);
118
+ const row = {
119
+ path,
120
+ format,
121
+ topology: parsed.topology,
122
+ entries: parsed.entries ?? 1,
123
+ total_bases: totalBases,
124
+ gc_percent: gcPercent,
125
+ n_percent: nPercent,
126
+ feature_count: parsed.features,
127
+ enzyme_sites: [],
128
+ error: '',
129
+ };
130
+ const enzymes = input.enzymes ?? [];
131
+ if (enzymes.length > 0) {
132
+ const circular = input.circular === true || parsed.topology === 'circular';
133
+ const result = digest(sequence, enzymes, circular);
134
+ row.enzyme_sites = result.map((entry, index) => ({
135
+ name: enzymes[index],
136
+ sites: entry.cut_positions.length,
137
+ }));
138
+ }
139
+ const orfMinAa = input.orfMinAa ?? 0;
140
+ if (orfMinAa > 0) {
141
+ if (totalBases > BATCH_LIMITS.maxOrfBases) {
142
+ row.orf_count = 0;
143
+ row.longest_orf_aa = 0;
144
+ row.orf_skipped = true;
145
+ } else {
146
+ const { orfs } = translateFrames(sequence, 'all', input.codonTable ?? 'standard', orfMinAa);
147
+ row.orf_count = orfs.length;
148
+ row.longest_orf_aa = orfs.length === 0 ? 0 : Math.max(...orfs.map((orf) => orf.length_aa));
149
+ row.orf_skipped = false;
150
+ }
151
+ }
152
+ if (format === 'fasta' || format === 'fastq') {
153
+ const stats = entryStats(parsed.fastaEntries ?? parsed.fastqEntries ?? [], format === 'fastq');
154
+ row.mean_length = stats.mean_length;
155
+ if (format === 'fastq' && stats.quality_mean !== undefined) row.quality_mean = stats.quality_mean;
156
+ }
157
+ return row;
158
+ }
159
+
160
+ /** Columns for the table, in a stable order. */
161
+ export function batchColumns({ orfs, enzymes }) {
162
+ const columns = ['path', 'format', 'topology', 'entries', 'total_bases', 'gc_percent', 'n_percent', 'feature_count', 'mean_length'];
163
+ if (orfs) columns.push('orf_count', 'longest_orf_aa', 'orf_skipped');
164
+ if (enzymes.length > 0) columns.push(...enzymes.map((name) => name + '_sites'));
165
+ columns.push('error');
166
+ return columns;
167
+ }
168
+
169
+ function csvField(value, delimiter) {
170
+ const text = value === undefined || value === null ? '' : String(value);
171
+ return text.includes(delimiter) || /["\n\r]/.test(text) ? '"' + text.replaceAll('"', '""') + '"' : text;
172
+ }
173
+
174
+ /** Render rows as CSV (delimiter = ",") or TSV (delimiter = "\t"). */
175
+ export function batchTable(rows, columns, delimiter = ',') {
176
+ const value = (row, column) => {
177
+ if (column.endsWith('_sites')) {
178
+ const name = column.slice(0, -'_sites'.length);
179
+ return row.enzyme_sites?.find((entry) => entry.name === name)?.sites;
180
+ }
181
+ return row[column];
182
+ };
183
+ const lines = rows.map((row) => columns.map((column) => csvField(value(row, column), delimiter)).join(delimiter));
184
+ return [columns.join(delimiter), ...lines].join('\n') + '\n';
185
+ }