dsh-vibe-math 2.3.13 → 2.3.15

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/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # Vibe Mathematics — 多代理数学问题求解与验证框架(四架构)
2
+
3
+ [English](README.en.md) | 中文
2
4
 
3
5
  [![npm](https://img.shields.io/npm/v/dsh-vibe-math)](https://www.npmjs.com/package/dsh-vibe-math)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -374,7 +376,7 @@ dsh plugin --profile <你的 profile> add github:ChongCyrus/Vibe-Mathematics
374
376
  > **2026 兼容性修复要点**(详见 `docs/COMPAT-AUDIT-ROUND2.md`):① `tools.restrict()` 对**未注册的工具名抛错**,而 filter 在建立子代理时应用,故权限名表必须只含本部署真正注册的名字——v2/v3 原先硬编码 `web`/`fetch`/`bash`(其中 `bash` 在 Windows 被 `disabled`)会导致"想收紧权限时子代理永远起不来";② v4 的真实 `/compact` 原先在 `subagent/end` 里查 `agents.get()`,但该事件在子代理**已被移出注册表之后**才触发,属死代码,已改为在 `subagent/start` 捕获引用;③ 可选服务改为**惰性读取**,不再在 `apply()` 快照(否则挂载顺序会让 `subprocess` 永久为 undefined 而静默不建目录)。
375
377
  - **DSH STORE 兼容声明**:`package.json` 的 `dsh.compatibility.dshReleases` 对每个完整 DSH 版本逐项声明 `compatible` / `incompatible` / `unknown`(当前已声明 `0.1.2-alpha.4` … `0.1.5-rc.2` 共 8 个版本为 `compatible`,实测目标为 `0.1.5-rc.2`);`engines.node` 为 `^22.19.0 || >=24.0.0`。
376
378
  - **运行时自检(能力 + 版本双检)**:安装器(bundle 插件)每次启动时:**① 尽力探测 DSH 版本**(读 `@deepseek-ai/dsh/package.json` 或 `DSH_VERSION` 环境变量;DSH 未通过公开 service/context 暴露版本,故为尽力而为,探测不到就跳过)。若探测到且该版本未被 `dshReleases` 声明为 `compatible`,会给出明确提示;**② 再对宿主服务与关键 API 做能力自检**(这是真正的挂载门槛):`subagents`/`agents`/`tools`/`commands`/`fs` 为**必需**(缺失即 warning),`subprocess`/`sandboxPolicy`/`compaction`/`sessionProjections`/`sessions` 为**可选**(缺失只提示"功能会静默降级",不影响挂载;`sessionProjections` 缺失时 v5 的研究所状态回退到加固 JSON),另含 `fs.resolve` 返回形状检测与 subagent `agentOptions`/`toolFilter` capability 检测。preset 挂载失败时先看 DSH 日志里的自检 warning。
377
- - **升级路径**:DSH 升级后无需重装本包;升级本包用 `dsh plugin update dsh-vibe-math`,重启 DSH 后安装器会把四个 preset 的受管文件整体更新到新版本(改过的文件同样被替换,原文先进 `<presetRoot>/.vibe-math-backup/`;见上文「安装」说明)。
379
+ - **升级路径**:DSH 升级后无需重装本包;升级本包用 `dsh plugin --profile <你的 profile> add dsh-vibe-math@latest`(`dsh plugin` 的 `--profile` 是必填项;用 `add` 而不是 `update`,因为 profile 里可能把版本钉成精确值,那时 `update` 不会跨过去),重启 DSH 后安装器会把四个 preset 的受管文件整体更新到新版本(改过的文件同样被替换,原文先进 `<presetRoot>/.vibe-math-backup/`;见上文「安装」说明)。
378
380
 
379
381
  ---
380
382
 
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # The dsh-vibe-math merged bundle patch. Installing this bundle (dsh plugin add
1
+ # The dsh-vibe-math merged bundle patch. Installing this bundle (dsh plugin --profile <name> add
2
2
  # dsh-vibe-math / dsh-market) runs ONE plugin row: the preset installer, which
3
3
  # copies the agent presets (vibe-math-v2 + vibe-math-v3 + vibe-math-v4 +
4
4
  # vibe-math-v5) into the DSH preset root. The presets themselves register the
@@ -320,4 +320,46 @@ provider **直接拒绝**。于是"提示词/规格/状态表都写着这个参
320
320
 
321
321
  - 缺陷要分**类**:"这一整类此前没有审计维度"比"修了 N 个 bug"更重要。
322
322
  - 假绿/假红必须单独说明:**审计本身失效**是最严重的发现。
323
- - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。
323
+ - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。
324
+
325
+ ---
326
+
327
+ ## 6. 发布说明(Release Notes / GitHub Release)格式
328
+
329
+ **中英双语、先中文后英文,只写"给用户看的信息"。** 工程过程(探针结果、断言条数、事故复盘、行号与
330
+ 内部编号)属于 `_oneoff/audit-findings-*.md` 台账与提交信息,**不进**发布说明。
331
+
332
+ 固定骨架(只有版本号、标题与正文随版本变,**节名不变**):
333
+
334
+ ```markdown
335
+ # dsh-vibe-math <版本> — 发布说明(中文)
336
+
337
+ > 上一版:<版本>。<一句话:这一版是干什么的、是否影响行为。>
338
+
339
+ ## 概览
340
+ ## 变更
341
+ ## 兼容性
342
+ ## 升级
343
+
344
+ ---
345
+
346
+ # dsh-vibe-math <版本> — Release Notes (English)
347
+
348
+ > Previous: <version>. <One sentence: what this release is and whether behaviour changes.>
349
+
350
+ ## Overview
351
+ ## Changes
352
+ ## Compatibility
353
+ ## Upgrade
354
+ ```
355
+
356
+ - [ ] 两个 H1 都在;**英文那份的 H1 必须含 `English`**(发布门禁阶段 1c 会检查);
357
+ - [ ] 中文部分 ≥ 150 个汉字(门禁同阶段检查),英文部分不得残留未翻译段落;
358
+ - [ ] 每条变更都回答"**对使用者有什么影响**"——不写内部编号、探针名、断言条数;
359
+ - [ ] 「兼容性」明确回答:四套预设的字节/行为是否变化、参数与工具面是否变化;
360
+ - [ ] 「升级」给出可直接复制的命令(`npm i dsh-vibe-math@latest`;若 profile 里把版本钉死了,
361
+ 写出对应的 `dsh plugin --profile <name> add dsh-vibe-math@latest`);
362
+ - [ ] **发布前把草稿交给用户确认**(在对话里展示全文,或用提问工具);用户同意或给出修改意见后再发布;
363
+ - [ ] GitHub Release 的正文 = 这份文件本身(`gh release create --notes-file <该文件>`),标题中英并列。
364
+
365
+ ---
@@ -11,8 +11,13 @@
11
11
  // · 门槛:**全体常驻一致**(全 1 或全 0)才进 Verified/,没有 forced / flat / 近共识收口;
12
12
  // · Lean 形式化:formalVerify = off / encourage / require(passed 后审查对象变成"忠实性")。
13
13
  //
14
- // 需要 PNG 时用浏览器打开 SVG 另存,或无头浏览器截图:
15
- // msedge --headless=new --window-size=1760,1300 --screenshot=框架图-v4.png 框架图-v4.svg
14
+ // 需要 PNG(市场轮播 / 预览图)时**不要**直接照着"窗口尺寸 = 画布尺寸"截图:
15
+ // msedge --headless=new --window-size=1720,1260 --screenshot=示例图/框架图-v4.png 示例图/框架图-v4.svg ← 会静默裁掉底部
16
+ // 原因:`--screenshot` 截的是**窗口**,而页面**视口**比窗口矮约 96px(浏览器 UI 占位),
17
+ // 画布底部那一截根本没被绘制——而 PNG 的尺寸又恰好等于画布尺寸,所以从尺寸上完全看不出来。
18
+ // (2.3.15 修掉的真实缺陷:v4/v5 的 PNG 因此丢了最下面的图例行,且连续两版没人发现。)
19
+ // 正确做法(按 SVG 自身尺寸渲染、裁掉多余高度、并校验墨迹到达内容底边):
20
+ // node docs/render_framework_diagram_png.mjs 示例图/框架图-v4.svg 示例图/框架图-v4.png 2
16
21
  //
17
22
  // 版式约定(改布局时请遵守):
18
23
  // · 每个 band 的标题在 y+27、副标题在 y+47,**内容从 y+56 开始**;
@@ -7,8 +7,13 @@
7
7
  // v2/v3 用 matplotlib 脚本生成 PNG;v4/v5 用 Node 直接生成 **SVG**:
8
8
  // · 本仓库的运行时就包含 Node,不需要额外装 Python / matplotlib;
9
9
  // · SVG 是纯文本,diff 友好、可评审、任意缩放不糊。
10
- // 需要 PNG 时用浏览器打开 SVG 另存,或用无头浏览器截图:
11
- // chrome --headless=new --window-size=1720,1100 --screenshot=框架图-v5.png 框架图-v5.svg
10
+ // 需要 PNG(市场轮播 / 预览图)时**不要**直接照着"窗口尺寸 = 画布尺寸"截图:
11
+ // chrome --headless=new --window-size=1720,1204 --screenshot=示例图/框架图-v5.png 示例图/框架图-v5.svg ← 会静默裁掉底部
12
+ // 原因:`--screenshot` 截的是**窗口**,而页面**视口**比窗口矮约 96px(浏览器 UI 占位),
13
+ // 画布底部那一截根本没被绘制——而 PNG 的尺寸又恰好等于画布尺寸,所以从尺寸上完全看不出来。
14
+ // (2.3.15 修掉的真实缺陷:v4/v5 的 PNG 因此丢了最下面的图例行,且连续两版没人发现。)
15
+ // 正确做法(按 SVG 自身尺寸渲染、裁掉多余高度、并校验墨迹到达内容底边):
16
+ // node docs/render_framework_diagram_png.mjs 示例图/框架图-v5.svg 示例图/框架图-v5.png 2
12
17
  //
13
18
  // 版式约定(改布局时请遵守,否则会重叠):
14
19
  // · 每个 band 的标题在 y+27、副标题在 y+47,**内容从 y+56 开始**;
@@ -5,7 +5,7 @@
5
5
 
6
6
  ---
7
7
 
8
- ## 1. 仓库根目录归档:68 个条目 → 18 个
8
+ ## 1. 仓库根目录归档:跟踪条目 77 → 19(其中文件 66 → 7)
9
9
 
10
10
  只留下"包必须暴露在顶层"的东西:
11
11
 
@@ -118,12 +118,12 @@ docs/release-notes/ 18 份历史发布说明(本版 +1)
118
118
 
119
119
  | 项 | 2.3.12 | 2.3.13 |
120
120
  |---|---|---|
121
- | 仓库根目录条目 | 68(含 59 个散落文件) | **18**(7 文件 + 11 目录) |
122
- | 全套件并行回归 | 25/25 | 25/25(断言条数逐项一致) |
123
- | 布局不变性(改前 vs 改后逐行比对) | — | **25/25 输出相同** |
121
+ | 仓库根目录的跟踪条目 | **77**(66 文件 + 11 目录) | **19**(7 文件 + 12 目录) |
122
+ | 全套件并行回归 | 25/25 | **26/26**(24/25 个原有套件断言条数逐项一致;市场守卫有意 +2) |
123
+ | 布局不变性(改前 vs 改后逐行比对) | — | **24/26 输出逐字节相同**(另 2 个是本轮改过内容的文件;无一个与布局有关) |
124
124
  | 安装器策略套件 | — | **52/0**(+ 对旧策略 10 条变红的灵敏度探针) |
125
125
  | 市场元数据守卫 | 19/0 | **21/0**(+ 2 条链接探针) |
126
- | README 本地链接 | 29(其中 2 条点开 404) | 29(全部存在且随包) |
126
+ | README 本地链接 | 29(npm 页上 2 条 404:两个 `.py` 生成器没随包) | 29(全部存在且随包) |
127
127
  | 发布产物自证 | 通过 | 通过(并在包内跑 6 个随包守卫) |
128
128
 
129
129
  ## 6. 升级
@@ -0,0 +1,83 @@
1
+ # dsh-vibe-math 2.3.14 — 升级命令修正 · 安装器对「降级」发声 · 计数口径纠正
2
+
3
+ > 上一版:2.3.13。本版**不改四套预设的任何字节**(预设代码、提示词、语料逐字未变),改动集中在三件事:
4
+ > 一条照抄会**失败**的升级命令、一次**静默的降级**、以及两处数字口径。另外删掉仓库里一张没有任何引用的图。
5
+
6
+ ---
7
+
8
+ ## 1. README 的升级命令原本会直接失败
9
+
10
+ `dsh plugin` 的 `--profile` 是**必填项**(`dsh/lib/bin.js`:`plugin.requiredOption('--profile <name>', …)`),
11
+ 而 README 写的是:
12
+
13
+ ```sh
14
+ dsh plugin update dsh-vibe-math # ✗ error: required option '--profile <name>' not specified
15
+ ```
16
+
17
+ 即使补上 `--profile`,`pnpm update` 也**跨不过被精确钉住的版本**——而 profile 里常见
18
+ `"dsh-vibe-math": "2.1.0"` 这种写法。现改为:
19
+
20
+ ```sh
21
+ dsh plugin --profile <你的 profile> add dsh-vibe-math@latest # ✓ 用 add:它会把依赖写到你指定的版本
22
+ ```
23
+
24
+ 同一处错误写法还出现在 `installer.js` 与 `cordis.patch.yml` 的说明注释里,一并改正。
25
+
26
+ ## 2. 安装器:降级不再静默
27
+
28
+ "版本一变就整体替换"这条策略**没有区分方向**。当记录里的版本比正在运行的包**新**时
29
+ (典型成因:profile 里的依赖还钉在旧版,而 preset 目录已经被更新的包同步过),下一次启动会把
30
+ preset 换回**旧**字节——而日志只会说一句 `preset auto-update: version 2.3.12 → 2.1.0`。
31
+
32
+ 现在补一条明确的告警(复用安装器里已有的 `parseSemver` / `compareSemver`,任一侧解析不出就跳过):
33
+
34
+ > `[dsh-vibe-math] 记录里的版本(2.3.13)比本包版本(2.1.0)新,本次会把 preset 换回旧字节。`
35
+ > `若这不是你想要的,请先升级 profile 里的依赖:dsh plugin --profile <name> add dsh-vibe-math@latest,再重启 DSH。`
36
+
37
+ **替换本身仍然执行**——"preset 目录等于已安装的那个包"就是这条策略的全部意义;变的只是它不再偷偷发生。
38
+ (这不是假想:发布 2.3.13 前的复核在本机实测到这个形态——`~/.dsh/profiles/web` 里钉着 `2.1.0`,
39
+ 而 preset 目录的状态记录是 `2.3.12`,用**已安装的那个安装器**跑一遍就会重写 12 个预设文件。
40
+ 本机已把 profile 依赖升到最新并同步,之后一致性探针输出 `consistent`。)
41
+
42
+ ## 3. 计数口径纠正(两处)
43
+
44
+ | 位置 | 原来 | 现在 |
45
+ |---|---|---|
46
+ | 2.3.13 发布说明 + `compatNote` | 根目录"68 个条目 → 18 个(7 个文件 + 11 个目录)" | **跟踪条目 77 → 19(文件 66 → 7,目录 11 → 12)** |
47
+ | `docs/test-timing.md` | 其余 16 个套件 / 其中 8 个 < 1 s | **其余 19 个 / 其中 12 个 < 1 s** |
48
+
49
+ 第一处原来把"仓库跟踪的**文件数**"与"目录里可见的**条目数**"混着比——两边都不是同一个口径。
50
+ 发布说明的验收表也一并改准(全套件 **26/26**、布局不变性 **24/26 逐字节相同**)。
51
+
52
+ ## 4. 仓库:删除一张没有任何引用的图
53
+
54
+ `示例图/框架图.png`(397 KB)在**仓库源码、`screenshots.json`、以及插件市场目录条目里都没有任何引用**
55
+ (目录 YAML 只有 `url` / `name` / `category` / `description` 四个字段;市场的轮播图与卡片图都来自
56
+ `screenshots.json` 里的 v5 / v4 / v3 / v2 四张)。已删除,`git log` 可查。
57
+
58
+ ## 5. 验收
59
+
60
+ | 项 | 2.3.13 | 2.3.14 |
61
+ |---|---|---|
62
+ | closing verification(发布门禁) | 19/19 | **19/19** |
63
+ | 全套件并行回归 | 26/26 | **26/26** |
64
+ | 安装器策略套件 | 52/0 | **57/0**(新增 5 条:降级必须告警、替换照旧、原文仍备份、状态记录实际版本) |
65
+ | 市场元数据守卫 | 21/0 | **21/0** |
66
+ | 静态守卫 | 157 / 94 / clean | **157 / 94 / clean** |
67
+ | 引用闭合审计(`main`/`exports`/`files`/链接/命令行/路径字面量) | 0 悬空 | **0 悬空** |
68
+ | 仓库相对链接 | 52 条 / 0 失效 | **52 条 / 0 失效** |
69
+
70
+ 发布前的复核还做了一次"牵连对象"审计:把 `_oneoff` 里**所有**引用被搬运文件的工具找出来并修正
71
+ (10 个按裸文件名、3 个按绝对路径),其中两个探针原本会**原地改写仓库里的预设源码/守卫脚本**、
72
+ 只在 `finally` 里恢复——被超时杀掉就会留下损坏的文件。它们现在改为**在临时副本上变异**
73
+ (v2/v3 探针走 `V2_PLUGIN` / `V3_PLUGIN` + 临时语料目录),另外两个加了"启动先自愈、退出必校验"。
74
+
75
+ ## 6. 升级
76
+
77
+ ```
78
+ npm i dsh-vibe-math@latest
79
+ ```
80
+
81
+ 无迁移;`off` 档行为、四个 preset 的字节与提示词均未变。
82
+ 若你把本包钉在 profile 里,记得同步那个依赖版本(见 §1),否则下一次启动会把 preset 换回钉住的那一版
83
+ (届时日志会明确告警,不会像以前那样悄悄发生)。
@@ -0,0 +1,137 @@
1
+ # dsh-vibe-math 2.3.15 — 发布说明(中文)
2
+
3
+ > 上一版:2.3.14。本版新增**英文 README 与中英切换**,把两张架构图重新导出为**完整的高清 PNG**,
4
+ > 并把发布说明固定为**中英双语**。四套预设的代码、提示词与语料**字节未变**,运行时行为不变。
5
+
6
+ ---
7
+
8
+ ## 概览
9
+
10
+ - **英文 README**:仓库现在同时提供中英文文档,页面顶部一行即可切换;中文版仍是默认的 `README.md`,
11
+ 所以 npm 页面与 GitHub 首页显示的内容**没有变化**。
12
+ - **架构图重新导出**:`示例图/框架图-v4.png` 与 `-v5.png` 此前**底部被裁掉一条**(图例整行不见了),
13
+ 现在重新导出为 **2× 高清、完整无裁切**的图(3440×2520 / 3440×2408)。
14
+ - **发布说明双语**:从现在起每一版都提供中文 + 英文两份,先中文后英文,节名固定。
15
+
16
+ ## 变更
17
+
18
+ ### 1. 英文 README 与中英切换
19
+
20
+ - 新增 `README.en.md`,与中文版**逐节对应**;两份 README 顶部各有切换链接
21
+ (`English | 中文`)。
22
+ - 中文版仍是 `README.md`,因此 npm 页面与 GitHub 首页的默认内容不变。
23
+ - 新增守卫:两份 README 的**章节结构、图片、本地链接、表格行数、内部锚点**必须一一对应,
24
+ 且英文版不得残留未翻译正文——以后只改一边会被测试直接检出。
25
+
26
+ ### 2. 架构图重新导出(底部不再被裁)
27
+
28
+ - **现象**:`框架图-v4.png` / `-v5.png` 最下面一条(图例行)不见了;而 PNG 的尺寸又恰好等于画布尺寸,
29
+ 所以从尺寸上完全看不出来。
30
+ - **原因**:导出用的是 `--window-size=<画布宽>,<画布高>` + `--headless=new`,而它截的是**窗口**,
31
+ 页面**视口**比窗口矮约 96px(浏览器 UI 占位),画布底部那一截根本没被绘制。
32
+ - **现在**:新增 [`docs/render_framework_diagram_png.mjs`](../../docs/render_framework_diagram_png.mjs),
33
+ 按 SVG 自身尺寸渲染、裁掉多余高度,并**校验墨迹到达内容底边**——被裁就报错退出,不会再悄悄发布。
34
+ - **新增守卫**:四张架构图 PNG 必须真的装得下它自己的画面(解 PNG 与 SVG 比对墨迹范围),
35
+ v2/v3 的 matplotlib 图同样纳入检查。
36
+
37
+ ### 3. 发布说明改为中英双语
38
+
39
+ - GitHub Release 正文固定为「中文 → English」两段,节名统一:
40
+ 概览 / 变更 / 兼容性 / 升级 ↔ Overview / Changes / Compatibility / Upgrade。
41
+ - 只写使用者需要知道的信息;工程过程与证据留在仓库的审计台账与提交信息里。
42
+ - 发布门禁新增检查:当版发布说明必须同时包含中文与英文,缺一半直接判红。
43
+
44
+ ## 兼容性
45
+
46
+ - 四套预设的代码、提示词与语料**字节未变**;参数、工具面与 `off` / `encourage` / `require` 行为不变。
47
+ - 安装器行为与 2.3.14 相同:版本一变即整体替换受管文件(手改原文先备份),同一版本不重写任何文件。
48
+ - 本版只改文档、图片与测试守卫,不涉及任何运行时行为。
49
+
50
+ ## 升级
51
+
52
+ ```sh
53
+ npm i dsh-vibe-math@latest
54
+ ```
55
+
56
+ 若你在 profile 里把版本钉死了,先升级那个依赖再重启 DSH:
57
+
58
+ ```sh
59
+ dsh plugin --profile <你的 profile> add dsh-vibe-math@latest
60
+ ```
61
+
62
+ 无需迁移。
63
+
64
+ ---
65
+
66
+ # dsh-vibe-math 2.3.15 — Release Notes (English)
67
+
68
+ > Previous: 2.3.14. This release adds an **English README with a language switcher**, re-exports two
69
+ > architecture diagrams as **complete high-resolution PNGs**, and fixes the release notes to a
70
+ > **bilingual** format. The four presets' code, prompts and corpora are byte-identical; runtime
71
+ > behaviour is unchanged.
72
+
73
+ ---
74
+
75
+ ## Overview
76
+
77
+ - **English README**: the repository now documents the project in both languages, switchable from one
78
+ line at the top of either file. The Chinese README is still the default `README.md`, so what the npm
79
+ page and the GitHub landing page show is unchanged.
80
+ - **Diagrams re-exported**: `示例图/框架图-v4.png` and `-v5.png` were **missing their bottom strip**
81
+ (an entire legend row). They are now re-exported **complete at 2× resolution** (3440×2520 / 3440×2408).
82
+ - **Bilingual release notes**: from now on every release ships Chinese and English, Chinese first,
83
+ with fixed section names.
84
+
85
+ ## Changes
86
+
87
+ ### 1. English README and language switching
88
+
89
+ - Added `README.en.md`, section-for-section parallel to the Chinese one; each file carries a switcher
90
+ link at the top (`English | 中文`).
91
+ - The Chinese README remains `README.md`, so npm and the GitHub landing page keep their current content.
92
+ - New guard: the two READMEs must agree on **heading structure, images, local links, table rows and
93
+ internal anchors**, and the English file must not contain untranslated prose — editing only one side
94
+ now fails the suite.
95
+
96
+ ### 2. Diagrams re-exported (nothing is cut off any more)
97
+
98
+ - **Symptom**: the bottom strip of `框架图-v4.png` / `-v5.png` (the legend row) was missing, while the
99
+ PNG still measured exactly the canvas size — so no size-based check could ever see it.
100
+ - **Cause**: the export used `--window-size=<canvas width>,<canvas height>` together with
101
+ `--headless=new`, which screenshots the **window** while the page **viewport** is ~96px shorter
102
+ (browser UI), so the bottom of the canvas was never painted.
103
+ - **Now**: [`docs/render_framework_diagram_png.mjs`](../../docs/render_framework_diagram_png.mjs) renders
104
+ at the SVG's own size, crops the extra height, and **verifies that the ink reaches the artwork's
105
+ bottom edge** — a short raster exits non-zero instead of shipping.
106
+ - **New guard**: each committed diagram PNG must actually contain its own artwork (pixel-extent
107
+ comparison against the SVG); the v2/v3 matplotlib diagrams are covered as well.
108
+
109
+ ### 3. Bilingual release notes
110
+
111
+ - The GitHub Release body is now fixed to a Chinese section followed by an English one, with stable
112
+ headings: 概览 / 变更 / 兼容性 / 升级 ↔ Overview / Changes / Compatibility / Upgrade.
113
+ - Only what a user needs to know; engineering evidence stays in the repository's audit ledger and
114
+ commit messages.
115
+ - The release gate now fails when the current version's notes lack either half.
116
+
117
+ ## Compatibility
118
+
119
+ - The four presets' code, prompts and corpora are **byte-identical**; parameters, tool surface and the
120
+ `off` / `encourage` / `require` behaviour are unchanged.
121
+ - Installer behaviour is the same as 2.3.14: a version change replaces the managed files (backing up
122
+ user edits first), and the same version rewrites nothing.
123
+ - This release changes documentation, images and test guards only — no runtime behaviour.
124
+
125
+ ## Upgrade
126
+
127
+ ```sh
128
+ npm i dsh-vibe-math@latest
129
+ ```
130
+
131
+ If you pinned the version in a profile, update that dependency first and restart DSH:
132
+
133
+ ```sh
134
+ dsh plugin --profile <your profile> add dsh-vibe-math@latest
135
+ ```
136
+
137
+ No migration needed.
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================
3
+ // 把生成好的框架图 SVG 光栅化为**完整**的高清 PNG(无头 Chrome)。
4
+ //
5
+ // node docs/render_framework_diagram_png.mjs 示例图/框架图-v5.svg 示例图/框架图-v5.png 2
6
+ //
7
+ // 为什么需要这个脚本(2.3.15 修掉的真实缺陷):过去那条一行命令
8
+ // chrome --headless=new --window-size=1720,1204 --screenshot=框架图-v5.png …
9
+ // 截的是**窗口**,而页面**视口**比窗口矮约 96px(浏览器 UI)——
10
+ // 于是 SVG 底部那一截(图例行)根本没被绘制,而 PNG 的尺寸又恰好等于 SVG 的画布尺寸,
11
+ // 所以"少了 96px"这件事从尺寸上看不出来,一直没人发现。
12
+ //
13
+ // 因此这里:
14
+ // 1. 用**多留一段窗口高度**的方式渲染(保证整个视口都被绘制);
15
+ // 2. 把顶部 `svg宽 × svg高 × scale` 区域裁出来;
16
+ // 3. **校验**结果:尺寸必须精确相等,且"墨迹"必须到达 SVG 自身内容的底边——
17
+ // 凡是比原图矮/被裁的产物都会**直接报错退出**,不会再悄悄发布出去。
18
+ // ============================================================
19
+ import { readFileSync, writeFileSync, copyFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs'
20
+ import { spawnSync } from 'node:child_process'
21
+ import { deflateSync, inflateSync } from 'node:zlib'
22
+ import { tmpdir } from 'node:os'
23
+ import { basename, join } from 'node:path'
24
+
25
+ const CHROME_CANDIDATES = [
26
+ 'C:/Program Files/Google/Chrome/Application/chrome.exe',
27
+ 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
28
+ 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
29
+ 'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
30
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
31
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
32
+ ]
33
+ const CHROME = process.env.CHROME_PATH || CHROME_CANDIDATES.find((p) => existsSync(p))
34
+ const PAD = 240 // 额外窗口高度:必须大于 new-headless 的浏览器 UI 高度
35
+
36
+ // ---------- SVG ----------
37
+ export function svgSize(file) {
38
+ const head = readFileSync(file, 'utf8').slice(0, 800)
39
+ const w = Number((head.match(/\bwidth="(\d+(?:\.\d+)?)"/) || [])[1])
40
+ const h = Number((head.match(/\bheight="(\d+(?:\.\d+)?)"/) || [])[1])
41
+ if (!w || !h) throw new Error('无法从 ' + file + ' 读出 width/height')
42
+ return { w, h }
43
+ }
44
+
45
+ /** SVG 内容(含嵌套 translate)的真实外接框,用来判断光栅是否被裁。 */
46
+ export function contentBox(file) {
47
+ const svg = readFileSync(file, 'utf8')
48
+ const stack = [{ x: 0, y: 0 }]
49
+ let maxBottom = -Infinity, maxRight = -Infinity
50
+ const re = /<(\/?)(g|rect|text|line|circle|polyline|polygon|path|image)\b([^>]*)>/g
51
+ let m
52
+ while ((m = re.exec(svg)) !== null) {
53
+ const [, close, tag, attrs] = m
54
+ if (tag === 'g') {
55
+ if (close) { if (stack.length > 1) stack.pop(); continue }
56
+ const t = /transform="translate\(\s*(-?[\d.]+)[ ,]+(-?[\d.]+)/.exec(attrs)
57
+ const cur = stack[stack.length - 1]
58
+ stack.push({ x: cur.x + (t ? Number(t[1]) : 0), y: cur.y + (t ? Number(t[2]) : 0) })
59
+ continue
60
+ }
61
+ if (close) continue
62
+ const cur = stack[stack.length - 1]
63
+ const num = (n) => { const r = new RegExp('\\b' + n + '="(-?[\\d.]+)"').exec(attrs); return r ? Number(r[1]) : null }
64
+ let bottom = null, right = null
65
+ if (tag === 'rect' || tag === 'image') {
66
+ const y = num('y'), h = num('height'), x = num('x'), w = num('width')
67
+ if (y !== null && h !== null) bottom = cur.y + y + h
68
+ if (x !== null && w !== null) right = cur.x + x + w
69
+ } else if (tag === 'text') {
70
+ const y = num('y')
71
+ if (y !== null) bottom = cur.y + y + (num('font-size') || 13) * 0.32
72
+ } else if (tag === 'line') {
73
+ const y1 = num('y1'), y2 = num('y2'), x1 = num('x1'), x2 = num('x2')
74
+ if (y1 !== null && y2 !== null) bottom = cur.y + Math.max(y1, y2)
75
+ if (x1 !== null && x2 !== null) right = cur.x + Math.max(x1, x2)
76
+ } else if (tag === 'circle') {
77
+ const cy = num('cy'), r = num('r')
78
+ if (cy !== null && r !== null) bottom = cur.y + cy + r
79
+ } else if (tag === 'polyline' || tag === 'polygon') {
80
+ const pts = /points="([^"]+)"/.exec(attrs)
81
+ if (pts) {
82
+ const v = pts[1].trim().split(/[\s,]+/).map(Number)
83
+ const ys = v.filter((_, i) => i % 2 === 1)
84
+ if (ys.length) bottom = cur.y + Math.max(...ys)
85
+ }
86
+ } else if (tag === 'path') {
87
+ const d = /d="([^"]+)"/.exec(attrs)
88
+ if (d) {
89
+ const nums = d[1].match(/-?\d+(?:\.\d+)?/g) || []
90
+ const ys = nums.map(Number).filter((_, i) => i % 2 === 1)
91
+ if (ys.length) bottom = cur.y + Math.max(...ys)
92
+ }
93
+ }
94
+ if (bottom !== null && bottom > maxBottom) maxBottom = bottom
95
+ if (right !== null && right > maxRight) maxRight = right
96
+ }
97
+ return { bottom: maxBottom, right: maxRight }
98
+ }
99
+
100
+ // ---------- PNG(只处理浏览器截图会产生的 8bit RGB/RGBA 非隔行) ----------
101
+ export function decodePng(buf) {
102
+ if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('不是 PNG')
103
+ let off = 8, w = 0, h = 0, depth = 0, color = 0, interlace = 0
104
+ const idat = []
105
+ while (off < buf.length) {
106
+ const len = buf.readUInt32BE(off)
107
+ const type = buf.toString('ascii', off + 4, off + 8)
108
+ const data = buf.subarray(off + 8, off + 8 + len)
109
+ if (type === 'IHDR') { w = data.readUInt32BE(0); h = data.readUInt32BE(4); depth = data[8]; color = data[9]; interlace = data[12] } else if (type === 'IDAT') idat.push(data)
110
+ else if (type === 'IEND') break
111
+ off += 12 + len
112
+ }
113
+ if (depth !== 8 || interlace !== 0 || (color !== 2 && color !== 6)) throw new Error(`不支持的 PNG(depth=${depth} color=${color} interlace=${interlace})`)
114
+ const bpp = color === 6 ? 4 : 3
115
+ const stride = w * bpp
116
+ const raw = inflateSync(Buffer.concat(idat))
117
+ const px = Buffer.alloc(h * stride)
118
+ let p = 0
119
+ for (let y = 0; y < h; y++) {
120
+ const ft = raw[p++]
121
+ const row = raw.subarray(p, p + stride); p += stride
122
+ const out = px.subarray(y * stride, (y + 1) * stride)
123
+ const prev = y > 0 ? px.subarray((y - 1) * stride, y * stride) : null
124
+ for (let i = 0; i < stride; i++) {
125
+ const a = i >= bpp ? out[i - bpp] : 0
126
+ const b = prev ? prev[i] : 0
127
+ const c = prev && i >= bpp ? prev[i - bpp] : 0
128
+ let v = row[i]
129
+ if (ft === 1) v = (v + a) & 0xff
130
+ else if (ft === 2) v = (v + b) & 0xff
131
+ else if (ft === 3) v = (v + ((a + b) >> 1)) & 0xff
132
+ else if (ft === 4) {
133
+ const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c)
134
+ v = (v + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 0xff
135
+ }
136
+ out[i] = v
137
+ }
138
+ }
139
+ return { w, h, bpp, px }
140
+ }
141
+
142
+ const CRC_TABLE = (() => {
143
+ const t = new Int32Array(256)
144
+ for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c }
145
+ return t
146
+ })()
147
+ const crc32 = (buf) => { let c = -1; for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ -1) >>> 0 }
148
+ function chunk(type, data) {
149
+ const out = Buffer.alloc(8 + data.length + 4)
150
+ out.writeUInt32BE(data.length, 0)
151
+ out.write(type, 4, 'ascii')
152
+ data.copy(out, 8)
153
+ out.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type, 'ascii'), data])), 8 + data.length)
154
+ return out
155
+ }
156
+ function encodePng(w, h, bpp, px) {
157
+ const stride = w * bpp
158
+ const raw = Buffer.alloc(h * (stride + 1))
159
+ for (let y = 0; y < h; y++) {
160
+ raw[y * (stride + 1)] = 0
161
+ px.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride)
162
+ }
163
+ const ihdr = Buffer.alloc(13)
164
+ ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4)
165
+ ihdr[8] = 8; ihdr[9] = bpp === 4 ? 6 : 2
166
+ return Buffer.concat([
167
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
168
+ chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0)),
169
+ ])
170
+ }
171
+
172
+ /** 墨迹范围(相对背景色):用来断言"图没被裁"。 */
173
+ function inkExtent(img) {
174
+ const { w, h, bpp, px } = img
175
+ const bg = [px[0], px[1], px[2]]
176
+ let lastInk = -1, lastX = -1
177
+ for (let y = 0; y < h; y++) {
178
+ for (let x = 0; x < w; x++) {
179
+ const i = y * w * bpp + x * bpp
180
+ if (Math.abs(px[i] - bg[0]) + Math.abs(px[i + 1] - bg[1]) + Math.abs(px[i + 2] - bg[2]) > 12) {
181
+ if (y > lastInk) lastInk = y
182
+ if (x > lastX) lastX = x
183
+ }
184
+ }
185
+ }
186
+ return { lastInk, lastX }
187
+ }
188
+
189
+ /**
190
+ * 同一个问题的**快速**版本:想知道"墨迹最低到哪一行、最右到哪一列",
191
+ * 从底边向上、从右边向左扫,命中即停——守卫里对 3400×2400 的图也只花毫秒级。
192
+ */
193
+ export function inkExtentFast(img) {
194
+ const { w, h, bpp, px } = img
195
+ const bg = [px[0], px[1], px[2]]
196
+ const isInk = (x, y) => {
197
+ const i = y * w * bpp + x * bpp
198
+ return Math.abs(px[i] - bg[0]) + Math.abs(px[i + 1] - bg[1]) + Math.abs(px[i + 2] - bg[2]) > 12
199
+ }
200
+ let lastInk = -1
201
+ for (let y = h - 1; y >= 0 && lastInk < 0; y--) {
202
+ for (let x = 0; x < w; x++) if (isInk(x, y)) { lastInk = y; break }
203
+ }
204
+ let lastX = -1
205
+ for (let x = w - 1; x >= 0 && lastX < 0; x--) {
206
+ for (let y = 0; y < h; y++) if (isInk(x, y)) { lastX = x; break }
207
+ }
208
+ return { lastInk, lastX }
209
+ }
210
+
211
+ // ---------- render ----------
212
+ function asciiTempDir() {
213
+ for (const c of [process.env.TEMP, process.env.TMP, tmpdir(), '/tmp']) {
214
+ if (c && /^[\x20-\x7e]+$/.test(c) && existsSync(c)) return c
215
+ }
216
+ throw new Error('找不到纯 ASCII 的临时目录;无头 Chrome 打不开非 ASCII 路径')
217
+ }
218
+
219
+ export function renderSvgToPng(svgPath, outPng, scale = 2) {
220
+ if (!CHROME) throw new Error('找不到 Chrome/Edge;可用 CHROME_PATH 指定。试过:\n ' + CHROME_CANDIDATES.join('\n '))
221
+ const { w, h } = svgSize(svgPath)
222
+ const { bottom, right } = contentBox(svgPath)
223
+ const dir = mkdtempSync(join(asciiTempDir(), 'svg-png-'))
224
+ try {
225
+ const inSvg = join(dir, 'in.svg')
226
+ const shot = join(dir, 'shot.png')
227
+ copyFileSync(svgPath, inSvg)
228
+ const args = ['--headless=new', '--disable-gpu', '--no-sandbox', '--no-first-run', '--hide-scrollbars',
229
+ `--force-device-scale-factor=${scale}`, `--window-size=${w},${h + PAD}`,
230
+ `--user-data-dir=${join(dir, 'profile')}`, `--screenshot=${shot}`, 'file:///' + inSvg.replace(/\\/g, '/')]
231
+ const r = spawnSync(CHROME, args, { encoding: 'utf8' })
232
+ if (!existsSync(shot)) throw new Error('Chrome 没有产出截图:' + String(r.stderr || '').slice(-300))
233
+
234
+ const img = decodePng(readFileSync(shot))
235
+ const needW = Math.round(w * scale), needH = Math.round(h * scale)
236
+ if (img.w < needW || img.h < needH) throw new Error(`截图 ${img.w}x${img.h} 小于所需 ${needW}x${needH}`)
237
+ // 裁掉多留的高度(以及 new-headless 的浏览器 UI 区域)
238
+ const stride = img.w * img.bpp
239
+ const cropped = Buffer.alloc(needW * img.bpp * needH)
240
+ for (let y = 0; y < needH; y++) {
241
+ img.px.copy(cropped, y * needW * img.bpp, y * stride, y * stride + needW * img.bpp)
242
+ }
243
+ const out = { w: needW, h: needH, bpp: img.bpp, px: cropped }
244
+ const ink = inkExtent(out)
245
+ const tol = 14 * scale
246
+ const problems = []
247
+ if (ink.lastInk < Math.round(bottom * scale) - tol) {
248
+ problems.push(`墨迹只到第 ${ink.lastInk} 行,而 SVG 内容底边在 ${Math.round(bottom * scale)} 行 —— 底部被裁掉了`)
249
+ }
250
+ if (right > 0 && ink.lastX < Math.round(right * scale) - tol) {
251
+ problems.push(`墨迹最右只到第 ${ink.lastX} 列,而 SVG 内容右边界在 ${Math.round(right * scale)} 列 —— 右侧被裁掉了`)
252
+ }
253
+ if (problems.length) throw new Error('光栅化结果不完整:\n - ' + problems.join('\n - '))
254
+ writeFileSync(outPng, encodePng(out.w, out.h, out.bpp, out.px))
255
+ return { svg: { w, h }, png: { w: out.w, h: out.h }, scale, ink, content: { bottom, right } }
256
+ } finally {
257
+ rmSync(dir, { recursive: true, force: true })
258
+ }
259
+ }
260
+
261
+ const invokedDirectly = process.argv[1] && basename(process.argv[1]) === 'render_framework_diagram_png.mjs'
262
+ if (invokedDirectly) {
263
+ const [svg, png, scale] = process.argv.slice(2)
264
+ if (!svg || !png) { console.error('用法: node docs/render_framework_diagram_png.mjs <svg> <png> [scale=2]'); process.exit(2) }
265
+ try {
266
+ const r = renderSvgToPng(svg, png, Number(scale || 2))
267
+ console.log(`wrote ${png}: svg ${r.svg.w}x${r.svg.h} × ${r.scale} → ${r.png.w}x${r.png.h}`
268
+ + ` (ink to row ${r.ink.lastInk}/${r.png.h - 1}, content bottom ${r.content.bottom.toFixed(0)})`)
269
+ } catch (e) {
270
+ console.error(String(e.message || e))
271
+ process.exit(1)
272
+ }
273
+ }
@@ -25,8 +25,10 @@ node tests/audit-v5-integrity.mjs # 静态:v5 完整性/理念门
25
25
 
