dsh-molbio-tools 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,167 @@
10
10
  改为按**包名**引用后模块缓存问题随之消失——没有拷贝,也就没有需要保持同步的版本目录。
11
11
  下文历史条目里出现的 `vN` 目录保持原样,作为当时的记录。
12
12
 
13
+ ## [0.21.1] — 2026-10-25(DSH STORE 提交前置:一个 patch、逐版本兼容声明、一个 tarball 一个清单)
14
+
15
+ **工具数不变(60),插件运行时行为零改动。** 这一版不加任何计算能力,只把"这个包以什么形状提交"
16
+ 改成 DSH STORE 的提交预检与 DSH 自身的清单契约**都能接受、且不再互相矛盾**的那一种。三处改动都
17
+ 来自同一次上架提交:预检里能靠改包解决的两条已经解决,剩下两条的原理与请求另写在
18
+ [docs/store-preset-exemption.md](docs/store-preset-exemption.md)。
19
+
20
+ ### 1. 两层 patch 合并成单个 `cordis.patch.yml`
21
+
22
+ 0.21.0 的 `dsh.bundle.patch` 是**有序数组**:`["./cordis.patch.yml", "./preset/molbio-lab/preset.patch.yml"]`。
23
+ DSH 运行时接受这种写法(`@deepseek-ai/dsh-app-boot` 的 `bundlePatchPaths`:one file, or an
24
+ ordered list of files),官方 `@deepseek-ai/dsh-web-app` 自己就列了 5 个文件;但 DSH STORE 的提交
25
+ 预检(`scripts/check-plugin-submission.mjs` 的 `safePatchPath`)只接受字符串,数组直接判
26
+ `SUBMISSION_BUNDLE_PATCH_INVALID`。两边都自称官方契约,解法不必等预检放宽:
27
+
28
+ - `build/preset-patch.mjs` 现在直接产出**根目录的 `cordis.patch.yml`**,内容是**两个 insert**:
29
+ `molbio-client → ./host.mjs`(惰性宿主锚点)在前,`preset-molbio-lab`(由
30
+ `preset/molbio-lab/agent.cordis.yml` 生成的 preset 行)在后;顺序即应用顺序。
31
+ - 不再生成 `preset/molbio-lab/preset.patch.yml`(文件已删);`package.json` 的
32
+ `dsh.bundle.patch` 是**单字符串**。
33
+ - 四个读者改为跟单文件走:`test/preset-health.mjs`、`test/drift-probe.mjs`、
34
+ `preset/install.mjs`、`test/preset-rows.mjs`(后者在同一个文件的两个 insert 里找 preset 段,
35
+ 不假设"preset.patch.yml 才是 preset")。
36
+
37
+ > 这一条同时守住一条更早的教训:**锚点行不能因为"文件变少了"而消失**。0.15.2 的两个侧栏 tab
38
+ > 就是锚点丢失后静默消失的;`test/client-mount.mjs` 仍然逐包断言"被选中的包必须有一个宿主行
39
+ > 能解析到它",`test/contract.mjs` 仍然断言"产物里的每一处 `register` 都在 `inject` 里"。
40
+
41
+ ### 2. `engines.dsh` 与逐版本 `dshReleases`
42
+
43
+ 两个 manifest 各声明一份:
44
+
45
+ ```jsonc
46
+ "engines": { "node": ">=26", "dsh": ">=0.2.1-rc.2 <0.3.0-0" },
47
+ "dsh": { "compatibility": { "dshReleases": {
48
+ "0.2.1-rc.1": "unknown", "0.2.1-rc.2": "compatible", "0.2.1-rc.3": "unknown" } } }
49
+ ```
50
+
51
+ 上界写 `<0.3.0-0` 而不是 `<0.3.0`:DSH 用 `semver.satisfies(..., { includePrerelease: true })`
52
+ 判定(`@deepseek-ai/dsh-app-boot` 的 `unmetRequirements`),带 prerelease 选项时 `<0.3.0`
53
+ 会放行 `0.3.0-alpha.1`,`-0` 不会。`rc.1` 早于本包基线、`rc.3` 未取得也未验证,两者都只能
54
+ 标 `unknown`——**范围不等于验收**:`engines` 是安装时的粗筛,商城的 `dshReleases` 记录才是
55
+ 逐版本的验收结论。
56
+
57
+ 面板包随之 `0.1.3 → 0.1.4`:它的版本线独立于根包,但清单变了,不发这个版本号就没有能承载新
58
+ 声明的发布物。
59
+
60
+ ### 3. 根 tarball 不再内嵌面板包
61
+
62
+ `files` 白名单去掉 `packages`。此前根 tarball 里同时躺着 `package.json` 与
63
+ `packages/molbio-panel/package.json` 两个 DSH 插件清单,还重复携带 427 KB 相同的 `lib/client.js`;
64
+ 商城把"一个包里出现第二个插件清单"视为提交歧义是对的。现在根 tarball **只有一个插件清单**:
65
+ **118 → 115 个文件、约 1.1 MB → 约 999 kB**(多出的 1 个文件是本次提交的
66
+ `docs/store-preset-exemption-issue.md`)。面板没有因此消失——根包的 `lib/client.js` 本来就带着它
67
+ (第 4 节的启动回读可以作证);面板包继续按自己的地址单独发布。
68
+
69
+ `test/contract.mjs` 的打包断言随之改向:不再要求 `files` 必须包含 `packages/`,而是断言
70
+ **一个 tarball 里恰好只有一个 DSH 清单**(把 `packages` 加回白名单会立刻 FAIL)。
71
+
72
+ ### 4. 一次性 profile 证据(tarball 就是发布物)
73
+
74
+ 证据记录在 [docs/store-evidence.md](docs/store-evidence.md),用临时 `DSH_HOME` 重跑,不碰真实
75
+ `~/.dsh`:`npm pack` → `--from-default-profile web` 建 profile → `plugin add` tarball →
76
+ `--dump-config`(`molbio-client` 解析到已安装包内,preset 行带 `tool-molbio`)→
77
+ `--no-open --port 0` 启动 → 带 token 回读页面(**HTTP 200**,`__DSH_BOOT__` 图里有
78
+ `dsh-molbio-tools/client.js`)→ `plugin remove` → 再 `--dump-config`(`molbio` 引用 **0** 处)。
79
+ 装 tarball 而不是目录,是因为 tarball 同时证明 `files` 白名单带全了运行文件。
80
+
81
+ ### 5. 未做 / 边界
82
+
83
+ - **`0.2.1-rc.3` 没有验证**,因此标 `unknown`。拿到该版本后按
84
+ [docs/workflow.md](docs/workflow.md) 第 4 节的升级流程重跑 `preset-health` / `drift-probe` /
85
+ 一次性 profile,再把它改成 `compatible`。
86
+ - **预检的另外两条(保护命名空间、身份一致)不是改包能解决的**:本包已自行消掉
87
+ `SUBMISSION_BUNDLE_PATCH_INVALID`;剩下两条的证据、代码位置与三个可执行请求写在
88
+ [docs/store-preset-exemption.md](docs/store-preset-exemption.md),可直接贴到 issue 的短版在
89
+ [docs/store-preset-exemption-issue.md](docs/store-preset-exemption-issue.md)。
90
+ - **工具行为与 benchmark 一律不动**:60 个工具的输出与 0.21.0 相同,按规则 6 的"没变就不必跑
91
+ 模型";`bench --offline` 仍在 `npm test` 里,期望值照样逐条校验。
92
+
93
+ ### 6. 版本与验证
94
+
95
+ - 根包 `0.21.0 → 0.21.1`;面板 `0.1.4`(见 §2)。
96
+ - `npm test` 全绿:smoke、画面四套、客户端四套、contract 16 项、drift-probe 14 条突变、
97
+ preset-health 30 行、release 13 项、benchmark 四道守卫 + `bench --offline` 61 题。
98
+ - `node build/preset-patch.mjs --check` 与 `node build/client-bundle.mjs --check` 均报告与源一致。
99
+ - `node build/release-preflight.mjs --tag v0.21.1`:本地不变量全过;npm 上 `dsh-molbio-tools`
100
+ 已有 0.21.0、本版按 `publish` 处理;面板 0.1.4 尚未发布。
101
+ - 文档同步:maintainer 的版本号、roadmap 的当前状态、history 的版本一览与本段、store-evidence
102
+ 的 tarball 名与文件数。
103
+
104
+ ## [0.21.0] — 2026-10-23(DSH 0.2.1-rc.2 适配;两条 benchmark 欠账清零)
105
+
106
+ **工具数不变(60),插件行为零改动。** 这是一次"只跟上游走"的版本:0.20.1 之后本机 harness
107
+ 升到 0.2.1-rc.2,`npm test` 立刻红了三处——组合漂移、契约断言、以及一条依赖旧 fixture 的
108
+ 漂移探针。三处都不是插件退化,但每一处都属于**守卫在说谎**,所以按发布阻断处理。
109
+
110
+ ### 1. preset 行清单按 0.2.1-rc.2 逐行重抄(12 处结构差异)
111
+
112
+ - 上游 `standard` **新增** `time-context`、`tool-schedule`、`tool-plugin-market`;
113
+ - **删除** `tool-subagent-codex` / `tool-subagent-claude-code`(两个 `disabled: true` 的可选
114
+ provider)与 `tool-ralph`;
115
+ - **去掉** persona 的 `suffix: Your working directory is {{cwd}}.`,以及 `tool-subagent` /
116
+ `tool-subagent-fork` 的 `backgroundMode: continuable`;
117
+ - plan-mode 的段落文案改为 "until the user approves your plan through exit_plan_mode…"。
118
+
119
+ `agent.cordis.yml` 现在是上游 `config.plugins:` 块的**整体去缩进** + 末尾一行 `tool-molbio`;
120
+ 0.2.1 的上游文件不带行内注释,因此行内注释随之消失,来源与升级规则集中写在文件头部。
121
+ `preset.patch.yml` 由 `build/preset-patch.mjs` 重新生成。
122
+
123
+ ### 2. `test/contract.mjs`:两条断言不再锚在 minified 字面量上
124
+
125
+ - **client 入口**:改为按包自己的 `exports["./client"]`(`browser` 条件)解析。
126
+ 0.2.1-rc.2 把 `@deepseek-ai/dsh-client-connection` 移到 `lib/client.browser.js`,旧断言
127
+ 硬编码 `lib/client.js`,会把一个加载正常的包判死;同时 Web shell 已是打包产物、不再出现
128
+ `require("包名")`,"seeded into the shell" 兜底只剩代码路径。新增两个对照:没有
129
+ `./client` 导出的包必须解析为 `undefined`,以及移动过的入口必须解析到真实文件。
130
+ - **`presentationMeta`**:0.2.1 删除了 `exec.parent === void 0` 闸门(投影移进
131
+ `createSuccessResult`,对每次成功执行都做)。断言改挂到卡片真正消费的东西:投影被
132
+ `snapshotProjection` 记录、并以 `meta` 附到成功结果上。
133
+
134
+ ### 3. `drift-probe`:突变样本换成上游仍存在的行
135
+
136
+ 上游删掉 `tool-ralph` 后,那条"上游 `disabled: true` 被丢掉"的突变在**空转**(`withRow`
137
+ 找不到目标,什么也没改,守卫却"通过")。改用上游仍以 `disabled: true` 发布的
138
+ `tool-plugin-manager`;14 条突变全部仍能被抓住。
139
+
140
+ ### 4. benchmark:profile 重建、两条欠账清零
141
+
142
+ - preset 新增的两行宿主可承载行(`time-context`、`tool-schedule`)进入 headless profile,
143
+ `benchmark/profile.mjs` 重建 `molbio-bench`:**12 行宿主行,仍注册 60 个工具**。
144
+ - `methylation-block` 与 `qpcr-primers` 的冻结响应停在 0.16.0 之前,`benchmark-score` 每次
145
+ 都点名。本版用一次完整真实模型跑重录,并用 `_freeze-trace.mjs --replace` **只替换这两条**,
146
+ 其余 47 条保持原样;`run.amendments` 记录每条替换来自哪一次跑分。fixture 现为
147
+ **49 条记录、0 条 suspended**。
148
+ - 完整跑分(2026-10-23,DSH 0.2.1-rc.2,61 题):**58/61** 通过,tool selection **60/61**、
149
+ arguments **60/61**、final answer **59/61**;11.2M tokens / 2161 s。
150
+ - `protein-props` 的失败是**断言不认千分位逗号**(答案写 `1,490`,断言写 `/1490/`)——
151
+ 与 0.20.0 修过的 `/3935/` 同一类,当时漏了这一条。已改为 `1[,.]?490`,`--replay` 后
152
+ **59/61**。
153
+ - 剩余两条是**真实的模型遗漏**,不是产品缺陷:`dilution-math` 答案正确(50 µL + 950 µL)
154
+ 却从未调用 `molbio_lab_math`;`batch-analysis` 写出了 CSV,但最终回答只说"报告已生成",
155
+ 没有报出指令要求的长度/特征数。
156
+ - `_freeze-trace.mjs` 新增 `--replace id[,id…]`:只重录被修复作废的条目,其余记录与顺序原样
157
+ 保留,并在 `run.amendments` 里写明每条替换的来源跑分——**替换的是真实录制,不是手改**。
158
+
159
+ ### 5. 未做 / 边界
160
+
161
+ - **`png_path` 仍然不做**:`contract` 的"工作区仍不能存二进制文件"断言在 0.2.1-rc.2 上**仍然
162
+ 通过**,说明上游 fs 缝依旧是 text-only;触发条件未到。
163
+ - **面板版本保持 0.1.3**:`lib/client.js` 逐字节未变,`release-preflight` 复核为
164
+ "already published with identical bytes",因此不 bump。
165
+
166
+ ### 6. 版本与验证
167
+
168
+ - 根包 `0.20.1 → 0.21.0`;DSH 基线钉 **0.2.1-rc.2**(`.github/workflows/{test,release}.yml`、
169
+ preset 头注、`docs/workflow.md`、`docs/roadmap.md`、`docs/maintainer.md`)。
170
+ - `npm test` 全绿:contract 16/16、drift-probe 14 条突变、preset-health 30 行全过、release
171
+ 13 项、benchmark profile/score/coverage/changed、`bench --offline` 61 题。
172
+ - 文档:roadmap 状态、history 的 v0.21.0 段(三条教训)、workflow 的升级提示同步。
173
+
13
174
  ## [0.20.1] — 2026-10-18(插件专属图标)
