Repository navigation
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无条件摆成[脚本] + 脚本参数(不再只在"有参数"时才摆)。 - 证据:
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、语料未被修改 —— 它验证的是"判源码字节"这条口径与每份脚本各自的隔离工作目录,不涉及图片产物,因此上面的已发布数字不受验证重跑影响。