@dsh-bio/dsh-bio-gem 0.1.15 → 0.1.17

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.
@@ -1,6 +1,6 @@
1
1
  # gapseq_wsl.py — dsh-bio-gem M2:gapseq 引擎的 WSL 桥(Windows → WSL2)
2
2
  # 实测背景(农杆菌 C58 项目 2026-08):gapseq 2.1.0 部署于 WSL2 Ubuntu-22.04
3
- # conda env "gapseq"(TUNA 渠道),序列库 F:\Datasets\gapseq\db\Bacteria(v1.5)。
3
+ # conda env "gapseq"(TUNA 渠道)+ 本地序列库(路径见文件内常量,换机器需相应调整)。
4
4
  # 关键实测(2026-08-29):
5
5
  # - 新版 wsl.exe(Win11)输出 UTF-8;旧版 UTF-16LE —— decode 双兼容(UTF-8 strict 优先)
6
6
  # - wsl.exe 命令行传含空格路径 + 引号必坏 -> 一切路径进脚本文件,脚本用 base64 传输
@@ -25,6 +25,7 @@ WSL_DISTRO = os.environ.get("GEM_GAPSEQ_DISTRO", "Ubuntu-22.04")
25
25
  CONDA_SH = "/opt/miniforge3/etc/profile.d/conda.sh"
26
26
  GAPSEQ_ENV = "gapseq"
27
27
  WSL_WORK = "/opt/gem-gapseq-work"
28
+ SEQDB_DIR = f"/opt/miniforge3/envs/{GAPSEQ_ENV}/share/gapseq/dat/seq/Bacteria"
28
29
 
29
30
 
30
31
  def wsl_run(bash_cmd, timeout=300):
@@ -79,13 +80,34 @@ def probe():
79
80
  import re
80
81
  mm = re.search(r"gapseq version:\s*([\d.]+)", out)
81
82
  res["gapseq_version"] = mm.group(1) if mm else out[:80]
82
- seqdb = "/mnt/f/Datasets/gapseq/db"
83
+ # doall 实际读取 conda 环境内的 seq/Bacteria;旧探针却查 F: 备份并调用
84
+ # update-sequences -c,后者依赖 Zenodo 在线记录。网络解析失败会误报后端缺失。
85
+ # 本地元数据 + 三类序列文件同时检查,缺任何一项都不宣称可用。
83
86
  rc, out, err = wsl_run(
84
- f"source {CONDA_SH} && conda activate {GAPSEQ_ENV} && "
85
- f"gapseq update-sequences -t Bacteria -D {seqdb} -q -c 2>&1", 300)
86
- res["checks"]["seqdb"] = "up-to-date" in out.lower()
87
+ f"set -o pipefail; cat {SEQDB_DIR}/version_seqDB.json && "
88
+ f"find {SEQDB_DIR}/rev -type f | wc -l && "
89
+ f"find {SEQDB_DIR}/rxn -type f | wc -l && "
90
+ f"find {SEQDB_DIR}/unrev -type f | wc -l", 120)
91
+ try:
92
+ meta, offset = json.JSONDecoder().raw_decode(out)
93
+ counts = [int(value) for value in out[offset:].split()]
94
+ metadata_ok = (isinstance(meta, dict)
95
+ and isinstance(meta.get("version"), list)
96
+ and len(meta["version"]) == 1
97
+ and isinstance(meta["version"][0], str)
98
+ and bool(meta["version"][0])
99
+ and isinstance(meta.get("zenodoID"), list)
100
+ and len(meta["zenodoID"]) == 1
101
+ and isinstance(meta["zenodoID"][0], int)
102
+ and meta["zenodoID"][0] > 0)
103
+ res["checks"]["seqdb"] = rc == 0 and metadata_ok and len(counts) == 3 and all(n > 0 for n in counts)
104
+ if res["checks"]["seqdb"]:
105
+ res["seqdb_version"] = meta["version"][0]
106
+ res["seqdb_counts"] = dict(zip(("rev", "rxn", "unrev"), counts))
107
+ except (IndexError, ValueError, TypeError, json.JSONDecodeError):
108
+ res["checks"]["seqdb"] = False
87
109
  if not res["checks"]["seqdb"]:
88
- res["detail"] = f"序列库未注册(可能触发在线下载灾难): {out[:400]}"
110
+ res["detail"] = f"gapseq 实际序列库缺失或不完整: {(out or err)[:400]}"
89
111
  res["level"] = "DEGRADED"
90
112
  return res
91
113
  res["capable"] = True
@@ -248,4 +270,4 @@ if __name__ == "__main__":
248
270
  f"source {CONDA_SH} && conda activate {GAPSEQ_ENV} && gapseq -v 2>&1", 120)
249
271
  print(out[:300] or err[:300])
250
272
  else:
251
- print("unknown cmd")
273
+ print("unknown cmd")
package/python/gem_ops.py CHANGED
@@ -243,7 +243,7 @@ def op_media_resolve(args):
243
243
 
244
244
 
245
245
  # ---------------------------------------------------------------------------
246
- # op: l3_fix — B' 后半:L3 内部路径补洞(L3a 模型内连通性 + L3b 白名单/BiGG 反应式)
246
+ # op: l3_fix — L3 内部路径补洞(L3a 模型内连通性 + L3b 白名单/BiGG 反应式)
247
247
  # 证据分级 EVIDENCE_sequence/math;防过补第五闸门(budget.py);补后 G1-G6 重验 + G6 失败回滚
248
248
  # ---------------------------------------------------------------------------
249
249
  def op_l3_fix(args):
@@ -264,7 +264,7 @@ def op_l3_fix(args):
264
264
 
265
265
  # ---------------------------------------------------------------------------
266
266
  # op: fluxscan — 阶段A-M1 通量区间制(FVA 区间 + pFBA 点值 + 条件对区间分离判定)