26
26
  | 脚本 | 串行(sum) | 并行(wall) | 实测输出 |
27
27
  |---|---|---|---|
28
- | `tests/run-tests.mjs`(26 个套件) | 225.8 s | **109.7 s**(并发 4,speed-up x2.06) | 关键路径 = `e2e-v4-fixes` 95.9 s |
28
+ | `tests/run-tests.mjs`(28 个套件) | ≈ 230 s | **≈ 110 s**(并发 4) | 关键路径 = `e2e-v4-fixes` ≈ 96 s |
29
29
  | `tests/audit-installer-policy.test.mjs` | ≈ 1 s | — | 在临时 DSH_HOME 里驱动真实安装器(复制 4 套预设 × 2 个版本) |
30
+ | `tests/audit-readme-bilingual.test.mjs` | ≈ 0.2 s | — | 中英 README 的结构/链接/图片/锚点一致性(纯静态) |
31
+ | `tests/audit-diagram-assets.test.mjs` | ≈ 1 s | — | 四张架构图 PNG 必须装得下它自己的画面(解 PNG 与 SVG,纯静态) |
30
32
  | `audit-formal-sensitivity.mjs`(49 探针) | 612.0 s | **154.6 s**(并发 4,speed-up x3.96) | 关键路径 = 12 个 v2 探针(每个 ≈32 s) |
31
33
  | `audit-persona-sensitivity.mjs`(11 探针) | ≈ 5 s | — | 本身很快,不需要并行 |
32
34
  | `audit-prompt-invariants.mjs`(157 条,含 X5–X8b 扫描器自检) | 0.4 s | — | 静态 |
@@ -50,7 +52,7 @@ node tests/audit-v5-integrity.mjs # 静态:v5 完整性/理念门
50
52
  | `e2e-d9-d13.test.mjs` | ≈ 13 s | |
51
53
  | `e2e-v3.test.mjs` | ≈ 12 s | |
52
54
  | `formal-verify-v3.test.mjs` | ≈ 12 s | |
53
- | 其余 16 个 | ≤ 6 s | 其中 8 个 < 1 s |
55
+ | 其余 21 个 | ≤ 6 s | 其中绝大多数 < 1 s |
54
56
 
55
57
  ## 3. 已经做过的优化(别再重复踩)
56
58