gate: draw-diagram 技能正文扩写——代为提交他人在飞内容

本文件在本会话开始前就已是工作区未提交状态(疑似技能自进化或另一会话所写,
非本次编辑)。按用户指示代为提交;提交前后逐字节核验内容未被改动、也没被
pre-commit 的改写型钩子动过:
- 提交前工作区 git hash-object = a6bdc570fe
- 本笔提交里的 blob                 = a6bdc570fe

内容(4,227 → 14,364 B,+129/−32 行):
- 工具选型补「为什么不用 Mermaid」的实测教训(台阶图被压成同一高度)
- 新增「二、从零新写生成器」:抄骨架三条约定、预检三件套 OVERFLOW / check_fit /
  wrap、emit() 按目标编码打印(generated-fresh 把脚本拷进临时目录重跑时不因
  GBK 终端崩)、路径锚在脚本自身而非 cwd
- 新增「三、目检」:Windows 无 SVG 渲染器时用本机 headless Chrome/Edge 截图回读
- 新增「五、generated-fresh 逐字节幂等」与「六、提交」,陷阱与验证清单同步

核验(手工跑,理由见下一段):skill-size Passed(14,364 B 远低于 65,536 写侧
上限,正文无 reference/ 指针故无悬空);trailing-whitespace Passed;无 BOM、
CR 0、U+FFFD 0、单个 LF 结尾、无三连空行。

