Skip to content

Releases: shdbl/mpltweak

v0.1.11

Choose a tag to compare

@shdbl shdbl released this 14 Oct 13:04

0.1.11

这一版用户能感知到的变化

  • 退出码收敛为 0 / 1 / 2 / 4,3 退役(行为变更):对外只有四档 —— 0 成功(含"没发现问题""未检测到改动",开窗拖完正常关窗也是 0)、1 跑通了但结果需要处理(check 发现问题、写回没落地)、2 跑不起来(所有入口同一口径:脚本找不到 / 脚本自己报错 / 顶层未知参数)、4 开窗但找不到可用图形后端。3 不再由任何路径返回(常量 messages.EXIT_SCRIPT 保留,外部 import 它不会断)。apply <不存在的脚本> 的首行也从 no params: 改为"找不到脚本"(rc 仍是 2)。
  • Ctrl+Z 不再动你的画布(行为变更):撤销只还原"这次操作改过的东西"。画布尺寸按图自己的 dpi 与显示缩放换算,撤销后 px 与英寸两个量一起回到操作前的原值 —— 不再出现"改完图按一次撤销,画布自己缩一半,而那个尺寸还会被写回源码"。
  • 极地图 / 色条按真实形状选中,被压住也选得到(新能力):命中判定改用轴自己的 patch 几何(极地图就是那个圆),不再用外接矩形 —— 贴着极地图的横向色条不再被抢走。同位置有多个对象时:Alt+点击 穿透到下一层,Tab 循环同级元素。
  • check 新增色条遮挡检查(新能力):colorbar 与别的轴矩形相交时报一条 warn(规则名 colorbar_overlap),不再静默通过。
  • 自定义 colormap 不再被写成非法值(修缺陷):ListedColormap(...) 这类自建色标的 .name 是 from_list —— 不是任何可用的 matplotlib colormap 名。现在它不进参数、也不写回,跳过时给一句 warning。
  • describe 导出的参数原样喂回 apply,不再凭空改写(修缺陷):色条轴按"你声明的位置"读(get_position(original=True))。实测:describe <脚本> -o p.json → apply <脚本> --params p.json --write ⇒ rc=0、"参数与原代码一致,无需改动"、脚本文件字节未变。
  • 源码里的画布尺寸不是字面量时,绝不被有损改写(修缺陷):figsize=(W, H) 这类由变量 / 表达式 / 环境变量驱动的写法,写回时一个字节都不动,并把 figsize 如实列进 pending + 给一句人话说明;changes 不再声称改过它,其余字段照常写。
  • describe -o 路径 自动建父目录,写不进去也不再假归因(修缺陷):-o .tweak_params/x.json 这类路径的父目录不存在时自动创建;写失败给可照做的文案,不再谎称"工具 bug / 已回滚"。
  • doctor 严格校验选项、点名被拒参数,中文在 Windows 管道下不再乱码(行为变更):doctor 只认自己的选项(未知选项 / 多余位置参数一律 rc=2),错误信息直接点名是哪个参数;输出显式按 UTF-8 编码,中文 Windows + PowerShell 管道(含重定向到文件)下自检结果可读。
  • 预览明说"会改哪些字段"(新能力):只读预览打出 会改:figsize (15.0, 9.5) → (7.5, 4.75) 这样的清单(与"没动"的字段区分开);没有要改的就写"会改:无(这份参数与源码当前值一致)"。预览末尾关于 colorbar 的提示给出可直接粘贴的两行(cb.ax.set_box_aspect(None) + cb.ax.set_axes_locator(None)),并写明带 extend= 的色条两行都要加。
  • apply --json 多一个机器可读的失败码 error_code(新能力):给用 agent 驱动的人一个可直接分支的失败归因字段 —— file_not_found / no_params / mutually_exclusive_flags / layout_conflict / bad_params / stale_params / no_axes / no_fig / writeback_failed / external_modified,兜底 apply_failed;取第一个失败文件的码,逐文件的细分原因照旧在 files[].reason / attribution / err 里。它只在失败载荷上出现(成功载荷没有这个键),退出码与既有键名一个没变。
  • 首次开窗会说一句"不是卡住"(新能力):第一次开窗要等几秒到几十秒(窗口现建 + 脚本先跑一遍),工具会打一句"第一次开窗会等几秒……不是卡住了;放心拖:关窗之前不会写你的脚本"。每个用户只提示一次(戳文件在 ~/.mpltweak/,MPLTWEAK_HOME 可改;MPLTWEAK_NO_HINT=1 可静默);这一版把它做成两段式 —— 跑你的脚本之前先打个招呼(那时还不知道窗开不开得成,所以不落戳),真要建窗口时才落戳。
  • 中英双语覆盖面扩大(行为变更):--help / doctor / revert / 预览 / 失败文案等更多输出随 --lang 切换,英文侧保持纯 ASCII(含 GBK 控制台);语言解析顺序 --lang > MPLTWEAK_LANG > locale > en 不变。