267
- # 语义 bsp 锁稿:overlap = 求解器伪影禁止引用;判定公式见 fluxscan.judge_interval(单测锁定)
267
+ # 语义锁定:overlap = 求解器伪影禁止引用;判定公式见 fluxscan.judge_interval(单测锁定)
268
268
  # ---------------------------------------------------------------------------
269
269
  def op_fluxscan(args):
270
270
  from fluxscan import fluxscan, DEFAULT_FRACTION, DEFAULT_TOL
@@ -371,7 +371,7 @@ OPS = {
371
371
 
372
372
 
373
373
  # ---------------------------------------------------------------------------
374
- # op: biomass_inspect / biomass_apply — Q2 任务一:biomass 精修(可选 profile,不默认替换)
374
+ # op: biomass_inspect / biomass_apply — biomass 精修(可选 profile,不默认替换)
375
375
  # ---------------------------------------------------------------------------
376
376
  def op_biomass_inspect(args):
377
377
  from biomass_tools import inspect_biomass
@@ -533,7 +533,7 @@ OPS["targets"] = op_targets
533
533
  # ---------------------------------------------------------------------------
534
534
  # op: precursor_scan — 阻塞前体分析
535
535
  # 「模型为什么不长」的结构级定位:逐前体做移除测试,找出卡住生长的前体。
536
- # 来源:2026-09-11 E2E 绕道归因(agent 手写该逻辑 6+ 次,无工具可用)。
536
+ # 来源:2026-09-11 实测归因(agent 手写该逻辑 6+ 次,无工具可用)。
537
537
  # 判据刻意用「相对判断」而非绝对可达性 —— 对可生长模型天然零误报。
538
538
  # ---------------------------------------------------------------------------
539
539
  def op_precursor_scan(args):
package/python/l3_fix.py CHANGED
@@ -1,4 +1,4 @@
1
- # l3_fix.py — B' 后半:L3 内部路径补洞(两级)
1
+ # l3_fix.py — L3 内部路径补洞(两级)
2
2
  # L3a 模型内连通性: 先"全内部反应放开方向"LP 预检(快速严谨判负),可行才用 cobra GapFiller
3
3
  # universal=模型自身反应池(放开方向副本,NEW id),MILP 选最小集 → 对原反应放宽 bounds(不复制反应)。
4
4
  # 注: cobra 0.32.1 GapFiller(universal=None) 语义是"空反应池"(只加 demand),不是"模型自身反应池"
@@ -28,7 +28,7 @@ EX_PREFIX = ("EX_", "DM_", "SK_")
28
28
  SOURCE_TAG = "gem-l3fix"
29
29
  NEW_RXN_SUFFIX = "_l3fix"
30
30
 
31
- # BiGG 基名 -> ModelSEED cpd 号(2026-08-29 本机以 C58 名字+公式+电荷逐一验证;
31
+ # BiGG 基名 -> ModelSEED cpd 号(2026-08-29 以 C58 名字+公式+电荷逐一验证;
32
32
  # 映射时仍做公式/电荷校验,不一致即弃用防静默污染化学计量)
33
33
  COFACTOR_BRIDGE = {
34
34
  "h2o": "00001", "atp": "00002", "nad": "00003", "nadh": "00004",
@@ -42,9 +42,15 @@ COFACTOR_BRIDGE = {
42
42
  COMP_MAP = {"c": "c0", "e": "e0", "p": "p0"} # BiGG 区室后缀 -> gapseq 区室 id
43
43
 
44
44
  WHITELIST_DIR = os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "whitelist")
45
- # 本地白名单数据库(license 守则: 仅本机,不进 git/发布包;GEM_WHITELIST_DB_DIR 可覆盖)
46
- RXN_DB_DIR = os.environ.get("GEM_WHITELIST_DB_DIR", r"D:\Program\hermes\temp\gem_whitelist")
47
- DEFAULT_UNIVERSAL = r"D:\Program\hermes\temp\gem_universal\iML1515.xml"
45
+ # 本地白名单数据库(license 守则: 仅本地留存,不进 git/发布包;GEM_WHITELIST_DB_DIR 可覆盖)
46
+ RXN_DB_DIR = os.environ.get(
47
+ "GEM_WHITELIST_DB_DIR",
48
+ os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "whitelist-db"),
49
+ )
50
+ DEFAULT_UNIVERSAL = os.environ.get(
51
+ "GEM_UNIVERSAL_MODEL",
52
+ os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "models", "iML1515.xml"),
53
+ )
48
54
 
49
55
  PTS_RE = re.compile(r"(?i)\bpts\b|phosphotransferase|pep:pyr")
50
56
 
package/python/ledger.py CHANGED
@@ -7,7 +7,7 @@
7
7
  # 幂等: 同 model+condition+type+content 哈希去重,重复运行不追加。
8
8
  # 完整性: 逐行 JSON 解析校验,损坏行跳过并在返回里报 corrupt_rows + 行号(不阻塞);
9
9
  # 写入失败只 WARN 不使主流程失败。update 重写文件但保留损坏行原样(不删行)。
10
- # 证据分级优先级(任务书锁定): EVIDENCE_literature > EVIDENCE_sequence > EVIDENCE_rule > EVIDENCE_math
10
+ # 证据分级优先级(口径锁定): EVIDENCE_literature > EVIDENCE_sequence > EVIDENCE_rule > EVIDENCE_math
11
11
  import os
12
12
  import re
13
13
  import sys
@@ -29,7 +29,7 @@ def _now_iso():
29
29
 
30
30
 
31
31
  def _content_hash(model, condition, rtype, content):
32
- # 阶段D-E2E P1:Windows 路径斜杠/大小写差异("F:/a" vs "F:\a")会使同一模型的
32
+ # 实测发现:Windows 路径斜杠/大小写差异("F:/a" vs "F:\a")会使同一模型的
33
33
  # 重复登记漏过去重——hash 前做 normcase+normpath 归一化(首登记的存储格式不变)。