⚠ 顺带发现(本笔未修):本仓 .pre-commit-config.yaml 的全局 exclude 含 .dsh/,
而钩子的 files: ^\.dsh/skills/ 打不过全局 exclude → **只碰 .dsh/skills/ 的提交
一条钩子都不会跑**(这两笔提交期全部钩子显示 no files to check Skipped 即此因;
手工 run --files 会绕过全局 exclude,所以能 Passed)。skill-size 因此目前拦不到
任何真实提交,需要单独裁定怎么接(去全局 exclude 还是给它单独道)。
This commit is contained in:
2026-10-29 16:28:21 +08:00
parent f08b12a46c
commit c66b5ad40d
+129 -32
View File
@@ -1,12 +1,14 @@
---
name: draw-diagram
description: docs/ 文档技术配图绘制流程(精确布局图用 SVG 生成器 / 拓扑图用 Mermaid):挑脚本 → 改顶部数据 → 重跑生成 → 目检 → md 相对路径引用 → 脚本+产物一并提交,产物统一放 docs/images/。
whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层架构、依赖/就绪屏障、流程图、时序、状态机),或用户要求"画个图 / 生成图 / 更新架构图 / 帧格式图"时。
description: docs/ 文档技术配图绘制流程(精确布局图用 tools/arch_diagram/ 的纯标准库 SVG 生成器 / 拓扑图用 Mermaid):挑脚本或照抄骨架新建生成器 → 改顶部数据 → 跑生成并把 check_fit/OVERFLOW 溢出预检清零 → Windows 无渲染器时用 headless Chrome/Edge 截图 + read_image 回读目检 → 回改生成器重跑 → 过 generated-fresh 逐字节幂等 → md 相对路径引用 → 脚本+产物一并提交,产物统一放 docs/images/。
whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层架构、依赖/就绪屏障、台阶/泳道对照、刻度尺、流程图、时序、状态机),从零新写一个 gen_*_svg.py 生成器,在 Windows 上想"目检"一张 SVG,或用户要求"画个图 / 生成图 / 更新架构图 / 帧格式图"时。
---
# 文档配图绘制(SVG / Mermaid)
`docs/` 文档技术配图的统一流程;完整选型依据 / 约定 / 检查清单见 `docs/架构设计/图形绘制流程.md`。**产物统一放 `docs/images/`**(专门配图目录,`.svg` 入库;PNG 预览属本地派生物、不入库)。
`docs/` 文档技术配图的统一流程;完整选型依据 / 约定 / 检查清单见 `docs/架构设计/图形绘制流程.md`。**产物统一放 `docs/images/`**(专门配图目录,`.svg` 入库;PNG 预览属本地派生物、不入库,`.gitignore` 已含 `docs/images/*.png` 与 `ci-artifacts/`)。
> 本窗实测(2026-10-29,新建 2 张图):**"浏览器打开 SVG 目检"这一步在 Windows 上不可直接执行**——没有现成 SVG 栅格化工具,只能走 headless Chrome/Edge 截图 + 图片回读;且 `generated-fresh` 是**逐字节**门禁,生成器自己的控制台打印也会成为失败源(见 §五)。
## 工具选型
@@ -15,15 +17,15 @@ whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层
| 协议帧 / 字节布局(字段宽 ∝ 字节数、偏移标尺) | SVG:`tools/arch_diagram/gen_frame_svg.py` |
| 分层架构 / 模块依赖 | SVG:`tools/arch_diagram/gen_arch_svg.py` |
| 就绪屏障 / 生产者-消费者 | SVG:`tools/arch_diagram/gen_barrier_svg.py` |
| 台阶 / 泳道对照 / 刻度尺等"高度差、并列对照"类 | SVG:**新写一个** `tools/arch_diagram/gen_<主题>_svg.py`(照抄最近生成器的骨架,见 §二) |
| 其他精确布局图 | **复制**最接近的生成器改顶部数据(`build()` 与数据解耦,通用) |
| 流程 / 时序 / 状态机 / ER(只表达拓扑关系) | Markdown 内嵌 ` ```mermaid ` 代码块(`flowchart` / `sequenceDiagram` / `stateDiagram-v2` / `erDiagram`),无需生成图片 |
| 流程 / 时序 / 状态机 / ER(只表达拓扑关系) | Markdown 内嵌 ```mermaid 代码块(`flowchart` / `sequenceDiagram` / `stateDiagram-v2` / `erDiagram`),无需生成图片 |
> Mermaid 盒子按文本自适应宽、自动加有向边,**只适合"关系"类图**;凡需精确像素 / 比例的布局图一律走 SVG 生成器。
> Mermaid 盒子按文本自适应宽、自动加有向边,**只适合"关系"类图**;凡需精确像素 / 比例 / 高度差的布局图一律走 SVG 生成器(实测教训:五级"台阶"用 Mermaid 会被压成同一高度,"还差一级"就看不出来了)。
## SVG 生成流程(仓库根目录执行)
## 一、改既有图(最短路线)
1. **挑 / 复制脚本**:新类型图复制最接近的生成器,只改顶部数据区。
2. **改顶部数据**:字段 / 节点 / 标题 / 配色。沿用既有调色板(主色 / 底色):
1. 改**顶部数据 / 常量区**(字段、节点、标题、配色),不改 `build()` 结构。配色沿用既有调色板(主色 / 底色):
| 语义 | 主色 | 底色 |
|---|---|---|
@@ -35,38 +37,133 @@ whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层
| 灰 | `#475569` | `#f1f5f9` |
| 玫红 | `#e11d48` | `#fff1f2` |
3. **重跑**(脚本内置 `ElementTree` 自检,XML 不合法直接抛错):
2. 重跑(仓库根;脚本内置 `ET.fromstring` 自检,XML 不合法直接抛错):
```powershell
& '.venv\Scripts\python.exe' tools/arch_diagram/gen_xxx.py
# 成功输出:OK <字节> bytes, canvas <W>x<H> -> <路径>
# 期望:OK <字节> bytes, canvas <W>x<H> -> <路径>,且没有 OVERFLOW WARN / overflow warnings 行
```
4. **目检**:浏览器打开 `docs/images/*.svg`——文字不溢出盒外、框 / 线 / 标签对齐、配色符合调色板。
5. **md 引用**(相对路径):`docs/` 根的 md 写 `![标题](images/xxx.svg)`;`docs/UI设计/` 等子目录 md 写 `![…](../images/xxx.svg)`。
6. **提交**:生成器脚本(源)+ 生成的 `.svg`(产物)**一并**提交。
3. 目检(§三)→ 引用(§四)→ 幂等与门禁(§五)→ 提交(§六)。
## SVG 约定(手写 / 新增生成器必须遵循)
## 二、从零新写生成器(先抄骨架,别裸写)
- **纯标准库**拼 SVG(只用 `html` / `os`(+ `math`)),不引第三方库(SVG 由浏览器用系统字体渲染)
- 所有文本经 `html.escape(s, quote=True)`,防止破坏 XML
- 字体栈:`Microsoft YaHei, Segoe UI, PingFang SC, sans-serif`
- 文本宽估算(自适应盒宽):CJK / 全角 / 圈码 / 箭头 ≈ `1.0em`,其余 ≈ `0.58em`(**宁大勿小**防溢出)
- 落盘:`open(OUT, "w", encoding="utf-8", newline="\n")`(UTF-8 无 BOM + LF)
- 写盘后 `xml.etree.ElementTree.fromstring(svg)` 自检
- 产物放 `docs/images/`,命名 `<主题>.svg`(可与同名 `.md` 对应)
1. **先读同目录 1~2 个最近的脚本**(如 `tools/arch_diagram/gen_adc_frontend_svg.py`、`gen_adc_code_scale_svg.py`),抄它们的三条约定:
- **文件头 docstring** 写清四件事:产物路径 + 这张图回答什么问题 + **「为什么不用 Mermaid」** + 用法("仓库根目录,任意 python3")与"改内容 vs 调坐标"的边界;
- **顶部集中常量块**:画布与几何(`W` / `H` / `PAD` / `RIGHT`)→ `FONT` → 各级字号 → 调色板 → 文案(`TITLE` / `SUB*` / 逐条目 / `FOOTNOTE`);坐标一律由常量推导,别在 `build()` 里塞魔法数;
- **纯标准库**:`import html / os / sys / xml.etree.ElementTree as ET`,helpers 只有 `esc` / `text_w` / `txt` / `line` / `rect` / `wrap` / `check_fit` / `emit` / `build`。
2. **照抄预检三件套**(`gen_adc_frontend_svg.py` 里还没有;从 `tools/arch_diagram/gen_data_trust_ladder_svg.py` 抄):`OVERFLOW = []` + `check_fit(s, fs, avail, where)` + `wrap(s, fs, avail, max_lines, where)`,标题 / 副标题 / 盒内标题走 `check_fit`,盒内正文与脚注走 `wrap`(`wrap` 按估宽折行、超行数用 `…` 收尾并记一条 OVERFLOW)。`build()` 末尾:`emit("OK ...")` 之后 `if OVERFLOW: emit("!! %d overflow warnings" % len(OVERFLOW))`。估宽口径不变:CJK / 全角 / 圈码 / 箭头 ≈ `1.0em`,其余 ≈ `0.58em`(宁大勿小)。
3. **照抄 `emit()`**(幂等门禁的地基,见 §五):
## SVG → PNG(可选,本地派生物不入库)
```python
def emit(s):
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
enc = getattr(sys.stdout, "encoding", None) or "ascii"
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
```
- 最简单:浏览器打开 `.svg` → 另存为图片
- 脚本化:`& '.venv\Scripts\python.exe' -m pip install cairosvg` 后
`& '.venv\Scripts\python.exe' -c "import cairosvg; cairosvg.svg2png(url='docs/images/xxx.svg', write_to='x.png', scale=2)"`
- `.gitignore` 已含 `docs/images/*.png`
4. 路径锚在脚本自身位置,别用 cwd(临时目录重跑才不会漂):
## 提交前检查清单
```python
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "<主题>.svg")
```
- [ ] 生成器输出 `OK … bytes`(无异常 = XML 合法)
- [ ] 浏览器目检:文字不溢出、框 / 线 / 标签对齐、配色符合调色板
- [ ] 引用它的 `.md` 里图片**相对路径**正确(能显示)
- [ ] 脚本 + 产物 `.svg` **一并**暂存
- [ ] `pre-commit run --files <脚本> <svg> <md>` 全过(无乱码 / 行尾空白 / 编码 OK)
5. 跑 `& '.venv\Scripts\python.exe' tools/arch_diagram/gen_<主题>_svg.py`,**把 OVERFLOW 告警打到 0 再进目检**——预检能在渲染前就报出 `foot: 974 > 968` 这类溢出(本窗实测两张图首跑各报一条脚注超宽),比靠眼睛反复量便宜一个数量级。
6. 新脚本落库时**同批**把这一行加进 `tools/arch_diagram/README.md` 的「脚本一览」表。
## 三、目检(Windows:本机没有任何现成 SVG 渲染器)
**先探测,别照抄 cairosvg 命令**:本仓 `.venv` 无 `cairosvg`(`ModuleNotFoundError: No module named 'cairosvg'`),`rsvg-convert` 不存在,只有 Edge / Chrome:
```bash
# ops_bash(git-bash)里探测;哪个存在用哪个
ls -1 "/c/Program Files/Google/Chrome/Application/chrome.exe" \
"/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" 2>/dev/null
command -v rsvg-convert inkscape magick convert 2>/dev/null || echo "no CLI rasterizer"
```
```powershell
Test-Path "C:\Program Files\Google\Chrome\Application\chrome.exe"
Test-Path "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
```
截图(**`--screenshot=` 必须给绝对 Windows 路径 + 绝对 `file:///` 输入**):
```bash
mkdir -p ci-artifacts # Chrome 没有建目录的参数,先自己建
OUT="D:\\Code\\DTU Code\\<工作树名>\\ci-artifacts\\<主题>.png" # 绝对路径、反斜杠
"/c/Program Files/Google/Chrome/Application/chrome.exe" \
--headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \
--window-size=1060,712 \
--screenshot="$OUT" \
"file:///D:/Code/DTU Code/<工作树名>/docs/images/<主题>.svg" 2>&1 | tail -2
```
成功标志是 Chrome 自己打印的 `<N> bytes written to file D:\...`。**回读 PNG 才算目检完成**:
```ts
const r = await tools.read_image({ file_path: "D:\\Code\\DTU Code\\<工作树名>\\ci-artifacts\\<主题>.png" });
console.log(r.image.width + "x" + r.image.height, "bytes=" + r.image.bytes); // 应等于 --window-size / 画布
```
看什么:文字压框 / 溢出盒外、标签互撞(本窗实测两轮才干净:台阶图首轮"文字压到盒下方 + 标签互撞",第二、三轮才修完)、`wrap` 收尾的省略号 `…`(有 = 文案超长)、虚线与盒边线重叠、配色是否还在调色板里。发现问题**回改生成器常量并重跑预检**,然后**重新截图**(PNG 是派生物,不会自己更新),`--window-size` 跟着 `canvas WxH` 改。
## 四、md 引用(相对路径)
`docs/` 根的 md:`![标题](images/xxx.svg)`;`docs/任务规划/`、`docs/UI设计/` 等子目录 md:`![…](../images/xxx.svg)`(本窗实例:`docs/任务规划/数据可信.md` 写 `![数据可信是五级台阶,现在踩在第 2 级](../images/数据可信台阶.svg)`)。alt 文本写成"这张图的结论是什么",别只重复文件名。
## 五、generated-fresh:逐字节幂等门禁(新图长期不假红的前提)
`generated-fresh` 把 `tools/arch_diagram/gen_*.py`(`docs-svg` 项:`per_script: true`、`pattern: gen_*.py`、`mode: exact`)**逐个拷进系统临时目录重跑**,与工作区产物**逐字节**比对:内容不同 = `DIFF`(生成物比生成器旧)、临时目录多出一个工作区没有的文件 = `NEW`、`rc≠0` = 「生成器报错」。由此推出四条硬约束:
1. **生成器只能写那一个 `.svg`**:绝不在脚本里顺手导出 PNG 预览(预览写 `ci-artifacts/`,不进 `docs/images/`)。
2. **stdout 不许崩**:GBK 终端遇上 `²` / `−` / CJK 路径会 `UnicodeEncodeError` → 钩子 `rc≠0`,一片假红。所以打印一律走 `emit()`,脚本里不留裸 `print()`(本窗实测:两份新生成器只改了一份,另一份照旧崩)。
3. **产物内容零不确定**:SVG 里不写时间戳 / 绝对路径 / 主机名;浮点坐标一律 `%g` / `%.1f` 定形;字典与 `os.listdir` 结果参与布局时先 `sorted()`。
4. **`gen_*.py` 命名即自动在册**(pattern 就是 `gen_*.py`)——故意不叫这个名字等于躲开门禁,门禁最大盲区正是"未登记的生成器",别利用它。
自查(仓库根):
```bash
.venv/Scripts/python.exe tools/arch_diagram/gen_<主题>_svg.py
sha1sum docs/images/<主题>.svg
.venv/Scripts/python.exe tools/arch_diagram/gen_<主题>_svg.py # 重跑后 sha1 必须不变
.venv/Scripts/python.exe scripts/check_generated_fresh.py # 期望 rc=0,无 DIFF/NEW,[BASELINE] 不增
```
```powershell
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py # 单独跑(不 stash 别人在飞的改动)
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py --list # 只看在册声明
& '.venv\Scripts\python.exe' -m pre_commit run generated-fresh --files tools/arch_diagram/gen_<主题>_svg.py docs/images/<主题>.svg
```
⚠ **别跑不带 `--files` 也不带 `--all-files` 的裸 `pre_commit run`**(会 stash 掉别人在飞的改动);判读口径 → `docs/门禁说明/generated-fresh.md` §4。
## 六、提交
生成器脚本(源)+ `docs/images/<主题>.svg`(产物)+ 引用它的 `.md` + `tools/arch_diagram/README.md` 那一行**同一笔**提交;PNG 预览不入库。
```powershell
& '.venv\Scripts\python.exe' -m pre_commit run --files <脚本> <svg> <md> <README.md>
```
## 陷阱(本窗实测)
- **照着现文 / `docs/架构设计/图形绘制流程.md` §四 跑 `pip install cairosvg` 或 `rsvg-convert`**:这台机器上没有(前者 `ModuleNotFoundError`、后者根本没有这个命令)→ 先按 §三探测,改用 headless Chrome/Edge。**别在没渲染出 PNG 的情况下声称"已目检"。**
- **`--screenshot=ci-artifacts/ladder.png`(相对路径)** → `EXIT 2` + `Failed to write file ci-artifacts/ladder.png: 系统找不到指定的路径。 (0x3)`,PNG 压根没落盘;换绝对 Windows 路径后写出 95549 bytes。这条报错**不区分**"相对路径"与"目录不存在"两种成因(实测 `mkdir -p ci-artifacts` 已先跑过、仍报同一句),排查时两条都排。
- **只做预检就收工**:`check_fit` / `OVERFLOW` 归零也不代表干净——本窗实测"门标签压第 5 级盒、蓝底标签挤、多余虚线"三类碰撞只有 PNG 回读看得见;反过来只目检不预检,脚注溢出就得靠眼睛反复量。**两者都要 0**。
- **手改 SVG 成品**:等于制造「不是我改的」diff,且 `generated-fresh` 立刻 DIFF;也**不要退化成 ASCII 框图**(AGENTS.md 明令勿手绘)。一切修改都落在生成器常量上再重跑。
- **批量改生成器时 `old_string` 不匹配**:一批 `edit` 会在中途抛错,但**前面的编辑已经落盘** → 半成品生成器(本窗实测:`emit` helper 缺失却仍打印 OK,几轮之后才查出)。正解:改前先 `read` 出精确原文,逐条改,末尾统一复验(`grep -n "def emit"`、`grep -c print`、再重跑看 rc 与 OK 行)。
- **PNG 尺寸 ≠ 画布**:`--window-size` 忘了跟着 `W`/`H` 改 → 截出来带大片空白或裁掉底部脚注;回读 `r.image.width/height` 与 `canvas WxH` 对一下再判定。
- **`docs/images/` 下手写 `.svg` 或放 PNG 预览**:前者没有生成器(NEW / 躲过门禁),后者污染"临时目录多了文件"判定。
## 验证清单
- [ ] 生成器输出 `OK … bytes, canvas WxH`,**无** `OVERFLOW WARN` / `!! N overflow warnings`;`grep -n "def emit"` 命中、`grep -c print` = 0。
- [ ] 绝对路径 PNG 存在、尺寸 = 画布,`read_image` 回读确认:无文字压框 / 溢出、无标签互撞、无 `…` 截断收尾、配色在调色板内。
- [ ] 连跑两次产物 sha1 不变;`scripts/check_generated_fresh.py` rc=0(无 DIFF / NEW,`[BASELINE]` 未增加)。
- [ ] 引用它的 `.md` 相对路径正确(根目录 `images/`,子目录 `../images/`);`tools/arch_diagram/README.md` 已加脚本行。
- [ ] 脚本 + 产物 `.svg` + `.md` + README 行一并暂存;`pre-commit run --files …` 全过(无乱码 / 行尾空白 / 编码 OK)。
> **TODO(口径未与权威文档同步)**:`docs/架构设计/图形绘制流程.md` §3.1 第 4 步「目检:浏览器打开 SVG」与 §四「给 `.venv` 装 cairosvg 转 PNG」在 Windows 上按本 skill 的实测是走不通的。该文档是规范归属(AGENTS.md:同一事实只在一处详述),采纳 §三 / §五 时应把这两段同步过去;目前两处不一致,**做图以本 skill 的实测命令为准,规范条文以该文档为归属**。