14
175
 
15
176
  **工具数不变(60),运行时零改动;包内只多一个 746 B 的 SVG。**
package/README.md CHANGED
@@ -79,7 +79,7 @@ dsh plugin --profile web update dsh-molbio-tools
79
79
  > **0.7.1 起不再需要"注册 preset 根目录"这一步。**
80
80
  > DSH 0.1.7-alpha.1 移除了 `@deepseek-ai/dsh-agent-presets` 及其目录发现机制:preset 现在是
81
81
  > bundle patch 里的一个 `@deepseek-ai/dsh-agent-preset` **行**(本包的是
82
- > `preset/molbio-lab/preset.patch.yml`,由 `dsh.bundle.patch` 声明)。
82
+ > `cordis.patch.yml` 里的第二个 insert,由 `dsh.bundle.patch` 声明)。
83
83
  > 因此 preset 随 bundle 一起安装,**装完即出现**。
84
84
  >
85
85
  > 老版本遗留的两样东西可以清理(`node preset\install.mjs --profile web` 会替你查出来):
@@ -95,12 +95,12 @@ dsh plugin --profile web update dsh-molbio-tools
95
95
 
96
96
  ### 工具只在专属模式里出现(设计如此)
97
97
 
98
- 本包的 bundle patch 分两层,分工是固定的:
98
+ 本包的 bundle patch 是**一个文件、两层 insert**,分工是固定的:
99
99
 
