Repository navigation
Releases: shdbl/mpltweak
Release list
v0.1.11
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× 显示缩放DPR9 格矩阵(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 smokeN=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_artifacts1 → 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无条件摆成[脚本] + 脚本参数(不再只在"有参数"...
v0.1.10
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_artifacts1 → 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
这一版用户能感知到的变化
- 坏参数不再被写进源码,也不再"假装成功":非法尺寸 / 字号 / 轴序号 / 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 真实文字尺寸
[0.1.8] — 2026-10-10
这一版把"布局调完能不能确定落地"这条链路补齐了:新增
revert回退、
--verify-fast快速档、--strict严格档、轴范围xlim/ylim(参数 schema v4)、
check按真实渲染尺寸查文字,并修掉了--json输出编码与元组/带符号写法
原位写回等 6 个真 bug。测试:七套件 +pytest -q25 项全绿(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
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_indexend-to-end.describenow emitsfig_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 --writenow 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 inchanges. - 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_LANGoverride);--jsonstays 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--jsonalways emits valid JSON —
including on failure paths.
Interaction
- Help window
NameError(the process used to hang after pressing?). Ctrl+clickmulti-select usedevent.key, which real Qt reports as
control— fixed to useevent.modifiers(with a reverse test proving it).- Bare clicks no longer consume an undo step; undo restores canvas size using
the real dpi;--figclose counting.
Parameters & tooling
- Previous rounds' parameter files are no longer deleted;
--dry-runno
longer cleans anything;applygained an mtime staleness guard (--force). checkno longer flags legitimate asymmetric layouts;describeno 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
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
New: usable from AI agents
mpltweak describe <script>— export the current layout as params JSON, without opening a windowmpltweak 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 filempltweak apply --json— machine-readable results; chatter goes to stderrmpltweak 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
printin your script used to corrupt the MCP JSON-RPC channel) figsize_inno longer assumes 100 dpicheckcolumn 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]"