修复与实现细节

  • 画布尺寸的撤销换算:撤销栈里的画布尺寸原先按固定 dpi=100 反算英寸,dpi≠100 或显示缩放 DPR≠1 时会把画布改成别的尺寸(用户现场:15.0×9.5 in → 7.5×4.75 in,即 dpi 被当成了 200)。现在 px 与英寸两个量都取自同一个权威换算,且只在"本来就跟踪过"时写回 —— 撤销到底逐字段 diff 为空、字节相同(toolbox.py 的 _snapshot / _restore_snap,断言见 tests/test_fuzz_events.py 的撤销往返与画布尺寸两条检测器)。
  • 色条位置的读值:_ColorbarAxesLocator 会在重绘时按父轴重算 active 位置(同一根色条实测宽度 0.1162 → 0.0289),于是"脚本原文 / 开窗落盘 / describe 导出"三处会读出不同的位置。现在对外统一读"你声明的位置"(get_position(original=True),toolbox.py),describe → apply 零改动往返因此判 no_change。
  • 极地图的命中几何:拾取原先统一取轴的外接矩形,而 aspect_locked 的 cartopy 极地图实际绘制是一个圆 —— 矩形盒把贴边的色条抢走。现在先用轴自己的 patch 做几何判定(ax.patch.contains_point,toolbox.py),拾取顺序与逃生通道(Alt+点击 / Tab)都走同一份优先级(_pick_at(..., skip=...))。
  • 源码 figsize 非字面量的处理:edit_figsize() 在表达式里取不出两个字面量数字时会直接把它格式化成字面量写回((W, H) 被换成运行期数值),且 changes 照报"改过 figsize"。现在这种情况直接返回未编辑(writeback.py 的 _seq_lit_nums 守卫 + 调用方 _fs_handled=False),只如实披露(pending=['figsize'] + 人话警告),其余字段照常写(断言见 tests/test_writeback.py 的 figsize 非字面量一节)。
  • 退出码的常量与路径:3 的常量保留(messages.EXIT_SCRIPT,外部可能 import),但没有任何路径再返回它;2 的覆盖面扩到顶层入口与未知参数。对外口径见 docs/exec-boundaries.md 的退出码表。
  • doctor 的选项校验:doctor 分支此前调了一个未登记的消息键,缺键时会把键名本身回显给用户(用户看到字面量 schema_unknown_args)。键已登记为"无法识别的参数:%(args)s"(与 argparse 同形),并加了消息键登记护栏。
  • 被调脚本的 stderr 刷屏:脚本自己往 stderr 打几十行时,工具的提示会混在中间。现在开窗收尾会汇总一行"上面的 stderr 里有 N 行来自你的脚本自身"(launch.py 打印、messages.py 的 zh/en 模板,断言见 tests/test_launch.py)。
  • apply --json 的失败码:apply.py 里有一张值域表把逐文件的 reason 映射成稳定码,_failure_code() 取第一个失败文件的码(兜底 apply_failed);码只在失败分支落键(if error_code:),成功载荷的键集与语义一字未变。对外清单见 docs/agent.md。
  • 首次开窗提示的两段式:launch.py 里两处调用 —— 早段(跑用户脚本之前)只说不落戳,建窗前那一刻才落戳;戳文件按用户家目录(~/.mpltweak/first_open_shown,MPLTWEAK_HOME 可改)⇒ 每个用户只提示一次。语言取当前 --lang 下的模板,MPLTWEAK_NO_HINT=1 静默(同样不落戳)。

数字

  • 画布撤销:dpi × 显示缩放 DPR 9 格矩阵(dpi ∈ {100, 150, 200} × DPR ∈ {1, 1.5, 2};DPR 为注入模拟,非真机):"拖一次 → Ctrl+Z"9/9 回到原值;旧换算在 DPR≠1 的 6 格会把画布改掉(例:dpi=100 / DPR=2 撤销后得到 7.5×4.75 in,原值 15.0×9.5 in)。原始输出:t70probe/out_matrix.txt(生成脚本 t70probe/probe_matrix.py)。
  • 语法化 AST 模糊测试 1,000 例(4×250 并行分片,四片 ALL PASS;每片 861~879 秒、墙钟 ~14.6 分钟、3.45~3.52 秒/例):坏语法 0 / 越界 0 / 不幂等 0 / 第四种结果 0;对抗形态一律安全拒写(0 字节、语法完好)。CI smoke N=9 实测 20.0 秒(套件 tests/test_ast_fuzz.py)。
  • CLI 参数 × 脚本组合:分母 5,880 = 7 命令 × 10 输入 × 6 参数文件 × 14 选项(tests/test_cli_fuzz.py 的 DENOM,套件运行时自己打印该分母;分层抽样、seed 可复现);N=240 实测 33.3 秒(0.14 秒/例,无争用实测)。
  • 跨读者一致性:同一条 colorbar 的"源码声明位置"在四个读者之间一致 —— 脚本原文 / 开窗落盘 / describe 导出 / 写回后(容差 5e-5);静态与运行时的"图数量"一致(fig.subplots(...) 不算新图);极地图 × colorbar 的重叠在两个读者的数据里都看得见(y 重叠 0.0059、x 重叠 0.2754)。台账与反例见 docs/consistency-ledger.md。
  • 真窗口实机(QtAgg):点色条选中 / Alt+点击 穿透 / Tab 循环 / 拖子图后 Ctrl+Z 画布不动 / 裁白边后 Ctrl+Z 还原 / 提示与手感 —— 六项全部符合预期(现场记录见 mpltweak_0.1.11_施工记录.md 的「真窗口验证记录」一节)。