100
- | 层 | 作用 |
100
+ | 层(同一个 `cordis.patch.yml` 里的两段) | 作用 |
101
101
  | --- | --- |
102
- | `cordis.patch.yml` | 只插一行**惰性宿主锚点**(`./host.mjs`),不注册任何工具 |
103
- | `preset/molbio-lab/preset.patch.yml` | 声明 **Molecular Biology Lab** 模式,60 个工具在这一层里 |
102
+ | `molbio-client` → `./host.mjs` | 只插一行**惰性宿主锚点**,不注册任何工具 |
103
+ | `preset-molbio-lab` → `@deepseek-ai/dsh-agent-preset` | 声明 **Molecular Biology Lab** 模式,60 个工具在这一层里 |
104
104
 
105
105
  所以 60 个工具**只在选中该模式的会话里加载**,其它模式(standard / ptc / minimal / cordis)
106
106
  不带它们,不占提示预算。bundle 安装 ≠ 工具全局可见:bundle 只是把"模式"和"面板"一起装进来。
@@ -741,11 +741,11 @@ dsh-molbio-tools/
741
741
  ├── lib/client.js # 客户端产物(exports["./client"],由 npm run build:client 生成)
742
742
  ├── packages/molbio-panel/ # 面板专用包(只面板、不带工具)
743
743
  ├── preset/molbio-lab/ # 专属模式 preset:agent.cordis.yml 是行清单(手改这里)
744
- │ # preset.patch.yml 由 build/preset-patch.mjs 生成
744
+ │ # preset.yml 是选择器文案;两者由生成器写进下面的 cordis.patch.yml
745
745
  ├── benchmark/ # 可用性评估:tasks/*.json + 判分器 + headless profile 推导(见其 README)
746
746
  ├── test/ # 冒烟 + 光栅化器 + 客户端/组合检查(含 preset 漂移与 benchmark profile 守卫)
747
747
  ├── docs/ # 维护者与实现文档(rules / workflow / roadmap / history 是本目录的骨架)
748
- └── cordis.patch.yml # bundle 的第一层补丁(当前为空列表;工具由 preset 层承载)
748
+ └── cordis.patch.yml # 唯一 bundle patch(生成物):锚点 insert + preset insert
749
749
  ```
750
750
 
751
751
  ---
@@ -159,6 +159,7 @@ benchmark/
159
159
  ├── _probe.mjs record ground truth (original suite)
160
160
  ├── _probe-all.mjs record ground truth (the rest of the catalog)
161
161
  └── _freeze-trace.mjs freeze a run's responses into the scorer's regression fixture
162
+ (`--replace id[,id]` re-records only the named entries and keeps the rest)
162
163
  ```
163
164
 
164
165
  A task's expected text lives in `tasks/`; the input that produces it lives in
@@ -221,7 +222,7 @@ session "…" runs under agent preset "molbio-lab", which the one-shot runner do
221
222
  Presets are an agent-plane feature the Web surface composes; the one-shot runner
222
223
  drives `agents.create()` directly and never joins one. So the tools must be
223
224
  mounted as **host rows**, which is the one shape this package otherwise refuses
224
- to ship (`cordis.patch.yml` is deliberately an empty list).
225
+ to ship (`cordis.patch.yml` only inserts the inert client anchor).
225
226
 