34
34
  m = os.path.normcase(os.path.normpath(model)) if model else ""
35
35
  raw = "\x1f".join([m, condition or "", rtype or "", content or ""])
@@ -1,10 +1,10 @@
1
- # model_card.py — 模型卡 schema v2 统一写入器(B' 收尾 + Q2 工程质量件)
1
+ # model_card.py — 模型卡 schema v2 统一写入器(工程质量件)
2
2
  # 职责: init_card(build 起卡)/ load / save / append_operation(lineage 版本递增 + changelog)
3
3
  # / set_verified_phenotypes(phenotype 结果)/ set_essential_genes(必需基因 + 证据分级)
4
4
  # 纪律: 各工具完成后**仅当产物模型旁已有 card** 才向后追加;无卡不动(不凭空造卡)。
5
5
  # 兼容: build.py 旧卡(无 schema 字段)读取时即时迁移到 v2(新增字段缺失不报错)。
6
6
  # units: growth_rate 一律 1/h(比生长速率;biomass 反应 gDW 归一化口径,数值 = μ)。
7
- # 2026-09-21 由 mmol/gDW/h 演进为 1/h:数值不变、生物语义更准(外部评审 P0;schema 向后兼容,旧卡照读)。
7
+ # 2026-09-21 由 mmol/gDW/h 演进为 1/h:数值不变、生物语义更准(schema 向后兼容,旧卡照读)。
8
8
  import os
9
9
  import json
10
10
  import time
@@ -1,6 +1,6 @@
1
1
  # precursor_scan.py — dsh-bio-gem 阻塞前体分析(「模型为什么不长」的结构级定位)
2
2
  #
3
- # 来源(2026-09-11 E2E 绕道归因):agent 在不生长模型上反复手写「逐前体探测」逻辑
3
+ # 来源(2026-09-11 实测归因):agent 在不生长模型上反复手写「逐前体探测」逻辑
4
4
  # (6+ 次调用),本模块把它固化为工具。
5
5
  #
6
6
  # ⚠️ 判据设计——**刻意不用「绝对可达性」**:
package/python/quality.py CHANGED
@@ -14,6 +14,7 @@ from cobra.flux_analysis import fastcc, find_blocked_reactions
14
14
 
15
15
  from gapfind import expand_medium, resolve_medium
16
16
  from silentio import silent_read_sbml
17
+ from fsutil import ensure_parent_dir
17
18
 
18
19
 
19
20
  EX_PREFIXES = ("EX_", "DM_", "SK_")
@@ -357,6 +358,7 @@ def _connectivity(model):
357
358
 
358
359
  def _write_export_csv(path, records):
359
360
  """Write long-form complete lists; JSON samples remain deliberately capped."""
361
+ path = ensure_parent_dir(path)
360
362
  with open(path, "w", encoding="utf-8", newline="") as handle:
361
363
  writer = csv.DictWriter(handle, fieldnames=("check", "item_id", "detail"))
362
364
  writer.writeheader()
@@ -1,6 +1,6 @@
1
- # roundtrip_check.py — SBML 往返保真自检(Q2 工程质量件 A)
1
+ # roundtrip_check.py — SBML 往返保真自检(工程质量件)
2
2
  # cobra 读 → write_sbml_model → 读回:断言反应/代谢物/基因数一致 + GPR 字符串精确一致
3
- # (GLM 经典暗坑:序列化静默丢 GPR——fbc v2 写入路径回归护栏;≥5 个复合 GPR 反应样本必查)
3
+ # (经典暗坑:序列化静默丢 GPR——fbc v2 写入路径回归护栏;≥5 个复合 GPR 反应样本必查)
4
4
  import os
5
5
  import sys
6
6
  import json
@@ -11,6 +11,7 @@ from cobra.util.solver import linear_reaction_coefficients
11
11
 
12
12
  from gapfind import expand_medium, resolve_medium
13
13
  from silentio import silent_read_sbml
14
+ from fsutil import ensure_parent_dir
14
15
 
15
16
 
16
17
  EX_PREFIXES = ("EX_", "DM_", "SK_")