这一版用户能感知到的变化

  • 写回验证默认只跑一遍脚本,等待时间约减半(行为变更):apply --write 的 happy path 由"跑 2 遍"(1 遍原样重跑 + 1 遍套壳取状态)改为 1 遍。这一遍同时回答两件事:脚本还跑得通吗、你要调的字段真落到目标图上了吗。取不到目标图状态时(脚本没画这张图 / 从没 savefig / 子进程没留下状态)才回退 2 遍,并把回退原因打印出来;需要归因"是不是脚本本来就跑不通"时最多 3 遍。--timeout 语义不变(每一次重跑各自受约束)。
  • 验证过程不再改坏用户目录里的图片(修缺陷):此前验证的那一遍为了在脚本跑完后还能读到图状态,会把 plt.close 拦成空操作;脚本里写成 savefig(...) → plt.close(fig) → 再裸 plot(...)(不新建 figure)时,后续绘制会画到上一张图上,而验证那一遍最后写盘 ⇒ 你目录里的 PNG 被静默覆盖成错的(此时工具可能仍报"验证通过")。现在取数改成 Figure.savefig 快照 + 按创建顺序持住图对象,两者都不改脚本行为,plt.close 不再被拦。
  • 关窗后直接给一条可复制的写回命令:拖过东西再关窗,终端会打印 mpltweak apply <脚本> --write(带过 --params / 透传参数就一并写全)。只是把命令打出来,写回仍要你自己确认——工具绝不自动改你的源码;没拖过时不打任何一行。
  • 成功时人读输出收敛成一行:[OK] 已原位写回 3 处(fig1.py);图已更新(重跑 + 语义验证通过);逐处清单、备份路径、各层结论移进 -v / --verbose。失败、回滚、降级(--verify-fast / --no-verify)与"未判定"(verified: null)不加 -v 也照旧全部打出来,--json 的字段集一个不少。
  • Tk 回退提示只说一次:开窗落到 Tk(没装 Qt)时那句"交互体验不如 Qt"不再每次开窗都念——每个用户只提醒一次;doctor 与文档里始终可查。
  • 失败时给四类归因 + 一句可照抄的下一步,写回前还能先预检:验证重跑失败时把「脚本跑不通(缺数据 / 依赖)」/「脚本需要参数」/「无头环境跑不通(要开窗 / 交互)」/「工具自身异常」分开报,每类各给一句下一步(记不住的参数会直接告诉你加 -- 透传的写法;工具自身异常会写明"没改动你的源码、已回滚")。--json 多出机器可读的 attribution / next_step,agent 可据此自动分支。此外写回与开窗之前会先做一次不执行脚本的静态预检(缺依赖 / 需要参数 / 会联网 / 等人输入 / 要弹窗),把"撞上之后才归因"改成"事前就说清楚"。
  • 脚本自己带参数时,验证重跑也拿得到参数(bug #6):mpltweak apply fig1.py --write -- --data x.npz——-- 之后的参数沿 apply → writeback → verify_run / verify_semantic → 子进程全链原样到达脚本的 sys.argv;"脚本本来就跑不通"的基线判别同样带上这些参数。不带 -- 时行为与以前完全一致。
  • 验证期间你在编辑器里保存的改动不会被覆盖:落盘前再确认一次"当前文件 == 最初读到的那份",检测到外部改动就拒绝写盘、明确告诉你、不覆盖你的编辑(原位与块两条路径都加)。

修复

每条格式:现象 → 根因 → 修法 → 证据。

写回验证会静默改坏用户图片(本轮最该记住的一条)

  • 现象:脚本里"savefig → plt.close(fig) → 再裸 plot()"这种写法下,apply --write 会把你目录里的图片覆盖成错的 —— 追因样本的目标图由 185,701 B / 6a7acb15d52f 变成 140,667 B / 252597fc6170(内容不同像素 15.2%),而工具这一路仍报成功,旧口径下 verified=True。
  • 根因:验证的语义层用「把 plt.close 拦成空操作」来保证"脚本跑完还能读到图"。图因此不销毁、仍是 current,后续绘制画回上一张图;而真实链路里原样跑那一遍先写出正确的图、套壳这一遍后写盘 ⇒ 覆盖。
  • 修法:不再拦 close;取数改成两件只读的东西 —— Figure.savefig 钩子在写盘那一刻抓快照 + 按创建顺序持强引用的 figure 注册表(脚本自己 close 过也读得到)。判据只严不松:主口径仍是"脚本跑完时图对象的状态"(与旧 verify_semantic 逐项一致),快照是追加判据——磁盘上那张图也必须体现这次改动。注册表只认 pyplot 托管的图(用 Figure() + FigureCanvasAgg 的 OO 风格脚本结论仍是"取不到状态")。
  • 证据:探针 5 臂双向定位(B′==A、B″==B:去掉拦 close 差异全消失、只加拦 close 差异完整复现)→ 产出文件一致率 143/144 → 144/144(diff_artifacts 1 → 0);同一脚本"旧代码 apply / 新代码 apply / 写回后脚本原样跑"三方对账:旧代码写出的图 ≠ 原样跑产物、新代码 ==;独立目录 A/B 对账 41/41 张 PNG 逐字节相同(含 pgl_sasa.png、S14_pca_plot.png)。素材见 tools/probe_close_vs_wrapper.py、_review/cmp_plain_vs_instrumented/(本地记录,未随仓库发布;已发布的等价材料见 onepass/,可重跑入口 python tools/cmp_plain_vs_instrumented.py)。

写回验证单遍化(2 遍 → 1 遍)

  • 现象:apply --write 每次要跑两遍脚本 —— 第一遍原样跑只为看退出码,第二遍套壳跑为拿图状态,而第二遍必须改脚本行为(即上一条的拦 close)。
  • 根因:取数方式与"跑不跑得通"两个问题被放在两个进程里问。
  • 修法:一次子进程同时回报"脚本自身成败"与"图状态"(verify.py 信封里多带 script_ok / script_exit / error),状态比对仍走同一个 _compare_state,判据不可能分叉;取不到状态一律 fail-closed 回退(绝不静默判通过)。
  • 证据:探针实测 2 遍 38.1 s → 1 遍 19.6 s(−48.7 %,24/24 可比样本单遍命中);--json 新增 verify_passes(1 = 单遍 / 2 = 已回退 / 0 = 没为验证重跑)与 semantic_basis,口径见 docs/verify.md 的「写回会跑几遍脚本」。

失败归因四类 + 一句下一步(新增)

  • 现象:带参数或需要 GUI 的脚本在验证重跑里跑不通时,用户只看到一句类似"缺数据 / 缺依赖"的失败,很容易归因成"这工具不行"。
  • 根因:验证子进程原样继承命令行,脚本自己的参数没有透传(bug #6 同源);失败也只有一个笼统原因。
  • 修法:把「脚本跑不通」与「工具改坏了」彻底分开,四类各给一句可照抄的下一步;--json 里给机器可读的 attribution / next_step(agent 可自动分支);另加"写回 / 开窗前"的静态预检(不执行脚本:只做 ast + find_spec,自身异常即放行)。
  • 证据:messages.py 的四类文案与下一步(script_cannot_run / script_needs_args / script_needs_gui_or_headless_impossible / tool_bug)、apply.py 的 attribution 输出;新增 src/mpltweak/preflight.py,在 apply.py 与 launch.py 两条入口接入。

验证期间的"外部编辑保护"落到实处

  • 现象:验证要真跑一遍脚本,可能等上几分钟;这段时间你在编辑器里保存的改动,过去可能被随后的落盘覆盖。
  • 根因:写回读入源码后到落盘之间没有二次确认。
  • 修法:落盘前比对"当前文件 == 最初读到的 src",不一致即拒绝写盘、明确报出、不覆盖;inplace / 块两条路径都加。
  • 证据:_guard_external_edit 与对应回归(tests/test_writeback.py,另有 docs/verify.md / docs/exec-boundaries.md 的对外口径)。

新增 -- 参数透传(bug #6)

  • 现象:依赖 sys.argv 的脚本(python fig1.py --data x.npz)在验证重跑里拿不到自己的参数 → 行为与基线不一致 → 默认回滚 + 误归因"缺数据 / 依赖"。
  • 根因:验证子进程的命令行是 [python, -m, mpltweak.verify, 脚本, out, idx],不带用户脚本的参数;而且套壳自己的位置参数还会残留在脚本的 sys.argv[1:] 里(表现为 unrecognized arguments)。
  • 修法:-- 之后的参数沿 apply → writeback → verify_run / verify_semantic → 子进程全链原样透传;sys.argv 无条件摆成 [脚本] + 脚本参数(不再只在"有参数"...
Read more

v0.1.10

Choose a tag to compare

@shdbl shdbl released this 13 Oct 09:20

0.1.10

这一版用户能感知到的变化

  • 写回验证默认只跑一遍脚本,等待时间约减半(行为变更):apply --write 的 happy path 由"跑 2 遍"(1 遍原样重跑 + 1 遍套壳取状态)改为 1 遍。这一遍同时回答两件事:脚本还跑得通吗、你要调的字段真落到目标图上了吗。取不到目标图状态时(脚本没画这张图 / 从没 savefig / 子进程没留下状态)才回退 2 遍,并把回退原因打印出来;需要归因"是不是脚本本来就跑不通"时最多 3 遍。--timeout 语义不变(每一次重跑各自受约束)。
  • 验证过程不再改坏用户目录里的图片(修缺陷):此前验证的那一遍为了在脚本跑完后还能读到图状态,会把 plt.close 拦成空操作;脚本里写成 savefig(...) → plt.close(fig) → 再裸 plot(...)(不新建 figure)时,后续绘制会画到上一张图上,而验证那一遍最后写盘 ⇒ 你目录里的 PNG 被静默覆盖成错的(此时工具可能仍报"验证通过")。现在取数改成 Figure.savefig 快照 + 按创建顺序持住图对象,两者都不改脚本行为,plt.close 不再被拦。
  • 关窗后直接给一条可复制的写回命令:拖过东西再关窗,终端会打印 mpltweak apply <脚本> --write(带过 --params / 透传参数就一并写全)。只是把命令打出来,写回仍要你自己确认——工具绝不自动改你的源码;没拖过时不打任何一行。
  • 成功时人读输出收敛成一行:[OK] 已原位写回 3 处(fig1.py);图已更新(重跑 + 语义验证通过);逐处清单、备份路径、各层结论移进 -v / --verbose。失败、回滚、降级(--verify-fast / --no-verify)与"未判定"(verified: null)不加 -v 也照旧全部打出来,--json 的字段集一个不少。
  • Tk 回退提示只说一次:开窗落到 Tk(没装 Qt)时那句"交互体验不如 Qt"不再每次开窗都念——每个用户只提醒一次;doctor 与文档里始终可查。
  • 失败时给四类归因 + 一句可照抄的下一步,写回前还能先预检:验证重跑失败时把「脚本跑不通(缺数据 / 依赖)」/「脚本需要参数」/「无头环境跑不通(要开窗 / 交互)」/「工具自身异常」分开报,每类各给一句下一步(记不住的参数会直接告诉你加 -- 透传的写法;工具自身异常会写明"没改动你的源码、已回滚")。--json 多出机器可读的 attribution / next_step,agent 可据此自动分支。此外写回与开窗之前会先做一次不执行脚本的静态预检(缺依赖 / 需要参数 / 会联网 / 等人输入 / 要弹窗),把"撞上之后才归因"改成"事前就说清楚"。
  • 脚本自己带参数时,验证重跑也拿得到参数(bug #6):mpltweak apply fig1.py --write -- --data x.npz——-- 之后的参数沿 apply → writeback → verify_run / verify_semantic → 子进程全链原样到达脚本的 sys.argv;"脚本本来就跑不通"的基线判别同样带上这些参数。不带 -- 时行为与以前完全一致。
  • 验证期间你在编辑器里保存的改动不会被覆盖:落盘前再确认一次"当前文件 == 最初读到的那份",检测到外部改动就拒绝写盘、明确告诉你、不覆盖你的编辑(原位与块两条路径都加)。

修复

每条格式:现象 → 根因 → 修法 → 证据。

写回验证会静默改坏用户图片(本轮最该记住的一条)

  • 现象:脚本里"savefig → plt.close(fig) → 再裸 plot()"这种写法下,apply --write 会把你目录里的图片覆盖成错的 —— 追因样本的目标图由 185,701 B / 6a7acb15d52f 变成 140,667 B / 252597fc6170(内容不同像素 15.2%),而工具这一路仍报成功,旧口径下 verified=True。
  • 根因:验证的语义层用「把 plt.close 拦成空操作」来保证"脚本跑完还能读到图"。图因此不销毁、仍是 current,后续绘制画回上一张图;而真实链路里原样跑那一遍先写出正确的图、套壳这一遍后写盘 ⇒ 覆盖。
  • 修法:不再拦 close;取数改成两件只读的东西 —— Figure.savefig 钩子在写盘那一刻抓快照 + 按创建顺序持强引用的 figure 注册表(脚本自己 close 过也读得到)。判据只严不松:主口径仍是"脚本跑完时图对象的状态"(与旧 verify_semantic 逐项一致),快照是追加判据——磁盘上那张图也必须体现这次改动。注册表只认 pyplot 托管的图(用 Figure() + FigureCanvasAgg 的 OO 风格脚本结论仍是"取不到状态")。
  • 证据:探针 5 臂双向定位(B′==A、B″==B:去掉拦 close 差异全消失、只加拦 close 差异完整复现)→ 产出文件一致率 143/144 → 144/144(diff_artifacts 1 → 0);同一脚本"旧代码 apply / 新代码 apply / 写回后脚本原样跑"三方对账:旧代码写出的图 ≠ 原样跑产物、新代码 ==;独立目录 A/B 对账 41/41 张 PNG 逐字节相同(含 pgl_sasa.png、S14_pca_plot.png)。素材见 tools/probe_close_vs_wrapper.py、_review/cmp_plain_vs_instrumented/。

写回验证单遍化(2 遍 → 1 遍)

  • 现象:apply --write 每次要跑两遍脚本 —— 第一遍原样跑只为看退出码,第二遍套壳跑为拿图状态,而第二遍必须改脚本行为(即上一条的拦 close)。
  • 根因:取数方式与"跑不跑得通"两个问题被放在两个进程里问。
  • 修法:一次子进程同时回报"脚本自身成败"与"图状态"(verify.py 信封里多带 script_ok / script_exit / error),状态比对仍走同一个 _compare_state,判据不可能分叉;取不到状态一律 fail-closed 回退(绝不静默判通过)。
  • 证据:探针实测 2 遍 38.1 s → 1 遍 19.6 s(−48.7 %,24/24 可比样本单遍命中);--json 新增 verify_passes(1 = 单遍 / 2 = 已回退 / 0 = 没为验证重跑)与 semantic_basis,口径见 docs/verify.md 的「写回会跑几遍脚本」。

失败归因四类 + 一句下一步(新增)

  • 现象:带参数或需要 GUI 的脚本在验证重跑里跑不通时,用户只看到一句类似"缺数据 / 缺依赖"的失败,很容易归因成"这工具不行"。
  • 根因:验证子进程原样继承命令行,脚本自己的参数没有透传(bug #6 同源);失败也只有一个笼统原因。
  • 修法:把「脚本跑不通」与「工具改坏了」彻底分开,四类各给一句可照抄的下一步;--json 里给机器可读的 attribution / next_step(agent 可自动分支);另加"写回 / 开窗前"的静态预检(不执行脚本:只做 ast + find_spec,自身异常即放行)。
  • 证据:messages.py 的四类文案与下一步(script_cannot_run / script_needs_args / script_needs_gui_or_headless_impossible / tool_bug)、apply.py 的 attribution 输出;新增 src/mpltweak/preflight.py,在 apply.py 与 launch.py 两条入口接入。

验证期间的"外部编辑保护"落到实处

  • 现象:验证要真跑一遍脚本,可能等上几分钟;这段时间你在编辑器里保存的改动,过去可能被随后的落盘覆盖。
  • 根因:写回读入源码后到落盘之间没有二次确认。
  • 修法:落盘前比对"当前文件 == 最初读到的 src",不一致即拒绝写盘、明确报出、不覆盖;inplace / 块两条路径都加。
  • 证据:_guard_external_edit 与对应回归(tests/test_writeback.py,另有 docs/verify.md / docs/exec-boundaries.md 的对外口径)。

新增 -- 参数透传(bug #6)

  • 现象:依赖 sys.argv 的脚本(python fig1.py --data x.npz)在验证重跑里拿不到自己的参数 → 行为与基线不一致 → 默认回滚 + 误归因"缺数据 / 依赖"。
  • 根因:验证子进程的命令行是 [python, -m, mpltweak.verify, 脚本, out, idx],不带用户脚本的参数;而且套壳自己的位置参数还会残留在脚本的 sys.argv[1:] 里(表现为 unrecognized arguments)。
  • 修法:-- 之后的参数沿 apply → writeback → verify_run / verify_semantic → 子进程全链原样透传;sys.argv 无条件摆成 [脚本] + 脚本参数(不再只在"有参数"时才摆)。
  • 证据:tests/test_verify_onepass.py;同一份需要位置参数的脚本,"原样跑"与"带 -- 的验证重跑"同 rc、同 stdout(不带 -- 时不再出现工具自身参数的 unrecognized arguments)。

测试与验收

  • 两种编码环境(UTF-8 / GBK)下套件均 ALL PASS;pytest -q = 61 passed / 5 skipped(含 hygiene 棘轮:宽兜底基线未涨、子进程后端 pin 与真实 GUI 对象两道新棘轮)。
  • 防污染回归网 tests/test_verify_onepass.py(新增,24 条 check:产物对账 / 单遍 + 快照追加 / 跑不通 / 改动没生效 / fail-closed 回退可见);反向验证(tools/teeth_onepass.py):把 close 拦截还原 ⇒ "用户目录里的每张图都与原样跑逐字节一致"必红,把单遍验证改成永远通过 / 把状态比对改成永不报错 ⇒ 对应用例分别变红,共 3/3 按预期变红,未变异代码上全绿。
  • tests/test_docs.py 补锚点 / 相对链接 / 图片 / 外链格式的冻结基线与 slug 自证(基线只允许降),并修掉一处"冻结基线引用未入库手册导致干净检出必红"的缺陷。

数字

  • 写回验证 2 遍 → 1 遍:探针实测 38.1 s → 19.6 s(−48.7 %),24/24 可比样本单遍命中;回退口径见上(只有取不到目标图状态才回退 2 遍,失败归因最多 3 遍)。
  • 验证产物一致率 143/144 → 144/144:探针在 144 份可比脚本上判"原样跑 vs 套壳跑"的产出文件是否一致,diff_artifacts 由 1 降到 0(剩下的不一致份里,退出码、stdout/stderr 差异已逐条归因)。
  • 静态层:语料 4,433 份过筛,判定口径为主表 3,531 / 3,610 = 97.8 %(五类完全划分 3,531 / 10 / 37 / 32 = 3,610);口径与完整材料见 docs/reliability.md。
  • 运行时真跑:池 1,837 份(含 28 份"真会开窗" + 29 份"联网 / 子进程"的静态排除)⇒ 真跑候选 1,780 份,其中 928 份写回并验证通过、四条铁律逐律 928 / 928、"说没写就一个字节都不能变"违约 0。
  • 0.1.10 收尾时另做了一次语料试点(167 份):判定差异 0、违约 0、语料未被修改 —— 它验证的是"判源码字节"这条口径与每份脚本各自的隔离工作目录,不涉及图片产物,因此上面的已发布数字不受验证重跑影响。

0.1.9

Choose a tag to compare

@shdbl shdbl released this 12 Oct 02:23

这一版用户能感知到的变化

  • 坏参数不再被写进源码,也不再"假装成功":非法尺寸 / 字号 / 轴序号 / NaN / inf 等在任何写盘或插块之前被拒绝(rc=2、字节不变、不插块)。只读预览同样会拒绝。
  • 拒绝或未写盘时,终端如实输出"未写回 + 原因",不再出现"没写却报 [OK] 已写回"。
  • 参数里没写 y 侧字号时,一个字节都不动 y。
  • 追加的调整块只用能静态证明的定位:证不出作用在哪张图 / 哪条轴时拒绝写盘,并打印一段可直接粘贴的手改片段。
  • describe 补上 y 侧字号字段,verify 真的判定 y;判不了的字段会如实列进 --json 的 detail。
  • 连改两次后 revert 回到原件(备份改为首份优先)。
  • 同图重名轴(fig, (ax, ax) = plt.subplots(1, 2))不再改错对象。

真实绘图代码语料静态验证

  • 语料 10,158 份公开绘图脚本(Zenodo 6,904 / GitHub 科研绘图仓库 2,045 / 官方 gallery 1,209),其中大量是不同论文的补充材料。
  • 真原位写回 3,531 / 可原位到达 3,610 = 97.8%;承诺违约 0 —— 没有任何一行"声称原位写回、字节却没变"。
  • 追加调整块 427 个:427 / 427 不使用运行期下标定位,原文逐字节保留。

以上判定均为静态层(只做 AST 定位与改写判定,不执行语料脚本)。

完整变更见 CHANGELOG.md。

v0.1.8 — revert 回退 / --verify-fast / --strict / xlim v4 / check 真实文字尺寸

Choose a tag to compare

@shdbl shdbl released this 10 Oct 07:28

[0.1.8] — 2026-10-10

这一版把"布局调完能不能确定落地"这条链路补齐了:新增 revert 回退、
--verify-fast 快速档、--strict 严格档、轴范围 xlim/ylim(参数 schema v4)、
check 按真实渲染尺寸查文字,并修掉了 --json 输出编码与元组/带符号写法
原位写回等 6 个真 bug。测试:七套件 + pytest -q 25 项全绿(GBK 与 UTF-8 环境各跑一遍),
12 项反向验证全部命中;两个模型家族的独立只读审阅共 10 条发现全部修复。

修复

  • --json 成功路径的 stdout 不是 UTF-8:check / describe / apply --json / schema
    原先用 print(),在管道被捕获时按本地编码(中国 Windows 上是 cp936/GBK)输出;JSON 里
    只要有一个中文(check 的 msg、schema 的 description),按 UTF-8 解码的 agent / MCP
    客户端 / CI 就会直接崩。现在统一走 fd 级 UTF-8 写出(与失败路径 fail_json 一致)。
  • 顶层兜底 guard_json_main 在异常路径上失效:messages.py 用了 sys 却没 import,
    写出结构化 JSON 之后就在 sys.stderr.write 上抛 NameError,把本该打印的人话提示、
    MPLTWEAK_DEBUG 的 traceback 和 return 1 全部吃掉(用户看到的是与真实故障无关的 traceback)。
  • 元组/带符号写法的原位写回:原先只认列表字面量与 ast.Constant,于是
    add_axes((-0.01, ...))(元组)和 set_xlim(-0.01, 1.01)(负号在 AST 里是 UnaryOp)
    会被静默丢进"未原位应用"——用户看到的是"拖了没反应"。
  • --json 的 style 字段在写回时永远是 null:写回路径没设 style,导致 apply 永远走
    "已插入调整块"那条消息分支,"原位修改 N 处"永远打不出来,agent 也判断不了这次是哪种写回。
    (只读预览 --json 里 style 现在填请求的模式:auto 表示"逐图待定"——预览阶段还没决定
    这张图会不会退到块。)

新增

  • 布局引擎冲突检测(layoutwarn.py):静态扫描脚本,命中
    constrained_layout=True / layout='constrained'|'compressed' / set_constrained_layout /
    set_layout_engine / rcParams['figure.constrained_layout.use'] / rcParams['figure.autolayout']
    时给出带行号的提示与修复建议(launch 开窗前、apply 写回时、check 的 layout_engine 字段)。
    原因:布局引擎在每次绘制时重算轴位置,写回/拖拽的位置会被它覆盖,而旧行为只表现为一句
    "验证不一致",指不到病因。
  • mpltweak revert <脚本> [--write]:回退到上次写回之前(默认只预览)。用现成的
    .tweak_params/<脚本>.tweak.bak,自身也可逆(覆盖前把当前版本留成 <脚本>.revert.bak),
    恢复是字节级原样复制(BOM / 换行 / 编码不动)。
  • check 按真实渲染尺寸查文字:绘制一次后用 Text.get_window_extent() 量外框 ——
    超出画布 = 问题(客观可判定),文字互相压住 = 提示;同轴超长刻度标签按
    "沿刻度方向的重叠比例"判"太密"。纯几何框查不出这类问题。--no-text 可退回纯几何口径。
  • 轴范围 xlim / ylim(参数 schema v4):describe 采集(只在脚本显式固定过时才记,
    autoscale 不记),apply 原位改写 set_xlim(a, b) / set_xlim((a, b)) / set_xlim(xmin=, xmax=)
    三种写法,--style block 的调整块也会带上,写回后由语义验证比对范围是否真的等于参数。
  • check 轴范围不统一提示:多面板里 ≥60% 面板共享同一范围、另有少数不同 → 提示
    (各自为政的多面板图不报)。
  • --verify-fast 快速验证档:只做语法静态检查(写坏当场回滚),不重跑脚本;
    --json 标 verify_mode=fast + verified=null,人读输出原话说清"不保证能跑通、也不保证布局正确"。
  • --strict:check --strict 把布局引擎冲突升级为问题(rc=1,接 CI);
    apply --write --strict 在冲突时拒绝写回(--allow-layout-conflict 可放行)。
  • 轴网格身份 cell:参数里记录每个轴调图时的网格格位 [行数, 列数, 起始行, 起始列],
    写回后回比 —— 脚本改动导致轴序漂移时,语义验证会指名道姓地失败并回滚
    (而不是默默把位置套到别的面板上);调整块注释里也带出这份身份。
  • tests/test_hygiene.py(宽兜底棘轮 + 消息表 GBK 可编码 + 新模块登记)、
    tests/test_suites.py(pytest 包装)、tools/audit_exceptions.py(宽兜底清单与可窄化提示)。
  • CHANGELOG.md(本文件)。

文档

  • CONTRIBUTING.md:改正 Qt 依赖描述([qt] extra,不是默认依赖);补 pytest 路线、
    "加一套测试要改哪四处"、宽兜底棘轮与可窄化清单的用法。
  • README(中/英):新增 revert / --verify-fast / --strict 三节;「不适用场景」;
    「原位改数字 vs 插入调整块」(--style auto 的逐图判定、跨轮混用与彻底统一的正确做法);
    参数文件示例补 xlim / ylim / cell 并升到 v4;check 一行写清新增检查项。

测试:七套件 ALL PASS;pytest -q 25 passed(GBK 控制台与 UTF-8 两种环境各跑一遍);12 项反向验证全部命中;两个模型家族的独立只读审阅共 10 条发现全部修复。

v0.1.7 — 27 fixes from the pre-launch stability audit

Choose a tag to compare

@shdbl shdbl released this 10 Oct 04:22

Release after a pre-launch stability audit: five independent audit
passes (write-back engine / interaction & lifecycle / CLI-agent interface /
environment portability / adversarial input) plus an independent verification
pass. 27 issues fixed, every one with a reproduction.

Write-back engine

  • fig_index end-to-end. describe now emits fig_index/n_figs; the
    engine refuses to write back when a multi-figure script has no figure number
    (it used to silently write into another figure and still report
    "verification passed").
  • Concurrency. apply --write now takes a per-script lock, and each run
    keeps its own pid-suffixed backup — two concurrent runs used to both report
    success, lose one edit, and overwrite the single shared backup.
  • Atomic replacement preserves file permission bits; fields skipped by the
    overlap guard are no longer counted in changes.
  • Encoding. GBK and UTF-8-BOM scripts are now readable and writable, and
    the original encoding is preserved.

Output & machine contract

  • The seven ✓/✗ markers became ASCII: on Chinese Windows with captured
    stdout the tool used to crash after the file had already been rewritten.
  • New messages.py: human output follows the system language (--lang /
    MPLTWEAK_LANG override); --json stays English + stable error codes.
  • Block sentinels are now language-neutral ASCII and still recognise the old
    Chinese ones.
  • stdout is redirected at the fd level; the multi-figure summary goes to
    stderr; top-level handling guarantees --json always emits valid JSON —
    including on failure paths.

Interaction

  • Help window NameError (the process used to hang after pressing ?).
  • Ctrl+click multi-select used event.key, which real Qt reports as
    control — fixed to use event.modifiers (with a reverse test proving it).
  • Bare clicks no longer consume an undo step; undo restores canvas size using
    the real dpi; --fig close counting.

Parameters & tooling

  • Previous rounds' parameter files are no longer deleted; --dry-run no
    longer cleans anything; apply gained an mtime staleness guard (--force).
  • check no longer flags legitimate asymmetric layouts; describe no longer
    reports log axes as linear.

⚠️ Change in default install

PyQt5 is no longer a default dependency. For the interactive window:

pip install -U "mpltweak[qt]"

The CLI (describe / check / apply / mcp) does not need a GUI backend
and works with a plain pip install -U mpltweak.

Also: mcp extra now carries a python_version marker (so Python 3.9 no
longer silently downgrades), python -m mpltweak works, and the demo GIFs
were redrawn with a low-saturation palette.

Full details: each fix is documented in the commit message and covered by
regression tests (tests/).

v0.1.6 — correct demo GIF speed, CDN images, English description

Choose a tag to compare

@shdbl shdbl released this 09 Oct 15:57

Fixes

  • Demo GIFs play at the right speed. The previous compression pass flattened the variable per-frame delays into a single value, which stretched a 5.4 s demo into 27 s. Recompressed with per-frame delays preserved, plus a global palette and no dithering — total size is within +2% of before.

Docs

  • README images now use absolute CDN URLs, so the PyPI project page renders the banner and demo GIFs too (relative paths silently break there).
  • English package description, so PyPI and share cards read properly.

Install: pip install mpltweak

v0.1.5 — agent interface, layout check, MCP server

Choose a tag to compare

@shdbl shdbl released this 09 Oct 15:35

New: usable from AI agents

  • mpltweak describe <script> — export the current layout as params JSON, without opening a window
  • mpltweak check <script> — geometric layout check: alignment / equal size / even gaps / font consistency / overflow / overlap (exit 1 on problems, CI friendly)
  • mpltweak schema — JSON Schema (draft-07) for the params file
  • mpltweak apply --json — machine-readable results; chatter goes to stderr
  • mpltweak mcp — stdio MCP server for Claude Desktop / Cursor / Claude Code

Read → check → write is now a complete loop: an agent no longer needs to "see" the figure.

Fixes

  • Script stdout is isolated while collecting (a print in your script used to corrupt the MCP JSON-RPC channel)
  • figsize_in no longer assumes 100 dpi
  • check column grouping is no longer order-sensitive

Docs

  • README restructured, with a table of contents plus MCP and skill sections
  • skills/mpltweak/SKILL.md — an operating manual written for AI assistants

Install: pip install mpltweak — MCP extra: pip install "mpltweak[mcp]"