226
227
  `benchmark/profile.mjs` therefore generates a dedicated profile
227
228
  (`$DSH_HOME/profiles/molbio-bench`) whose patch layer is **derived** from
@@ -12,6 +12,20 @@
12
12
  * Run this only when you WANT to move the golden fixture forward (for example
13
13
  * after deliberately changing a task). Test failures point here when a recorded
14
14
  * response stops satisfying its task.
15
+ *
16
+ * ── amending a subset (`--replace`) ─────────────────────────────────────────
17
+ *
18
+ * Re-recording the one or two entries a fix invalidated used to mean freezing a
19
+ * whole new run and discarding every other recording. `--replace` takes just
20
+ * those responses from the new report and keeps the rest, and records where each
21
+ * amended response came from in `run.amendments` — explicit provenance beats a
22
+ * fixture that pretends to be a single run:
23
+ *
24
+ * node benchmark/_freeze-trace.mjs benchmark/reports/<stamp>-suite.json \
25
+ * --replace methylation-block,qpcr-primers
26
+ *
27
+ * The replacement is a REAL recording from the named run, never a hand edit: the
28
+ * original ids, order and every untouched response survive.
15
29
  */
16
30
 
17
31
  import { readFile, writeFile } from 'node:fs/promises';
@@ -19,7 +33,16 @@ import { resolve } from 'node:path';
19
33
 
20
34
  const source = process.argv[2];
21
35
  if (source === undefined) {
22
- console.error('usage: node benchmark/_freeze-trace.mjs <report.json>');
36
+ console.error('usage: node benchmark/_freeze-trace.mjs <report.json> [--replace id[,id…]]');
37
+ process.exit(2);
38
+ }
39
+
40
+ const replaceAt = process.argv.indexOf('--replace');
41
+ const replaceIds = replaceAt === -1
42
+ ? undefined
43
+ : (process.argv[replaceAt + 1] ?? '').split(',').map((id) => id.trim()).filter((id) => id !== '');
44
+ if (replaceAt !== -1 && (replaceIds === undefined || replaceIds.length === 0)) {
45
+ console.error('--replace needs a comma-separated list of task ids');
23
46
  process.exit(2);
24
47
  }
25
48
 
@@ -29,23 +52,74 @@ if (!Array.isArray(report.results) || report.results.length === 0) {
29
52
  process.exit(1);
30
53
  }
31
54
 
32
- const out = {
33
- $comment:
34
- 'Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces.',
35
- run: {
36
- recorded_at: report.startedAt,
37
- harness_version: report.harnessVersion,
38
- profile: report.profile,
39
- model: report.model,
40
- summary: { total: report.summary.total, passed: report.summary.passed },
41
- responses: report.results.map((result) => ({
42
- id: result.id,
43
- tool_calls: result.toolCalls,
44
- answer: result.answer,
45
- })),
46
- },
47
- };
55
+ const responses = report.results.map((result) => ({
56
+ id: result.id,
57
+ tool_calls: result.toolCalls,
58
+ answer: result.answer,
59
+ }));
48
60
 
49
61
  const target = resolve(import.meta.dirname, '..', 'test', 'fixtures', 'benchmark-traces.json');
62
+
63
+ /** The whole-fixture shape: one run, every response from it. */
64
+ function wholeRun() {
65
+ return {
66
+ $comment:
67
+ 'Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces.',
68
+ run: {
69
+ recorded_at: report.startedAt,
70
+ harness_version: report.harnessVersion,
71
+ profile: report.profile,
72
+ model: report.model,
73
+ summary: { total: report.summary.total, passed: report.summary.passed },
74
+ responses,
75
+ },
76
+ };
77
+ }
78
+
79
+ /** The amended shape: keep the fixture, swap the named entries, say so. */
80
+ async function amendedRun() {
81
+ const existing = JSON.parse(await readFile(target, 'utf8'));
82
+ const byId = new Map(existing.run.responses.map((response) => [response.id, response]));
83
+ // Replacing the same id twice must not pile up amendment entries or repeat
84
+ // the provenance sentence: this command is re-run whenever a report is rebuilt.
85
+ const amendments = (existing.run.amendments ?? []).filter((entry) => !replaceIds.includes(entry.id));
86
+ for (const id of replaceIds) {
87
+ const fresh = responses.find((response) => response.id === id);
88
+ if (fresh === undefined) {
89
+ console.error(`--replace: the report has no result for "${id}" — nothing was written`);
90
+ process.exit(1);
91
+ }
92
+ if (!byId.has(id)) console.log(` note: "${id}" was not in the fixture; appending it`);
93
+ byId.set(id, fresh);
94
+ amendments.push({
95
+ id,
96
+ recorded_at: report.startedAt,
97
+ harness_version: report.harnessVersion,
98
+ model: report.model,
99
+ });
100
+ }
101
+
102
+ const ordered = existing.run.responses.map((response) => byId.get(response.id));
103
+ for (const id of replaceIds) {
104
+ if (!existing.run.responses.some((response) => response.id === id)) ordered.push(byId.get(id));
105
+ }
106
+
107
+ return {
108
+ $comment: existing.$comment.includes('run.amendments names each one')
109
+ ? existing.$comment
110
+ : `${existing.$comment} Some responses were re-recorded later; run.amendments names each one and the run it came from, and every other response is unchanged. A replaced entry loses any suspension marker it carried, because the replacement is a fresh recording.`,
111
+ run: {
112
+ ...existing.run,
113
+ amendments,
114
+ responses: ordered,
115
+ },
116
+ };
117
+ }
118
+
119
+ const out = replaceIds === undefined ? wholeRun() : await amendedRun();
50
120
  await writeFile(target, `${JSON.stringify(out, null, 2)}\n`, 'utf8');
