Skip to content

feat(diagram): close official resvg asset contract - #386

Merged
HansBug merged 24 commits into
dev/python-diagram-umbrellafrom
dev/python-diagram-assets-official
Jul 20, 2026
Merged

feat(diagram): close official resvg asset contract#386
HansBug merged 24 commits into
dev/python-diagram-umbrellafrom
dev/python-diagram-assets-official

Conversation

@HansBug

@HansBug HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Merged State

  • Merged at: 2026-07-20T12:45:18Z.
  • Implementation head: b78ae178b7649aea48b8a7f313642e1caf863f08.
  • Merge target: dev/python-diagram-umbrella.
  • Merge commit: aa745161980799a4ba9e56e3654040339f7faa98.
  • This PR did not merge into main; the umbrella remains blocked pending PR-C, PR-D, and final integration gates.
  • Final evidence: https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450.

This section supersedes the historical pre-merge state below.

Pre-Merge State (historical)

  • Head: b78ae178b7649aea48b8a7f313642e1caf863f08
  • Base: dev/python-diagram-umbrella@bcadb92a9cc510de419b72f1a8935c377e9e6978
  • Target: merge into dev/python-diagram-umbrella; this PR must not merge directly into main.
  • Remote CI: 60 checks passed, 1 expected Docs generated resources skip;
    template-suite-gate passed. Codecov is complete at 94.54% (25 sessions).
  • Final post-push multi-agent review: code APPROVE, verifier READY, architecture/scope CLEAR.
  • Evidence archive: https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450#file-pr-b-b78ae178-md

This PR closes the Python-side official resvg asset/runtime stage. The latest
maintainer decisions in this section supersede conflicting request-only, output-limit,
and production-validator text preserved in older planning comments.

Production Runtime

  • Uses the locked official @resvg/resvg-wasm@2.6.2 binding and WASM assets.
  • Uses py-mini-racer>=0.6.0,<0.7 on Python 3.7 and mini-racer>=0.7.0 on Python 3.8+.
  • Keeps loud, actionable failures for missing, corrupt, or unusable JS, WASM, font,
    and packaged resources.
  • DiagramAssetEngine(timeout=None) applies no default artificial timeout. Callers
    may opt into a finite positive timeout; timeout/OOM still discards the context.
  • scale must be finite and positive. Production imposes no canvas, pixel, encoded
    output, or scale upper limit.
  • Production performs basic SVG type/XML/root checks, rejects real DTD/entity
    declarations, and accepts legal comments and CDATA.
  • Production checks PNG base64, signature, IHDR/RGBA8 fields, positive dimensions,
    and final IEND placement without imposing byte/dimension/pixel caps.
  • render_png() and expand_svg() accept DiagramData dictionaries and canonical SVG
    compatibility input. The string path is not a general-purpose SVG sanitizer.
  • Non-mapping requests and NaN/positive Infinity/negative Infinity fail at the Python
    JSON boundary.
  • MiniRacer errors retain an actionable headline without embedding evaluated source.
  • A temporary startup-only loader converts legacy MiniRacer JS parse failures into
    DiagramAssetError; it is deleted after bundle load, while global eval and
    Function remain blocked for renderer requests.

Maintenance And CI

Strict checks remain in tools/check_diagram_rendering.py, outside production and
outside pytest imports of tools.*:

  • SVG element/attribute/namespace/URI/ID/reference/marker/path-only checks;
  • PNG chunk ordering, CRC, zlib, scanline/filter, dimensions, and visible ink;
  • arrow endpoint geometry, CJK rendering, official/custom parity, and deterministic
    corpus evidence.

The checked-in corpus covers 35 layouts and 306 arrows: canonical 15/130 plus shared
20/176, including LR/TB, four directions, block-border endpoints, and five CJK locales.

Verification

  • Python 3.7.17 + py-mini-racer 0.6.0: 61 diagram tests passed.
  • Python 3.10.10 + mini-racer 0.14.1: 61 diagram tests passed.
  • Python 3.7.17, Python 3.8.1 floor, and current modern engine checks: 15 cases each,
    SVG/PNG/expanded SVG and timeout reset passed.
  • Rendering: 15 layouts / 130 arrows and 20 layouts / 176 arrows passed.
  • Frozen custom-reference parity: 35 layouts / 306 arrows, CJK, memory, provenance,
    and expanded-SVG clip-ID-only comparison passed.
  • Official npm/source/patched-source hashes and license provenance matched the lock.
  • wheel/sdist inventory and fresh-wheel SVG/PNG smoke passed.
  • ruff check, ruff format --check, compileall, git diff --check, and test-boundary checks passed.
  • The final three regression cases cover renderer-output DTD rejection and legal
    comment/CDATA declaration literals on the production render_svg() path.

Merge Boundary

This PR does not add StateMachine.diagram(), model.show(), a public Diagram facade,
HTML viewer/cache, Notebook display, a standalone diagram CLI, PDF APIs, backend
selectors, or ELK/VSCode geometry changes. Those remain PR-C/PR-D work.

PR-B is merged into the umbrella branch at aa745161. The umbrella remains blocked
from main until PR-C, PR-D, and final integration gates are complete.

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

claude reviewer 独立审查:PR-B 计划入口一致性核查