@@ -219,6 +220,7 @@ def sample_fluxes(
219
220
  growth_summary = _describe(samples[growth_reaction.id].to_numpy())
220
221
 
221
222
  if export_csv:
223
+ export_csv = ensure_parent_dir(export_csv)
222
224
  samples.to_csv(export_csv, index=False)
223
225
 
224
226
  if growth_floor_fraction is None:
@@ -18,6 +18,7 @@ from silentio import silent_read_sbml
18
18
  from gapfind import build_ex_index, ex_index_is_boundary
19
19
  from essential_scan import setup_model_medium
20
20
  from sensitivity import find_biomass_gam
21
+ from fsutil import ensure_parent_dir, write_meta_sidecar
21
22
 
22
23
  EPS = 1e-6
23
24
  GROWTH_FRACTIONS = [0.25, 0.5, 0.75, 0.9, 0.99, 1.0]
@@ -151,10 +152,15 @@ def secretion(model_path, medium=None, fractions=None, export_csv=None,
151
152
 
152
153
 
153
154
  def _export_csv(path, rows, out):
155
+ """纯数据 CSV(标准表头)+ 旁车 meta。
156
+
157
+ 2026-10-05 修:boundary_note 移出 CSV 首行(原为 `"# boundary_note", note` 双字段行,
158
+ pandas/自动解析会当表头或脏行)→ 写入 <path>.meta.json(返回体 export_csv_meta)。
159
+ """
160
+ path = ensure_parent_dir(path)
154
161
  n = 0
155
162
  with open(path, "w", newline="", encoding="utf-8-sig") as f:
156
163
  w = csv.writer(f)
157
- w.writerow(["# boundary_note", out["boundary_note"]])
158
164
  w.writerow(["rxn", "met_id", "name", "feasible", "max_prod", "growth_at_max",
159
165
  "fraction", "prod"])
160
166
  for r in rows:
@@ -162,6 +168,17 @@ def _export_csv(path, rows, out):
162
168
  w.writerow([r["rxn"], r["met_id"], r["name"], int(r["feasible"]),
163
169
  r["max_prod"], r["growth_at_max"], e["fraction"], e["prod"]])
164
170
  n += 1
171
+ meta = write_meta_sidecar(path, {
172
+ "boundary_note": out.get("boundary_note"),
173
+ "model": out.get("model"),
174
+ "medium": out.get("medium"),
175
+ "medium_preset": out.get("medium_preset"),
176
+ "units": out.get("units"),
177
+ "growth_fractions": out.get("growth_fractions"),
178
+ "wt_growth": out.get("wt_growth"),
179
+ })
180
+ if meta:
181
+ out["export_csv_meta"] = meta
165
182
  return n
166
183
 
167
184
 
@@ -19,6 +19,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
19
  from silentio import silent_read_sbml
20
20
  from validate import parse_formula
21
21
  from essential_scan import setup_model_medium, scan_essentiality
22
+ from fsutil import ensure_parent_dir
22
23
 
23
24
  BIOMASS_SCALES = [0.75, 1.0, 1.25]
24
25
  GAM_GRID = [1, 5, 10, 20, 30, 40, 50]
@@ -127,7 +128,7 @@ def find_biomass_gam(m):
127
128
 
128
129
  def _apply_biomass_scale(m, bio_id, f, gam_info):
129
130
  """biomass 组分缩放 ×f(排除 GAM stub 与 Biomass 产物——正交化)。
130
- cobra add_metabolites 是增量语义:绝对设定必须用 delta = 目标-现值(Q2 教训),回读校验。
131
+ cobra add_metabolites 是增量语义:绝对设定必须用 delta = 目标-现值(实测教训),回读校验。
131
132
  注意:原始系数必须在 add 之前快照(add 后 bio.metabolites 已是新值)。"""
132
133
  bio = m.reactions.get_by_id(bio_id)
133
134
  skip = set((gam_info.get("gam_mets") or {}).values()) | \
@@ -216,7 +217,7 @@ def sensitivity(model_path, medium=None, biomass_scales=None, gam_grid=None,
216
217
  run_component_sensitivity=True, run_drift=True, top_n=10,
217
218
  export_csv=None, progress=None, baseline_check=None):
218
219
  """M2 主入口。baseline_check: 可选外部基线必需集(来自 essential_scan 直跑)——
219
- 基准组合与其做集合相等断言(任务书锚点)。返回完整结果 dict。"""
220
+ 基准组合与其做集合相等断言(基准锚点)。返回完整结果 dict。"""
220
221
  log = progress or (lambda s: sys.stderr.write(str(s) + "\n"))
221
222
  biomass_scales = biomass_scales or BIOMASS_SCALES
222
223
  gam_grid = gam_grid or GAM_GRID
@@ -421,6 +422,7 @@ def sensitivity(model_path, medium=None, biomass_scales=None, gam_grid=None,
421
422
 
422
423
 
423
424
  def _export_csv(path, grid, comp_rows, drift, stability):
425
+ path = ensure_parent_dir(path)
424
426
  rows = 0
425
427
  with open(path, "w", newline="", encoding="utf-8-sig") as f:
426
428
  w = csv.writer(f)
package/python/targets.py CHANGED
@@ -13,6 +13,8 @@ import json
13
13
 
14
14
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
15
 
16
+ from fsutil import ensure_parent_dir
17
+
16
18
  SCHEMA_FIELDS = ["target_id", "type", "genes", "met_ids", "condition", "rationale",
17
19
  "evidence_tier", "status", "growth_or_maxprod", "source", "exported_at"]
18
20
  TYPE_ORDER = ["essentiality", "synthetic_lethal", "secretion"]
@@ -112,6 +114,7 @@ def targets(model_path=None, types=None, condition=None, ledger_path=None,
112
114
  d = os.path.join(os.path.expanduser("~"), ".dsh", "dsh-bio-gem", "exports")
113
115
  os.makedirs(d, exist_ok=True)
114
116
  export_path = os.path.join(d, f"targets_{time.strftime('%Y%m%d_%H%M%S')}.{export_format}")
117
+ export_path = ensure_parent_dir(export_path)
115
118
  if export_format == "json":
116
119
  with open(export_path, "w", encoding="utf-8") as f:
117
120
  json.dump(out_rows, f, ensure_ascii=False, indent=1)
@@ -1,7 +1,7 @@
1
- # validate.py — dsh-bio-gem 五道验证关卡(M1)
1
+ # validate.py — dsh-bio-gem 五道验证关卡
2
2
  # G1 加载统计 / G2 内部反应元素平衡 / G3 生长真实性 / G4 底物表型(条件) / G5 必需基因抽检(条件)
3
3
  # 规格: docs/ARCHITECTURE.md §5;判据口径 = FBA objective_value(biomass 归一化 → 比生长速率 μ,单位 1/h)
4
- # 实现从 HANDOFF-03 五道关卡协议产品化(农杆菌项目验证过的逻辑)
4
+ # 五道关卡协议产品化(农杆菌项目验证过的逻辑)
5
5
  import re
6
6
  import os
7
7
  import json
@@ -16,7 +16,7 @@ CORE_ELEMS = ("C", "N", "P", "S") # 硬核:不平衡必须 = 0
16
16
  REPORT_ELEMS = ("H", "O") # 报告不阻塞
17
17
  ELM_RE = re.compile(r"([A-Z][a-z]?)(\d*)")
18
18
 
19
- # 关卡注册器(GLM 建议 + 2026-08-29 采纳):未来加 G7 不改主流程
19
+ # 关卡注册器(2026-08-29 采纳):未来加 G7 不改主流程
20
20
  GATE_REGISTRY = {}
21
21
 
22
22
 
@@ -107,7 +107,7 @@ class Validator:
107
107
  frac = 1.0 - n_bad / checked if checked else 0.0
108
108
  status = "PASS" if n_bad == 0 else ("WARN" if frac >= 0.85 else "FAIL")
109
109
 
110
- # 2026-09-11 修复(agent 在真实 E2E 中发现并指出):**公式覆盖率是 PASS 结论的
110
+ # 2026-09-11 修复(agent 在真实会话中发现并指出):**公式覆盖率是 PASS 结论的
111
111
  # 作用域上界** —— 覆盖率低时「无反应不平衡」只说明被检查的那部分没问题,不能
112
112
  # 外推为整体 PASS。实测 iNX1344_v3:覆盖率 68.35% 却判 PASS,agent 据此指出
113
113
  # 「g2 的 PASS 是假阳性,因为它只检查了有 formula 的 68.35% 代谢物」。
@@ -210,7 +210,7 @@ class Validator:
210
210
  补洞后必跑(context.post_gapfill 时不再跳过)。
211
211
  P1-5 修复(2026-08-31 LBA9402 会话实测):CarveMe 模型 ATP id 为 M_atp_c,
212
212
  旧匹配只看 atp_c/cpd00002_c0 -> 误 SKIP「未找到 ATP」——扩展命名模式 + SKIP 时列出尝试模式与模型内候选。
213
- P2-9 修复(2026-09-10 iNX1344 E2E 实测):MetaCyc/BioCyc 导出模型 ATP 为 M00002_c
213
+ P2-9 修复(2026-09-10 iNX1344 实测):MetaCyc/BioCyc 导出模型 ATP 为 M00002_c
214
214
  (name='ATP',formula 为去质子化变体),仍不在模式表内 → 二次误 SKIP(G6 直接失效)。
215
215
  改为三级通用解析:id 模式 → name 匹配 → formula 匹配,跨 ID 体系自适应。"""
216
216
  m = self.m
@@ -262,7 +262,7 @@ class Validator:
262
262
  # ------------------------------------------------------------------ G4
263
263
  def g4_phenotype(self, table_path=None, substrates=None, medium=None, carbon_mode="supplement"):
264
264
  """条件执行:需参照表(TSV: substrate<TAB>published 0/1)或 substrates+published。
265
- carbon_mode: supplement=基准培养基不变+底物-10(对齐 HANDOFF-03 关卡4 基线 16/19→17/19);
265
+ carbon_mode: supplement=基准培养基不变+底物-10(对齐关卡4 基线 16/19→17/19);
266
266
  sole=去含碳交换后底物-10(唯一碳源严格语义,氮源类测试会误判)。"""
267
267
  if table_path and os.path.exists(table_path):
268
268
  rows = []
@@ -61,15 +61,15 @@ language: mixed
61
61
 
62
62
  ## 硬规则(实测,违反会拿错结果)
63
63
 
64
- 0. **外部事实断言必须带证据**(2026-08-29 采纳 GLM 教训):凡涉及"某物种/文献/工具是否存在、是否已发表"等**模型外事实断言**,agent 必须真实检索并附来源(如 PubMed/PMC 链接);无法检索时降级表述为"未验证假设 [假设]",不得由记忆下断言。此项与 EVIDENCE 分级同位:**事实断言也有证据分级**(检索+来源=high;仅记忆=unverified)。
64
+ 0. **外部事实断言必须带证据**(2026-08-29 采纳):凡涉及"某物种/文献/工具是否存在、是否已发表"等**模型外事实断言**,agent 必须真实检索并附来源(如 PubMed/PMC 链接);无法检索时降级表述为"未验证假设 [假设]",不得由记忆下断言。此项与 EVIDENCE 分级同位:**事实断言也有证据分级**(检索+来源=high;仅记忆=unverified)。
65
65
  1. **培养基一律用自然名成分**:`{"D-Glucose": -5, "NH3": -10, "O2": -12.5, ...}`。插件跨引擎自动解析(gapseq EX_cpdXXXXXX_e0 与 CarveMe/BiGG EX_glc__D_e 命名空间不同,硬编码 ID 会失配)。
66
- 2. **数字必须来自工具输出**:agent 报告生长值/基因数/复制子数时引用工具 result,不要凭模型知识猜测(防幻觉铁律)。**生长值单位是 mmol/gDW/h(FBA 通量),不是 h⁻¹/μ**——汇报时不要换算成比生长速率。
66
+ 2. **数字必须来自工具输出**:agent 报告生长值/基因数/复制子数时引用工具 result,不要凭模型知识猜测(防幻觉铁律)。**生长值单位以工具返回的 `units` 字段为准**:归一化 biomass 反应(产物系数=1)的生长值为 **1/h(比生长速率 μ)**;一般反应通量为 mmol/gDW/h。汇报时不得混用口径。
67
67
  3. **CarveMe 模型的介质边界**:`gem_build` 默认在 M9 最小培养基验证(生长阳性);**AB 等自定义培养基若 FAIL 且 gapfind 判定 L3(内部路径),规则补洞修不了**——此时如实报告「模型可用介质=M9,目标介质需要 L3 级文献补充」,绝不要假装自愈。
68
68
  4. **缺口 90% 是 L1/L2**(缺交换/转运,非缺分解酶)——先诊断再补,别直接加反应。
69
69
  5. **多复制子正常**:质粒/多染色体基因都在同一模型内(C58 实测 4 复制子:967/434/65/18 基因分布于 ChrⅠ/ChrⅡ/pTi/pAt)。
70
- 6. **通量区间制(阶段A-M4 硬规则)**:无区间不点数——任何条件间通量对比必须消费 `gem_fluxscan` 的区间分离判定;两条件区间分离才是解空间无关硬结论(a_higher/b_higher);overlap 反应的任何点值 diff 必须标注"伪影,禁止引用"。单点 FBA 生长/通量值(gem_validate/gem_phenotype/gem_essentiality 等输出)只是该条件下的一个解,跨条件直接 diff 是求解器伪影。
71
- 7. **模型指代消歧(阶段D-E2E 硬规则)**:用户说"我们的 C58 模型"等未给路径的指代时——①先查预测账本(`gem_ledger query` 按 model 前缀)与项目档案中已登记的 canonical 路径;②同一名字存在多个模型(如 gapseq 原版 vs carveme 重建)时,必需性/富集等锚点敏感分析必须用登记在案的原始版本或向用户确认,并说明选择了哪个文件;③不同引擎重建的同一物种是不同模型(基因数/必需集都不同),不得混用结论。
72
- 8. **文件出处/来源推断必须标注 [推断](阶段D-P2 硬规则)**:凡非本次会话工具 result 直接给出、而是凭文件名/目录布局/常识推断的出处(如"这个 faa 是 gem_annotate 产出的"),汇报时必须标注 [推断];可验证时先验证再陈述。例证:阶段 D 轮 1 agent 称 faa 为"前面流程 gem_annotate 产出"——事实上正确但出自文件名推断而非会话验证。
70
+ 6. **通量区间制(硬规则)**:无区间不点数——任何条件间通量对比必须消费 `gem_fluxscan` 的区间分离判定;两条件区间分离才是解空间无关硬结论(a_higher/b_higher);overlap 反应的任何点值 diff 必须标注"伪影,禁止引用"。单点 FBA 生长/通量值(gem_validate/gem_phenotype/gem_essentiality 等输出)只是该条件下的一个解,跨条件直接 diff 是求解器伪影。
71
+ 7. **模型指代消歧(实测硬规则)**:用户说"我们的 C58 模型"等未给路径的指代时——①先查预测账本(`gem_ledger query` 按 model 前缀)与项目档案中已登记的 canonical 路径;②同一名字存在多个模型(如 gapseq 原版 vs carveme 重建)时,必需性/富集等锚点敏感分析必须用登记在案的原始版本或向用户确认,并说明选择了哪个文件;③不同引擎重建的同一物种是不同模型(基因数/必需集都不同),不得混用结论。
72
+ 8. **文件出处/来源推断必须标注 [推断](硬规则)**:凡非本次会话工具 result 直接给出、而是凭文件名/目录布局/常识推断的出处(如"这个 faa 是 gem_annotate 产出的"),汇报时必须标注 [推断];可验证时先验证再陈述。例证:一次实测中 agent 称 faa 为"前面流程 gem_annotate 产出"——事实上正确但出自文件名推断而非会话验证。
73
73
 
74
74
  ## C58 回归锚(验证工具是否正常)
75
75
 
@@ -1,6 +1,6 @@
1
1
  // dsh-bio-gem — capabilities 单源(single source of truth for tool manifest & capability metadata)
2
2
  //
3
- // 目的(外部评审共识,2026-09-21 裁决 §3.3):
3
+ // 目的(2026-09-21 设计共识):
4
4
  // 1. 工具清单/能力分级/成本与副作用元数据集中一处,供 integration API(/v1/capabilities)
5
5
  // 与 genie 宿主侧动态消费——消灭「工具数变化需同步 ≥12 处」的手工漂移。
6
6
  // 2. 校验脚本 scripts/check-capabilities.mjs 强制本 MANIFEST 与 tools.js 真实注册集合一致;
@@ -225,7 +225,7 @@ async function probeGapseqEnvironment({ isWindows, distro, runner }) {
225
225
  return { available: false, detail: 'gapseq 仅支持 Windows WSL 的只读探测。' }
226
226
  }
227
227
  // 快速预检:先确认目标发行版存在(wsl.exe -l -q 亚秒级),避免发行版缺失时
228
- // 白等一次 bash 长命令直到超时刹车(真实运行时实测:本机 bash 启动即 ~3s)。
228
+ // 白等一次 bash 长命令直到超时刹车(实测:bash 启动即 ~3s)。
229
229
  try {
230
230
  const listed = await runner('wsl.exe', ['-l', '-q'])
231
231
  const names = String(listed?.stdout ?? '')
package/src/python.js CHANGED
@@ -10,7 +10,7 @@ import os from 'node:os'
10
10
  const PYTHON_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'python')
11
11
 
12
12
  /**
13
- * 候选解释器,按优先级(**通用化,不写死任何本机路径**):
13
+ * 候选解释器,按优先级(**通用化,不写死任何机器特定路径**):
14
14
  *
15
15
  * 1. `GEM_PYTHON` — 用户显式指定,最高优先级
16
16
  * 2. 宿主插件自举环境 — `$DSH_HOME/dsh-bio-genie/python-env`
@@ -125,7 +125,7 @@ export function callGem(op, args, opts = {}) {
125
125
  return new Promise((resolve, reject) => {
126
126
  const py = pythonExe()
127
127
  const script = join(PYTHON_DIR, 'gem_ops.py')
128
- const cp = spawn(py, ['-I', script], { cwd: PYTHON_DIR, windowsHide: true })
128
+ const cp = spawn(py, ['-I', script], { cwd: opts.cwd || PYTHON_DIR, windowsHide: true })
129
129
  let out = ''
130
130
  let err = ''
131
131
  cp.stdout.on('data', (d) => { out += d })
package/src/tools.js CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'
8
8
  import { spawn } from 'node:child_process'
9
9
  import { callGem, stampProvenance } from './python.js'
10
10
  import { startBuild, jobStatus } from './jobs.js'
11
+ import { resolveWorkdir } from './workdir.js'
11
12
 
12
13
  /** 校验输入存在(绝对路径或用户给定路径)。 */
13
14
  function requirePath(v, label) {
@@ -27,8 +28,8 @@ function gemTool(opts) {
27
28
  schema: { type: 'object', additionalProperties: true },
28
29
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
29
30
  },
30
- async execute(args) {
31
- return callGem(opts.op, args, { timeoutMs: opts.timeoutMs ?? 300_000 })
31
+ async execute(args, exec) {
32
+ return callGem(opts.op, args, { timeoutMs: opts.timeoutMs ?? 300_000, cwd: resolveWorkdir(exec) })
32
33
  },
33
34
  })
34
35
  }
@@ -41,7 +42,7 @@ function buildTool() {
41
42
  '从细菌全基因组构建基因组尺度代谢模型(GEM)。' +
42
43
  'engine=carveme(默认,纯 Windows 快):输入蛋白 FASTA(*.faa),CarveMe -g M9 gapfill → M9 介质验证 → 目标介质 L1/L2 补洞,' +
43
44
  '约 1-2 分钟(C58 实测 70s)。' +
44
- 'engine=gapseq(质量档,需本机 WSL2 gapseq 环境):输入核苷酸 FASTA(*.fna),WSL 桥 gapseq doall → 模型拷回 → 目标介质验证,' +
45
+ 'engine=gapseq(质量档,需 WSL2 gapseq 环境):输入核苷酸 FASTA(*.fna),WSL 桥 gapseq doall → 模型拷回 → 目标介质验证,' +
45
46
  '约 30-60 分钟(后台进度日志旁观,不要误判超时)。' +
46
47
  '输出标准 SBML(fbc v2)+ 模型卡(sidecar JSON:引擎版本/验证结果/补洞记录)。' +
47
48
  '生长值为比生长速率口径(1/h,biomass 归一化);通量为单点 FBA 值(mmol/gDW/h,非硬结论)。条件间通量对比用 gem_fluxscan(区间制)。' +
@@ -259,7 +260,7 @@ export function registerTools(ctx) {
259
260
  timeoutMs: 120_000,
260
261
  })))
261
262
 
262
- // gem_l3_fix:L3 内部路径补洞(B' 后半:L3a 模型内连通性 + L3b 白名单/BiGG 反应式 + 证据分级)
263
+ // gem_l3_fix:L3 内部路径补洞(L3a 模型内连通性 + L3b 白名单/BiGG 反应式 + 证据分级)
263
264
  disposers.push(ctx.tools.register(gemTool({
264
265
  name: 'gem_l3_fix',
265
266
  description:
@@ -287,7 +288,7 @@ export function registerTools(ctx) {
287
288
  timeoutMs: 900_000,
288
289
  })))
289
290
 
290
- // gem_biomass:biomass 精修(Q2:inspect 只读 / apply 显式覆盖表 + 三联对照)
291
+ // gem_biomass:biomass 精修(inspect 只读 / apply 显式覆盖表 + 三联对照)
291
292
  disposers.push(ctx.tools.register(defineTool({
292
293
  name: 'gem_biomass',
293
294
  description:
@@ -317,9 +318,9 @@ export function registerTools(ctx) {
317
318
  schema: { type: 'object', additionalProperties: true },
318
319
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
319
320
  },
320
- async execute(args) {
321
+ async execute(args, exec) {
321
322
  const op = args.action === 'apply' ? 'biomass_apply' : 'biomass_inspect'
322
- return callGem(op, args, { timeoutMs: 900_000 })
323
+ return callGem(op, args, { timeoutMs: 900_000, cwd: resolveWorkdir(exec) })
323
324
  },
324
325
  })))
325
326
 
@@ -416,7 +417,7 @@ export function registerTools(ctx) {
416
417
  name: 'gem_benchmark',
417
418
  description:
418
419
  '通用基准对比(benchmark):任何两个代谢模型跑规范对比表,产出论文级对比。model_a/model_b 支持本地 SBML 绝对路径' +
419
- '或 "bigg:<model_id>" URI(如 bigg:iML1515,BiGG 静态库下载到 ~/.dsh/dsh-bio-gem/models/,直连失败自动走本机代理,下载后缓存)。' +
420
+ '或 "bigg:<model_id>" URI(如 bigg:iML1515,BiGG 静态库下载到 ~/.dsh/dsh-bio-gem/models/,直连失败时按环境变量代理(HTTP_PROXY/HTTPS_PROXY)重试,下载后缓存)。' +
420
421
  '输出:ID 体系探测 / 六道关卡 G1-G6 逐项并列 / 声明介质生长(含介质层两级策略——无 EX_ 层的模型自动回退 ' +
421
422
  'boundary 单代谢物反应解析,boundary_style 标注)/ biomass 可行性探针(逐组分净产测试,结构性断供清单)/ ' +
422
423
  '必需性对比(复用 essential_scan;任一侧 wt<=EPS 判退化只报结构信息不做垃圾对比,基因映射尽力而为如实报覆盖率,' +
package/src/workdir.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * dsh-bio-gem — 工作区解析(工具执行的工作目录)
3
+ *
4
+ * 优先级:
5
+ * 1. 会话工作区 `exec.agent.session.header.cwd`(与 dsh 内置 fs 工具一致:
6
+ * 每个会话操作自己的工作区,而不是服务器启动目录)
7
+ * 2. 保底工作区 `~/.dsh/sessions/default`(自动创建)
8
+ *
9
+ * 背景(2026-10-04 实测):python 子进程此前固定 cwd=插件 python 目录,
10
+ * 工具参数里的相对路径(如 gem_fluxscan 的 export_csv)因此解析到插件目录、
11
+ * 写入失败(agent 被迫改用绝对路径自愈)。本模块对齐 dsh-bio-genie 的
12
+ * workdir 语义:相对路径基于会话工作区解析。
13
+ *
14
+ * 说明:dsh 对未显式指定 cwd 的会话会把 header.cwd 填充为服务器启动目录
15
+ * (process.cwd()),此时视为「未指定工作区」,走保底链。
16
+ *
17
+ * @module dsh-bio-gem/workdir
18
+ */
19
+ import { existsSync, mkdirSync } from 'node:fs'
20
+ import { homedir } from 'node:os'
21
+ import { join } from 'node:path'
22
+
23
+ /** 保底工作区目录(自动创建;仅无会话 cwd 时使用)。 */
24
+ export function fallbackWorkspace() {
25
+ return join(homedir(), '.dsh', 'sessions', 'default')
26
+ }
27
+
28
+ /**
29
+ * 会话工作区目录。
30
+ * @param {object} [exec] 工具执行上下文(defineTool execute 的第二参数)。
31
+ * @returns {string|undefined} 会话 cwd;未指定或不可读时返回 undefined。
32
+ */
33
+ export function sessionWorkspace(exec) {
34
+ const cwd = exec?.agent?.session?.header?.cwd
35
+ if (typeof cwd !== 'string' || cwd.length === 0) return undefined
36
+ if (!existsSync(cwd)) return undefined
37
+ return cwd
38
+ }
39
+
40
+ /** 幂等确保目录存在(失败静默——由调用方决定回退)。 */
41
+ function ensureDir(dir) {
42
+ if (existsSync(dir)) return true
43
+ try {
44
+ mkdirSync(dir, { recursive: true })
45
+ return existsSync(dir)
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 解析一次工具调用的工作目录(供 python 子进程 cwd 使用)。
53
+ * @param {object} [exec] 工具执行上下文。
54
+ * @returns {string} 解析后的绝对路径(保底可用)。
55
+ */
56
+ export function resolveWorkdir(exec) {
57
+ const serverCwd = process.cwd()
58
+ const rawSessionCwd = sessionWorkspace(exec)
59
+ const sessionCwd = rawSessionCwd && rawSessionCwd !== serverCwd ? rawSessionCwd : undefined
60
+ if (sessionCwd) {
61
+ ensureDir(sessionCwd)
62
+ return sessionCwd
63
+ }
64
+ const fb = fallbackWorkspace()
65
+ ensureDir(fb)
66
+ return fb
67
+ }
@@ -1,37 +0,0 @@
1
- # DECISIONS-2026-08-29 — GLM 两轮评审裁决与路线固化
2
-
3
- > 第三方 LLM(GLM)对纯 Windows 路线功能/架构的两轮评审;按"以实测为准、独立裁决"原则的采纳记录。
4
-
5
- ## 第一轮(功能拓展+优化)裁决
6
-
7
- | 建议 | 裁决 | 备注 |
8
- |---|---|---|
9
- | P0 pyrodigal 注释(兑现"基因组→模型"承诺)| ✅ 原则采纳 | **待用户解锁**(用户已说暂不补注释步骤);落地=官方注释优先+pyrodigal 兜底+meta 模式(<100kb 强制)|
10
- | P0 G5 全量必需性扫描(FVA 预筛)| ✅ 采纳 | 排入路线;模型卡加必需基因章节(证据着色)|
11
- | P1 gapseq 白名单 + L3 MILP 组合拳 | ✅ 采纳 | 白名单仅本地基准(license 见下);L3 候选池=自建同源映射(license 干净)|
12
- | P1 biomass 精修(参考对照)| ✅ 采纳但后置 | 级联重置验证基线风险;做成可选 --biomass-profile + before/after 三联对照 |
13
- | P2 accession 下载 / P3 表型 schema / P3 差分 / P4 交叉 / P4 可视化 | ✅ 按序采纳 | 可视化=Escher JSON 导出(不做内嵌渲染)|
14
- | 建模卡 RPC/架构优化 | ✅ 见第二轮 | — |
15
- | 五道关卡→关卡注册器 + G6 ATP 泄漏 | ✅ **已实现** | G6 实测 C58 PASS(flux 0.0)|
16
- | 防过补第五闸门(全局预算 <5%)| ✅ 采纳 | 表型回填/长期迭代防温水煮青蛙 |
17
- | 证据分级枚举(sequence/literature/rule/math)| ✅ 采纳 | 白名单+MILP 落地后填充 model card |
18
- | SBML 往返保真测试 | ✅ 采纳 | 进 smoke |
19
-
20
- ## 第二轮(7 问)裁决
21
-
22
- 1. **注释质量**:✅ 官方注释优先+pyrodigal 兜底;验证协议(预测蛋白覆盖率 85-92%、反应 delta<3%、生长差<5%)待解锁后实施。
23
- 2. **license**:✅ **分离消费**——seqDB 仅本地基准不分发;自建 UniProt→EC→BiGG 作可分发白名单/L3 候选池(license 干净);止损 2 周;GPLv3 环绕注意。
24
- 3. **介质的共享形态**:✅ 单一 Python 来源 + RPC/CLI 子命令(不做双语言双包);schema v2 采纳,**修正 units=mmol/gDW/h(非 1/h)**。
25
- 4. **modelseedpy**:✅ P2 交叉分歧清单主体(非并列引擎);BiGG↔SEED 桥复用 MET_ALIAS 模式。
26
- 5. **G6**:✅ 已实现(ATP demand 全关最大化 >0.01 WARN);补洞后必跑;注册器已内置。
27
- 6. **证据呈现**:✅ 分区+徽标+行着色+`--strict-evidence` 开关(不隐藏不全表高亮)。
28
- 7. **排期冲突**:✅ 衔接前置(schema v2/media_resolve RPC)归入 gem 内部工作流;genie 解锁即消费,gem 主线不阻塞。
29
-
30
- ## 落地状态
31
-
32
- - [x] G6 ATP 泄漏关卡 + 关卡注册器(validate.py;C58 PASS 0.0)
33
- - [ ] schema v2(supported_mediums/verified_phenotypes/model_lineage/evidence_summary)——unit 已定为 mmol/gDW/h
34
- - [ ] G5 全量(FVA 预筛)——路线 P0
35
- - [ ] 白名单 + L3 MILP + 证据分级——路线 P1(license 结论已定:自建映射)
36
- - [ ] 注释步骤(pyrodigal)——**待用户解锁**
37
- - [ ] genie 衔接(RPC 子命令)——待用户解锁