51
- console.log(`froze ${out.run.responses.length} response(s) into ${target}`);
121
+ console.log(
122
+ replaceIds === undefined
123
+ ? `froze ${out.run.responses.length} response(s) into ${target}`
124
+ : `amended ${replaceIds.length} response(s) (${replaceIds.join(', ')}) in ${target}; ${out.run.responses.length} total`,
125
+ );
@@ -17,7 +17,8 @@
17
17
  * agent-plane feature the Web surface composes; the one-shot runner drives
18
18
  * `agents.create()` directly and never joins one. So the tools have to be
19
19
  * mounted as HOST rows, which is the one shape this package otherwise refuses
20
- * to ship (`cordis.patch.yml` is deliberately empty — see the preset's header).
20
+ * to ship (`cordis.patch.yml` only inserts the inert client anchor — see the
21
+ * preset's header).
21
22
  *
22
23
  * ── What keeps the reconstruction honest ────────────────────────────────────
23
24
  *
@@ -44,7 +44,8 @@
44
44
  },
45
45
  {
46
46
  "where": "answer",
47
- "pattern": "1490"
47
+ "pattern": "1[,.]?490",
48
+ "note": "the extinction coefficient; the same thousands-separator tolerance the MW assertion already carries (0.20.0 fixed /3935/, not this one)"
48
49
  }
49
50
  ]
50
51
  },
@@ -1,35 +1,55 @@
1
1
  /**
2
2
  * dsh-molbio-tools/build/preset-patch.mjs
3
3
  *
4
- * Generate the AGENT-PRESET bundle patch from the canonical row list.
4
+ * Generate the ONE bundle patch this package ships: the client anchor plus the
5
+ * "Molecular Biology Lab" agent preset.
5
6
  *
6
- * WHY THIS EXISTS
7
- * ---------------
8
- * DSH 0.1.7-alpha.1 replaced the preset DIRECTORY mechanism. Before it, a preset
9
- * was an `agent.cordis.yml` the harness discovered from `<dshHome>/.agent-presets`
10
- * or from a configured root (`@deepseek-ai/dsh-agent-presets` scanned
11
- * directories). That package is gone, and the registry that replaced it "neither
12
- * scans directories nor accepts preset paths" — a preset is now a
13
- * `@deepseek-ai/dsh-agent-preset` ROW inside a bundle patch:
7
+ * WHY ONE FILE
8
+ * ------------
9
+ * DSH's runtime accepts `dsh.bundle.patch` as a file path or an ordered list of
10
+ * file paths (`@deepseek-ai/dsh-app-boot` → `bundlePatchPaths`), and the shipped
11
+ * `@deepseek-ai/dsh-web-app` uses the list form. DSH STORE's submission precheck
12
+ * only accepts a single string (`safePatchPath`), so the two layers this package
13
+ * used to ship as a list are emitted as one file with two top-level `insert`
14
+ * operations, in the same order they were applied:
14
15
  *
15
- * - insert:
16
- * - id: preset-molbio-lab
17
- * name: '@deepseek-ai/dsh-agent-preset'
18
- * config:
19
- * id: molbio-lab
20
- * name: Molecular Biology Lab
21
- * description: ...
22
- * plugins: [ ...the rows that used to be the composition... ]
16
+ * 1. molbio-client → './host.mjs' (inert host anchor; see below)
17
+ * 2. preset-molbio-lab → '@deepseek-ai/dsh-agent-preset'
23
18
  *
24
- * The rows themselves did not change — only their container did. Keeping the
25
- * rows in ONE place and generating the container from them is what stops the two
26
- * from drifting, because `test/preset-health.mjs` validates every row against
27
- * the INSTALLED harness's own schemas.
19
+ * Nothing about either layer changes: a patch file is an ordered list of
20
+ * operations, and DSH applies them in file order exactly as it applied the two
21
+ * files in list order.
22
+ *
23
+ * WHY THE ANCHOR EXISTS
24
+ * ---------------------
25
+ * DSH's client-module scan walks the HOST Loader tree only. It reads each live
26
+ * host row's package manifest, takes `dsh.client` and `exports["./client"]` from
27
+ * it, and composes the browser graph that becomes `window.__DSH_BOOT__`. A
28
+ * preset's rows live in an isolated subtree that scan never visits.
29
+ *
30
+ * So when the tools moved into the preset, this package lost its only host-plane
31
+ * row, its bundle stopped reaching the page, and the two right-sidebar tabs
32
+ * (Molbio / Papers) silently disappeared. `host.mjs` restores exactly one inert
33
+ * row so the browser half is discoverable again — without putting any tool into
34
+ * a session that did not choose the molbio-lab mode.
35
+ *
36
+ * WHY THE PRESET IS GENERATED
37
+ * ---------------------------
38
+ * DSH 0.1.7-alpha.1 removed the `<dshHome>/.agent-presets` directory scan: a
39
+ * preset is now a `@deepseek-ai/dsh-agent-preset` ROW in a bundle patch, which
40
+ * is what the second insert below declares. Because the BUNDLE registers it,
41
+ * `dsh plugin --profile <p> add dsh-molbio-tools` puts the mode in the picker
42
+ * with no copy step and no preset-root patch — see README「安装」.
43
+ *
44
+ * Keeping the rows in ONE place and generating the container from them is what
45
+ * stops the two from drifting, because `test/preset-health.mjs` validates every
46
+ * row against the INSTALLED harness's own schemas.
28
47
  *
29
48
  * source : preset/molbio-lab/agent.cordis.yml (top-level row list, comments intact)
30
- * output : preset/molbio-lab/preset.patch.yml (the bundle patch, GENERATED)
49
+ * source : preset/molbio-lab/preset.yml (picker name + description)
50
+ * output : cordis.patch.yml (GENERATED; anchor + preset)
31
51
  *
32
- * The source is emitted as-is: no re-serialization, so the comments that explain
52
+ * The rows are emitted as-is: no re-serialization, so the comments that explain
33
53
  * each row survive and the file stays diffable against the shipped `standard`
34
54
  * patch it descends from.
35
55
  *
@@ -55,20 +75,56 @@ export function rowBlockOf(source) {
55
75
  /** Indent every line of a block by `width` spaces. */
56
76
  const indent = (block, width) => block.split('\n').map((line) => (line === '' ? line : ' '.repeat(width) + line)).join('\n');
57
77
 
78
+ const HEADER = [
79
+ '# dsh-molbio-tools bundle patch — the one file `dsh.bundle.patch` names.',
80
+ '#',
81
+ '# GENERATED by build/preset-patch.mjs. Edit the row list in',
82
+ '# preset/molbio-lab/agent.cordis.yml (and the picker strings in preset.yml), then',
83
+ '# re-run `node build/preset-patch.mjs`; hand edits here are reverted by the next',
84
+ '# run, and `node build/preset-patch.mjs --check` fails while the two disagree.',
85
+ '#',
86
+ '# Two top-level inserts, applied in this order:',
87
+ '# 1. molbio-client — inert host anchor for the browser half (see below)',
88
+ '# 2. preset-molbio-lab — the "Molecular Biology Lab" agent preset',
89
+ ].join('\n');
90
+
91
+ const ANCHOR = [
92
+ '# ── 1. the CLIENT-BUNDLE ANCHOR ────────────────────────────────────────────',
93
+ '#',
94
+ '# `host.mjs` registers no tool and publishes no service. Its only job is to be a',
95
+ "# loader row DSH's client-module scan can see: that scan walks the HOST Loader",
96
+ "# tree, reads the row's package manifest, and takes `dsh.client` +",
97
+ '# `exports["./client"]` from it to compose `window.__DSH_BOOT__`. A preset\'s rows',
98
+ '# live in an isolated subtree the scan never visits, so a `dsh.client` package',
99
+ '# referenced only from inside a preset loses its only discoverable row and its',
100
+ '# right-sidebar tabs silently disappear.',
101
+ '#',
102
+ '# Keep the tool in the PRESET and only the anchor here. An earlier version of',
103
+ "# this file inserted a host row for the tool itself (`name: dsh-molbio-tools`),",
104
+ "# which both registered all 60 tools globally and collided with the preset's own",
105
+ '# copy of that row:',
106
+ '#',
107
+ '# broken: "tool-molbio (...): never started"',
108
+ '- insert:',
109
+ ' - id: molbio-client',
110
+ " name: './host.mjs'",
111
+ ].join('\n');
112
+
113
+ /**
114
+ * Build the bundle patch: anchor first, preset second.
115
+ *
116
+ * @param {{rows: string, description: string, displayName?: string}} input
117
+ * @returns {string} the complete `cordis.patch.yml` text (single trailing newline)
118
+ */
58
119
  export function buildPatch({ rows, description, displayName = 'Molecular Biology Lab' }) {
59
- const header = [
60
- '# dsh-molbio-tools bundle layer: declares the "Molecular Biology Lab" agent preset.',
120
+ const presetHeader = [
121
+ '# ── 2. the MOLECULAR BIOLOGY LAB PRESET ───────────────────────────────────',
61
122
  '#',
62
- '# GENERATED by build/preset-patch.mjs from preset/molbio-lab/agent.cordis.yml.',
63
- '# Edit the ROW LIST there and re-run `node build/preset-patch.mjs`; editing the',
64
- "# `plugins:` block below by hand will be reverted by the next run, and",
65
- '# `node build/preset-patch.mjs --check` fails while the two disagree.',
66
- '#',
67
- '# DSH 0.1.7-alpha.1 removed the `<dshHome>/.agent-presets` directory scan: a preset is',
68
- '# now a `@deepseek-ai/dsh-agent-preset` ROW in a bundle patch, which is what this',
69
- '# layer inserts. Because the BUNDLE registers it, `dsh plugin --profile <p> add',
70
- '# dsh-molbio-tools` puts the mode in the picker with no copy step and no',
71
- '# preset-root patch — see README「安装」.',
123
+ '# DSH 0.1.7-alpha.1 removed the `<dshHome>/.agent-presets` directory scan: a',
124
+ '# preset is now a `@deepseek-ai/dsh-agent-preset` ROW in a bundle patch, which',
125
+ '# is what this insert declares. Because the BUNDLE registers it,',
126
+ '# `dsh plugin --profile <p> add dsh-molbio-tools` puts the mode in the picker',
127
+ '# with no copy step and no preset-root patch — see README「安装」.',
72
128
  '#',
73
129
  '# `order: 5` is deliberate: the shipped presets occupy 1..4 (standard, ptc,',
74
130
  '# minimal, cordis) and the registry sorts by `order` with an id tie-break, so a',
@@ -76,7 +132,11 @@ export function buildPatch({ rows, description, displayName = 'Molecular Biology
76
132
  ].join('\n');
77
133
 
78
134
  return [
79
- header,
135
+ HEADER,
136
+ '',
137
+ ANCHOR,
138
+ '',
139
+ presetHeader,
80
140
  '- insert:',
81
141
  ' - id: preset-molbio-lab',
82
142
  " name: '@deepseek-ai/dsh-agent-preset'",
@@ -95,7 +155,7 @@ function main() {
95
155
  const check = process.argv.includes('--check');
96
156
  const sourcePath = join(repoRoot, 'preset', 'molbio-lab', 'agent.cordis.yml');
97
157
  const metaPath = join(repoRoot, 'preset', 'molbio-lab', 'preset.yml');
98
- const outPath = join(repoRoot, 'preset', 'molbio-lab', 'preset.patch.yml');
158
+ const outPath = join(repoRoot, 'cordis.patch.yml');
99
159
 
100
160
  const rows = rowBlockOf(readFileSync(sourcePath, 'utf8'));
101
161
  if (rows === undefined || !rows.includes('- id:')) {
@@ -124,16 +184,16 @@ function main() {
124
184
  process.exit(1);
125
185
  }
126
186
  if (current !== text) {
127
- console.error('preset-patch: the committed preset patch is STALE — run `node build/preset-patch.mjs`');
187
+ console.error('preset-patch: the committed bundle patch is STALE — run `node build/preset-patch.mjs`');
128
188
  process.exit(1);
129
189
  }
130
- console.log('preset patch is current');
190
+ console.log('bundle patch is current');
131
191
  return;
132
192
  }
133
193
 
134
194
  writeFileSync(outPath, text, 'utf8');
135
- const rows_ = rows.split('\n').filter((line) => /^\s*- id:/.test(line)).length;
136
- console.log(`preset patch written: ${outPath} (${rows_} rows)`);
195
+ const rowCount = rows.split('\n').filter((line) => /^\s*- id:/.test(line)).length;
196
+ console.log(`bundle patch written: ${outPath} (${rowCount} preset rows + anchor)`);
137
197
  }
138
198
 
139
199
  if (process.argv[1] !== undefined && import.meta.url === pathToFileURLSafe(process.argv[1])) main();
@@ -201,7 +201,7 @@ export function tagVersion(tag) {
201
201
  * `rc.10 < rc.2`, so a release of `rc.2` after `rc.10` would look like a
202
202
  * DOWNGRADE and the gate would refuse a legitimate publish (or, in the other
203
203
  * direction, wave through a stale one). This repository does cut prereleases
204
- * (the harness baseline itself is `0.2.0-rc.2`), so the rule is implemented
204
+ * (the harness baseline itself is `0.2.1-rc.2`), so the rule is implemented
205
205
  * rather than approximated.
206
206
  *
207
207
  * Semver's rule: split on dots; numeric identifiers compare numerically and rank