身份:claude reviewer(独立自动审查,不代表主会话或维护者决定)。
范围:仅审查当前 PR body 与上游事实(伞 PR #383 body / 2.6.2 实施计划评论 / PR-A #384 / PR #377 / issue #89 / issue #376 / 当前仓库树 HEAD 3d738fb3)的一致性;不评审尚未存在的实现代码。

事实回读

  • PR-B head 3d738fb3182b36a118bcf5a0b5f13df7e3896a2b,仅 1 个提交 chore(diagram): open official asset closure PR-Bfiles=[],与 body "空 PR-B,目前只有不改代码的开门提交" 一致。
  • base 分支 dev/python-diagram-umbrella 相对 origin/main ahead=35 / behind=0,与 body "从已同步最新 main 的 umbrella 分支创建" 一致。
  • PR-A 实现:共享 Python Diagram renderer 与离线资产链路 PR-A #384 MERGED 2026-07-18 into umbrella,PR fix(visualization): make nested A -> B arrow terminals normal to target border #377 MERGED 2026-07-15 into main,issue 嵌入 jsfcstm 可视化 (SVG/PNG):mini-racer + WASM 栅格化 #89 OPEN status:planned,issue fix(visualization): 保证 A -> B 箭头以可见法向终段垂直接入目标边框 #376 CLOSED status:completed;与 body 对 PR-A/PR-C/D 的阶段区分一致。
  • 现仓库中 tools/build_diagram_assets.pytools/check_diagram_assets.pytools/check_diagram_package.pytools/diagram_assets/{asset-lock.json,host-shim.js,python-renderer-entry.ts,resvg-bridge.js} 存在;计划中新增的 tools/check_diagram_rendering.pytools/check_diagram_engine_floor.pytools/diagram_assets/corpus/{canonical-arrows,shared-layouts}.json尚不存在,与 body "空 PR-B,不包含实现" 一致,未被伪装为现有工具。
  • requirements-viz.txt / requirements-test.txt 当前是 py-mini-racer>=0.2; python_version < "3.8"mini-racer; python_version >= "3.8";未被 body 描述为已修正,与 "修正...真实 lower bound" 的"待实施"表述一致。
  • editors/jsfcstm/package.json 当前无 @resvg/resvg-wasm@2.6.2 devDependency,与 body 描述的 "锁定官方 @resvg/resvg-wasm@2.6.2" 归入 PR-B 待实施范围一致。
  • Body 明确不包含 PR-C/D 项(公共 Diagram API、HTML viewer、CLI、Notebook、最终 PDF facade、VSCode/ELK 几何修改、双 backend/fallback、独立 resvg 构建仓库),与实施计划评论第 3 节 "明确不得因本计划变化" 一致。
  • Body 的 hard gate 摘要("任何未解释的视觉差异、来源/许可证无法回读、runtime floor 失败、资源遗漏或 fail-loud 回退都会阻止合并")与实施计划评论第 6 节列表压缩后一致。
  • Body 明确写 "当前 PR 仅表示计划入口,不能据此宣称 official backend 已落地",未把 custom 0.37 结论重写为 2.6.2 已交付,未把未来 tool/命令混入现有工具。

结论:C=0 / I=0

未发现 Critical 或 Important 级别的事实错误、范围越权、上游偏离或"未来工具伪装为现有工具"的问题。当前 body 是合规的空 PR 计划入口:

Minor 观察(不阻塞,供作者按需处理)

  • M1(用词精度):body 第 4 条把 130-arrow 语料称为 "15-layout/130-arrow";实施计划评论第 2 节 / 第 5.2 节使用 "15-case corpus 固定 130 arrows"、"15 个唯一 fixture id/130 arrows"。当同一 PR 后续同时出现 15-case(130 arrow)与 20-layout(176 arrow)时,两者都写作 "layout" 可能让读者误解 130 与 176 的关系;建议 body 与后续报告统一使用 "15 个 fixture / 130 arrows" 与 "20 layouts (LR/TB 各 10) / 176 arrows",或至少在首次出现处标注 fixture 数与 LR/TB 分组,与计划 §5.2 中的 --expected-groups LR=10,TB=10 门禁定义对齐。参考位置:PR body 第 4 条第 5 行。
  • M2(可追溯性):body 仅在文本中引用 "official 2.6.2 实施计划:规划:建立 Python Diagram API 与共享离线渲染链路 #383#issuecomment-5014969693";实施计划评论明确要求 §7.5 "PR current-state comment 必须记录 reference.json 与根 SHA256SUMS 的 SHA-256",这类同步物在 PR-B 实施过程中会累计。建议在首个实现提交同时以 PR-B 评论方式登记:本次开门提交 SHA、custom baseline 期望的 reference bundle 尚未生成的显式声明,方便后续 reviewer 与 body 的当前态区分。
  • M3(回退条款覆盖):body 的失败停止列表压缩到 5 项,遗漏计划 §6 中的两项相对易被误当成"非阻塞"的条件:"需要修改 VSCode geometry/endpoint 才能让 resvg gate 通过" 和 "maintainer 未批准 official unmodified package 的 MPL source-availability 分发模型"。建议在 body "任何未解释的视觉差异…" 一行末尾显式补充这两个 stop 条件,避免后续 reviewer 只按 body 摘要判断回退触发。参考位置:PR body "执行与验收" 段末尾。

复核依据(可复现命令)

GH_TOKEN=<token> gh pr view 386 --json commits,files,baseRefName,headRefName,mergeable
GH_TOKEN=<token> gh api repos/HansBug/pyfcstm/issues/comments/5014969693 --jq '.body' > /tmp/plan.md
git log --oneline origin/main..origin/dev/python-diagram-umbrella | wc -l   # -> 35
git log --oneline origin/dev/python-diagram-umbrella..origin/main | wc -l   # -> 0
ls tools/ | grep diagram
ls tools/diagram_assets/
grep -E 'py-mini-racer|mini-racer' requirements-viz.txt requirements-test.txt
grep resvg-wasm editors/jsfcstm/package.json

以上命令与本次审查所依赖的事实一一对应。C=0, I=0;仅有 M1/M2/M3 三项可选改进。审查完毕。

@HansBug HansBug left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review:PR-B #386 独立审查(planning 阶段,空 PR 占位)

Reviewer: 独立 reviewer(按伞 PR #383 planning ready 合同执行审查)

审查对象: PR #386dev/python-diagram-assets-officialdev/python-diagram-umbrella),仅有 1 个 chore commit 3d738fb3,零文件变更。

审查范围: PR body 是否与伞 PR #383、official 2.6.2 实施计划(comment #5014969693)、PR-A #384、issue #89/#376、PR #377 一致;范围是否与 PR-A/C/D 不重叠;引用的命令/文件/门禁是否可执行;是否把未来工具伪装为现有。


C=0(无阻塞性问题)

# 审查项 证据
1 PR body 正确引用伞 PR #383、official 2.6.2 实施计划、PR-A #384 gh pr view 386 --json body 回读一致
2 范围与 PR-A(foundation,已交付)不重叠 PR-A 合并 commit 8a6eba7d,47 文件;PR-A 不涉及 official resvg 迁移、closed SVG allow-list、source/license 切换
3 范围与 PR-C(API + Browser)、PR-D(Headless + Delivery)不重叠 伞 PR body "PR-B Asset Closure / PR-C API + Browser / PR-D Headless + Delivery" 三阶段分配明确;PR-B body "明确不在本 PR" 列表与伞 PR 一致
4 PR body 声明"空 PR-B,不包含实现"属实 git diff dev/python-diagram-umbrella..dev/python-diagram-assets-official --stat 输出为空;分支仅多一个 chore commit
5 分支基线正确:基于 umbrella HEAD bcadb92a git rev-parse dev/python-diagram-assets-official~1 = bcadb92a,与 umbrella HEAD 一致
6 umbrella 已包含 main 全部内容 git rev-list --count dev/python-diagram-umbrella..main = 0(behind=0)
7 npm 包 @resvg/resvg-wasm@2.6.2 存在、MPL-2.0 许可证、integrity 可验证 npm view @resvg/resvg-wasm@2.6.2 → license=MPL-2.0, integrity=sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==
8 源仓库 commit 9ca05846(resvg-js v2.6.2)可访问 curl -sI https://github.com/yisibl/resvg-js/commit/9ca05846... → HTTP 200
9 现有工具可执行:tools/check_diagram_assets.pytools/check_diagram_package.pytools/build_diagram_assets.py 三者均存在,--help 正常
10 未来工具(check_diagram_rendering.pycheck_diagram_engine_floor.py)仅在 official 2.6.2 实施计划中出现,PR-B body 未声称其已存在 PR body 只描述"先在 custom backend 上锁定红测和 reference bundle"等执行步骤,不引用具体不存在的工具路径
11 Issue #89 保持 OPEN(正确)、#376 CLOSED(正确,PR #377 已修复)、PR #377 MERGED(正确) gh issue view / gh pr view 回读一致
12 当前代码与 PR-B 计划一致:render_png(svg: str) / expand_svg(svg: str) 签名(pyfcstm/diagram/engine.py:624, :658)、无 _CanonicalSvg、无 XML allow-list、manifest.json 仍指向 custom resvg 0.37 Gist bundle(pyfcstm/diagram/assets/manifest.json:6-8 均确认 PR-B 改造尚未实施

I=0(无重要问题)

无。PR body 对"15-layout/130-arrow、20-layout/176-arrow"的引用与 official 2.6.2 实施计划中的实验基线一致(旧 corpus 130 arrow 方向 32/130 / 新 corpus 176 arrow 176/176 passing)。marker-end + orient="auto" + refX="10" 合同源于 PR #377,固化描述准确。CJK 五地区 × Regular/Bold 的字体覆盖在 pyfcstm/diagram/assets/fonts/ 中已由 PR-A 交付(SC/TC/HK/JP/KR, JetBrainsMono),PR-B 计划在此基础上添加 CJK resvg rendering gate,不重造字体资产。


M=2(轻微建议,不阻塞)

# 内容 建议
M1 PR body 末尾:"达到 ready-to-merge 后停止,不在此 PR 合并" — "不在此 PR 合并"语义模糊 建议改为更明确的表述,如"达到 ready-to-merge 后合入 umbrella,不直接合入 main"或"本 PR 仅合入 umbrella 分支,最终由伞 PR #383 合入 main",以与伞 PR 执行契约保持一致
M2 伞 PR #383 body 中 "umbrella ahead 34 / behind 42" 和 "main@901f30e" 已过期(实际:ahead 78 / behind 0,main 当前 HEAD 为 6b8edb51,非 901f30e9)。虽然这是伞 PR body 的维护问题而非 PR-B body 直接问题,但 PR-B body 将伞 PR 列为"规划权威入口",建议在伞 PR 中修正或在 PR-B body 中加一句"伞 PR 已同步最新 main(behind=0,commit bcadb92a)"作为当前态澄清 伞 PR #383 body 建议更新 ahead/behind 数字与 main HEAD,或在 PR-B 中以脚注注明当前实际 git 状态

整体结论

PR #386 是一个合规的空占位 PR

  • PR body 与伞 PR #383、official 2.6.2 实施计划、PR-A #384 的事实描述一致;
  • 范围与 PR-A/C/D 明确不重叠,ownership 边界清晰;
  • body 诚实声明"不包含实现",不虚假宣称 official backend 已落地;
  • 所有引用的上游实体(npm 包、源码 commit、issue/PR 状态)均可远端验证;
  • 不存在把 PR-B/C/D 未来工具伪装为现有工具的问题;
  • 现有可执行工具(check_diagram_assets.py/check_diagram_package.py/build_diagram_assets.py)工作正常。

C=0 / I=0 / M=2,M 不阻塞。PR 作为 planning 入口可以接受,但必须在实施提交 land 后重新进行完整实现审查(含 closed SVG allow-list 红测、source/license/integrity gate、双 MiniRacer floor、CJK 视觉门禁、package/fail-loud 和箭头 endpoint parity),届时 C/I/M 结论将完全重置。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer

结论:C=0, I=0, M=1

审查对象:PR #386 body/提交、伞 PR #383 的 official 2.6.2 实施计划评论、PR #384、issue #89/#376、PR #377,以及 PR-B head 与 umbrella base 的实际差异。

C

C=0。 未发现会阻止规划继续推进的致命矛盾。PR-B 没有修改生产代码、依赖或生成资产;head 3d738fb3 只有一个空占位提交,PR #386 相对 dev/python-diagram-umbrella 的文件差异为空。

I

I=0。 关键事实与 ownership 一致:

M

M=1:current-state hash 已过期,建议在实现前补 current-state comment。

#383 body 和 #89 当前正文仍称 umbrella/continuation HEAD 为 490565a5;但远端实际:

  • PR feat(diagram): close official resvg asset contract #386baseRefOidbcadb92a
  • dev/python-diagram-umbrella 当前远端也是 bcadb92a
  • bcadb92amerge main into diagram umbrella,且 main 为 901f30e9
  • PR-B head 的父提交正是 bcadb92a,与 umbrella 无文件差异。

这不构成 PR-B ownership 或“实现未开始”的 I/C 矛盾,但会让远端 current-state 证据和实际基线不一致。建议只追加/更新 current-state 说明,保留历史 hash,不改写历史考据。

权威依据:

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

当前状态同步(主会话)\n\n三名独立 reviewer 已完成 planning 审查,均为 ;M 级建议已记录。为避免与 umbrella/issue 中合入主线后的旧 HEAD 混淆,当前 PR-B 实际基线为:\n\n- : \n- : (已合入最新 main)\n- PR-B head: \n\nPR-B 仍只有不改代码的占位提交;下一步先在 custom backend 上写 closed-SVG/raw-input 红测,再实施官方资产切换。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

当前状态同步(修正版)

三名独立 reviewer 已完成 planning 审查,均为 C=0 / I=0;M 级建议已记录。为避免与 umbrella/issue 中合入主线后的旧 HEAD 混淆,当前 PR-B 实际基线为:

  • origin/main: 901f30e981c29eb8e304b33d61985652d2e85b2e
  • dev/python-diagram-umbrella: bcadb92a9cc510de419b72f1a8935c377e9e6978(已合入最新 main)
  • PR-B head: 3d738fb3182b36a118bcf5a0b5f13df7e3896a2b

上一条评论中的 Markdown 反引号被 shell 展开,以上内容是权威修正版。PR-B 仍只有不改代码的占位提交;下一步先在 custom backend 上写 closed-SVG/raw-input 红测,再实施官方资产切换。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

CLAUDE reviewer — 独立强对抗 review(PR-B #386

审阅范围

本地工作树、远端 PR #386(占位)、伞 PR #383 的 official 2.6.2 计划评论、PR-A #384、PR #377、issue #89 / #376

审阅命令:git diff HEAD, python -m pytest test/diagram/test_assets.py(51 passed),针对 _validate_canonical_svg / _validate_expanded_svg / _canonical_input / _render_failure / check_diagram_provenance.py 的白盒对抗探测。禁止 sub-agent、禁止修改用户改动,只读运行。

结论:C=0 / I=3 / M=4。 未发现阻断合并的正确性或安全性缺陷;实施合同的 SVG allow-list、marker 契约、DiagramData-only 输入、_CanonicalSvg 私标、npm/source 归档双 hash、runtime floor 隔离与 raw SVG 拒绝均按 PR-B 计划落地。以下 I/M 建议在本 PR 内解决或在正文明确记录为后续跟进边界。


Important(应修复)

I1 — _render_failure 把整份缩小后的 renderer 源码回显给终端用户

根因pyfcstm/diagram/engine.py::_render_failure 直接把底层 Exceptionstr(err) 拼进消息。MiniRacer 抛出 TypeError 时会把整份已被 _eval_asset 求值的 JS 源码(当前 renderer bundle ≈15KB 缩小 IIFE)粘到异常里。当 render_svg(request) 收到形状不对但 JSON-兼容的 dict(例如缺 events/actions/transitions.effectLines 之类可选键)时,用户看到的 DiagramRenderError 消息包含全部 minified JS,并且附带 run 'make build_assets' and retry 这条错误恢复提示(这是 dev checkout 分支)。

复现

python -c "
from pyfcstm.diagram.engine import DiagramAssetEngine
e = DiagramAssetEngine()
e.render_svg({
  'diagram': {'summary': {'transitions':1,'states':2},
    'rootState': {'id':'root','kind':'composite','children':[
       {'id':'a','kind':'state','children':[]},
       {'id':'b','kind':'state','children':[]}],
      'transitions':[{'id':'t','source':'a','target':'b','label':'x'}]}},
  'options': {'direction':'LR'}})
"

实际结果:单一 DiagramAssetError 消息尾部包含整份 renderer bundle(十几 KB),并以 “development checkout; run `make build_assets`” 结尾——这是数据 shape 错误却被提示成资产错误,会把用户导向错误的排查路径,且信息可能在日志/issue 中造成噪音。

建议

  1. _render_failure 中对 str(error) 做长度截断(例如 ≤512 字符 + ...),并允许一个可读的 cause 前缀独立呈现。
  2. 对 JS 侧原生 TypeError/ReferenceError 特化一条 DiagramRenderError,明确"DiagramData request 缺少必要字段",不要引导 make build_assets;把 dev checkout 提示只留给 _asset_failure 路径(真的资产读取或初始化问题)。
  3. 建议 render_svg 在把 request 交给 MiniRacer 之前对顶层 shape 做最小校验(diagram.rootStateoptions.direction ∈ {LR,TB}summary.transitions 存在),至少把最常见的字段缺失挡在 Python 层,避免把 JS 栈里的 minified 源送出去。

I2 — GitHub codeload.github.com/.../tar.gz/<commit> 归档 SHA-256 不保证长期稳定

根因tools/check_diagram_provenance.pysourceArchiveUrl(resvg-js)和 patchedSourceArchiveUrl(zimond/resvg)都固定了 sourceArchiveSha256 / patchedSourceArchiveSha256。GitHub 明确没有承诺 codeload 归档的字节稳定性——历史上 GitHub 曾数次调整压缩参数(默认压缩级别、Content-Encoding、Git object 打包),一次调整会让所有基于 codeload 的固定 SHA-256 门禁在同一天同时失败。近例包括 2023 年 1-2 月和 2025 年多个仓库的报告。

影响make diagram_provenance_check发布/CI 门禁;一次远端归档格式变动就会把 main 上的资产链锁死,任何时间点都无法通过。不是本 PR 的实现错误,但本 PR 主动引入的门禁风险,因此必须记录在 PR 合同里。

建议(择一)

  • 首选:将 sourceArchiveSha256 / patchedSourceArchiveSha256 降级为软引用(记录但不校验),仅对 LICENSE / Cargo.toml / patched-resvg/LICENSE.txt 三个稳定成员做 SHA-256 校验(这三项已经存在于 sourceFiles 且是长期稳定的树内文件);把 tarball(npm registry)作为唯一强 hash 门禁(npm 保证 immutable)。
  • 次选:把 archive 换成 GitHub 官方发布 tarball(release asset 是 immutable 的),或者 pin 到自维护镜像。
  • 若坚持保留,请在 PR-B body 的 hard gate 段明确写“若 GitHub codeload 归档格式变动,本 gate 需按上述预案降级/迁移”。

验证grep -n sourceArchiveSha256 tools/check_diagram_provenance.py tools/diagram_assets/asset-lock.jsonverify() 强等号校验。


I3 — 关闭 SVG dialect 的 URL 方案 blacklist 覆盖不完全且过度耦合到 DSL 标识符规则

根因pyfcstm/diagram/engine.py::_validate_svg_treere.search(r"(?:javascript:|data:|https?:|file:)", value, re.IGNORECASE) 阻挡外部/危险 URL:

  1. 未覆盖 vbscript:livescript:blob:ftp:ws(s):about:chrome: 等已知敏感方案;
  2. 该 regex 对所有属性值生效——包括 data-fcstm-iddata-fcstm-composite 等 data 属性。当前 FCSTM DSL 的标识符规则([A-Za-z_][A-Za-z0-9_]*)恰好保证不会出现 ://,所以现在不会触发;但这是一个隐式契约,未在代码或 PR body 中声明。任何未来放宽 DSL identifier 或将非 identifier 字符串塞入 data-fcstm-*(例如 range 之外的 label 字段、future annotations)都会让 renderer 输出被自己的 closed dialect 拒收。

复现(证明耦合存在,不是当前 exploit)

python -c "
from pyfcstm.diagram.engine import _validate_canonical_svg, DiagramAssetError
svg = '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g data-fcstm-kind=\"state\" data-fcstm-id=\"see https://x\"></g></svg>'
try: _validate_canonical_svg(svg)
except DiagramAssetError as e: print('rejected:', e)"
# rejected: closed SVG dialect rejected an external URL

建议

  1. URL 检查按属性名分类:
    • 引用性属性href/xlink:href/style/marker-end/clip-path/filter/fill/stroke 等能承载 url(...) 或方案的属性):保留严格 scheme blacklist,并扩展到 vbscript|livescript|blob|about|chrome|ftp|ws|wss(或改为 allow-list:只允许空、none#local fragment、url(#local))。
    • 纯数据属性data-fcstm-*classfont-familytext-anchor 等):完全不做 scheme 匹配,交给 XML/output 属性 allow-list 已经保证的边界。
  2. _validate_svg_tree 顶部注释里显式记录"渲染器输出中的所有 data-fcstm-* 值必须来自 DSL identifier 规则或纯整数 range,任何未来加入 URL/自由文本内容都会破坏此契约"。

Minor(非阻塞)

  • M1_validate_svg_tree 只检查 element.text.strip() 是否为空,未检查 element.tail。expanded SVG 里 <g> <path/> </g> 结构中 whitespace tail 会静默通过,"no residual text nodes" 的严格性描述与实现存在偏差。当前 resvg 输出不会产生非空 tail,无实际危害。修复:迭代时同步检查 element.tail
  • M2_validate_svg_tree 中 markers 多于 1 个 或 orient!="auto" 都被统一报为 "marker orientation contract"。多 marker 场景应报 duplicate/too many marker definitionsrefX!="10" 也应有独立错误分类。当前 refX="9" 已单独报 endpoint contract,只差 duplicate marker 场景。
  • M3pyfcstm/diagram/__init__.py 模块 docstring 里的 >>> engine = DiagramAssetEngine(timeout=30.0) 例子在没有 built assets 的环境(源码 checkout 首次运行)里跑 doctest 会报 asset-missing。当前仓库未开启 sphinx doctest,因此只是隐患;若后续加 doctest 需要给这段加 # doctest: +SKIP 或改为 .. 目录级 example。
  • M4docs/source/api_doc/diagram/engine.rst 新增 DiagramRenderError 段落下划线只有 53 个 -,而 title DiagramRenderError 是 18 字符——reST 要求下划线至少与标题等长,虽然 docutils 允许更长但一致性上项目其他条目使用 53--,此处按现网风格是一致的(其他条目也这么排)——无问题,仅标记为已核对。

验证证据

  • python -m pytest test/diagram/test_assets.py -x -q51 passed(modern MiniRacer;env mini-racer 0.14.1)。
  • 白盒对抗探测覆盖:raw SVG 拒收、DTD/ENTITY 前置子串(含 BOM + 前导空格)、<!doctype> 小写变体、numeric char ref、多 marker/orient 违约、refX!="10"、悬空 url(#missing)、外部 url(https://...)<foreignObject/<script> 注入、onload=xlink:href="file://"clip-path broken ref、非法 xmlns、nested <defs/>(silently ok, 语义合规)。
  • Dict 端到端:合规 request → SVG/PNG/expanded SVG 全部产出;非法 shape 触发 I1 中的 verbose error。

结论

PR-B 的实施合同(DiagramData-only、marker auto+refX=10、closed SVG allow-list、npm+source 双 hash、runtime floor、metadata scrub、双 MiniRacer 版本地板一致)在本地实现层面全部落地并被 51 test 单元覆盖。没有 Critical3 项 Important 需要在合并前处理或明确记录为跟进边界(I1 verbose error 分流、I2 GitHub archive hash 稳定性预案、I3 URL blacklist 分类与耦合声明),Minor 可留到后续 hardening。

远端 PR #386 目前仍是空占位,本地实现尚未推送为 HEAD;本条 review 针对本地待推送实现做前置对抗,正文中 hard gate 与实施合同建议在推送前把上述 I1/I2/I3 的应对写入 PR body。

@HansBug HansBug left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review:PR-B #386 DeepSeeker 独立实现审查(本地未提交实现 + all gates)

Reviewer: DeepSeek reviewer(强对抗独立审查)
审查对象: PR #386 dev/python-diagram-assets-official,含 15 个已修改文件 + 5 个新未跟踪文件,全部 gates 已实操运行
方法: 全量 diff 阅读 + 真实 MiniRacer 构建 + 51 项 pytest 全绿 + 4 个 maintenance gate 真实执行 + 白盒 edge-case 构造


执行证据

Gate 命令 结果
build_assets make build_assets ✅ renderer.js 1625370 bytes
diagram_assets_check make diagram_assets_check ✅ hashes/size/syntax/ignore + resvg037 stale check + manifest ↔ lock 一致性
engine_floor python tools/check_diagram_engine_floor.py ✅ mini-racer 0.14.1, resvg Exports function:function, 全路径通过
corpus canonical-arrows check_diagram_rendering.py --corpus canonical-arrows.json --expected-layouts 15 --expected-arrows 130 --expected-directions right=32,down=78,left=5,up=15 --check-cjk ✅ 15/130 全部通过,direction 分布精确匹配
corpus shared-layouts check_diagram_rendering.py --corpus shared-layouts.json --expected-layouts 20 --expected-arrows 176 --expected-groups LR=10,TB=10 ✅ 20/176 全部通过
provenance (网络) python tools/check_diagram_provenance.py ✅ npm tarball + resvg-js source + patched resvg source 三链 SHA-256 验证通过
51 pytest SKIP_SLOW_TESTS=1 pytest test/diagram/test_assets.py -x -v ✅ 51 passed

C=0(无阻塞性问题)

以下各项全部通过真实执行 / 白盒验证:

# 审查项 验证方法
1 @resvg/resvg-wasm@2.6.2 npm identity 一致 npm view → version=2.6.2, license=MPL-2.0, dist.integrity=sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw== 与 asset-lock.json / package-lock.json 完全匹配
2 installed binding + WASM hash 一致 index.min.js SHA-256 = 590115ae25... = lock → npm → asset 三端一致;index_bg.wasm SHA-256 = 22bf6e9f... 同上
3 npm tarball SHA-256 验证 网络下载 tarball → SHA-256 = ff51acbb5e... 与 lock 一致
4 resvg-js source archive (commit 9ca05846) 网络下载 → SHA-256 = 7ce86974... → LICENSE/Cargo.toml hash 匹配;Cargo.toml 确认 version = "0.34.0" + patched commit 记录
5 patched resvg source archive (zimond/resvg 3495d87) 网络下载 → SHA-256 = 08d15a07... → LICENSE.txt hash 匹配
6 license provenance MPL-2.0 一致:npm metadata、package.json、NOTICE.txt 三处均记录
7 manifest.json 无 stale resvg037 引用 grep resvg037|"resvg 0.37" → 零匹配;check_diagram_assets.py 有防御性检测
8 asset builder 收敛为本地 npm 复制 build_diagram_assets.py 改用 load_local_locked_file() → 从 node_modules/@resvg/resvg-wasm/ 直接读取,不再网络下载 resvg 0.37 Gist bundle
9 resvg-bridge.js 全局变量名切换 resvg037resvgresvg037.initWasmresvg.initWasmnew resvg037.Resvgnew resvg.Resvg
10 closed SVG input allow-list _SVG_INPUT_ELEMENTS (20 elements) + _SVG_INPUT_ATTRIBUTES 全面白名单,reject script/foreignObject/image/use/style/unknown/非 SVG namespace
11 closed SVG output allow-list _SVG_OUTPUT_ELEMENTS (13 elements) + _SVG_OUTPUT_ATTRIBUTES 仅允许 path/g/clipPath/defs/filter 族,reject text/marker
12 marker-end orient="auto" + refX="10" 合同 _validate_svg_tree(output=False) 强制:唯一 <marker> + orient="auto" + refX="10" + 无 marker-start + 无 auto-start-reverse
13 输入验证拒绝 raw SVG _canonical_inputisinstance(request, dict) 仅接受 DiagramData;直接传 raw SVG string → ValueError
14 A→B endpoint: resvg tip meets target border test_rendered_a_to_b_resvg_tip_meets_target_border → expanded SVG transform 验证 tip 落在 target border
15 marker direction contract (4 arrows) test_resvg_marker_tip_lands_on_path_endpoint → 4 方向 arrow transform 方向向量精确匹配
16 CJK 5 locale Regular/Bold 全通过 test_each_locale_regular_and_bold_have_visible_distinct_glyphs[sc/tc/hk/jp/kr] → 每 locale 的 Regular/Bold PNG bounding box 不同 + expanded SVG 含 <path
17 dual MiniRacer runtime floor check_diagram_engine_floor.py → py-mini-racer≥0.6.0 (3.7) / mini-racer≥0.7.0 (3.8+);实际运行:mini-racer 0.14.1 → resvg export=function:function + TextEncoder/TextDecoder=function:function
18 fail-loud 错误链 _render_failure / _asset_failure 所有路径 → 包含 make build_assets 或 issue URL 指引;DiagramRenderError 继承 DiagramAssetError
19 timeout/context discard test_engine_discards_context_after_renderer_deadline / test_engine_discards_context_after_resvg_deadline → 超时后 _context is None
20 package/fresh wheel requirements-viz.txt + requirements-test.txt 均更新为 py-mini-racer>=0.6.0,<0.7 / mini-racer>=0.7.0check_diagram_assets.py 验证 manifest.json/NOTICE.txt 无 stale 引用
21 pydoc engine.rst 新增 DiagramRenderError autoclass;__init__.py 导出 + docstring example 可执行验证
22 test boundary JS 端 editors/jsfcstm/ 测试树独立于 Python test/diagram/;Python 端仅依赖 pyfcstm.diagram 模块
23 oversized/malformed SVG rejection 白盒验证:16MB 超限 → reject;100K+ elements → reject;unclosed tag → reject;empty/duplicate id → reject;<!DOCTYPE/<!ENTITY/<?/<!--/<![ → reject
24 URL/script injection rejection javascript:/data:/https:/file: 在 attribute value 中 → reject;url(https://...) 非 local reference → reject
25 @resvg/resvg-wasm as devDependency jsfcstm 自身不 import resvg-wasm,仅 asset builder 在 build time 复制 → devDependencies 语义正确;npm ci --include=dev 确保可用

I=0(无重要问题)

无。所有门禁在实际运行中全部通过。以下领域经深度审查后确认无问题:

  • _validate_svg_tree 性能: 对 100K element SVG 做 root.iter() 多次扫描是 O(n) 但常量极小;实际 corpus 最大 case 仅 ~130 arrow,SVG 远低于 limit。
  • request_id 冲突: time.monotonic_ns() 在单 MiniRacer 单线程上下文中不存在竞争条件。
  • _CanonicalSvg str 子类: isinstance(cs, str) = True, hash/eq 与 str 兼容,不破坏任何 str 行为。
  • renderer crash on invalid DiagramData: 当传入缺少必需字段(如 actions/events)的 DiagramData 时,JS 端抛出 TypeError,Python 端 catch 为 _render_failure → fail-loud 行为正确;正常路径中 DiagramData 总是由 jsfcstm builder 生成完整 shape。

M=3(轻微建议,不阻塞)

# 内容 证据 建议
M1 5 个未跟踪文件未提交 git status 显示 ??tools/check_diagram_rendering.pytools/check_diagram_engine_floor.pytools/check_diagram_provenance.pytools/diagram_assets/corpus/(含 canonical-arrows.json 297KB + shared-layouts.json 402KB)、tools/diagram_assets/generate_corpus.js。Makefile 已引用前三个 tools 和 corpus,make diagram_assets_verify 依赖它们。 将这些文件 git add 并在本 PR 提交;corpus 是 rendering gate 的输入 oracle,tools 是 CI gate 的可执行体。如果 corpus JSON 体积过大(~700KB),可考虑在 .gitattributes 中标记为 linguist-generated=true
M2 render_png/expand_svg 类型标注与 docstring 不一致 类型标注为 request: Dict[str, Any],但 docstring 写 "or the private canonical SVG returned by render_svg",且实际实现通过 _canonical_input 接受 _CanonicalSvg。内部测试大量使用 engine.render_png(_canonical_svg(svg)) 两种修法:(a) 保持类型标注为 Dict[str, Any],docstring 删除对 _CanonicalSvg 的提及(此为内部优化,不应出现在公共文档中);或 (b) 将类型改为 Union[Dict[str, Any], str] 并移除 "private" 限定词。推荐 (a),与 plan 中"不在此 PR 暴露公共 API"一致。
M3 generate_corpus.js 依赖 jsfcstm dist,但未在 Makefile 中暴露 generate_corpus.js 通过 require("../../editors/jsfcstm/dist/diagram") 依赖 jsfcstm 构建产物。当前无 make target 可重建 corpus,corpus 更新需手动执行 node tools/diagram_assets/generate_corpus.js 建议添加 make diagram_corpus target,并文档化:corpus 应在 jsfcstm fixture 变更后重建。或至少在 tools/diagram_assets/corpus/README.md 中记录生成命令和预期契约。

整体结论

PR-B 本地实现质量非常高。官方 @resvg/resvg-wasm@2.6.2 完整替换了 custom resvg 0.37,provenance/hash/license 三链可验证;closed SVG input/output allow-list 覆盖全面,对 XSS/URL injection/foreign namespace/oversized/malformed 均有防御;marker-end orient=auto refX=10 合同在 validator 和 test 中双重锁定;15/130 + 20/176 corpus 全部通过,CJK 五地区 Regular/Bold 字形区分可验证;双 MiniRacer runtime floor 在真实环境中通过;fail-loud 错误链完整;timeout/context discard 行为正确。

C=0 / I=0 / M=3,M 不阻塞。3 个 M 均为未提交文件/文档一致性建议,不影响功能正确性。

本审查不含任何 sub-agent,所有结论均来自直接 diff 阅读 + 真实命令执行 + 白盒 edge-case 构造。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

审查结论:C=2 / I=1 / M=0

C1:official/custom reference parity 没有形成默认、可重放的门禁

Makefile:246-262 只在外部传入 DIAGRAM_REFERENCE 时才执行 --compare-reference;当前工作树没有 reference bundle(reference.json、sidecar、逐 fixture outputs、根 SHA256SUMS),仓库/CI 也没有设置该变量。因此默认 make diagram_assets_verify 只验证当前 official renderer 的计数、方向和 CJK,不证明 PR #383 要求的 source-swap 前 custom 0.37 与 official 2.6.2 的 20/20 PNG byte parity 和 expanded SVG clip-ID-only parity。reference 缺失未被 fail closed。

C2:memory gate 仍未接入可执行闭环

虽然 tools/check_diagram_rendering.py:726-900 已开始采集 RSS/heap/bridge metrics,但该 gate 仍只在显式 --check-memory 下运行,且是当前 Python 进程内的一次 probe;Makefile:246-273 默认不调用它,也没有每个 MiniRacer distribution 的独立 subprocess runtime 报告。更关键的是,计划中的 capture 命令不带 --check-memory,compare 命令带 --check-memory;当前实现会在此合法流程中直接失败:ValueError: reference lacks the required custom memory baseline。因此计划要求的 custom baseline、official 对比和 fail-closed 阈值目前不能按规定执行。

I1:reference manifest 的 memory 设计与计划命令不兼容

上述 capture/compare 组合是可直接重放的最小失败用例;请在修复时统一 capture 与 compare 的 schema/命令,确保 baseline、两种 runtime、p95、最后 25 次斜率、末 5 次均值和 active context/font 约束都在同一套可审计报告中,并把 parity/memory gate 接入默认维护目标或 CI。

已通过的证据

  • make diagram_rendering_check:canonical 15 layouts / 130 arrows(right=32, down=78, left=5, up=15);shared 20 layouts / 176 arrows。
  • python -m pytest -q test/diagram/test_assets.py:56 passed;1 个 MiniRacer teardown 的 PytestUnraisableExceptionWarning
  • modern floor:Python 3.10.10 + mini-racer 0.14.1;legacy floor:Python 3.7.17 + py-mini-racer 0.6.0;SVG/PNG/expanded SVG smoke 均通过。
  • python tools/check_diagram_provenance.py:npm tarball/source archive/license/Cargo hashes 通过。

在 reference parity、memory baseline/threshold、默认门禁和上述流程修复并重新运行 PR #383 计划中的 package/visual/runtime gates 前,建议保持 PR-B blocked。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

补充实测:在当前 official 2.6.2 / modern mini-racer 0.14.1 上运行
python tools/check_diagram_rendering.py --corpus tools/diagram_assets/corpus/canonical-arrows.json --expected-layouts 15 --expected-arrows 130 --expected-directions right=32,down=78,left=5,up=15 --check-memory
经过 110 renders 后以 ValueError: memory switch RSS slope exceeds 1 MiB/switch 失败。说明现有 memory gate 当前并未达到计划中的 green baseline;需在两个 runtime 的独立 subprocess 中取得 custom/official 对照并处理该失败后再宣称通过。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

claude reviewer — PR-B #386 审查(planning consistency & executability)

Reviewer identity:claude reviewer(以 CLAUDE.md gh 身份规则运行;gh auth token --user HansBuglogin=HansBug, email=hansbug@buaa.edu.cn,与 git config user.name/email 一致,未使用 sub-agent,只跑只读命令)。
审查范围:PR-386 dev/python-diagram-assets-official → dev/python-diagram-umbrella(HEAD 3d738fb3)、伞 PR-383、PR-A #384official 2.6.2 实施计划评论、CLAUDE.md label & documentation 治理、本地 working tree 已完成但未 push 的变更(+1134 / -206、16 modified + 6 untracked)。
约束:15 分钟;不改代码、不 commit/push;gh pr comment 已按身份规则执行。


事实基线

项目 观察值
PR 386 pushed diff additions=0, deletions=0, changedFiles=0;唯一 commit 3d738fb3 chore(diagram): open official asset closure PR-B
Base / head base dev/python-diagram-umbrella(伞 PR);head dev/python-diagram-assets-official(符合伞 PR §7 建议分支名)
Checks 30/30 pass(Code test 8Py×2OS、template full、fresh CLI、jsfcstm、template-suite-gate 等)
Mergeable MERGEABLE;但合入等于合入 chore-only
Labels
Linked issues closingIssuesReferences=[];PR body 未提到 #89
本地未 push 16 modified(engine.py +543、test_assets.py +241、CI job、Makefile、asset-lock、bridge、README/NOTICE、requirements-viz/test 等)、6 untracked(tools/check_diagram_provenance.pytools/check_diagram_rendering.pytools/check_diagram_engine_floor.pytools/fetch_diagram_reference.pytools/diagram_assets/generate_corpus.jstools/diagram_assets/reference-lock.jsontools/diagram_assets/corpus/*.json

PR body 自称 "空 PR-B,目前只有不改代码的开门提交"。与 pushed diff 事实相符(chore-only),因此PR body 对当前状态本身不是虚假描述。但下文所有 C 级问题都基于:PR-386 目前不能承担计划所列的任何 hard gate 证明。


C — 阻塞(必须修复)

C1. Pushed HEAD 无实现,reviewer 无法对任一 hard gate 复现红/绿测

证据gh pr view 386 --json additions,deletions,changedFiles0/0/0;本地 git diff --stat origin/dev/python-diagram-assets-official 显示 1134/-206。计划 §5 列的 hard gate 命令全部依赖本地未推送文件(tools/check_diagram_rendering.pycheck_diagram_engine_floor.pycheck_diagram_provenance.pyfetch_diagram_reference.pytools/diagram_assets/corpus/*reference-lock.jsonMakefile diagram_assets_verify target、.github/workflows/test.yml diagram_assets job、asset-lock.json 官方 provenance 字段、engine.py _CanonicalSvg/_validate_svg_tree/_validate_expanded_svg 等)。
失败情景:任何独立 reviewer clone HansBug/pyfcstmdev/python-diagram-assets-official@3d738fb3 后跑 make diagram_assets_verify DIAGRAM_REFERENCE=... 会直接命中 "no such target"(diagram_assets_verify 只存在于未 push 的 Makefile)。当前 PR 因此不可技术审查,只是 planning entry。
复现git fetch origin dev/python-diagram-assets-official && git checkout FETCH_HEAD && grep -n diagram_assets_verify Makefile(找不到)。
修复建议

  1. 立即将 PR-386 切换为 Draftgh pr ready --undo 386),明确告知 reviewer "开门 PR,非可审查实现";或
  2. 按计划 §4 的实施顺序分阶段 push(先 oracle+红测 → 关入口 → 切依赖 → runtime floor → 视觉),每个 push 独立可审;不要一次 push 1134 行未分片的实现。

C2. Issue #89 未在 PR body 或 GitHub linked issues 中互链,违反伞 PR §7 与 CLAUDE.md issue/PR 治理

证据gh issue view 89 --json labelskind: feature / status: planned / area: visualization / area: packaging / state=OPEN。umbrella #383 与 PR-A #384 都显式 link #89;PR-B #386 body 无 "#89"、"issue"、"关联" 字样,GraphQL closingIssuesReferences 空数组。umbrella §7 明确要求 "official asset 计划、reference bundle、Gist、PR-B body 和 issue current-state comment 互链并远端 hash/readback"。
失败情景#89 的 status/labels 与 PR-B 交付实际不同步;后续在 issue 页无法从时间线看到 PR-B 计划评论,status: in progress 触发条件不明。
修复建议

C3. PR-386 label 为空,违反 CLAUDE.md "GitHub Issue and Pull Request Label Governance" 硬性要求

证据gh pr view 386 --json labels[]。CLAUDE.md 明确规定 "Pull request | 必须 exactly one kind:* and one to four area:* labels | Optional role:*, scope:* ...",且 "role: sub-pr" 在 base 为 umbrella branch 时是必须的。
失败情景:仓库 label taxonomy 是 "maintained project contract",PR-B 缺 label 直接违约;label 缺失使 is:pr label:area:visualization 类查询漏掉 PR-B,破坏 governance 检索能力。
复现gh pr list --repo HansBug/pyfcstm --label "area: visualization" 不会返回 386。
修复建议

  • 添加 kind: design(开门 PR 主要交付是 planning)或 kind: maintenance(若以后 PR-386 承载实现);
  • 添加 primary area: 组合:area: visualizationarea: packagingarea: ciarea: testing(最多 4 个);
  • 添加 role: sub-pr(base 是 umbrella branch);
  • 不要scope: non-main(CLAUDE.md 明确禁止 sub-PR 仅因 base ≠ main 而戴 scope: non-main)。

I — 重要(必须修复,但不立即阻塞开门)

I1. 计划 §5.4 的 --check-expanded-id-only flag 在本地实现中不存在

证据grep -n 'check-expanded-id-only\|expanded_id_only' tools/check_diagram_rendering.py Makefile 无匹配;tools/check_diagram_rendering.py:1126-1140 只声明 --corpus / --expected-* / --capture-reference / --compare-reference / --check-cjk / --check-memory / --report-dir;本地 Makefile diagram_parity_check--compare-reference $(DIAGRAM_REFERENCE) --check-cjk --check-memory
失败情景:外部 reviewer 按计划 §5.4 复制粘贴命令会得到 unrecognized arguments: --check-expanded-id-only。计划文本与实现的 CLI 接口 diverge;expanded SVG 的 "clip-ID-only parity" 是否总是默认执行、还是需要显式 opt-in,PR-B 审计不清楚。
修复建议:要么在 check_diagram_rendering.py 新增 --check-expanded-id-only(即使内部是 no-op / 已默认开启),要么在 official 2.6.2 计划评论里 amend 掉这一 flag。选一,别双写。

I2. Custom reference bundle 从个人 Gist 恢复,与 §3 "删除 Gist bundle/WASM 下载路径" 精神冲突

证据tools/diagram_assets/reference-lock.json 3 个 runtime 桶 (3.7 / 3.8 / 3.11) 全部指向 https://gist.githubusercontent.com/HansBug/b43d202ccc9436cada8bc57fa3763450/raw/...。计划 §3 明确要求 PR-B "删除 Gist bundle/WASM 下载路径",把 resvg 从 Gist 迁到 npm;同一 PR 却把 custom 参考基线依赖到同一个人 Gist。
失败情景:Gist 属个人账户,可被 revision/删除;SHA-256 pin 只能保证 "如果拿到内容一致",无法保证 "永远拿得到"。若 Gist 被移除或作者身份变化,CI diagram_assets job 会永久 fail-closed,卡住 umbrella。这是与 official npm 决策的同类风险
修复建议

  • 首选:把 base64 encoded reference archive 直接 checked-in 到 tools/diagram_assets/reference/(每 runtime bucket 一个文件,或统一 SHA256SUMS),避免任何外部下载;
  • 次选:显式在 NOTICE / README 记录 "custom backend reference bundle 仅用于 PR-B 迁移期,PR-C 前将 checked-in 并弃用 Gist",并给出弃用日期。

I3. PR-386 body 未包含 rollback / kill-switch 显式清单

证据:PR-386 "明确不在本 PR" 节只列出未来 scope(公共 API、CLI 等),未提及计划 §6 "PR-B 失败时完整撤销该 PR,umbrella 保持当前单一 custom backend" 的具体回退操作(revert commit list、恢复 asset-lock 到 custom 0.37 的路径、复原 requirements-viz 到 >=0.2 的兜底)。
失败情景:如果 official swap 在 double runtime gate 上失败,reviewer/maintainer 无 PR body 内锚点告诉他 "怎么退回";容易演化成 "在 PR-B 里加 fallback 掩盖失败"(正是计划禁止的)。
修复建议:在 PR body 加一节 "## 回退协议",一句话链到 umbrella §6 "失败与回退条件" + 明确 "若失败则本 PR 全撤,不留 fallback"。

I4. PR-386 body 未链任何 landing evidence(Gist / hash ledger / PNG contact sheets)

证据:PR body 声明 "任何未解释的视觉差异... 会阻止合并",但正文只指向 umbrella、PR-A、implementation plan 三条,缺 §7 要求的 "同一 Gist 上传脚本、JSON、PNG contact sheets、hash ledger 和中文报告" 的入口。当前本地 reference-lock.json 指的 Gist b43d202c...3763450 是 reference archive 本身,不是 evidence Gist。
失败情景:reviewer 无法从 PR-B 单页看到 "official/custom PNG 20/20 byte-identical" 或 "130/176 arrow" 的实际证据文件,只能相信文字。
修复建议:PR-B 每次 push 迭代时,同步更新 body "证据 Gist:"(可以是空占位加 TBD 直到有第一份产物)。


M — 次要(不阻塞,但值得处理)

M1. PR title 前缀 "实现:" 与 pushed head 事实(chore-only)不符

建议:改为 "规划:官方 resvg 资产收敛与 closed SVG 合同 PR-B 入口",或将 PR 转 Draft。

M2. pyfcstm/diagram/__init__.py 本地新增的 list-table:: docstring 若被 rst_auto 拉进 Sphinx,请按 CLAUDE.md reST inline markup 规则复核 tight boundary(当前英文段落安全,中文页翻译时留意 **/```` 与 相邻问题)。

M3. 本地 .agents/ 目录 untracked

确认不要意外 add 进 PR-386;若属工具产物,加 .gitignore 更保险。

M4. requirements-test.txt / requirements-viz.txt>=0.2 直接跳到 >=0.6.0,<0.7 (3.7) / >=0.7.0 (3.8+) 是符合计划§5.3 的正确修正

但 PR-386 body 当前未在 "范围" 列表里显式点名 "同步 requirements-test.txt",只提到 "package metadata"。建议 body 中把 requirements-test.txt / requirements-viz.txt 列出,避免 reviewer 以为只改 setup.py extras。


校验的正面结论(避免虚假否定)

  • 分支/base 命名与 umbrella §7 §PR-B 建议一致 (dev/python-diagram-assets-official → dev/python-diagram-umbrella);
  • pushed HEAD 与本地 HEAD 同为 3d738fb3,不存在 push 落后;
  • mergeable=MERGEABLE,合并冲突为空;
  • 本地 CI 已加 diagram_assets matrix (Py 3.7/3.8/3.11) 并纳入 template-suite-gate needs 列表;DIAGRAM_REFERENCE fail-closed 由 Makefile test -n "$(DIAGRAM_REFERENCE)" 守住(行 257);
  • check_diagram_rendering.py 已实现 SHA256SUMS ledger、schema pyfcstm-diagram-reference、目录逃逸拒绝、reject-unknown top-level 字段;
  • asset-lock.json 已包含 npm integrity、tarball SHA-256、source archive 双 SHA-256、resvg-js Cargo.toml / resvg LICENSE.txt hash(对齐 §5.1);
  • check_diagram_provenance.py 实际 unpack tarball 并校验 resvg = "0.34.0" 与 patch commit(对齐 §5.1 "不能只相信 PR 文本");
  • jsfcstm package.json 只把 @resvg/resvg-wasm@2.6.2 加入 devDependenciespackage-lock.json 保留 npm integrity、dev: true flag(对齐 §3)。

最终结论

当前 PR-386 是可接受的 "planning door-opener",但

  • 若 PR-386 的意图仅是 "开门":必须先 Draft 化加齐 label互链 issue 嵌入 jsfcstm 可视化 (SVG/PNG):mini-racer + WASM 栅格化 #89(C1/C2/C3)。做到这三点即可继续在此分支迭代实施。
  • 若 PR-386 的意图是 "承载后续 official 2.6.2 实施":以上 3 个 C 级仍是硬阻塞;I1–I4 必须在第一次真正的实现 push 之前修复,尤其 I2(Gist 依赖)与 I3(回退协议),否则 P0 无法在此 PR 收敛。

阻塞项清单:C1 无可审查实现 · C2 issue #89 未互链 · C3 label 完全缺失。
Ready-to-merge❌ 不满足(当前 pushed head 无法证明计划任何 hard gate)。
建议:立即执行 C1(转 Draft)→ C3(加 label)→ C2(body 加 #89 + issue current-state comment),随后按 §4 五阶段分片 push 实现,每次 push 附证据 Gist 更新 body。

— claude reviewer, 2026-07-20

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

deepseek reviewer — PR #386 规划/验收可行性审查

审查上下文


C(Critical — 必须在合入前修复)

C1. PR body 与代码现状矛盾:声称 custom 0.37 仍是 production backend,实际代码已使用 official 2.6.2

证据

  • PR body:「当前 production candidate backend 仍是 custom resvg 0.37;官方 @resvg/resvg-wasm@2.6.2 只完成实验和实施规划,尚未落地」
  • 但 PR-A 实现:共享 Python Diagram renderer 与离线资产链路 PR-A #384 已合入 umbrella,asset-lock.json 声明的 resvg 版本为 @resvg/resvg-wasm@2.6.2
  • check_diagram_assets.py 第 283–288 行明确拒绝任何对 "resvg037" / "resvg 0.37" 的引用
  • build_diagram_assets.py 第 6 行注释:「copies the exact official resvg 2.6.2 package」
  • 本地验证:当前 resvg-binding.js SHA256 匹配 asset-lock 中的 official 2.6.2 bindingSha256

影响:PR body 作为规划合同,描述的执行顺序「先在 custom backend 上锁定红测和 reference bundle,再实施 official source swap」与现实相反——official swap 已通过 PR-A 完成。

修复建议:重写 PR body,明确:

  1. official 2.6.2 已通过 PR-A 落地
  2. reference bundle 来源待更新(见 C2)
  3. PR-B 的真实剩余工作是:更新 frozen reference bundle 到 official 2.6.2 产出、替换 _CUSTOM_REFERENCE_FILES 哈希

C2. 冻结 reference bundle 与当前 runtime 不匹配,parity gate 硬编码 custom 0.37 哈希会拒绝 official 2.6.2

证据

  • check_diagram_rendering.py 第 33–39 行的 _CUSTOM_REFERENCE_FILES 硬编码了 custom 0.37 的 WASM/binding 哈希:
    • resvg-binding.js: 435b67b9... (custom 0.37)
    • resvg.wasm: 65a71d37... (custom 0.37)
  • 当前 official 2.6.2 产出:
    • resvg-binding.js: 590115ae... (official 2.6.2)≠ 435b...
    • resvg.wasm: 22bf6e9f... (official 2.6.2)≠ 65a7...
  • reference-lock.json 指向的 Gist bundle 是由 custom 0.37 产出的
  • 第 32 行注释:「A reference captured from the official package must never be accepted as a custom baseline」

影响:CI diagram_assets job 运行 diagram_parity_check 时,_validate_reference_files() 检查 reference 内部 wasm/binding 哈希 → 与当前 official 2.6.2 不匹配 → parity gate 必然失败。

修复建议

  1. 用 official 2.6.2 运行时重新 capture reference bundle,上传新 Gist,更新 reference-lock.json
  2. 同时更新 _CUSTOM_REFERENCE_FILES 哈希为 official 2.6.2 产出值

C3. PR 无任何代码变更,CI diagram_assets 门禁被跳过

证据

  • PR feat(diagram): close official resvg asset contract #386 只有 1 个空 chore commit,0 additions / 0 deletions
  • CI route detector 因无代码变更将 run_expensive_code_test 设为 false
  • diagram_assets job(含 diagram_assets_verify = parity/CJK/provenance/engine-floor/package 等全部门禁)被全体跳过
  • template-suite-gaterun_expensive_code_test=false 时自动通过

影响:无法从 CI 证明 PR 通过 charted gates。


I(Important — 强烈建议修复)

I1. DIAGRAM_REFERENCE 未设置时 parity gate fail-loud,本地开发者无法自测

  • Makefile 第 26 行:DIAGRAM_REFERENCE ?=
  • diagram_parity_checkDIAGRAM_REFERENCE 为空时 exit 2
  • diagram_assets_verify 依赖 diagram_parity_check → 任何未显式设置 DIAGRAM_REFERENCE 的环境都无法运行完整门禁

修复建议:增加 make diagram_reference_fetch target 自动拉取,或在 docs 中给出明确指引。

I2. PR body 缺少可执行验收命令

PR body 只描述目标,未给出可执行命令。建议补充:

make diagram_assets_verify DIAGRAM_REFERENCE=/path/to/reference.json
make diagram_corpus
make diagram_package_check

M(Minor — 不阻塞合入)

M1. 本地全部门禁通过,代码质量良好

make diagram_assets_check           # PASS
make diagram_rendering_check        # 15L/130A + 20L/176A PASS
make diagram_provenance_check       # tarball + source + patched archive PASS
make diagram_engine_floor           # modern mini-racer 0.14.1, 15 cases ALL PASS
python tools/check_diagram_package.py --check  # self-check PASS
pytest test/diagram/test_assets.py -v          # 57 passed

箭头端点合同验证通过(marker-end + orient="auto" + refX="10"):

  • test_rendered_a_to_b_resvg_tip_meets_target_border
  • test_resvg_png_and_vector_expansion_keep_marker_direction
  • test_resvg_marker_tip_lands_on_path_endpoint

CJK 五地区全通过(SC/TC/HK/JP/KR Regular + Bold),glyph visible,无 tofu/裁切。

M2. official 2.6.2 上游 provenance 可回读

npm tarball + resvg-js source archive + patched resvg source archive 全部通过 hash 验证,Cargo.toml 确认 resvg 0.34.0。

M3. mergeStateStatus: CLEAN,无冲突


总结

分类 数量 关键问题
C 3 PR body 声称 custom 0.37 但代码已是 2.6.2;reference bundle 与 runtime 不匹配 parity gate 会失败;CI 因零变更跳过了 diagram_assets
I 2 DIAGRAM_REFERENCE 默认空导致本地无法自测;PR body 缺少可执行命令
M 3 本地门禁全绿、provenance 可回读、无冲突

当前 verdict:PR #386 是一个空规划占位符(0 代码变更),不能据此宣称 official backend 已通过验收门禁。PR body 描述的状态与仓库实际状态严重不一致。需要在 PR-B 落地任何实现前修复 C1–C3。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

当前一致性审查:C=1 / I=2 / M=1

C1:远端 PR #386 仍是空 PR,当前实现与 CI 不在可审查提交中

远端回读显示 PR head 仍为 3d738fb3gh pr diff 386 --name-only 返回 0 个文件;PR body 也仍写“空 PR-B / 不包含实现”。但当前共享工作树已经有 16 个 modified 文件和多个 untracked 实现(.github/workflows/test.ymltools/check_diagram_rendering.pytools/fetch_diagram_reference.py、corpus/reference lock 等)。当前 PR 的 checks 也没有 Diagram assets (Python ...) job,只有旧的 template/code-test gate。因此 GitHub 上的 PR 不能重放当前本地实现,不能据此审查或合并。

修复要求:将完整 tracked + untracked PR-B 文件提交并推送到 PR head;同步 PR-B body 的当前状态、HEAD、reference lock/Gist 和验收结果。否则 umbrella 仍只看到空占位 PR。

I1:custom reference provenance 未锁定全部 renderer 输入

tools/check_diagram_rendering.py_CUSTOM_REFERENCE_FILES 只固定校验 asset-lock.jsonresvg-binding.jsresvg.wasm 三个 hash;reference 中的 renderer.jsresvg-bridge.js hash 没有与已知 custom 0.37 基线比对。实测把已下载 reference.json 的 renderer hash 改成全零、重算 sidecar 后,compare_reference() 仍接受。计划要求 renderer/bridge/asset-lock provenance 不匹配必须 fail closed;请补齐这些文件的 custom hash 或验证完整 provenance 集合。

I2:active-context memory 证据仍是静态值

tools/diagram_assets/resvg-bridge.js:84-88activeContext 写死为 1,所以 check_diagram_rendering.py 读取的 context ownership 指标无法发现实际残留 context。RSS/heap/font 数据应继续保留,但 active-context 必须由 Python context lifecycle/token 或独立 subprocess 统计产生,不能用常量满足门禁。

M1:伞 PR current-state 文案仍有旧 HEAD

PR #383 正文“截至 2026-07-19”仍引用 umbrella 490565a5,而当前 umbrella/PR-B base 已是 bcadb92a。历史 hash 可以保留,但应追加 current-state comment,避免 reviewers 将旧基线当作当前事实。

已复核通过

  • 3.11 custom reference 下载/锁 hash、official 35-layout/306-arrow parity + CJK + memory compare:exit 0;custom/official memory baseline 均有 switchSlopeWindow=25
  • 3.7、3.8 reference archive 下载成功,manifest 标识 custom-resvg-0.37、35 cases、memory baseline。
  • python -m pytest -q test/diagram/test_assets.py:57 passed。
  • python tools/check_diagram_assets.py、Ruff check/format、git diff --check:通过。

当前建议保持 PR-B blocked,直到实现进入远端 PR、Diagram assets required job 实际运行并回读成功,且 provenance/context 证据补齐。

- switch the Python asset builder to the official @resvg/resvg-wasm 2.6.2 package\n- enforce the closed SVG, runtime floor, CJK, parity, provenance, and package gates\n- add corpus and CI coverage for deterministic cross-runtime rendering
@HansBug HansBug added the kind: maintenance Repository health, cleanup, testing, or engineering maintenance. label Jul 19, 2026
@HansBug HansBug changed the title 实现:官方 resvg 资产收敛与 closed SVG 合同 PR-B feat(diagram): close official resvg asset contract Jul 19, 2026
@HansBug HansBug added area: visualization PlantUML, ELK, diagrams, and rendered images. area: packaging Packages, bootstrap/self-check, frozen artifacts, resources, and publishing. area: tooling Repository-local tools, linting, and maintenance utilities. role: sub-pr Implements a bounded slice on an umbrella or stacked branch. labels Jul 19, 2026
@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

claude reviewer 强对抗审查(仅本PR代码/文档,黑白盒结合)

  • 目标 head: 9418c9ec on dev/python-diagram-assets-official
  • base: dev/python-diagram-umbrella@bcadb92a
  • 独立执行:白盒读全部 diff、本地跑 pytest test/diagram/test_assets.py -q(57 passed)、构造 SVG allow-list 边界例复现、验证 corpus 35/306 与方向计数、验证 CI matrix/template-suite-gate 接线、验证锁文件与 npm package-lock 一致、验证 pyfcstm/diagram/assets/.gitignore 白名单与产物入口对齐。
  • 未启动子智能体,未修改生产代码。

C(Critical, 阻塞 ready-to-merge)

C = 0。未复现出会破坏 official resvg 资产合同、closed SVG 合同、A→B/marker 终点合同、PNG 结构、CJK/字体所有权、runtime floor 或 CI 门禁的问题;测试 test_assets.py::* 57 项在本地干净 checkout 全过,corpus/方向计数与 PR body 声明完全一致(canonical 15/130、shared 20/176、合计 35/306 及 right=107,down=153,left=18,up=28 经 JSON 校验通过)。

I(Important, 应修但可讨论)

I = 0。以下位置我逐一构造攻击例并复现,均已被更早的分支拒绝:

  • <script>/foreignObject/style/image/onload/use/href/xlink:href/style → 元素或属性 allow-list 命中,拒绝正确;
  • url(#missing)url(#)url(mystyle) → 本地引用/空引用/非本地引用逐一分档命中;
  • 双 marker、缺 refXrefX="10.0"(字面比较,不做数值归一) → marker 合同强制单实例、orient="auto"refX="10" 严格通过字符串比较拒绝(与 PR body 明示的 marker-end + orient="auto" + refX="10" 契约一致);
  • render_png/expand_svg 对 raw SVG/str 全部走 _canonical_inputValueError,不接受 raw 输入;
  • reference 门禁 _assert_custom_reference_provenance 硬编码 5 个 custom 侧文件哈希且 --capture-reference 与 official backend 名称不同,防止用 official 侧结果冒充 custom 基线;
  • _validate_reference_files 强制 sidecar、SHA256SUMS、路径遏制在 path.parent、size/hash 双校验、expected_ledger == ledger 严格集合相等,避免加料;
  • CI diagram_assets 三个 runtime job 均进入 template-suite-gate needs 且加入 needs 汇总代码路径;pin Python 3.8 runtime floor 明确 pip install --force-reinstall --no-deps 'mini-racer==0.7.0' 与 requirements 的 >=0.7.0 合力使 3.8 严格上 0.7.0,与 reference gist 对齐。

M(Minor, 不阻塞)

  1. pyfcstm/diagram/engine.py:431-432 marker-start 无法到达。 _SVG_INPUT_ATTRIBUTES 里没有任何元素允许 marker-start 属性,所以在 line 381-384 就会先以 "closed SVG dialect rejected attribute marker-start on <path>" 抛错;实证:
    _validate_canonical_svg('<svg xmlns="http://www.w3.org/2000/svg"><defs><marker id="m" orient="auto" refX="10"/></defs><path d="M0 0" marker-start="url(#m)"/></svg>')
    → closed SVG dialect rejected attribute marker-start on <path>
    
    建议要么删掉 line 431-432 的死枝,要么把 marker-start 加进某个 spec 白名单,以便这条更晚的检查真正在 spec 层拒绝多 marker 组合。
  2. pyfcstm/diagram/engine.py:1030 冗余 wasm 读盘。 命中"_resvg_ready 且 locale 匹配"的常见短路路径时,wasm_bytes = _asset_bytes("resvg.wasm") 与 base64 编码仍旧执行,只是被 if not self._resvg_ready: 跳过;虽然 2.4MB 每次 render 都读一次并不致命,但对 memory gate 的稳态 100 次 steady 存在无意义 IO/GC 压力,建议延迟到 if not self._resvg_ready or ... 分支内部再读。
  3. _render_failure(..., request_error=True) 的建议措辞误导。 现在无论 renderer 内部 bug 还是用户 DiagramData 缺字段,都统一提示 "check the DiagramData shape and renderer options";在 _poll / _eval_asset 由于 renderer.js 本身逻辑错误抛的场合,这条建议会把责任推给用户。建议对由 _poll 走过来且 error.status == "error" 且非 JSEval 的路径改成 "report at issue tracker with the request" 之类。
  4. PR body 与 requirements-viz.txt 略有出入。 body 写 "Python 3.7 使用 py-mini-racer==0.6.0",requirements 实际是 py-mini-racer>=0.6.0,<0.7;当前 PyPI 仅存在 0.6.0,所以行为一致,但 body 应改为 "py-mini-racer>=0.6.0,<0.7"以避免误读为固定 pin。
  5. _svg_parse 里的 pre-filter 使用 any(token in svg for token in (...)) 是纯子串扫描。 目前不会造成误伤(合法 SVG 里 <!-- 等只可能出现在 comment/DTD/PI 中,而 fromstring 也会拒绝),但注释里没有说明 "为什么全字符串扫描比 ET 拦截更安全",未来重构可能被误删。建议加一行 comment 说明这是抵御定制化解析器 bypass 的第二道防线。
  6. _bridge_metricsactiveContext == 1 and registeredFonts == 5 硬编码 5。 一旦未来把 Latin fonts 从 3 换成 2/4,或某 locale 只带一个 Bold,这里会静默失败。建议由 _LATIN_FONT_ASSET_PATHS + _CJK_FONT_ASSET_PATHS[locale] 派生 expected 数量,让引擎与 gate 同源。
  7. tools/check_diagram_provenance.py 未接入 diagram_assets_verify Makefile 里 diagram_assets_verify 包含 diagram_provenance_check,而 CI job 也确实调 make DIAGRAM_REFERENCE=... diagram_assets_verify;但 provenance 涉及外网访问,_download 用 60s 超时,ubuntu-22.04 上一次到 codeload/registry.npmjs.org 失败就会让整个 diagram_assets matrix 红。虽然目前 body 声明"必须能从 immutable URL 重新读取",不建议放弃 gate,但可以考虑给 provenance 加一个明确的 retry(如 3 次)或让 CI 单开一个可容忍失败的 job,以免上游偶发 5xx 阻断 ready-to-merge。

最终结论(verdict)

C = 0, I = 0, M = 7。就本 PR 已提交的 diff、tests、Makefile、CI 与 body 声明相互对齐这一断面看,PR-B 的 official resvg 资产合同、closed SVG 输入/输出合同、marker/refX 契约、PNG 结构、五地区 CJK、Python 3.7/3.8/3.11 runtime 分档与 template-suite-gate 已经达到 ready-to-merge 阈值;上表 7 条 M 都是"能改一下更稳"的小修补,不阻塞合入伞分支。

以上审查仅覆盖代码/资产/测试/CI/文档一致性;不包含对未来 PR-C / PR-D 或 upstream main 冲突的推测,只在当前 base dev/python-diagram-umbrella@bcadb92a 与 head 9418c9ec 上有效。

— by claude reviewer(独立强对抗,未启动子智能体)

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer:C=0,I=1,M=4,REQUEST CHANGES

这是对 PR #386 head 9418c9ec 相对 base bcadb92 的独立实现审查;已回读 PR #386、伞 PR #383、PR-A #384、issue #89、PR #377/issue #376,并检查当前工作树。工作树除用户已有的未跟踪 .agents/ 外干净,未修改生产代码、未启动子智能体。

I1:render_png 接受无 ink 的 PNG,未兑现“非空 ink bbox”合同

位置:pyfcstm/diagram/engine.py:1080L1128_canonical_input 明确允许内部 _CanonicalSvg,但 render_png 只调用 _valid_png;后者检查签名、IHDR、CRC、IDAT、RGBA8 和 filter,却不检查可见 ink。非空 ink 只在维护脚本 tools/check_diagram_rendering.py:466 / L950 外部检查,因此引擎本身可以成功返回空白图。

复现命令和输入:

python - <<'PY'
from pyfcstm.diagram import DiagramAssetEngine
from pyfcstm.diagram.engine import _CanonicalSvg, _valid_png
from tools.check_diagram_rendering import _png_ink_bbox

svg = _CanonicalSvg(
    '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'
)
png = DiagramAssetEngine().render_png(svg)
print("png_bytes=", len(png), "valid_structure=", _valid_png(png))
try:
    print("ink_bbox=", _png_ink_bbox(png))
except Exception as err:
    print("ink_bbox_error=", type(err).__name__, str(err))
PY

实际输出:

png_bytes= 68 valid_structure= True
ink_bbox_error= ValueError PNG output contains no visible ink

这直接违反 PR body 的“验证完整 PNG 结构、RGBA 数据、非空 ink bbox”,也违反当前 gate 对同一输出的定义。建议把非空 ink 检查下沉到 DiagramAssetEngine.render_png(或共享一个不依赖维护脚本的 PNG 解码器),并增加空白 canonical 输入的回归测试;否则应明确删除该公共/内部合同,不能只靠外部门禁补救。

M1:所谓 35/306 “真实 corpus”包含删边和伪造边,source hash 仍声称来自原 fixture

位置:tools/diagram_assets/generate_corpus.js:192-210

复现读取结果:

canonical 01-simple-leaf arrows=4
shared-lr-01 sourceFixture=01-simple-leaf sourceSha256=4b7c... arrows=3
shared-tb-01 sourceFixture=01-simple-leaf sourceSha256=4b7c... arrows=3
shared-lr-09 sourceFixture=09-traffic-light-stubs sourceSha256=a54b... extra=['shared-lr-traffic-light-extra'] arrows=9
shared-tb-09 sourceFixture=09-traffic-light-stubs sourceSha256=a54b... extra=['shared-tb-traffic-light-extra'] arrows=9

实现为保持 176 计数从 simple-leaf 删除一个 initial transition,并把 traffic-light 的 diagnostic self-loop 删除后复制一个 InService -> Idle transition,改写 id/range;但 corpus metadata 仍标记同一个真实 source fixture/hash。这样 gate 覆盖的是“改造后的序列化请求”,不是声明中的真实 fixture corpus,306-arrow 统计也不能作为真实 source coverage 证据。建议要么保留原始 fixture 数据并接受真实计数变化,要么把 synthetic cases 单独放入有明确 provenance 的 corpus,不能混入“real visual fixture”总数。

M2:pydoc/reST 声明的异常类型与真实最小复现不一致

位置:pyfcstm/diagram/engine.py:945-946L955-L965

render_svg 文档写 request 非 JSON-compatible 时抛 ValueError,但序列化发生在进入 _eval_asset 前:

python - <<'PY'
from pyfcstm.diagram import DiagramAssetEngine
try:
    DiagramAssetEngine(timeout=1).render_svg({"diagram": set()})
except BaseException as err:
    print(type(err).__name__, str(err))
PY

实际输出:

TypeError Object of type set is not JSON serializable

建议在请求序列化边界捕获明确的 TypeError 并转成文档约定的 ValueError/DiagramRenderError,同时补测试;当前错误虽 fail-loud,但调用方无法按 pydoc 合同稳定处理。

M3:CI 条件允许用全局 skip token 绕过本 PR 专项门禁

位置:.github/workflows/test.yml:386L568

diagram_assets 只在 run_expensive_code_test == true 且没有 ci skiptest skip[python skip] 时运行;gate 在同样的 Python skip 分支中接受 skipped job。PR body 又明确写“任何 parity 差异、arrow endpoint、CJK、source/license、package 漏资源或 CI gate 缺失都会阻止 ready-to-merge”。仓库的全局 skip policy 可以是有意设计,但它与 PR-B 自己的 hard gate 合同冲突,且该 PR 修改的正是 Python diagram 资产。建议对 diagram-owned paths 强制执行三个 runtime + provenance/parity,或在 PR body 明确声明 skip token 优先级并禁止用于 PR-B。

M4:远端状态和 PR/伞 PR body 已过时,不能按当前描述判定 ready

证据命令:

gh pr view 386 --repo HansBug/pyfcstm --json state,mergeStateStatus,headRefOid,baseRefOid
# OPEN, UNSTABLE, base=bcadb92..., head=9418c9ec...

git log --oneline bcadb92..9418c9ec
# 9418c9ec fix(ci): align Python 3.8 diagram memory runtime
# bddc6e89 fix(diagram): run rendering gate from a clean checkout
# 241299ba feat(diagram): close official resvg asset contract
# 3d738fb3 chore(diagram): open official asset closure PR-B

gh pr view 383 --repo HansBug/pyfcstm --json headRefOid,baseRefOid
# head=bcadb92..., base=901f30e...

PR #386 “当前状态”仍写“落地到提交 241299ba”,漏掉 head 上的两个后续修复;伞 PR #383 body 仍描述旧的 umbrella HEAD 490565a5,实际 PR head 是 bcadb92。当前 gh pr checks 386 也显示三个 Diagram assets job 已通过,但仍有多路 Code test/fresh CLI pending,PR merge state 为 UNSTABLE。建议同步 body 的 current-state、commit、umbrella 指针和 required-check 快照,完成全部 required checks 后再声称 ready-to-merge。

验证记录

  • pytest --collect-only -q test/diagram/test_assets.py:57 tests collected;实际 pytest -q test/diagram/test_assets.py 通过。
  • python tools/check_diagram_assets.py --check:passed。
  • python tools/check_diagram_rendering.py --check:provenance self-check passed。
  • python tools/check_diagram_provenance.py --skip-tarball:两个 source archive hash passed。
  • python tools/check_diagram_engine_floor.py --formats svg,png,expanded-svg --check-timeout-reset:本机 Python 3.10 / mini-racer 0.14.1 passed。
  • 从锁定 gist 恢复 3.11 reference 成功;本机是 Python 3.10,故用 3.11 reference 执行 parity 时按实现预期 fail-closed:ValueError: reference and current runtime versions differ。不能把该本地不匹配当作 parity 通过;远端回读的 Diagram assets 3.7/3.8/3.11 jobs 当前为 pass,但整个 PR 仍不是 ready(UNSTABLE/pending)。

最终 verdict:REQUEST CHANGES。C=0,I=1,M=4;I1 修复并重新验证 PNG non-empty contract 后,才可重新评估。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

deepseek reviewer — PR #386 实现型状态审查

审查范围:PR #386 head 9418c9ec、base bcadb92,实际 diff、当前工作树、伞 PR #383、PR-A #384、issue #89、PR #377/issue #376;未修改生产代码,未启动子智能体。远端回读时 Diagram assets (3.7/3.8/3.11) 已通过,但 Code Test 仍在运行,PR 状态为 MERGEABLE/UNSTABLE,不能把 body 的本地“全部通过”当作最终 required-check 证据。

C(Critical,阻塞合入)= 1

C1. 伞 PR/issue 的 current-state 与实现型 PR 及实际仓库互相矛盾

PR #383 顶部 current status 仍写 PR-B Asset Closure | 待实施,并写“当前 production candidate backend 仍是 custom resvg 0.37;official 2.6.2 尚未落地”;其 head 是当前 PR 的 base bcadb92。issue #89 顶部仍写 PR-A 后续 continuation、当前 umbrella asset continuation HEAD 490565a5,并把当前实现叙述为 custom backend foundation。

但 PR #386 body 声明 official 2.6.2、closed SVG、package、CI 已落地;实际 diff 也确实包含 tools/build_diagram_assets.pypyfcstm/diagram/engine.py、provenance/parity/CI 和 35/306 corpus。这直接违反 PR #386 自己的 ready-to-merge 条件 4:“PR #383、PR #384、issue #89 和 Gist 的 current-state 信息与本 PR 一致”。旧历史段可以保留,但顶部 current-state 必须显式标记 superseded,并同步当前 head、official backend、reference lock 和 PR-B closed evidence。

修复:更新 PR #383 与 issue #89 的顶部 current-state/comment,明确 bcadb92 umbrella 与 9418c9ec PR-B 的关系;再以远端最终 checks 通过后的链接作为 current evidence。

I(Important,必须在 ready 前处理)= 3

I1. “完整 PNG 结构 + 非空 ink bbox”没有在 engine runtime enforce

位置:pyfcstm/diagram/engine.py:615-680:1120-1132。实际复现 1:构造 1x1 RGBA PNG,插入重复 IHDR 后执行 _valid_png,命令结果为 bytes 95 duplicate-IHDR accepted=True。实际复现 2:输入 <svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>,真实 resvg call 返回 png 1 1 valid=True decoded scanline=01ffffffff nonwhite-ink=False

当前 _valid_png 只校验 magic/IHDR/CRC/IDAT/zlib/filter/scanline 长度,未拒绝重复 IHDR、未严格检查 chunk ordering,也未检查非空 ink;render_png:1128 只调用它。维护 checker 的 _png_ink_bbox 不能替代 engine 对每次 runtime 输出的保护。

修复:实现 exactly-one IHDR/IEND、合法 IDAT ordering/contiguity 和 chunk state machine;在 decode 后执行非空 ink bbox,失败抛 DiagramRenderError,并补对应 runtime regression tests。

I2. scale/尺寸上限和错误分类未闭合

位置:pyfcstm/diagram/engine.py:1091-1119_eval_asset:886-907。上游 umbrella #383 已冻结 0 < scale <= 4 以及 scaled width/height、pixel、RGBA、encoded PNG 上限;当前实现只检查 finite positive,实际允许 scale=5.0。输入为 test/diagram/test_assets.py:_request(),实际输出:scale 5.0 accepted bytes 56621

同一输入 scale=1000.0 进入 WASM 后实际报 DiagramAssetError,消息为 recursive use of an object detected which would lead to unsafe aliasing in rust。这是已初始化 resvg 后的 request/output failure,却因为 render_png 调用 _eval_asset(..., request=False) 被归入 asset failure;没有在进入 WASM 前做 checked multiplication。

修复:在 WASM 前统一解析 finite viewBox、checked scale/dimension/pixel/encoded limits;超限使用计划中的 limit error,request/render failure 使用 DiagramRenderError,并测试 4.0/4.0001/huge/overflow 与错误 subtype/message。

I3. active-context memory 指标是可自报的静态值,不能证明 native ownership

位置:pyfcstm/diagram/engine.py:879-883tools/diagram_assets/resvg-bridge.js:84-88tools/check_diagram_rendering.py:1003-1046。bridge 的 activeContext 直接返回 globalThis.__pyfcstm_active_context_count,而 Python 在加载 bundle 时固定写入 1;checker 再把这个值与同一可变 Python 计数比较。

白盒复现:执行 e._eval("globalThis.__pyfcstm_active_context_count = 7;"); e._active_context_count = 7; print(_bridge_metrics(e)),实际输出:{'activeContext': 7, 'registeredFonts': 0}。这只能证明两个 bookkeeping 字段彼此相等,不能发现 MiniRacer/native context 泄漏或 close() 未释放。RSS slope 仍有价值,但不能把静态 counter 描述成 ownership proof。

修复:使用独立 context token/lifecycle registry 或 subprocess/native observable 计数;至少把 Python 创建/关闭事件与 bridge token 绑定,不能由 JS global 常量自报并与同一 Python 字段互相校验。

M(Minor,不单独阻塞)= 2

M1. floor timeout negative path 静默吞掉预期异常

tools/check_diagram_engine_floor.py:184-196 对故意的 DiagramAssetError 直接 pass。仓库 CLAUDE.md 明确禁止无可观察记录的 silent swallow。至少打印“预期 timeout 已触发、context 已 reset”,否则日志无法区分测试真的触发 timeout 还是逻辑未执行。

M2. Python 3.8+ 的 mini-racer>=0.7.0 无上限

requirements-viz.txtrequirements-test.txt 使用开放上界;CI 只在 Diagram job 临时强制 mini-racer==0.7.0。当前 0.7/0.14 floor 已实测通过,但未来发行版 API 变化会使 clean checkout 的 renderer 结果不可重放。应锁定经过 runtime floor 的上界,或增加显式兼容矩阵。

正面验证与边界

以下实际命令通过:make build_assetsmake diagram_assets_checkmake diagram_rendering_check(15/130 + 20/176)、make diagram_engine_floormake diagram_provenance_checkpytest test/diagram/test_assets.py -q(57 passed)、make package(wheel + sdist checker)、wheel/sdist 临时安装后的 SVG/PNG/expanded-SVG smoke、Python 3.7 smoke、git diff --check。因此本评论不是“测试失败即否定”,而是针对门禁未覆盖的真实结构/语义缺口及远端状态一致性。

结论:REQUEST CHANGES / NOT READY。C=1, I=3, M=2。

- reject malformed, duplicate, oversized, and blank PNG payloads at the engine boundary
- preflight scale and canvas limits before entering resvg WASM
- bind memory metrics to a Python context lifecycle token
- document and regress the new limit and request validation behavior

Tests: pytest test/diagram/test_assets.py -q
Tests: SKIP_SLOW_TESTS=1 make unittest
Tests: make DIAGRAM_REFERENCE=/tmp/pyfcstm-custom-reference-final/reference.json diagram_assets_verify
Tests: make package
Tests: make test_boundary_check
@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

PR-B current-state update: 98fd215

本轮三路强对抗 review 的有效 C/I 已处理并重新验证:

  • engine 运行时现在拒绝结构合法但无可见 ink 的 PNG,并执行 duplicate IHDR/IEND/IDAT、CRC、scanline 和 RGBA size 检查;
  • scale/画布/像素/RGBA/encoded PNG 上限在进入 WASM 前 fail closed,新增 DiagramRenderLimitError;
  • MiniRacer memory metric 改为 Python context token 与 bridge 闭包握手;
  • 非 JSON-compatible request 在序列化边界给出稳定 ValueError。

证据:

新 head 的完整 CI 正在运行;尚未宣称 ready-to-merge,也不执行 merge。

@HansBug

HansBug commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer\n\n独立复核对象:PR #386 当前 head 62a4bf5,base bcadb92;覆盖 PR-B 实现、伞 PR #383、PR-A #384、PR #377、issue #89/#376,以及 SVG/PNG 边界、箭头/CJK、MiniRacer、来源/许可证、打包和 CI。\n\n### 结论\n\nC=0 / I=0 / M=4。 未发现会阻止合并的正确性、安全性或范围一致性缺陷。官方 resvg 资产、request-only 输入、closed SVG allow-list、PNG 结构/尺寸/编码限制、timeout/context 生命周期、双 MiniRacer floor、CJK、reference provenance、package 和 CI 接线均有实现与门禁证据。\n\n### C / I\n\n- C=0。 没有致命矛盾;PR-B 仍只交付内部资产运行时,不提前宣称公共 Diagram API、CLI、HTML/Notebook 或 PDF facade 已完成。\n- I=0。 4caed29/62a4bf5d 已补齐 lockfile esbuild(npm ci --include=dev、npx --no-install)、截断 HTTP body(IncompleteRead)重试和 reference tar link 拒绝;没有发现新的阻断级回归。\n\n### M\n\n1. 伞 PR #383 current-state hash 过期。 当前正文仍把 PR-B head 写为 98fd215,实际 #386/分支 head 为 62a4bf5。保留历史 hash 没问题,但应补一条 current-state 同步,避免审查者误读基线。\n2. esbuild 已安装树的快路径缺少字节回读。 validate_esbuild_provenance() 校验 package-lock 的 version/resolved/integrity,但 ensure_js_dependencies() 在已有 node_modules/esbuild 目录时不重新核验已安装文件;CI fresh npm ci 已覆盖主要路径,建议后续与 ELK tree SHA 同等级加固。\n3. reference Gist 下载没有瞬态重试。 tools/fetch_diagram_reference.py::_download 只有一次 120 秒请求;该外部 fetch 是 required CI 的前置步骤,短暂 5xx/网络中断会直接失败。可复用资产下载的有界重试策略。\n4. expanded SVG 失败后的 context 复位不对称。 expand_svg() 对 malformed/closed-dialect 输出映射为 DiagramRenderError,但没有像 render_svg() 那样 discard context;建议补充复位或明确保持上下文的理由。\n\n### 上游/范围一致性\n\n- PR-A #384 已合入 umbrella(merge 8a6eba7);PR #377 已合入 main(05c4156);issue #89 仍 OPEN,issue #376 已 CLOSED。\n- #383 仍 OPEN 且明确 PR-C/D 尚待实施;#386 body 的 official 2.6.2、source/license、双 runtime、reference 和 package 边界与实现一致。\n\n### 可复现证据\n\n本地(HEAD=62a4bf5d):\n\n python -m pytest -q test/diagram/test_assets.py\n 68 passed\n python tools/build_diagram_assets.py --check\n ... deterministic and safety self-check passed\n python tools/check_diagram_rendering.py --check\n ... provenance and parity self-check passed\n ruff check ...\n All checks passed!\n git diff --check origin/dev/python-diagram-umbrella..HEAD\n ... exit 0\n make PYTHON=/home/zhangshaoang/.pyenv/versions/3.11.1/bin/python \n DIAGRAM_REFERENCE=/tmp/pr386-final-reference/reference.json diagram_assets_verify\n 35 layouts / 306 arrows; CJK, expanded parity (clip-ID-only), memory, runtime floor and provenance passed\n python tools/check_diagram_provenance.py\n npm tarball/source archive SHA-256 values matched the lock\n\n远端新 head 的 Diagram assets (Python 3.7/3.8/3.11) 与 Code Test/Docs/Package jobs 在复核时仍处于运行队列,尚未观察到失败;因此本评论不把未完成的 required CI 宣称为已通过。\n\n审查完成;不执行 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Review:PR-B #386 DeepSeek 最终强对抗白盒审查

Reviewer: DeepSeek reviewer(三路独立审查之第三路)

审查对象: PR #386 commit 62a4bf5d739fca4932fb691815ba530c48fd3fc6 (head),base dev/python-diagram-umbrella

审查方法: 独立读取 PR body、diff、伞 PR #383、PR-A #384、issue #89/#376、已有 review;运行 68 个 diagram 测试全部通过 (pytest test/diagram/test_assets.py -q: 68 passed)、check_diagram_provenance.py 通过、check_diagram_engine_floor.py --all-cases --check-timeout-reset 通过(15 cases, SVG/PNG/expanded-SVG 均通过)、check_diagram_rendering.py --check self-check 通过;构造并验证了原始 SVG 拒绝、<script>/<use>/onload/href/xlink:href/style/javascript: 均被正确拒绝。


C=0(无阻塞性问题)

经白盒逐行审计以下安全边界,确认为正确实现:

边界 验证方法 结论
PNG CRC 校验 + IHDR/IDAT/IEND 合同 _decode_png_rgba L770-890:每个 chunk 校验 CRC;拒绝非允许 chunk;IHDR 必须在首位;IEND 后拒绝尾随字节
_MAX_RENDER_PNG_BASE64_BYTES padding 窗口 手动验证 %3==0/1/2 三种场景;test_render_png_rejects_unpadded_encoded_limit_boundary
SVG 元素/属性 allowlist _validate_svg_tree L~430-500:输入 19 元素 + 带属性级白名单;输出 13 元素
on*/href/xlink:href/style 事件拦截 L~476-482:name.lower().startswith("on") 及硬编码拒绝列表
_DANGEROUS_URI_SCHEME_RE L335-338:javascript|vbscript|data|blob|about|file|https?|ftp|ws|wss|chrome|chrome-extension
_URI_REFERENCE_RE + 本地引用验证 L339-340 + L~487-493:所有 url(...) 引用必须指向 # 开头的已声明 ID
Reference tar symlink/hardlink 拒绝 fetch_diagram_reference.py L43-46:member.issym() / member.islnk() 提取前拦截
Reference tar path traversal L52-56:root != candidate and root not in candidate.parents;已验证 ...、绝对路径三种情况均正确拦截
下载重试 transient HTTP + IncompleteRead build_diagram_assets.py L128-160:408/429/500/502/503/504 重试 + IncompleteRead/URLError/TimeoutError/OSError 重试,指数退避
esbuild 本地锁定 provenance build_diagram_assets.py validate_esbuild_provenance:从 jsfcstm/package-lock.json 读取精确版本,npx --no-install esbuild 仅使用本地安装
MiniRacer 双重安装拒绝 _create_context L~1050:DiagramEngineConflictError
CJK locale 切换 context 重建 _ensure_resvg L~1310:self._resvg_locale != locale_discard_context()
raw SVG 入口拒绝 实测 engine.render_png('<svg...')ValueError
broad except Exception 合规 _eval L~1115:仅 isinstance(err, self._interrupt_errors) 分支捕获;其他 propagate
head.checkSumAdjustment 零化后校验 _valid_opentype L~808:table 数据 zero fill positions 8-12 后再校验 table checksum

I=2(重要问题,建议修复)

I-1: _valid_png 是生产死代码

文件: pyfcstm/diagram/engine.py line 909

_valid_png() 定义了完整的 PNG 验证逻辑(调用 _decode_png_rgba + 异常转 False),但生产路径 render_png() 直接调用 _decode_png_rgba 并自己处理异常(转为 _render_failure),从不调用 _valid_png。grep 确认:_valid_png 仅被 test/diagram/test_assets.py 中的 6 个测试引用,生产代码路径无调用。

影响: 死代码不会导致 bug,但会在未来维护中产生歧义——新接手的人可能错误地使用 _valid_png 返回的布尔值而丢失 DiagramRenderLimitError 的区分语义(render_png 需要区分 limit error 和普通 render error)。

建议: 两个方案任选其一:

  • 方案 A(推荐): 删除 _valid_png,将测试改为直接调用 _decode_png_rgba 并验证异常类型
  • 方案 B: 保留 _valid_png 但将其标记为 @deprecated 并注明 "tests only; production path uses _decode_png_rgba directly"

验证命令:

grep -rn '_valid_png' pyfcstm/diagram/engine.py
# 输出:909:def _valid_png(data: bytes) -> bool:
# 仅此一处定义,无调用点

I-2: _png_ink_bbox 的 PNG 验证弱于 _decode_png_rgba

文件: tools/check_diagram_rendering.py lines 318-399

_png_ink_bbox(parity 对比工具中的 PNG 解析器)与 _decode_png_rgba(engine.py 中的生产 PNG 解析器)存在以下几处验证差距:

验证项 _decode_png_rgba (production) _png_ink_bbox (parity tool)
IHDR 必须是第一个 chunk ✓ (L797: if position != 8) ✗ 不检查位置
拒绝 IHDR 重复出现 ✓ (L797: if saw_ihdr) ✗ 不检查重复
IDAT 必须连续 ✓ (L819: if idat_closed) ✗ 接受 IDAT 在非 IDAT chunk 后
拒绝非允许 chunk ✓ (L785: chunk_type not in _PNG_ALLOWED_CHUNKS) ✗ 静默跳过未知 chunk
拒绝 IEND 后尾随数据 ✓ (L828: position != len(data)) break 后不检查剩余字节
CRC 校验 ✓ (L790-793) ✓ (L338-339)

影响: 当前场景下无实际风险(parity 工具只处理 engine 自己生成的、CRC 已校验的 PNG),但两个解析器的实现差距构成维护风险——如果有人将 _png_ink_bbox 复制到生产路径或安全边界,将失去多层防御。

建议:

  • _png_ink_bbox 函数 docstring 中明确标注:"This parser processes engine-generated PNG output that was already CRC-validated by _decode_png_rgba; it intentionally skips chunk ordering and allow-list checks."
  • 或:添加缺失的 LHDR-first/IDAT-contiguity/IEND-final 检查(约 15 行代码)

验证命令:

python -c "
# 构造一个 IHDR 不在首位的 PNG — engine 会拒绝但 parity 工具不会
# 此命令不出现在评论中,仅说明审查逻辑
" 

M=4(轻微建议,不阻塞)

M-1: Diagram CI gate 可被 skip token 绕过

文件: .github/workflows/test.yml line 387

diagram_assets job 的触发条件与 unittest 共享相同的 skip 逻辑:ci skiptest skip[python skip] 均跳过此 job。在 template_suite_gate 中,当 skip_python_side 为 true 时,diagram_assets 结果完全不被检查 (line 568: if run_expensive_code_test and diagram_assets != 'success'skip_python_side 短路)。

这意味着:如果有人在 commit message 中使用 [python skip],diagram 资产安全门禁将被完全绕过,而 branch protection 依赖的 template-suite-gate 仍然通过。

影响: 虽然与现有 CI skip pattern 一致,但 diagram assets 涉及 WASM 执行、SVG 解析、PNG 解码等安全敏感操作,绕过门禁的风险高于普通 Python 测试。

建议:

  • template_suite_gate 中增加独立检查:即使 skip_python_side 为 true,如果 DIAGRAM_ASSETS_RESULT 存在且非 success/skipped,仍报告失败;或
  • 至少在此 PR body 或 CI workflow 中添加注释,说明 diagram assets gate 与现有 skip 行为一致但未来可能需要独立加固

验证命令:

# 检查 template_suite_gate 中的 diagram_assets 检查是否受 skip_python_side 保护
git show 62a4bf5d:.github/workflows/test.yml | grep -A5 'diagram_assets != .success'

M-2: _DANGEROUS_URI_SCHEME_REhttps? 缺少说明注释

文件: pyfcstm/diagram/engine.py line 335

_DANGEROUS_URI_SCHEME_RE = re.compile(
    r"(?:javascript|vbscript|data|blob|about|file|https?|ftp|ws|wss|"
    r"chrome|chrome-extension):",
    re.IGNORECASE,
)

https? 匹配所有 HTTP/HTTPS URL,但当前被属性 allowlist 间接保护(xmlns 不在 allowlist 中,其他属性值含 http:// 也会命中此 regex 被拒绝)。当前实现正确,但 regex 视觉上会误伤合法 SVG 属性如 xmlns="http://www.w3.org/2000/svg",让人疑惑为什么 SVG 还能工作。

建议: 在 regex 定义上方添加注释:

# https? is safe here because the xmlns attribute (the only SVG attribute
# that embeds http://www.w3.org/2000/svg) is excluded from every per-element
# attribute allowlist. This scheme check guards against future allowlist
# additions that could accidentally admit executable or exfiltrating URIs.

M-3: 字体路径在 engine.py 与 check_diagram_assets.py 中重复维护

文件:

  • pyfcstm/diagram/engine.py lines 114-129(_LATIN_FONT_ASSET_PATHS_CJK_FONT_ASSET_PATHS
  • tools/check_diagram_assets.py lines 33-54(EXPECTED_FONT_PATHS

两组 13 个字体路径完全重复。如果新增字体(如未来加入 NotoSansMonoCJK),需同步修改两处,容易遗漏。

建议: 从 asset-lock.jsonfonts.faces[].path 动态推导,或抽取 pyfcstm/diagram/_font_paths.py 共享常量。


M-4: _valid_opentype 全字体 checksum 计算缺少行内注释

文件: pyfcstm/diagram/engine.py lines 796-808

total_checksum = (
    sum(word[0] for word in struct.iter_unpack(">I", data)) & 0xFFFFFFFF
)
return total_checksum == 0xB1B0AFBA

此处的 total_checksum 使用 data 的完整字节(包括 head.checkSumAdjustment 字段的原始值,因为在 table-level loop 中 zero-fill 的是 table_data 的副本)。OpenType 规范要求全字体校验和 == 0xB1B0AFBA 的前提正是 checkSumAdjustment 字段已被字体构建工具设为能令总和为此魔数的值。

当前实现正确,但注释仅说明 "Table-level checksums alone cannot detect a damaged adjustment field",未解释为什么此处可以直接使用原始 data 而不需要手动 zero-fill head.checkSumAdjustment

建议: 在 line 807 前添加注释:

# The font builder sets head.checkSumAdjustment so that the total
# checksum (including the adjustment itself) equals this magic value.
# Table-level zero-fill (above) was for per-table verification only.

未发现问题(已验证安全)

以下区域经过白盒逐行审计,确认为正确实现:

区域 验证方法
base64 padding 边界攻击(_MAX_RENDER_PNG_BASE64_BYTES 手动验证 %3==0/1/2 三种场景 + test 覆盖
PNG chunk CRC + IHDR/IDAT/IEND 合同 代码审查 + test_render_png_rejects_* × 6
closed SVG input/output element/attribute allowlist 白盒审计 + 构造 <script>/<use>/onload/href/style/javascript: 均被拒绝
Reference tar symlink/hardlink/path traversal 白盒审计 _safe_extract + 验证 ./../绝对路径
下载重试(IncompleteRead + transient HTTP + 退避) 代码审查 + _check_download_retry self-check
esbuild provenance(lockfile + --no-install 代码审查 + validate_esbuild_provenance
CJK locale 切换 context 重建 test_locale_switch_rebuilds_context_for_each_font_pair
MiniRacer timeout/context 生命周期 test_engine_restarts_context_after_native_timeout 等 5 个 timeout 测试
marker-end + orient="auto" + refX="10" + target-border endpoint test_canonical_svg_validator_accepts_renderer_output_and_marker_contract
broad except Exception 合规 逐个审计:仅 _eval 使用 isinstance guard

总评

代码质量高。PNG 链路(CRC、chunk 合同、padding 窗口)、SVG 方言(三层 allowlist + URI scheme + local reference)、字体 OpenType 校验、CJK locale 切换、timeout/context 生命周期、下载重试、esbuild provenance 均实现扎实。68 个测试 + 3 个 maintenance check 全部通过。

C=0 / I=2 / M=4。2 个 Important 是维护一致性问题(死代码 + 两个 PNG parser 验证深度不对称),不影响当前正确性。4 个 Minor 是文档/注释/维护性建议。

结论: 满足 ready-to-merge 条件中的 C/I 要求(C=0,I 无阻塞性安全问题)。建议 I-1(删除 _valid_png 死代码)在后续清理 PR 中处理。


审查于 commit 62a4bf5d739fca4932fb691815ba530c48fd3fc6;68 个 diagram 测试通过、provenance/engine floor/rendering self-check 通过。

- remove the unused boolean PNG validator and test the typed decoder contract\n- reject malformed parity PNG structure before ink decoding\n- add maintenance self-checks for ordering, chunk, and trailing-byte failures
@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Claude reviewer 最终审查 — head 3ea1a34

审查范围与方法

  • base = dev/python-diagram-umbrella, head = 3ea1a343 fix(diagram): align parity PNG validation
  • 白盒审读 24 个变更文件(含 13 个提交),本地实测跑 pytest / self-check / boundary check / 手工对抗 PNG 输入
  • 未 spawn 子代理;未修改任何代码

3ea1a34 是最后一个提交,两处关键改动:

  1. pyfcstm/diagram/engine.py:删除 _valid_png() 布尔包装(19 行)
  2. tools/check_diagram_rendering.py:给 parity 检查器 _png_ink_bbox() 加严格结构检查 + 新增 _self_check_png_parser()

C(Critical, 阻塞 merge)

无。


I(Important, 建议处理但不阻塞)

无。 本轮审查未发现新的功能/安全问题。上一轮 reviewer 指出的两个 Important(_valid_png 死代码 & parity checker 校验对称性不足)在 3ea1a34 都已经修掉,见下方"回归验证"部分。


M(Minor, 建议)

M-1 _decode_png_rgba 的 broad-except 分类可简化

文件: pyfcstm/diagram/engine.py:852-865

except (struct.error, ValueError, DiagramRenderError, zlib.error, OverflowError) as err:
    ...
    if isinstance(err, (ValueError, DiagramRenderError)):
        raise
    raise ValueError("PNG output has malformed compressed data") from err

在解析循环里所有 struct.unpack 都从已长度校验的切片读,理论上 struct.error 不会真触发;OverflowError 也一样。留着无害但注释与实际可达路径不完全对齐。属于风格性 Minor。

M-2 tools/fetch_diagram_reference.py 无 retry

文件: tools/fetch_diagram_reference.py:20-30_download

with urlopen(request, timeout=120) as response:
    return response.read()
except (HTTPError, URLError, OSError) as err:
    raise RuntimeError("unable to download diagram reference archive") from err

对比 tools/build_diagram_assets.py:98-117 的三次退避重试,fetch_diagram_reference.py 从同一 Gist 一次性下载即失败。CI-only 路径,仅影响偶发的 gist transient 抖动导致的 job 重跑。建议后续统一到共享 retry helper。

M-3 死代码残留检查建议

文件: Makefile:275tools/check_diagram_provenance.py 引用)

grep -rn "_valid_png" 已经干净(0 处生产引用)。此项已通过;仅提示后续再有类似 helper 删除时保持 grep 快检习惯。

M-4 test 里 pytest.raises((ValueError, DiagramRenderError)) 过宽

文件: test/diagram/test_assets.py:926,958,978,988,1024,1059

改为期望 ValueError 就够(构造的对抗输入都不会先触发 limit 分支),当前写法允许 DiagramRenderError 会掩盖将来某个应报 ValueError 的路径变成 limit error。非阻塞。


回归验证(关键:对 3ea1a34 的白盒对抗测试)

1. _valid_png 删除是否安全

grep -rn "_valid_png\|_decode_png_rgba" pyfcstm/ tools/ test/

生产代码 0 处引用 _valid_png;只 test 用过,全部改为直接调用 _decode_png_rgba 并断言异常。运行时(engine.py:1431)本来就调 _decode_png_rgba 并 catch (TypeError, ValueError, struct.error, zlib.error, OverflowError)。删除等价重构。

2. _decode_png_rgba 对抗输入(13 个)

python -c "..." # 见附录脚本

全 13 项按预期抛出:

场景 结果
valid RGBA8 1×1 OK
bad signature ValueError: lacks PNG signature
SIG only ValueError: missing IHDR/IDAT/IEND
missing IEND ValueError: missing IHDR/IDAT/IEND
trailing bytes after IEND ValueError: trailing bytes after IEND
duplicate IHDR ValueError: duplicate/misplaced IHDR
IDAT before IHDR ValueError: non-IHDR first chunk
tEXt ancillary chunk ValueError: unsupported ancillary chunk
bad CRC ValueError: invalid chunk checksum
non-contiguous IDAT ValueError: trailing bytes after IEND(因为 _PNG_ALLOWED_CHUNKS 仅 3 个,间隔只能是 IEND)
IDAT + IEND + IEND ValueError: trailing bytes after IEND
zero dimensions ValueError: non-positive dimensions
interlaced ValueError: violates RGBA8 size contract

3. _png_ink_bbox(parity checker)对抗输入(6 个)

python -c "sys.path.insert(0,'tools'); from check_diagram_rendering import _png_ink_bbox; ..."

全 6 项按预期抛:valid → OK;trailing / dup_ihdr / tEXt / idat_before_ihdr / non_contig_idat 全部 raise ValueError

对比修改前 parity checker 只有 CRC + IEND + IHDR-长度 检查,本次补齐后与运行时 _decode_png_rgba 结构合同对齐。maintenance self-check _self_check_png_parser 也覆盖 valid + 4 个反例。

4. tools/check_diagram_rendering.py --check 通过

diagram rendering checker: provenance, parity, and PNG self-check passed

5. pytest 全绿

pytest test/diagram/test_assets.py -q  →  68 passed in 38.88s
make test_boundary_check              →  test boundary check passed

6. 其它必查项(继承上轮 review、快速回读)

区域 结果
resvg 2.6.2 lock(tarball/binding/wasm SHA-256 + MPL-2.0 + upstream 双 commit) tools/diagram_assets/asset-lock.json 完整;builder 会校验
esbuild 0.28.1 lock + --no-install editors/jsfcstm/package.json 已锁定为 devDependency;build 从 jsfcstm 工作目录解析本地 binary
reference archive path-traversal / symlink _safe_extract 逐 member 拒绝 sym/hard/非普通 + 绝对路径 resolve 校验;13 项对抗 tar 由测试覆盖
marker-end / refX=10 / orient=auto engine.py:463-467 硬编码断言
PNG base64 padding 边界 engine.py:1404-1416 %3 padding 检查完整
5 CJK locale (SC/TC/HK/JP/KR) Regular+Bold asset-lock.json fonts.faces 10 face 覆盖
py-mini-racer / mini-racer split requirements-viz.txt python_version < "3.8" / >= "3.8"

是否可以 merge

可以合入 umbrella dev/python-diagram-umbrella

理由:

  • C=0, I=0, M=4;M 全部是可延后处理的建议
  • 3ea1a34 是等价重构 + parity checker 收紧,对抗输入实测 13+6 项全部按预期拒绝,无静默回归
  • 上一轮 reviewer 指出的 I-1/I-2 在本 commit 全部关闭
  • 68 diagram + boundary check + rendering self-check 全绿

注意事项(不阻塞)

  • 本 PR 仅合入 umbrella,最终并入 main 前仍需等 umbrella 三路 review 与全 required checks
  • CI 目前尚未在此 head 全跑(若 required checks 未 green,仍要按 PR body 的 ready-to-merge 停止条件走)
  • M-2(fetch_diagram_reference 无 retry)建议在后续清理 PR 处理

审查基准:head 3ea1a343;本地 venv Python 3.11;未修改任何被审代码。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

DeepSeek 强对抗白盒审查 — PR #386 (head 3ea1a34)

审查人: DeepSeek reviewer
审查范围: PR #386 完整 diff(27170 行),重点聚焦 head 3ea1a343fix(diagram): align parity PNG validation
Base: dev/python-diagram-umbrella
关联: #383(伞 PR)、#384(PR-A)、#377#89#376
本地验证: git rev-parse HEAD3ea1a3438202c3d0dde336f548d433194783f57f


总体判断:通过,无阻塞性问题

本次 PR 的核心变更——删除 _valid_png 包装函数,增强 _decode_png_rgba / _png_ink_bbox 的结构化 PNG 合同,并添加 _self_check_png_parser 维护自检——在执行质量和安全边界上都是正向的。以下按 C(Critical)/ I(Important)/ M(Minor)分类列出所有发现。


[I-1] _self_check_png_parser 未覆盖三个失败模式

文件: tools/check_diagram_rendering.py:437-473
影响: 维护自检声称 "keep parity PNG structure checks aligned with the runtime decoder",但未测试以下三种运行时解码器正确拒绝的失败模式:

缺失测试用例 _decode_png_rgba 拒绝 _png_ink_bbox 拒绝
non-zero-length IEND PNG output has an invalid IEND PNG output has an invalid IEND
non-contiguous IDAT(IDAT → 非 IDAT → IDAT) ✔ 被 chunk-type 拒绝 ✔ 被 chunk-type 拒绝
IDAT after IEND trailing bytes after IEND trailing bytes after IEND

验证命令:

import struct, zlib
from pyfcstm.diagram.engine import _decode_png_rgba
from tools.check_diagram_rendering import _png_ink_bbox

def chunk(kind, payload):
    crc = zlib.crc32(kind + payload) & 0xFFFFFFFF
    return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", crc)

sig = b"\x89PNG\r\n\x1a\n"
hdr = struct.pack(">IIBBBBB", 1, 1, 8, 6, 0, 0, 0)
img = chunk(b"IDAT", zlib.compress(b"\x00\x00\x00\x00\xff"))

# IEND with non-zero payload
nonzero_iend = sig + chunk(b"IHDR", hdr) + img + chunk(b"IEND", b"x")
_decode_png_rgba(nonzero_iend)  # raises ValueError
_png_ink_bbox(nonzero_iend)     # raises ValueError
# But _self_check_png_parser does NOT test this case.

建议: 在 _self_check_png_parserinvalid_cases 元组中添加这些用例,使其与 PR 声明 "keep parity PNG structure checks aligned" 一致。


[M-1] _decode_png_rgba IHDR 错误消息将三个不同失败合并为一

文件: pyfcstm/diagram/engine.py:940(行号可能因代码前后偏移 ±5 行)
位置: if saw_ihdr or position != 8 or length != 13: raise ValueError("PNG output has duplicate or misplaced IHDR")

问题: 条件 length != 13 触发时,错误的实际原因是 "IHDR 长度不是 13 字节",但消息说 "duplicate or misplaced IHDR",误导诊断。对比 parity parser(_png_ink_bbox)正确地将 IHDR 长度检查分离出来:

# Parity parser — 正确分离:
if saw_ihdr or chunk_start != 8:
    raise ValueError("PNG output has duplicate or misplaced IHDR")
if len(payload) != 13:
    raise ValueError("PNG output has an invalid IHDR")

# Runtime decoder — 三合一:
if saw_ihdr or position != 8 or length != 13:
    raise ValueError("PNG output has duplicate or misplaced IHDR")

建议: 将 length != 13 分离为独立检查,以匹配 parity parser 的粒度。


[M-2] 缺少 _png_ink_bbox_decode_png_rgba 的跨验证

文件: tools/check_diagram_rendering.py:437
影响: _self_check_png_parser 仅验证 _png_ink_bbox 的自洽性(接受已知 valid、拒绝 known-malformed),不验证它与 _decode_png_rgba 对任意输入的输出一致。如果其中一个无意中偏离,自检不会捕获。

验证命令:

cd /home/zhangshaoang/oo-projects/pyfcstm && python -c "
from pyfcstm.diagram.engine import _decode_png_rgba
from tools.check_diagram_rendering import _png_ink_bbox
# 无现成跨验证 — 两个解析器独立演化
"

建议: 添加一个跨验证测试,至少在 valid 1×1 RGBA PNG 上确保两个解析器输出一致。不需要覆盖所有 malformed 路径(因为两者是独立维护面),但 valid 路径的一致性应被 enforce。


[I-2] make build_assets fast-path 仅检查文件存在而非完整性

文件: tools/build_diagram_assets.py:132-148
位置: _install_lockfile_packages 的 early-return 条件

if (
    ELK_API_PATH.is_file()
    and ELK_WORKER_PATH.is_file()
    and (RESVG_PACKAGE_DIR / "index.min.js").is_file()
    ...
    and (JSFCSTM_DIR / "node_modules" / "esbuild").is_dir()
):
    return  # <-- fast path: 仅检查文件存在

如果 node_modules 包含错误版本的文件(例如旧版 esbuild、损坏的 WASM),fast-path 会跳过 npm ci,然后 build_renderer 会用错误工具链构建。最终 SHA-256 检查会在 make diagram_assets_check 中捕获,但错误消息可能让人困惑("hash mismatch" 而非 "stale node_modules")。

建议: 在 fast-path 条件中添加至少一个关键文件的 SHA-256 检查(如 resvg.wasm),或者添加文档说明需要先 make tpl_clean 后再重建。


已验证通过的安全/正确性检查(全部 PASS)

以下所有用户指定的审查面均通过对抗验证,无发现问题:

审查面 验证方式 结果
IHDR 首位/唯一性 手工构造 duplicate IHDR / non-first IHDR,执行 engine + parity 解析器 ✔ 正确拒绝
IDAT 连续性 non-contiguous IDAT(tEXt 间隙)/ IDAT-after-IEND ✔ 正确拒绝
IEND 尾随字节 valid PNG + trailing bytes ✔ 正确拒绝
允许 chunk tEXt (ancillary), sRGB (ancillary), TEXT (invalid ASCII), PLTE, gAMA, pHYs ✔ 全部正确拒绝
chunk type 字节校验 非 ASCII chunk type (\x01\x02\x03\x04), \x80\x00\x00\x00 ✔ 正确拒绝
chunk checksum 故意错误 CRC ✔ 正确拒绝
PNG 尺寸/维度限制 1×1 valid, zero-dimension IHDR, huge length overflow ✔ 边界正确
_valid_png 删除 grep -rn _valid_png → 零引用 ✔ 完全删除
Self-check 通过 python tools/check_diagram_rendering.py --check ✔ PASS
Reference tar link/path traversal 代码审查 _safe_extract: 拒绝 symlink/硬链/路径穿越/非文件成员 ✔ 安全
Reference 自引用 reference-lock.json 使用 immutable Gist SHA ✔ 不可变
IncompleteRead/5xx 重试 download_locked 三次有界重试,永久 4xx 立即失败 ✔ 正确
esbuild lockfile 安装 npm ci + npx --no-install 从 jsfcstm 目录解析 ✔ 锁定
esbuild provenance 检查 validate_esbuild_provenance 交叉检查 asset-lock ↔ package-lock ✔ 锁定
CI skip/gate 绕过 diagram_assetstemplate-suite-gate 的 needs 中,仅 ci skip/[python skip] 显式跳过 ✔ 无绕过
CI gate 漏检 run_expensive_code_test=false 时跳过 gate — 但 pyfcstm/diagram/** 匹配 _CODE_PATTERNS 触发 true ✔ 正确
CJK 字体 13 字体 SC/TC/HK/JP/KR Regular/Bold + SHA-256 锁 + 大小预算 ✔ 完整
Arrow marker-end inspect_arrows tipError/tolerance, terminalLength < 18.0, rect/pseudo-exit 双容差 ✔ 验证
MiniRacer timeout/OOM JSTimeoutException/JSOOMException_discard_context → context close + _DiagramEvaluationInterruptedError ✔ 正确
MiniRacer 双安装检测 DiagramEngineConflictError 拒绝 mini-racer + py-mini-racer 共存 ✔ 正确
Package old-path 拒绝 check_diagram_package.py LEGACY_PATH_PREFIXES 拒绝 pyfcstm/assets / pyfcstm/diagram_runtime ✔ 正确
Package fresh install check_diagram_package.py wheel/sdist + SHA-256 全量验证 ✔ 通过
SVG allow-list DOCTYPE/ENTITY/PI/script 拒绝 + element/attribute allow-list + depth/element/byte 限制 ✔ 安全
Test suite pytest test/diagram/test_assets.py -q → 68 passed in 39.53s ✔ 通过
Boundary check make test_boundary_check → passed ✔ 通过

结论

PR #386 的 PNG 校验对齐(3ea1a343)是正确且安全的改进。_valid_png 删除无遗留引用,_decode_png_rgba / _png_ink_bbox 均正确拒绝所有 20+ 种 malformed PNG 结构。发现的所有问题是自检覆盖缺口(I-1)、错误消息精度(M-1)、跨验证缺失(M-2)和 fast-path 深度不足(I-2),均为非阻塞性质。建议在后续 PR 中补充 I-1 的 self-check 用例,其余可在维护周期内修复。

最终判断: ✅ APPROVED — 无阻塞性问题,可 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer\n\n这是针对 PR #386 精确 head 3ea1a34 的最终独立强对抗复核;base 为 bcadb92。范围覆盖 official resvg 2.6.2 provenance/license/lock、esbuild、reference archive/retry、closed SVG/raw input、箭头/CJK、PNG/limits、MiniRacer timeout/context、package/CI/pydoc 与测试边界。\n\n### 结论\n\nC=0 / I=0 / M=6。 本次删除 _valid_png 并将 parity PNG 检查改为严格结构解析后,没有发现阻断级正确性、安全性或范围问题。官方资产锁、source/license 回读、request-only/closed SVG、PNG 生产解码、双 MiniRacer floor、箭头/CJK、reference/package/CI 门禁均有可复现证据。\n\n### C / I\n\n- C=0。 没有致命矛盾;PR-B 仍限定为内部资产运行时,不提前宣称公共 Diagram API、CLI、HTML/Notebook 或 PDF facade 完成。\n- I=0。 3ea 的 parity parser 能拒绝错误 chunk、IHDR 顺序、非连续 IDAT、缺失/错误 IEND、尾随数据和坏 CRC;生产引擎继续使用 _decode_png_rgba 做完整 RGBA PNG 校验。未发现新的阻断回归。\n\n### M\n\n1. Parity PNG limit drift。 tools/check_diagram_rendering.py::_png_ink_bbox() 未复用生产的最大 PNG 字节数、最大维度、最大像素数和最大 RGBA 字节数。真实负例:构造合法 16385x1 RGBA PNG 时,pyfcstm.diagram.engine._decode_png_rgba 抛 DiagramRenderLimitError,而 parity parser 返回 (16385, 1)。当前 renderer 输出受生产入口限制,故不阻塞,但 parser 声称与生产结构合同对齐并不完整。\n2. 伞 PR #383 current-state 过期。 正文仍把 PR-B head 写为 98fd215,实际 head 为 3ea1a34;历史 hash 可保留,但需追加 current-state 同步。\n3. PR #386 body current-state 过期。 body 的当前 head/最新修复仍写 62a4bf5,未同步 3ea1a34。\n4. esbuild 已安装树快路径无字节回读。 validate_esbuild_provenance() 校验 package-lock 的 version/resolved/integrity,但 ensure_js_dependencies() 发现 node_modules/esbuild 目录后不核验已安装文件;fresh CI npm ci 覆盖主要路径,后续可与 ELK tree SHA 同等级加固。\n5. reference Gist 下载无瞬态重试。 tools/fetch_diagram_reference.py::_download 只有一次 120 秒请求;Gist 的短暂 5xx/网络中断会直接失败 required CI,建议复用资产下载的有界重试。\n6. expanded SVG 错误后的 context 复位不对称。 expand_svg() 对 malformed/closed-dialect 输出映射为 DiagramRenderError,但没有像 render_svg() 一样 discard context;建议补充复位或明确保持上下文的理由。\n\n### 上游/范围一致性\n\n- PR-A #384 已合入 umbrella,merge commit 8a6eba7;PR #377 已合入 main,merge commit 05c4156。\n- issue #89 仍 OPEN,issue #376 已 CLOSED;#383 仍 OPEN 且明确 PR-C/D 尚待实施。\n- #386 body 的 official 2.6.2、source/license、双 runtime、reference/package 所有权与当前实现一致,只有上述 current-state hash 需要同步。\n\n### 可复现证据\n\n本地 HEAD=3ea1a343:\n\n python -m pytest -q test/diagram/test_assets.py\n 68 passed\n python tools/check_diagram_rendering.py --check\n diagram rendering checker: provenance, parity, and PNG self-check passed\n python tools/build_diagram_assets.py --check\n diagram asset builder: deterministic and safety self-check passed\n ruff check ... / ruff format --check ...\n passed\n make test_boundary_check\n passed\n make PYTHON=/home/zhangshaoang/.pyenv/versions/3.11.1/bin/python \n DIAGRAM_REFERENCE=/tmp/pr386-final-reference/reference.json diagram_assets_verify\n 35 layouts, 306 arrows; CJK, expanded parity, memory, runtime floor and provenance passed\n python tools/check_diagram_provenance.py\n npm tarball/source archive SHA-256 values matched the lock\n python tools/check_diagram_package.py --check\n adversarial self-check passed\n\n真实 PNG limit probe:\n\n _decode_png_rgba(16385x1 PNG) -> DiagramRenderLimitError\n _png_ink_bbox(16385x1 PNG) -> (16385, 1)\n\n远端在评论时 Diagram assets Python 3.11 已通过,Python 3.7/3.8 与其余 Code Test/Docs/Package required jobs 仍在运行队列;因此不把未完成 CI 宣称为通过。审查完成,不执行 merge。

- validate an existing esbuild tree before reusing it\n- cover additional malformed PNG structures in the maintenance self-check\n- share runtime PNG size limits with parity validation
@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Claude reviewer 最终强对抗白盒审查(head 710ee26)

审查基线:git rev-parse HEAD = 710ee2616a074a78af3d3c00ed023492b3cc721b,base dev/python-diagram-umbrella,增量相对 3ea1a343,仅两文件 67 行:tools/build_diagram_assets.pytools/check_diagram_rendering.py。已完整读取 PR body、#383/#384/#377/#89/#376 的关联描述以及所有 24 个变更文件的路径与增量骨架;本次审查焦点严格锁定在 710ee26 引入的两处修复。

验证方式(全部亲自执行):

  1. python tools/build_diagram_assets.py --checkdiagram asset builder: deterministic and safety self-check passed
  2. python tools/check_diagram_rendering.py --checkdiagram rendering checker: provenance, parity, and PNG self-check passed
  3. python tools/check_diagram_assets.py --checkdiagram assets: hashes, size, syntax, and ignore rules passed
  4. SKIP_SLOW_TESTS=1 pytest test/diagram/test_assets.py -q68 passed
  5. 对抗性 PNG 交叉解析矩阵:构造 21 个 malformed PNG(bad_sig / trunc_hdr / trunc_chunk / bad_crc / dup_ihdr / ihdr_not_first / iend_data / trailing / trailing_chunk / tEXt_after_idat / split_idat / interlaced / rgb_only / zero_dim / huge_dim / huge_pixels / huge_bytes / bad_zlib / ancillary_gAMA / nonascii_chunk + valid baseline),对 _png_ink_bbox_decode_png_rgba 双向解析;0 drift,parity 与生产 decoder 在接受/拒绝语义上完全一致(生产额外抛 DiagramRenderLimitError,parity 仍抛 ValueError,均为拒绝路径,符合 check_diagram_rendering 只保结构合同的分层设计)。
  6. 对抗性 esbuild 快路径矩阵:baseline True,篡改 version=9.9.9 / name=not-esbuild / 删除 version / 损坏 package.json JSON / 清空 bin/esbuild / 删除 bin/esbuild / 非字符串 version=123 全部 → False,每一种破坏都会正确 fallthrough 到 npm ci 分支。
  7. 复核生产 _PNG_ALLOWED_CHUNKS = {IHDR, IDAT, IEND} 与 parity {IHDR, IDAT, IEND} 完全一致,无白名单 drift。

C(critical / 阻断):无

I(important / 需处理):无

M(minor / 建议 follow-up,不阻断 merge)

  • M1. _installed_esbuild_is_usablesubprocess.run 未设置 timeout(tools/build_diagram_assets.py:153-161)。同文件 download_lockedurlopen 设了 timeout=120(line 100),风格上不一致。npx 通常几百毫秒返回,实际风险很低;但如果某个 CI runner 上 Node 进程被子进程 hook 拖住 stdin,快路径会永久阻塞。可以在下一次维护窗口补一个宽松的 timeout=30,配合 subprocess.TimeoutExpired 归入现有的 OSError-并列返回 False 分支。
  • M2. stdout.strip() == expected_version 依赖 esbuild 输出格式(tools/build_diagram_assets.py:163)。esbuild 0.28.x 的 --version 是干净的 "0.28.1\n",.strip() 已经处理 CRLF 与前后空白;stderr 被 subprocess.PIPE 吞掉不会污染 stdout。但若上游未来在 stdout 加 deprecation notice(极小概率),快路径会误判需要重装 —— 属于"多做一次 npm ci"的安全降级,不会造成假成功。可选:未来若被误判频繁触发,再改成 startswith(expected_version) 与显式换行检查。
  • M3. parity self-check 的交叉断言只覆盖 dimensions(tools/check_diagram_rendering.py:471-472)。_decode_png_rgba(valid)[:2] != _png_ink_bbox(valid)[:2] 只对齐 (width, height),不对齐 bbox 或 error class。当前 valid 只含 IHDR/IDAT/IEND 允许 chunk,足够抓 dimension drift;若未来生产 decoder 扩展白名单,建议同步扩展 parity self-check 的正样本集,而不是让二者的 _PNG_ALLOWED_CHUNKS 独立演化。属于长期维护 note。

与 PR body 已声明范围一致性

  • 官方 @resvg/resvg-wasm@2.6.2 唯一来源、Python 3.7 py-mini-racer / 3.8+ mini-racer、CJK / parity / expanded / memory / provenance 门禁均已由 check_diagram_rendering.py --checkcheck_diagram_assets.py --check 覆盖并通过。
  • 62a4bf5dexpand_svg() timeout / decoded-output-limit / base64 一字节窗口 / archive symlink-hardlink 拒绝 / IncompleteRead retry / esbuild 锁 devDep 与 npx --no-install / editors/jsfcstm cwd 定位等修复,在 710ee26 之上均维持有效,未被新增改动回归。
  • 710ee26 精确解决了"已有 node_modules 情况下 esbuild 缺失或版本漂移不被检测"以及"parity PNG parser 与生产 byte/dimension 限制不同步"两个门禁窗口,与 body 中"最新修复"章节完全对应。

结论

  • 变更聚焦于安全门禁的 fail-closed 收敛,不修改 renderer 语义、不引入新 runtime 依赖、不改变 API 表面。
  • 21 个 malformed PNG 与 7 个 esbuild 破坏场景均按预期 fail-closed;68 个 diagram 单测通过;三个 maintenance self-check 通过。
  • C / I 均为空;M1/M2/M3 为增量小改进建议,不阻断 merge。

以上审查为 Claude reviewer 独立完成,基于 710ee26,未修改代码,未执行 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer\n\n这是针对 PR #386 精确 head 710ee26 的最终独立强对抗复核;base 为 bcadb92。范围覆盖 official resvg 2.6.2 provenance/license/lock、esbuild、reference archive/retry、closed SVG/raw input、箭头/CJK、PNG/limits、MiniRacer timeout/context、package/CI/pydoc 与测试边界。\n\n### 结论\n\nC=0 / I=0 / M=5。 本次新增的 esbuild 快路径校验、PNG 限制复用和 parity self-check 均通过;未发现阻断级正确性、安全性或范围问题。官方资产/source/license 回读、request-only/closed SVG、PNG 生产解码、双 MiniRacer floor、箭头/CJK、reference/package/CI 门禁均有证据。\n\n### C / I\n\n- C=0。 没有致命矛盾;PR-B 仍限定为内部资产运行时,不提前宣称公共 Diagram API、CLI、HTML/Notebook 或 PDF facade 完成。\n- I=0。 esbuild 已安装树会校验 package.json/name/version、wrapper 存在且 npx --no-install esbuild --version 与锁定版本一致;parity parser 复用了生产 PNG byte/dimension/pixel/RGBA limits,并覆盖非法 IEND、尾随 IDAT、未知/非连续 chunk 和 runtime/parity valid cross-check。未发现新的阻断回归。\n\n### M\n\n1. Parity PNG 解压后扫描线限制仍有轻微漂移。 tools/check_diagram_rendering.py::_png_ink_bbox() 已同步压缩文件字节、IHDR 维度/像素/RGBA limits,但没有同步生产 _decode_png_rgba() 在 zlib.decompress 后对 decoded scanlines 执行的 _MAX_RENDER_RGBA_BYTES 上限。恶意压缩流仍可能先展开较大内存再因 scanline 长度失败;当前 reference/renderer 受可信来源与生产入口限制,故不阻塞。\n2. esbuild 已安装树仍未做 integrity/字节回读,也没有版本探测 timeout。 快路径验证 package-lock metadata 与可执行版本,但没有把已安装 wrapper/binary 与 integrity 摘要逐字节核对;被篡改 wrapper 也可能长时间阻塞 --version。fresh CI npm ci 覆盖主要路径,建议后续继续加固。\n3. 伞 PR #383 current-state 过期。 正文仍把 PR-B head 写为 98fd215,实际 head 为 710ee26;历史 hash 可保留,但需追加 current-state 同步。\n4. PR #386 body current-state 过期。 body 的当前 head/最新修复仍写 62a4bf5,未同步 710ee26。\n5. reference Gist 下载与 expanded SVG 失败复位仍可加固。 fetch_diagram_reference.py::_download 只有一次 120 秒请求,短暂 5xx/网络中断会直接失败 required CI;同时 expand_svg() 对 malformed/closed-dialect 输出映射为 DiagramRenderError,但没有像 render_svg() 一样 discard context。两者均不阻塞当前可信输入路径。\n\n### 上游/范围一致性\n\n- PR-A #384 已合入 umbrella,merge commit 8a6eba7;PR #377 已合入 main,merge commit 05c4156。\n- issue #89 仍 OPEN,issue #376 已 CLOSED;#383 仍 OPEN 且明确 PR-C/D 尚待实施。\n- #386 body 的 official 2.6.2、source/license、双 runtime、reference/package 所有权与当前实现一致,只有 current-state hash 需要同步。\n\n### 可复现证据\n\n本地 HEAD=710ee261:\n\n python -m pytest -q test/diagram/test_assets.py\n 68 passed\n python tools/check_diagram_rendering.py --check\n diagram rendering checker: provenance, parity, and PNG self-check passed\n python tools/build_diagram_assets.py --check\n diagram asset builder: deterministic and safety self-check passed\n ruff check ... / ruff format --check ...\n passed\n make test_boundary_check\n passed\n make PYTHON=/home/zhangshaoang/.pyenv/versions/3.11.1/bin/python \n DIAGRAM_REFERENCE=/tmp/pr386-final-reference/reference.json diagram_assets_verify\n 35 layouts, 306 arrows; CJK, expanded parity, memory, runtime floor and provenance passed\n python tools/check_diagram_provenance.py\n npm tarball/source archive SHA-256 values matched the lock\n python tools/check_diagram_package.py --check\n adversarial self-check passed\n\n真实负例:\n\n installed_esbuild_usable=True\n npx --no-install esbuild --version -> 0.28.1\n tampered package.json version -> _installed_esbuild_is_usable() False\n nonzero IEND -> ValueError\n IDAT after IEND -> ValueError\n unknown/non-contiguous separator -> ValueError\n 16385x1 PNG -> ValueError: PNG output exceeds the bounded dimension limit\n\n远端在评论时没有观察到 FAILURE/CANCELLED;Diagram assets Python 3.11 已通过,其余新 head required jobs 仍在运行或排队,因此不把未完成 CI 宣称为通过。审查完成,不执行 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

DeepSeek Reviewer — PR #386 最终强对抗白盒审查

审查 head: 710ee2616a074a78af3d3c00ed023492b3cc721b
Base: dev/python-diagram-umbrella (bcadb92)
审查时间: 2026-07-20 Asia/Shanghai
审查范围: 完整 diff (24 files, +26070/-248),关联 #383/#384/#377/#89/#376


审查方法论

对以下每个领域进行了白盒路径追踪 + 手动对抗构造:

  • _installed_esbuild_is_usable stale/corrupt node_modules 快路径
  • Parity PNG parser 与生产限制合同
  • Self-check 对 IEND/IDAT/unknown/跨验证的覆盖
  • 官方 resvg 2.6.2 溯源
  • Reference archive/retry/provenance
  • Closed SVG 合同
  • CJK/arrow endpoint 验证
  • MiniRacer runtime/timeout 生命周期
  • Package checker 旧路径拒绝
  • CI skip/gate 机制
  • pydoc 完整性

C (Critical) — 0 项

无关键问题。


I (Important) — 3 项

I1. Self-check 未覆盖 CRC 校验码验证分支

位置: tools/check_diagram_rendering.py:453-490 (_self_check_png_parser)
问题: _self_check_png_parser 内部使用正确的 zlib.crc32 生成测试 PNG 数据,因此 CRC mismatch 分支从未在 self-check 中被触发。运行时 parser (_decode_png_rgba 第 800-802 行) 确实会检查 actual_crc != expected_crc,但 self-check 不能证明该检查未被静默绕过。

验证: 手动构造 CRC 损坏的 PNG 确认运行时 parser 确实拒绝:

python3 -c "
from pyfcstm.diagram.engine import _decode_png_rgba
import struct, zlib

def bad_crc_chunk(kind, payload):
    checksum = (zlib.crc32(kind + payload) ^ 0xFF) & 0xFFFFFFFF
    return struct.pack('>I', len(payload)) + kind + payload + struct.pack('>I', checksum)

signature = b'\x89PNG\r\n\x1a\n'
header = struct.pack('>IIBBBBB', 1, 1, 8, 6, 0, 0, 0)
image = bad_crc_chunk(b'IDAT', zlib.compress(b'\x00\x00\x00\x00\xff'))

# CRC mismatch on IHDR
bad = signature + bad_crc_chunk(b'IHDR', header) + image + bad_crc_chunk(b'IEND', b'')
_decode_png_rgba(bad)  # ValueError: PNG output contains an invalid chunk checksum
"
# Output: ValueError: PNG output contains an invalid chunk checksum

运行时拒绝正确。但 self-check 缺此覆盖。

建议: 在 _self_check_png_parserinvalid_cases 中添加一个 CRC 损坏的 case(例如 IDAT chunk 带有翻转的 CRC bits),确保 _png_ink_bbox 对其抛出 ValueError


I2. fetch_diagram_reference.py 缺少瞬断重试逻辑

位置: tools/fetch_diagram_reference.py:28-35
问题: tools/build_diagram_assets.pydownload_locked 有 3 次有界重试 + 指数退避(覆盖 HTTPError(408/429/5xx), URLError, TimeoutError, OSError, IncompleteRead),但 fetch_diagram_reference.py_download 没有重试。GitHub Gist 高度可靠,但在 CI 环境网络瞬断时,本地维护者运行 make diagram_assets_verify 会因单次网络失败而中断。

对比:

# build_diagram_assets.py — 有重试
_DOWNLOAD_ATTEMPTS = 3
_DOWNLOAD_BACKOFF_SECONDS = 1.0
_TRANSIENT_HTTP_CODES = frozenset({408, 429, 500, 502, 503, 504})

# fetch_diagram_reference.py — 无重试
def _download(url: str) -> bytes:
    request = Request(url, headers={"User-Agent": "pyfcstm-diagram-reference/1"})
    try:
        with urlopen(request, timeout=120) as response:
            return response.read()
    except (HTTPError, URLError, OSError) as err:
        raise RuntimeError("unable to download diagram reference archive") from err

影响: 低。CI job 本身会重试整个 step,本地维护者可以手动重试。但一致性不佳。

建议: 对齐 fetch_diagram_reference.py 的重试策略与 build_diagram_assets.py,或在其 docstring 中显式声明"CI retries the entire job; local runners should use make with retry wrapper"。


I3. reference-lock.json 依赖 GitHub Gist 而非 immutable release artifact

位置: tools/diagram_assets/reference-lock.json
问题: 参考锁的 URL 是 GitHub Gist raw URL(含 gist commit hash dbe7c73645bb30f4650a65b3556023c8c6982693),并带有 SHA256 内容校验。这比纯 URL 好很多,但 Gist 在极端情况下(GitHub 管理员操作、账户删除)可能不可恢复。

当前双重锁定:

{
  "3.8": {
    "encoding": "base64",
    "sha256": "67fd4394ade6c01f94379e6d2c0bd2d864833ed8bb60bbdf2a7b3a5e5fb850e2",
    "url": "https://gist.githubusercontent.com/HansBug/b43d202ccc9436cada8bc57fa3763450/raw/dbe7c73645bb30f4650a65b3556023c8c6982693/reference-3.8.tar.gz.b64"
  }
}

影响: 低。SHA256 校验确保内容完整性,且该参考仅用于 CI parity gate,不进入生产路径。即使 Gist 不可用,CI job 会失败而非静默通过。

建议: 长期可考虑将参考归档到 pyfcstm 仓库的 Git LFS 或 GitHub Release artifact。当前方案对 CI-only baseline 可接受。


M (Minor) — 4 项

M1. Self-check 仅测试最小 PNG (1×1, RGBA, filter 0)

位置: tools/check_diagram_rendering.py:453-490
问题: _self_check_png_parser 使用 1×1 RGBA filter-0 PNG 作为 valid case。运行时 parser 还覆盖了 filter 1-4(Paeth predictor 等)、scanline 长度验证、zlib decompression 失败路径,但这些不在 self-check 覆盖范围内。

验证: 手动确认运行时拒绝 unsupported filter:

python3 -c "
from pyfcstm.diagram.engine import _decode_png_rgba
import struct, zlib

def chunk(kind, payload):
    checksum = zlib.crc32(kind + payload) & 0xFFFFFFFF
    return struct.pack('>I', len(payload)) + kind + payload + struct.pack('>I', checksum)

header = struct.pack('>IIBBBBB', 1, 1, 8, 6, 0, 0, 0)
signature = b'\x89PNG\r\n\x1a\n'
# filter type 5 is unsupported
image = chunk(b'IDAT', zlib.compress(b'\x05\x00\x00\x00\xff'))
bad = signature + chunk(b'IHDR', header) + image + chunk(b'IEND', b'')
_decode_png_rgba(bad)  # ValueError: PNG output uses an unsupported filter
"
# Output: ValueError: PNG output uses an unsupported filter ✓

建议: 可以在 self-check 中添加一个 filter-4 (Paeth) 的 case 或一个 zlib-corrupted IDAT case,但不阻塞。


M2. pydoc 示例引用未实现的公开 API

位置: pyfcstm/diagram/__init__.py:47-48, pyfcstm/diagram/engine.py:39-42
问题: __init__.py 的 module docstring 和 engine.py 的 class docstring 中 Example 使用 DiagramAssetEngine 正确。但 PR 描述中说"明确不包含 StateMachine.diagram()model.show()",当前 pydoc 也没有引用这些。经检查,所有 Example 均使用 DiagramAssetEngine无实际问题


M3. diagram_assets_verify 中的网络检查 (provenance) 与本地检查串行运行

位置: Makefile:279 (diagram_assets_verify target)
问题: diagram_assets_verify 依次运行 diagram_assets_checkdiagram_rendering_checkdiagram_parity_checkdiagram_engine_floordiagram_provenance_check。其中 diagram_provenance_check 是纯网络操作(下载 npm tarball 和 source archive),而其他检查是纯本地操作。串行运行意味着网络延迟阻塞本地检查的快速反馈。

建议: 可考虑拆分为 diagram_assets_verify_local + diagram_assets_verify_network,或使用 Make 的并行机制 (-j)。不阻塞。


M4. _MAX_RENDER_PNG_BASE64_BYTES 边界检查存在数学恒真式

位置: pyfcstm/diagram/engine.py:1407-1410
分析:

  • _MAX_RENDER_PNG_BYTES = 33_554_432% 3 = 2required_padding = '='
  • 合法 base64 编码的 33_554_432 字节产生 44_739_244 字符,以 = 结尾
  • len == 44_739_244ends_with('=') 时合法通过
  • len == 44_739_244!ends_with('=') 时拒绝 — 但此场景在数学上不可能出现(44_739_244 字符的合法 base64 必然以 = 结尾)
  • 换言之:required_padding 分支是 belt-and-suspenders,运行时不可达,但不构成缺陷

结论: 无害防御性代码,不阻塞。


已验证通过的领域

领域 验证方法 结果
esbuild 快路径 路径追踪所有 corrupt/stale 场景 ✅ 正确 fallback 到 npm ci
PNG chunk 白名单 (IEND/IDAT) 手动构造恶意 PNG + 运行时验证 ✅ 正确拒绝所有非法 chunk
Closed SVG 合同 代码审查 allow-list + marker 合同 ✅ 严密
CJK font 注册 & locale 切换 68 个 pytest 全部通过 ✅ 5 个 locale 正确渲染
Arrow endpoint (marker-end) test_rendered_a_to_b_resvg_tip_meets_target_border ✅ 通过
MiniRacer timeout/context 生命周期 test_engine_restarts_context_after_native_timeout ✅ 通过
Package checker 旧路径拒绝 check_diagram_package.py LEGACY_PATH_PREFIXES ✅ 正确拒绝 pyfcstm/assetspyfcstm/diagram_runtime
Official resvg 2.6.2 provenance tools/check_diagram_provenance.py --skip-tarball ✅ source archive SHA256 匹配
Reference archive 提取安全 _safe_extract 拒绝 symlink/hardlink/path traversal ✅ 严密
Base64 padding 边界 数学分析 + 手动测试 ✅ 无溢出窗口
Self-check 运行 python3 tools/build_diagram_assets.py --check + tools/check_diagram_rendering.py --check ✅ 通过
Diagram rendering parity tools/check_diagram_rendering.py --check ✅ 通过
Engine floor (timeout reset) tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset ✅ 通过 (15 cases, SVG+PNG+expanded SVG)

总结

等级 数量 说明
C (Critical) 0
I (Important) 3 Self-check CRC 覆盖缺口、fetch retry 不一致、Gist 依赖
M (Minor) 4 Self-check 最小 PNG 限制、串行检查、数学恒真式、pydoc (无实际缺陷)

结论: PR #386 head 710ee261 在审查范围内无关键缺陷。3 个 Important 项均为防御性改进建议(self-check 覆盖率、retry 一致性、长期 immutable storage),不阻塞合入。所有核心安全边界(PNG/SVG 合同、esbuild 锁定、字体打包、MiniRacer 生命周期、provenance 溯源)均已正确实现并通过验证。

推荐: Ready-to-merge 到 dev/python-diagram-umbrella


DeepSeek Reviewer 通过 codex-deepseek exec 执行,审查时长 ~12 分钟

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

claude reviewer — 最终强对抗审查(head 09ce4b9b

身份:claude reviewer(独立自动审查,gh auth token --user HansBuglogin=HansBug, email=hansbug@buaa.edu.cn;未使用 sub-agent,未修改 tracked 文件)。
基线:head 09ce4b9bb4cc7da42ac5255f752d4bb130ff3762,base dev/python-diagram-umbrella@bcadb92a9cc510de419b72f1a8935c377e9e6978。相对 base 共 15 提交、24 文件、+26216/-248 行;不沿用旧 head 结论。
方法:白盒读 pyfcstm/diagram/engine.pytools/check_diagram_*.pytools/fetch_diagram_reference.pytools/diagram_assets/asset-lock.jsonreference-lock.jsonMakefile.github/workflows/test.yml、NOTICE/README;本地执行 self-check、hostile SVG/PNG/scale 构造、端到端 render_svg/png/expand_svg、真实资产 SHA256 对齐、CI 结果核对。


结论:C = 0,I = 0,M = 3(非阻塞)


已独立验证的关键合同

结果
@resvg/resvg-wasm@2.6.2 provenance/license/hash pyfcstm/diagram/assets/resvg-binding.js SHA256 = 590115ae25dead0d688da192f2d31586cdf1f8c70fe294919419c168e03e5c42resvg.wasm SHA256 = 22bf6e9f9a100d972da0411a69c5ba504367fc1fa87b3b64e3f35e53926d2d70,与 tools/diagram_assets/asset-lock.jsonbindingSha256/wasmSha256 精确一致;MPL-2.0/EPL-2.0/OFL-1.1 三份 LICENSE 与 NOTICE 齐备并覆盖 elkjs、resvg-wasm、JetBrains Mono、Noto Sans SC/TC/HK/JP/KR
Closed SVG allow-list 用 11 组 hostile 输入(<?xml, <!DOCTYPE, <!ENTITY, <script>, onclick, <a href="javascript:">, style, <image href="data:">, xlink:href, url(https://…), url(#missing))全部被 DiagramAssetError 拒绝,marker orient!="auto"refX!="10" 各自命中显式契约错误
Raw SVG 拒绝 render_png("<svg …/>") / expand_svg("<svg …/>") 命中 ValueError: render_png/expand_svg require a DiagramData request; raw SVG input is not supported
PNG 边界 手工构造覆盖 duplicate/misplaced IHDR、无 IDAT、IEND 长度非 0、trailing bytes、CRC 破坏、unknown critical/ancillary chunk、scanline mismatch、纯白无 ink、bit_depth/color_type 非 RGBA8、base64 上限;_decode_png_rgba 全部拒绝,有 ink 的 1×1 通过
Base64 encoded PNG 上限 base64.b64decode 之前即命中 _MAX_RENDER_PNG_BASE64_BYTES 及 padding-窗口检查,多一字节窗口路径被覆盖
Scale/canvas/pixel/RGBA 上限 scale=0/-1/NaN/inf/True/4.1 均被 ValueError/DiagramRenderLimitError 拒绝;_checked_render_dimensions 计算与实际 PNG 尺寸精确一致
Marker geometry _validate_svg_tree 强制单 marker、orient="auto"refX="10"、拒绝 marker-start,与 arrow 端点检查(tools/check_diagram_rendering.py tipError/normalCosine)匹配;make diagram_parity_check 使用 35/306 corpus
CJK 五地区 _CJK_FONT_ASSET_PATHS/_CJK_FONT_FAMILIES 覆盖 sc/tc/hk/jp/kr Regular+Bold;每次切换 locale 会 _discard_context(),避免 WASM heap 中残留其他 face
Dual MiniRacer runtime mini-racerpy-mini-racer 同装时 DiagramEngineConflictErrortimeout_sec vs 毫秒的模式切换用 inspect.signature(MiniRacer.eval).parameters 检测;Python 3.8 CI 强制回落到 mini-racer==0.7.0
Timeout/OOM/discard/fail-loud _DiagramEvaluationInterruptedError 触发 _discard_context() 并按 request 分流为 DiagramRenderErrorDiagramAssetErrorrender_svg deadline 到达后同样 discard;本地 python tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset 通过(15 case × {svg,png,expanded-svg},timeout 复位路径正常)
Provenance 网络门禁 tools/check_diagram_provenance.py 校验 npm tarball、resvg-js commit 归档、patched resvg 归档、LICENSE/Cargo.toml/patched-resvg/LICENSE.txt 三份 SHA256;下载器 60s 超时;_download_archive 支持 codeloadarchive 双端点
Reference fetch 重试 tools/fetch_diagram_reference.py --check 通过 IncompleteRead → OSError → Response 三阶段 flaky、单次 503 → success、404 立即失败三条自检;_safe_extract 拒绝 symlink/hardlink/非普通成员及 path traversal
Make/CI 硬门禁 make unittest/make template_unittest/make package 均依赖 build_assets diagram_assets_check tplDiagram assets (Python 3.7/3.8/3.11)template-suite-gate 的 needs 项之一,diagram_assets_verify = diagram_assets_check + diagram_rendering_check + diagram_parity_check + diagram_engine_floor + diagram_provenance_check + diagram_reference_checkFresh wheel/sdist self-check 全平台通过
测试边界 test/diagram/test_assets.py 仅使用 pytest + 公共 pyfcstm.diagram API 与 pyfcstm.diagram.engine 私有常量;未触碰 jsfcstm test tree;make test_boundary_check pass
Pydoc / reST 6 个公共类与 3 个渲染方法均含 :param/:type/:return/:rtype/:raises/Example::__init__.py 使用 .. list-table:: 表格 roadmap,符合 CLAUDE.md 规范
CI 状态 57 项 required checks 全 pass(Diagram assets 3.7/3.8/3.11、Code test × 33、Template full × 5、Fresh CLI/wheel/sdist、jsfcstm、Docs HTML/PDF、Bilingual Docs PDF、CLI Build 及 Release、template-suite-gate 全绿)
干净树 git status --porcelain 只有 untracked .agents/(预置外部目录),无 tracked 改动

本地实测:SKIP_SLOW_TESTS=1 pytest test/diagram/test_assets.py -q68 passed / 46spython tools/check_diagram_rendering.py --check / tools/fetch_diagram_reference.py --check / tools/check_diagram_assets.py --check / tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset 全部通过。


C — 0

未发现阻塞性缺陷。所有闭包(allow-list、PNG 契约、raw SVG 拒绝、marker 几何、CJK、runtime floor、provenance、reference parity、fail-loud、Make/CI 硬门禁)均可复现并对抗性构造验证过。

I — 0

未发现应在 merge 前解决的重要问题。上一轮 codex reviewer 指出的 "encoded base64 PNG 上限需前置" 已在 pyfcstm/diagram/engine.py:1407-1416 显式实现,独立复现验证正确(_MAX_RENDER_PNG_BASE64_BYTES + 1 及 padding-窗口测试均直接命中 DiagramRenderLimitError)。上一轮 "reference 网络下载 mid-body 断流 + 连接断开 + 第三次成功" 与 "esbuild 从 editors/jsfcstm 本地锁定解析" 均可复现且被 --check 固化。

M — 3(观察,均非阻塞)

M1pyfcstm/diagram/engine.py:1383-1391render_png(scale=...) 允许任何 float() 可解析的类型(例如 " 2.0 ""2e10")。对空格串我可复现被接受、对上限外的字符串仍走 DiagramRenderLimitError 拒绝,因此没有安全影响;但 docstring :type scale: floatmax_memory 里显式拒绝 bool 的输入卫生风格不一致,未来 PR-C/D 增加公共 API 时可考虑显式 isinstance(scale, (int, float)) 拒绝 str

M2tools/fetch_diagram_reference.py:52raise RuntimeError("unable to download diagram reference archive") 是死代码——最后一次 attempt 的 except 分支永远会 raise。无功能影响,属可读性小瑕疵。

M3pyfcstm.diagram.engine._CanonicalSvg 是内部标记类型;_canonical_input(_CanonicalSvg) 会直接返回而不再走 SVG allow-list。虽然它 leading-underscore、未导出 __all__、无用户文档,但仍可通过 from pyfcstm.diagram.engine import _CanonicalSvg 触及。当前风险为 0(Python 私有约定),值得在未来公共 API 上台前把这条 fast-path 收敛为"只接受内部产出的 sentinel"(例如加 context_token 绑定或改成集合成员判断)。


独立复现命令

git rev-parse HEAD            # 09ce4b9b
sha256sum pyfcstm/diagram/assets/{renderer.js,resvg-binding.js,resvg.wasm}
python tools/check_diagram_rendering.py --check
python tools/fetch_diagram_reference.py --check
python tools/check_diagram_assets.py --check
python tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset
SKIP_SLOW_TESTS=1 pytest test/diagram/test_assets.py -q
make test_boundary_check

hostile 构造脚本(本审查已运行):

  • _validate_canonical_svg 输入:<!DOCTYPE<!ENTITY<?xml<script>onclickjavascript:style<image href="data:…">xlink:hrefurl(https://…)、broken url(#…);marker orient="90"refX="5" — 全部按契约拒绝。
  • _decode_png_rgba 输入:pre-IHDR IDAT / duplicate IHDR / IEND non-empty / trailing bytes / bad-CRC IDAT / tEXt ancillary / unknown critical XYZW / 全白 no-ink / scanline size mismatch — 全部按契约拒绝,1×1 red RGBA PNG 通过。
  • render_png(scale=…)0 / -1 / NaN / inf / True / 4.1 / "abc" / "1e1000" / "2e10" 全部拒绝;" 2.0 ""2.0"float() 通过(对应 M1)。

Ready-to-merge 建议

  • 可标记 ready-to-merge:C=0、I=0;M1/M2/M3 均非阻塞,可作为 PR-C 或后续 hardening 提案。
  • 本 PR 定位为 official resvg 资产收敛底座,不含公共 StateMachine.diagram API 与 CLI;请维护者按伞 PR 规划:建立 Python Diagram API 与共享离线渲染链路 #383 计划节奏合入 umbrella,不合入 main

结论:LGTM。C=0 / I=0 / M=3(非阻塞)。

— claude reviewer

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

codex reviewer

审查对象:PR #386,当前精确 HEAD 09ce4b9bb4cc7da42ac5255f752d4bb130ff3762(base bcadb92a)。本轮没有修改 tracked files,也没有执行 merge;工作树只有既有未跟踪的 .agents/

结论

  • Critical:0
  • Important:0
  • Minor:3

Minor

  1. 上游 current-state 仍是旧 hash。 PR feat(diagram): close official resvg asset contract #386 正文已同步到 09ce4b9b,但伞 PR #383issue #89 的当前状态仍写 PR-B head 98fd2158。这不会改变代码行为,但会让验收者误读审查对象和证据,建议把两处 current-state、测试计数和“正在等待 CI”表述更新到本 SHA;旧 hash 可以保留在历史说明中。

  2. esbuild 本地复用检查仍不是字节级 provenance 合同。 tools/build_diagram_assets.py::_installed_esbuild_is_usable()(约 127-162 行)检查 lock/package 元数据、版本、wrapper 和 npx --no-install esbuild --version,但没有用 lockfile 的 integrity 校验已存在的 esbuild bytes,也没有给版本探测 subprocess 设置 timeout。CI 的 clean npm ci 路径降低了实际风险;在被篡改的持久化 node_modules 或挂起的 wrapper 场景,构建仍可能误复用或卡住。建议后续补充完整性校验和有界执行。

  3. expand_svg() 的错误路径未与 render_svg() 保持 context discard 一致。 pyfcstm/diagram/engine.py:1449-1490 在 malformed/closed-dialect expanded SVG 上直接抛出 _render_failure(),而 render_svg() 在对应校验失败前明确调用 _discard_context()。当前测试覆盖了错误映射,但若 native context 已保留 renderer 状态,后续请求可能继承不必要的旧上下文。建议在这两个 expanded-output failure 分支先 discard,再映射错误。

本地可复现实据

  • pytest test/diagram/test_assets.py -q68 passed
  • SKIP_SLOW_TESTS=1 make unittest(Python 3.7.17):46838 passed, 756 skipped, 106 deselected, 5 warnings(636.45s)。
  • make DIAGRAM_REFERENCE=/tmp/pr386-final-reference/reference.json diagram_assets_verify:canonical 15 layouts / 130 arrows,shared 20 / 176,合计 35 layouts / 306 arrows;CJK、expanded parity、memory、runtime floor、provenance 全部通过。
  • python tools/check_diagram_rendering.py --checkpython tools/build_diagram_assets.py --checkpython tools/fetch_diagram_reference.py --checkmake diagram_reference_checkpython tools/check_diagram_package.py --checkpython tools/check_diagram_provenance.pyruff checkruff format --checkmake test_boundary_checkgit diff --check origin/dev/python-diagram-umbrella..HEAD 均通过。
  • 负例也实际验证:esbuild lock/package 版本不一致返回 False;非零 IEND、IEND 后尾随 chunk、未知 chunk、超限 PNG 分别被拒绝,并得到对应 ValueError

远端检查

在当前 SHA 上执行 GH_TOKEN="$(gh auth token --user HansBug)" gh pr checks 386:所有已列 required checks 均 pass,包括 Diagram assets Python 3.7/3.8/3.11、全 Python Code Test 矩阵、Template full (c/c_poll/cpp/cpp_poll/python)、package/fresh CLI、Docs HTML/PDF、jsfcstm、template-suite-gate;未观察到 failure 或 pending。

以上三项均为可后续修复的 Minor,不构成阻止合入 umbrella 的 Critical/Important 风险。按 PR 正文约定,本评论只完成审查,不执行 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

deepseek reviewer — 最终独立强对抗审查报告

审查范围: PR #386 head 09ce4b9b vs base bcadb92a(umbrella 分支)
关联: 伞 PR #383、PR-A #384、issue #89、issue/PR #376/#377
审查时间: 2026-07-20
方法: 白盒逐行审计 + 12 向量 SVG/PNG 攻击注入 + full slow-skip test suite + provenance/rendering/engine-floor/package gate 实际运行


总评

代码质量扎实。official @resvg/resvg-wasm@2.6.2 的 npm/NOTICE/license/source provenance 可通过 asset-lock.jsoncheck_diagram_provenance.py 完整追溯(含 tarball SHA256、binding/wasm SHA256、source commit、patched source commit)。closed SVG 的 XML/URL/元素/属性/尺寸/深度合同经受 12 个攻击向量全部正确拒绝。PNG CRC/IHDR/IDAT/IEND/scanline/ink 验证链完整且与 parity 工具一致。raw SVG 入口正确拒绝。CJK 五 locale Regular/Bold 13 字体覆盖正确。reference 防伪造机制(_CUSTOM_REFERENCE_FILES frozen hash + backend=custom-resvg-0.37 check)设计合理。timeout/OOM/context discard/错误分类/开发&发布恢复提示正确。CI workflow 中 diagram_assets job 是 template_suite_gate 的 hard dependency。


结论

C = 0 / I = 0 / M = 2,审查通过,M 不阻塞 merge。


Minor (M)

M-1: _CUSTOM_REFERENCE_FILESresvg-bridge.js 的 hash 7620bd2e 与 git 历史中所有已知版本均不匹配

证据:

# 验证 asset-lock.json hash 匹配 umbrella base(正确)
$ git show bcadb92a:tools/diagram_assets/asset-lock.json | sha256sum
9147139082850957bd24b7845dfe9ea460722eeff20a7e1af84c971156dbf1bc  # ✓ 匹配

# resvg-bridge.js hash 不匹配任何已知版本
$ for c in 98fd2158 241299ba 9d65adfb 5b758f01 11b0d06f; do
    echo -n "$c: "; git show $c:tools/diagram_assets/resvg-bridge.js | sha256sum
  done
98fd2158: 164738485be5e6af09ed9878e7fc75bb9534af56d839d3fec88349caa40a4b14  # 当前 official
241299ba: 5ed8588c6e4adad85b756adf57861099ab7ee8c2e09a829b2193c679f9acb7f3
9d65adfb: 72d28f99f0b90bf26c29fd2c95fb48a12c711e16f732ce1630cb27306429bf52  # PR-A 原版
5b758f01: 0dbb3aa8ecc333bb60851fbaf8b94bfa428f9867a6172136029ee552939c0dbd
11b0d06f: 72d28f99f0b90bf26c29fd2c95fb48a12c711e16f732ce1630cb27306429bf52  # PR-A head

_CUSTOM_REFERENCE_FILES 中声明的 hash 7620bd2ee02d63baa665946c8d5b8c741f49cfdf54c60fad0afdf0689784ef1d 与上述全部不匹配。

分析: 该 hash 可能是从 frozen reference bundle 内的实际文件计算得来(reference bundle 内的 resvg-bridge.js 可能在 capture 时有微小差异,例如 /* global resvg037 */ vs /* global resvg */)。如果 frozen reference bundle 内的 provenance.files 与此一致,则 parity gate 在 DIAGRAM_REFERENCE 实际运行时不会误拒绝——因为 _assert_custom_reference_provenance 比对的是 reference JSON 内声明的 hash 与代码中硬编码的 hash。但当前无法从 git 历史自证该 hash 来源,建议在 _CUSTOM_REFERENCE_FILES 上方添加注释说明各 hash 是如何从 frozen reference bundle 提取的,方便未来维护者验证。

严重程度: M — 不影响实际 parity gate 运行(因为 frozen reference bundle 已固化),但降低代码可审计性。


M-2: _DANGEROUS_URI_SCHEME_RE 包含 https? 但缺少安全注释

文件: pyfcstm/diagram/engine.py line 335

_DANGEROUS_URI_SCHEME_RE = re.compile(
    r"(?:javascript|vbscript|data|blob|about|file|https?|ftp|ws|wss|"
    r"chrome|chrome-extension):",
    re.IGNORECASE,
)

https? 在此 regex 中,但因 xmlns 不在任何元素的 attribute allowlist 中,所以 SVG 元素上的 xmlns="http://www.w3.org/2000/svg" 不会进入此检查(会被 name not in attrs 提前拒绝)。当前运行正确,但 regex 读起来像会误伤合法 SVG 属性。建议加一行注释说明保护机制:

# https? is safe here because xmlns (the only SVG attribute that embeds
# http://www.w3.org/2000/svg) is excluded by the per-element attribute
# allowlist. The scheme check guards against future allowlist additions
# that could accidentally admit executable or exfiltrating URIs.

严重程度: M — 不影响正确性,但降低可读性。


未发现问题(已验证安全)

以下区域经过白盒逐行审计或实际攻击测试,确认为正确实现:

区域 验证方法
12 向量 SVG 安全合同(XSS/entity/外部URL/foreignObject/script/event handler/style/data URI/超大SVG) 全部 12 攻击被 _validate_canonical_svg 正确拒绝,见下方
raw SVG 入口拒绝 _canonical_input("raw string")ValueError
PNG CRC + IHDR/IDAT/IEND 合同 代码审计 + parity 工具 _self_check_png_parser pass
PNG scanline filter 0-4 全覆盖 _decode_png_rgba 代码审计 + coverage
PNG base64 padding 边界攻击 手动验证 _MAX_RENDER_PNG_BASE64_BYTES 边界公式(%3==0/1/2)
marker-end orient=auto refX=10 合同 _validate_svg_tree marker 强制检查
CJK SC/TC/HK/JP/KR Regular/Bold 13 字体 _CJK_LOCALE_PATTERN 全部 10 测试通过
dual MiniRacer 拒绝 _create_context 检测到两者同时安装时 raise ✓
timeout → context discard _eval 捕获 interrupt → _discard_context
错误分类(asset vs render vs limit) _asset_failure / _render_failure / DiagramRenderLimitError 三分法 ✓
开发/发布恢复提示 _is_development_checkout()make build_assets vs issue URL ✓
reference 防伪造 _CUSTOM_REFERENCE_FILES + backend=custom-resvg-0.37 + _self_check_reference_provenance pass
broad except Exception 合规 _eval 中一处,带 isinstance(err, self._interrupt_errors) guard + re-raise ✓
无 TODO/FIXME/HACK grep 结果为空 ✓

实际运行证据

# 68 diagram tests
$ pytest test/diagram/test_assets.py -q
68 passed in 39.73s

# Full slow-skip test suite
$ SKIP_SLOW_TESTS=1 make unittest
46948 passed, 752 skipped

# Engine floor (15 cases × 3 formats)
$ python tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset
{"cases": 15, "counts": {"expanded-svg": 15, "png": 15, "svg": 15}, ...}

# Canonical arrows (15 layouts, 130 arrows)
$ python tools/check_diagram_rendering.py --corpus canonical-arrows.json ...
{"arrows": 130, "directions": {"down": 78, "left": 5, "right": 32, "up": 15}, "layouts": 15}

# Shared layouts (20 layouts, 176 arrows)
$ python tools/check_diagram_rendering.py --corpus shared-layouts.json ...
{"arrows": 176, "directions": {"down": 75, "left": 13, "right": 75, "up": 13}, "layouts": 20}

# Provenance
$ python tools/check_diagram_provenance.py
{"patchedSourceArchiveSha256": "08d15a07...", "sourceArchiveSha256": "7ce86974...", "tarballSha256": "ff51acbb..."}

# Package + diagram checker
$ make package
diagram package archives: 1 wheel(s), 1 sdist(s) passed

# Boundaries
$ make test_boundary_check
test boundary check passed: no violations found

# reST
$ make rst_auto
Generated: ./docs/source/api_doc_en.rst
Generated: ./docs/source/api_doc_zh.rst

# Self-checks
PNG parser self-check: PASSED
Reference provenance self-check: PASSED
Reference comparison self-check: PASSED

12 向量 SVG 攻击全部拦截

OK: XSS onclick      → DiagramAssetError: rejected attribute onclick
OK: <script>         → DiagramAssetError: rejected element: script
OK: <a href=javascript:> → DiagramAssetError: rejected element: a
OK: url(http://...)  → DiagramAssetError: rejected an external URL
OK: <!DOCTYPE/ENTITY → DiagramAssetError: rejected XML declarations or entities
OK: <!-- comment -->  → DiagramAssetError: rejected XML declarations or entities
OK: <?xml ...?>      → DiagramAssetError: rejected XML declarations or entities
OK: <foreignObject>  → DiagramAssetError: rejected element: foreignObject
OK: style="..."      → DiagramAssetError: rejected attribute style
OK: xlink:href       → DiagramAssetError: rejected malformed XML (unbound prefix)
OK: data: URI        → DiagramAssetError: rejected malformed XML (invalid token)
OK: >16MB SVG        → DiagramAssetError: rejected an oversized SVG

与上游 PR/issue 计划的对照

计划项 状态
official @resvg/resvg-wasm@2.6.2 ✅ asset-lock.json verified
closed SVG dialect ✅ 12-attack pass
PNG bounded contract ✅ CRC/IHDR/IDAT/IEND/scanline/ink
raw SVG entry rejected _canonical_input gate
CJK 五 locale Regular/Bold ✅ 13 fonts, 10 pattern tests
dual V8 + timeout/OOM/context _discard_context + error taxonomy
reference custom-baseline 防伪造 ✅ mechanism correct (M-1 note)
parity strict/clip-ID-only ✅ self-check passed
Makefile/CI hard gate diagram_assetstemplate_suite_gate
wheel/sdist package make package passed
pydoc/reST make rst_auto passed
PR #377 arrow endpoint 继承 ✅ marker orient=auto refX=10 contract

deepseek reviewer, 2026-07-20

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

PR-B 当前改动、三运行时实测与可视证据(09ce4b9b

这份更新回答三个问题:本 PR 实际改了什么、我如何在低/高版本 Python 上真实运行、当前生成结果是否有可直接检查的图。完整归档已更新到:

1. 当前 PR 实际改了什么

相对 dev/python-diagram-umbrella@bcadb92a,当前 head 修改 24 个文件,约 26216 insertions / 248 deletions

  1. 官方 resvg 构建链路make build_assets 从锁定的 jsfcstm 源码和本地 esbuild@0.28.1 构建 ES2017 IIFE,恢复并校验官方 @resvg/resvg-wasm@2.6.2 binding/WASM,不使用自制 resvg 二进制,也不通过 npx --yes 临时联网取构建器。
  2. Python 双运行时桥接:Python 3.7 使用 py-mini-racer legacy API;Python 3.8+ 使用 mini-racer modern API。实现冲突检测、timeout、context 丢弃/重建、内存所有权和资源损坏时的 loud error。
  3. 封闭输出合同:PNG 检查 signature、IHDR、CRC、IDAT、IEND、RGBA、scanline、尺寸、scale 和编码/解码上限;canonical/expanded SVG 检查 XML、allow-list、URL/script、尺寸、深度和输出上限。任意 raw SVG 入口明确拒绝。
  4. 箭头与视觉合同:维护语料合计 35 layouts / 306 arrows;逐箭头检查 target-border tip、最后一段沿目标边框法向接入、marker-endorient="auto"refX="10",覆盖普通 A→B、自循环、LR/TB、四方向、forced、深层组合状态和 crossing。
  5. CJK 与交付边界:wheel/sdist/CLI 带 JetBrains Mono 3 个字体及 Noto Sans SC/TC/HK/JP/KR Regular/Bold 10 个字体,并校验字体、WASM、manifest、许可证和 package inventory。
  6. 维护门禁:新增 rendering、engine-floor、provenance、frozen custom-reference parity、下载重试/安全解包和 package 检查;CI 独立覆盖 Python 3.7、3.8、3.11。

本 PR 没有实现 StateMachine.diagram()model.show()、公共 export facade、Notebook/HTML viewer、用户 CLI 或 PDF API;这里仍只是后续 PR-C/PR-D 使用的离线资产与运行时底座。

2. 三个真实 Python 环境

Python MiniRacer 路径 engine-floor 完整视觉语料
3.7.17 py-mini-racer==0.6.0 legacy 15 cases × 3 formats 35 layouts / 306 arrows
3.8.1 mini-racer==0.7.0 modern 最低版本 15 cases × 3 formats 35 layouts / 306 arrows
3.11.1 mini-racer==0.14.1 modern 当前版本 15 cases × 3 formats 35 layouts / 306 arrows

每个环境都独立生成 35 个 canonical SVG、35 个 PNG、35 个 expanded SVG 和 SC/TC/HK/JP/KR 五地区 CJK 输出。不是只做 import smoke。

3. 我如何实测

先构建和检查当前 head 的运行资源:

make build_assets
make diagram_assets_check

三个环境分别执行 runtime floor、三格式渲染和 timeout context-reset:

PYTHONPATH=. python3.7 tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset
PYTHONPATH=. python3.8 tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset
PYTHONPATH=. python3.11 tools/check_diagram_engine_floor.py --all-cases --check-timeout-reset

三个环境再分别生成完整视觉目录:

PYTHONPATH=. pythonX.Y tools/check_diagram_rendering.py \
  --corpus tools/diagram_assets/corpus/canonical-arrows.json \
  --corpus tools/diagram_assets/corpus/shared-layouts.json \
  --expected-layouts 35 --expected-arrows 306 \
  --expected-directions right=107,down=153,left=18,up=28 \
  --check-cjk --report-dir /tmp/prb-visual-pyXY

逐案例 SHA-256 比较结果:

canonical SVG mismatch: 0
PNG mismatch: 0
expanded SVG mismatch: 0
CJK PNG mismatch: 0
CJK expanded SVG mismatch: 0

方向覆盖为 right=107, down=153, left=18, up=28。三个 engine-floor 的 timeout 路径都实际触发中断,并验证旧 context 被丢弃、下一次调用在新 context 中恢复。

此外还完成:

pytest test/diagram/test_assets.py -q
68 passed

SKIP_SLOW_TESTS=1 make unittest
46838 passed, 756 skipped, 106 deselected

make DIAGRAM_REFERENCE=/tmp/pyfcstm-custom-reference-final/reference.json diagram_assets_verify
35 layouts / 306 arrows、CJK、parity、memory、runtime floor、provenance 全部通过

make package
wheel、sdist、fresh-install SVG/PNG/expanded SVG 与 package inventory 通过

4. 当前实际生成图

三运行时一致性汇总

Python 3.7.17:12 个代表布局

Python 3.7 代表布局

Python 3.8.1:12 个代表布局

Python 3.8 代表布局

Python 3.11.1:12 个代表布局

Python 3.11 代表布局

全部 35 个布局:

五地区 CJK:

SC/TC/HK/JP/KR

12 个未缩放的原始 expanded SVG,可逐个放大检查端点:

  1. simple leaf / 普通 A→B
  2. nested HVAC
  3. deep nesting
  4. many transitions / 四方向
  5. forced expansion / LR dark
  6. guard and effect
  7. absolute event
  8. rich state details
  9. traffic light stubs / LR dark
  10. nested crossing / LR dark
  11. CJK labels
  12. long labels

5. 视觉验收和 Gist 证据可靠性

  • 最初联系表使用 SVG 内嵌 data:image/png;本地 Chrome 可见,但 Gist raw 的严格 CSP 会阻止。最终版本改为直接组合各运行时实际生成的 expanded SVG path,并隔离内部 ID;不含 data: 或外部资源。
  • 三个 12-case 图页和三个 35-case 图页均重新栅格化;去掉仅含版本号的标题区后,Python 3.7 对 3.8、3.7 对 3.11 的主体像素差都为 0
  • 人工检查普通 A→B、自循环、LR/TB、forced、nested crossing、四方向、dark/light、长标签和深层组合状态,未见端点贴边、裁切、异常重排或 backend 切换副作用。
  • SC/TC/HK/JP/KR 均显示实际字形,无 tofu 方框;中文长标签没有越出 block。联系表的说明文字已改为中文,fixture ID 和状态名保留原始被测字面量。
  • Gist 最终为 54 个文件;全新 clone 的 head 与 API 最新版本均为 2abd897…SHA256SUMS 的 53 个内容文件全部通过,25 个 SVG 均通过 XML 解析和精确外部资源扫描。
  • Chrome 已直接打开远端 raw 的中文汇总图和 Python 3.7 图;Python 3.8、3.11 与 CJK 图先从 raw URL 下载并逐字节核对 SHA-256,再由同一 Chrome 打开。这样不会把大 SVG 的网络等待超时误判成渲染失败。

当前 PR 仍保持 OPEN / CLEAN,本次只补证据和评论,没有执行 merge。

@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

已按意见把资产元数据文档收敛到原有英文基线,提交为 a176c232

  • pyfcstm/diagram/assets/NOTICE.txt:恢复原英文全文,只把 custom resvg 0.37 条目替换为官方 @resvg/resvg-wasm 2.6.2 的必要来源和许可证说明。
  • pyfcstm/diagram/assets/README.md:恢复原英文结构和原有 CJK 文件清单,只保留官方 npm lock 来源、强制 frozen-reference parity 命令,以及 resvg-binding.js / resvg.wasm 的官方 2.6.2 身份。
  • 已删除整篇中文翻译、维护工具长清单、raw SVG 合同重复说明和 reference provenance 重复段落。
  • 相对 umbrella base,这两个文件最终只剩 14 insertions / 8 deletions,且无中文残留。

本地验证:

make diagram_assets_check
diagram asset builder: deterministic and safety self-check passed
diagram assets: hashes, size, syntax, and ignore rules passed

python tools/check_diagram_package.py --check
diagram package checker: adversarial self-check passed

git diff --check
passed

这是文档最小化提交,没有修改运行时代码、语料、package inventory 或生成资产;当前 head 的 GitHub checks 正在重跑,PR 未执行 merge。

HansBug added 8 commits July 20, 2026 14:44
- detect real document type declarations without rejecting comment or CDATA literals
- reject non-mapping and non-finite DiagramData requests at the Python boundary
- add legacy-compatible regression coverage for both contracts
Keep the first MiniRacer error headline while dropping evaluated source lines and stack text. Add a real JavaScript failure regression to preserve actionable causes without exposing the evaluated request.
Evaluate packaged JavaScript through a temporary in-context loader so legacy py-mini-racer can report syntax corruption instead of aborting when a timeout is configured. Remove the loader after startup and keep eval/Function blocked for renderer requests.
@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

PR-B final current-state review (b78ae178)

The final candidate is pushed and reviewed against the actual remote head:

  • Head: b78ae178b7649aea48b8a7f313642e1caf863f08
  • Base: bcadb92a9cc510de419b72f1a8935c377e9e6978
  • Remote CI: 60 checks passed, 1 expected Docs generated resources skip;
    template-suite-gate passed. Codecov is complete at 94.54% (25 sessions).
  • Multi-agent result: code APPROVE; verifier READY; architecture and PR-C/PR-D scope CLEAR.
  • Evidence archive: https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450#file-pr-b-b78ae178-md

Findings closed after the previous review

  1. Real DTD/entity declarations now use the XML parser's doctype callback; legal comment
    and CDATA declaration literals are accepted.
  2. Non-mapping requests and NaN/positive Infinity/negative Infinity fail at the Python
    JSON boundary.
  3. MiniRacer errors retain the actionable headline without embedding evaluated source.
  4. Python 3.7 legacy py-mini-racer no longer aborts when malformed packaged JS is
    evaluated with an explicit timeout. A temporary startup-only loader returns a
    SyntaxError asset failure, is deleted after bundle load, and leaves global eval
    and Function blocked.

Verification

  • Python 3.7 legacy and modern MiniRacer diagram suites: 61 passed each.
  • Python 3.7 / Python 3.8 floor / current modern runtime: 15 cases each across
    SVG/PNG/expanded SVG, with timeout reset.
  • 15 layouts / 130 arrows and 20 layouts / 176 arrows passed.
  • 35 layouts / 306 arrows frozen-reference parity, five-locale CJK, memory, provenance,
    and expanded SVG comparison passed.
  • wheel/sdist inventory, fresh-wheel SVG/PNG smoke, ruff check/format, compileall, diff-check, and
    test-boundary checks passed.
  • The final three regression cases cover renderer-output DTD rejection and legal
    comment/CDATA declaration literals.

No P0/P1/P2 blocker remains. No model/public Diagram API/CLI/viewer/Notebook/PDF/ELK
work entered this PR. PR-B is ready to merge into the umbrella branch after the recorded
CI state is green; the umbrella itself remains blocked from main pending PR-C/PR-D.

@HansBug
HansBug merged commit aa74516 into dev/python-diagram-umbrella Jul 20, 2026
61 checks passed
@HansBug

HansBug commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

PR-B 已完成 merge。

  • 合入时间:2026-07-20T12:45:18Z
  • 实现 head:b78ae178b7649aea48b8a7f313642e1caf863f08
  • 目标分支:dev/python-diagram-umbrella
  • merge commit:aa745161980799a4ba9e56e3654040339f7faa98
  • merge 前门禁:60 checks passed、1 个设计内 skipped、template-suite-gate passed、Codecov 94.54%(25 sessions)
  • 最终 review:code APPROVE、verifier READY、architecture/scope CLEAR
  • 最终证据归档:https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450

本 PR 只合入 umbrella,没有直接进入 main。后续公共 Diagram/API/browser 与 headless delivery 仍由 PR-C/PR-D 完成。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: packaging Packages, bootstrap/self-check, frozen artifacts, resources, and publishing. area: tooling Repository-local tools, linting, and maintenance utilities. area: visualization PlantUML, ELK, diagrams, and rendered images. kind: maintenance Repository health, cleanup, testing, or engineering maintenance. role: sub-pr Implements a bounded slice on an umbrella or stacked branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant