Files
Just-DataInspector/Documents/Code_Review_Progress.md
T
DeepSeek V4.1 Flash 353c553721 test(tools): rowgeom 逐行几何基线 + 长度不变式按类别判定
新增 `OffscreenChecks -run rowgeom`(§13.6 第 1 项),把 7 个面板的每个矩形的
每个字段导出为文本基线并逐项比对,作为"完整结构合并 / 重写布局算式"的前置护栏。

两层覆盖
- 细节层:6 信号 1 网格、7 面板全开、参数行展开、3 个真实固定点、折起+展开各一份,
  983 条键(-data MF4-Record.MF4 时 1304 条),失败时直接给出字段名+行号+新旧值;
- 规模层:全部信号合并成 1 个网格(MF4 = 566 行),每面板一个 FNV-1a 摘要 + 行数,
  任何一行变动都会改哈希,同时不产生几十万行基线。

负向自检(证明护栏不是空转)
- 篡改基线副本 `mark.markerRowCount=3 → 2` ⇒ `CHANGED … mismatches=1` + FAIL。

顺带修正第 36 轮定得过严的不变式
- `rowRefs` 是重载字段:MARK 面板用它表示"信号分组",真正的条目行在 markerRows,
  且只填 rowNameRects / rowNameCloses,其余 14 列必须为空;
  ⇒ `panels` / `panelscroll` 的"18 列全等"改为按 `PanelStyle::markerGroups` 分流判定,
  失败时打印 15 个计数字段便于定位。
- 导出器/不变式一律**逐列按自身长度**遍历(按 rowRefs.size() 索引其它列会越界,
  这是本轮第一次 0xC0000005 的根因)。

测试用例里的两处修正
- `MERGE_LANES` 之后补 `SynchronizeView(true)`:`SetMode` 只重建 TabConfig 的网格配置,
  `PlotView::m_lanes` 要等下一次布局才刷新;
- 场景 B 重新播固定点:`ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
  是 RebuildLanes 的既有设计,网格级状态随之清空(不是缺陷),因此不能跨场景依赖。

验证
- make release:零 warning,785,408 B 不变(本轮只动工具/检查与文档)
- make -C Tools clean && make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS(原 6 / 101)
- rowgeom -mode compare:默认数据 983+7 键、MF4 1304+7 键,0 差异
- panels -mode compare:7 个面板像素哈希 + 几何逐项不变

文档:Code_Review_Plan §九、Progress 第 37 轮、Report §14、agent_mem 第 37 轮、Tools/README §4.1/4.3/4.4
2026-11-04 15:49:25 +08:00

494 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Code Review 进度(分支 `Code_Review`)
> 每个节点完成后追加:节点号、改动摘要、验证方式、实测数字。
> 计划见 `Documents/Code_Review_Plan.md`;最终报告见 `Documents/Code_Review_Report.md`。
## 基线(起点 `764e5be`)
| 项 | 数值 |
| --- | --- |
| Release 产物 | **890,880 B** |
| `.text` | 753,136 B |
| `.rdata` | 36,936 B |
| `.pdata` | 30,420 B |
| `.xdata` | 29,496 B |
| `.rsrc` | 20,656 B |
| `.idata` | 12,856 B |
| Debug 产物 | 1,801,617 B |
| 源码规模 | 约 5.6 万行 |
关键发现(详见计划 §一):
* `dynamic_cast`(`Toolbar.cpp:370`)使最小复现程序从 39,936 B 涨到 115,200 B
⇒ **+75,264 B**;map 中 `cp-demangle.o` 占 **56,320 B**。
* 每个 `EdgePanelKind` 在源码中出现 21–26 次(6 种面板 ⇒ 约 140 处手写分支)。
---
## 节点记录
(下面按完成顺序追加)
### P1 移除 `dynamic_cast` + 关闭 RTTI —— ✅ 完成(提交 `5de3763`)
**改动**
1. `Toolbar::BuildBoxes` 里唯一的 `dynamic_cast` 是**多余强转**(`Button::widget` 本来就是
`UiButtonWidget*`)。改为直接遍历 `m_buttons`(与 `m_root->Children()` 同源同序)。
2. 仅关 RTTI **不够**:`cp-demangle.o` 仍在链接里。真凶是 libstdc++ 的 `vterminate.o`
(引用 `__cxa_demangle`),它被 `eh_terminate.o` 的 `std::terminate` 拉入。
⇒ 在 `Utilities/Src/MemProbe.cpp` 里**自行定义** `__gnu_cxx::__verbose_terminate_handler()`,
归档成员 `vterminate.o` 不再被拉入,demangler 整体消失。
应用启动后本来就会 `std::set_terminate` 换成自己的处理函数,行为等效。
3. `Makefile` 新增 `NO_RTTI=1`(默认开)→ `CXXFLAGS += -fno-rtti`。
**实测**
| 指标 | 改前 | 改后 | 差 |
| --- | --- | --- | --- |
| Release | 890,880 B | **829,440 B** | **−61,440 B** |
| `.text` | 753,136 B | 703,616 B | −49,520 B |
| `.rdata` | 36,936 B | 26,616 B | −10,320 B |
| Debug | 1,801,617 B | 1,734,578 B | −67,039 B |
**验证**:release + debug **全量重建**(删 `*.o`/`*.d`)零 error 零 warning;
map 中 `cp-demangle.o` 已无 `.text` 段分配。
### P2 Release 关闭异步展开表 + 修正 Debug 构建参数 —— ✅ 完成(提交 `94fc1c4`)
**改动**
1. 新增 `RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables`,**只**加在 Release 的两条
编译规则上。`-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`。
* 代价(已评估接受):Release 崩溃时 WER 拿不到可靠调用栈;不打印栈回溯的程序
功能不受影响,Debug 构建保留完整展开表。
* `-fno-unwind-tables`(全关)会链接失败,不采用。
2. **修正真实缺陷**:`DEBUG_OPT = -O0` 定义了却**从未被使用** —— Debug 的两条编译规则
误用 `$(RELEASE_OPT)`(`-Oz`),且全程没有 `-g`。所谓 "Debug 构建" 其实只是
"不 strip 的 Release",根本无法调试。现改用 `$(DEBUG_OPT) -g`,链接也加 `-g`。
**实测**
| 指标 | 改前 | 改后 |
| --- | --- | --- |
| Release | 829,440 B | **793,088 B**(−36,352) |
| `.pdata` | 30,420 B | 12,684 B |
| `.xdata` | 28,904 B | 10,156 B |
| Debug | 1,734,578 B | 5,992,064 B(真实 `-O0 -g`,属预期) |
**验证**:release 与 debug 均全量重建,零 error 零 warning(`-O0` 下也没有新警告)。
### P7 面板状态/滚动量收敛为唯一派发点 —— ✅ 完成(提交 `7a3fefb`)
**问题(屎山核心之一)**:`Lane` 里 7 种实验性功能各有 1 份 `LanePanelState` + 1 个滚动偏移
(14 个字段)。旧实现在**每一处**“按种类取用”的地方各写一遍嵌套三元式或 `switch`:
`EdgePanelLayoutFor` 5 条 7 层嵌套三元式;标题栏拖拽、✕ 关闭、滚轮、滚动条、错开序数各写一遍。
新增面板要改 20+ 处,漏一处就是「拖不动 / 关不掉 / 滚轮滚不到底」
(`MARK`、`RESAMPLE`、`CLIP` 均真实踩过)。
**改动**:`PlotViewInternal.hpp` 新增唯一派发点
`LanePanelEnabled` / `LanePanelStateOf` / `LanePanelScrollOf`(可写 + 只读)。
这些 `switch` **穷举 7 种且不写 `default:`** ⇒ `-Wswitch` 在漏登记时编译报警
(已用最小程序验证 `-Wswitch` 确实会报)。
调用点:布局 5 条三元式 → 3 行;标题栏拖拽 7 分支 switch → 1 次写入;
滚轮/滚动条/✕ 关闭/折叠切换全部改为穷举无 `default`。
**验证**:release + debug 零 warning;产物 **793,088 B 不变**(等价变换,说明只是结构收敛)。
**未做(受验证条件限制)**:尚未把 `EdgePanelLayoutFor` 的 957 行分支整体改成
“每面板一行元数据表”。原因:没有可用的离屏像素对比工具(`.preflight` 已删除,
且崩溃会弹模态框),纯逻辑重写无法自证“像素完全一致”。已通过上面的派发收敛
消除最容易出错的一类重复,剩余重复属于“几何算式本身”,风险高于收益。
### P11 拆分 `ApplicationCommands.cpp` —— ✅ 完成(提交 `8437100`)
原文件 **2,165 行**,混杂六类职责。按职责**纯搬移**(零逻辑改动):
| 新文件 | 行数 | 内容 |
| --- | --- | --- |
| `ApplicationTabs.cpp` | 355 | 标签页新建/关闭/切换/重排、视图同步、窗口标题、标签栏回调、标签页右键菜单 |
| `ApplicationDerive.cpp` | 786 | 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整) |
| `ApplicationMenus.cpp` | 409 | 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、菜单结果派发 |
| `ApplicationCommands.cpp` | 642 | 分组/信号增删、移动与重排、文件对话框、工具栏动作、键盘鼠标分发、布局边界绘制 |
Makefile 按目录通配收集源文件,**无需登记新文件**。
**验证**:release 零 error 零 warning;产物 793,088 → 794,112 B(+1,024,LTO 内联边界
变化,< 0.13%)。
### P15 注释/文档校正 —— ✅ 完成(提交 `f139440`)
* `PlotView.hpp`:删除指向已删除代码的 `FormulaEnvelopeEntry` 注释块。
* `agent_mem.md` §2:`-Os` → `-Oz`;补体积开关(`NO_EXCEPTIONS` / `NO_RTTI` / `LTO` /
`RELEASE_ONLY_OPT`)与「改 Makefile 必须删 `.o` 全量重编」提醒;补 `msimg32`;
忽略列表补 `Temp/`、`.preflight/`、`Tools/`。
* `agent_mem.md` §3:目录结构按现状重写(删除不存在的 `CsvTable/`、`DataModel/`、
`PaintUtil/` 等旧条目;写明 `Components/Shared` 与 `Utilities` 的真实职责和依赖方向)。
* `agent_mem.md` 末尾新增「第 33 轮」记录。
### P16 最终验证与报告 —— ✅ 完成
* release(全量)+ debug(全量 `-O0 -g`)**零 error 零 warning**。
* 最终产物 **794,112 B**(起点 890,880 B,−96,768,−10.9%)。
* `cp-demangle.o` 已无 `.text` 段分配;`__gnu_cxx::__verbose_terminate_handler`
定义在本产物内(`0x140057ed1`);导入 DLL 无新增。
* 报告:`Documents/Code_Review_Report.md`。
### 未完成/搁置的节点(均已在报告中说明原因)
* **P8** `SceneContentHash` 混入收敛:无逐像素验证手段,合并会改变哈希值,风险不划算。
* **P9** `EdgePanelLayoutFor` 表驱动化:同上(957 行几何分支,无法自证像素一致)。
* **P10** 缓存五件套抽象:同上,且热路径复杂。
* **P12** `PlotView.hpp` 拆分:嵌套类型外移需改 70+ 处引用,收益仅为可读性。
* **P13/P14** UI 基础层与绘制边界核对:已做**依赖方向与引用统计**,未发现重叠或反向依赖,
因此不再拆分。
* **性能项**:未发现新的可复现热点;已记录 3 个"还能再快一点"的点(见报告 §6.2)。
---
## 第 34 轮(2026-11-04):验证基础设施 + §10 后续建议落地
### T1 验证工具入库 —— ✅ 完成(提交 `97e9163`)
**改动**
1. `.gitignore`:删除整目录忽略的 `Tools/`,改为 `Tools/**/Build/`(只忽略各工具产物)。
2. 新增 `Tools/Makefile`:`make` / `make UnitTests` / `make OffscreenChecks` / `make clean` / `make help`;
工具链接本体 `../Build/Release/*.o`(排除 `Main.o`),`-MMD -MP` 跟踪头文件依赖。
3. 新增 `Tools/UnitTests`(`Inc` + `Src` + `Build`)与 `Tools/OffscreenChecks`(同构)与 `Tools/README.md`。
**踩到并修掉的两个构建陷阱**
* 工具源码也加 `-flto` 会让 GCC 15.2.0 在链接阶段 `choose_baseaddr`(i386.cc:7447)**ICE**
⇒ 工具源码不带 `-flto`,只在链接参数里带 `-flto -flto-partition=one`。
* `$(foreach)` 生成的工具目标早于 `all` 出现 ⇒ make 把**第一个**显式目标当默认目标,
直接 `make` 只会构建 `UnitTests` ⇒ 显式 `.DEFAULT_GOAL := all`。
**实测**:`make -C Tools` 零 warning;两个产物分别在 `Tools/<Tool>/Build/<Tool>.exe`。
### T2 单元测试 —— ✅ 完成(219 条断言 / 18 个用例)
覆盖:`TryParseDouble` 与 libc `strtod` **逐位一致**(6 万条随机十进制串)、
输入接受性(尾随字符 / 十六进制浮点 / `inf` / `nan`)、`FormatDecimal*` 永不科学计数法、
`NiceStep` / `BuildTicks`、公式语法与语义、`FormulaProgram` 与 `EvaluateFormula` 逐位一致、
`ApplyDrawTransform` 顺序、`TransformValueRange`、滑动平均多阶等价性、
`DataSet::FormulaTransformFor` 值域与分组极值(逐点参考实现比对、命中同指针、样式变化失效)、
以及新增的 `stdio_mingw_vs_ucrt_formats`(见 T6)。
**过程中修正了 5 处"我以为的语义"**(都以源码为准,测试跟着产品改):
`9.999999999999999` 属 >15 位有效数字(允许 1 ULP);`1..2` 其实合法;
`FormulaIsIdentity` 不去空格;非法公式求值时 `EvaluateFormula` **原样返回采样值**(不是 NaN)。
### T3 离屏验证 —— ✅ 完成(5 个检查 / 71 条断言)
* `smoke`:初始化 + 加载 + 渲染 + 落盘;**同一状态两帧逐像素相同**(实测差异 0 像素)。
* `frames`:连续帧抓取(PNG + 区域 FNV 哈希 + 耗时),帧序列至少出现一次可见变化。
* `panels`:7 个面板全部展开后的几何不变式(必须被绘图区夹住)+ 像素基线 `-mode record/compare`。
* `edgestats`:边沿统计与工具内**独立朴素实现**逐位比对(阈值 2.4875/4.975/7.4625 →
20/19、20/19、20/19 完全一致;中心轴 1200/1179 也能完成)。
* `perf`:整帧中位数、`EdgePanelLayoutFor` 单次、198 个统计状态的整轮查询耗时。
**全程离屏**:窗口 `SW_HIDE`、"鼠标"用 `Application::HandleMouseMove()` 合成注入、
像素从后备缓冲区读取;进程用 `std::_Exit()` 收尾(避免单例析构晚于 `GdiplusShutdown`)。
### T4 `EdgeScanFor` 状态查找哈希化 —— ⛔ **实测后回退**(建议 #2)
* **改动前**:`mutable std::vector<EdgeScanState>` + 逐字段线性扫描。
* **改法**:容器改 `std::deque`(指针稳定)+ `mutable std::unordered_multimap<uint64_t, EdgeScanState*>`
按身份哈希索引,桶内仍逐字段比对(碰撞安全);新建 / 淘汰 / 定向作废 / 清空四处同步索引。
* **正确性**:`edgestats` 全绿(真实数据的计数与独立参考实现逐位相同),`panels` 像素基线不变。
* **性能(198 个状态,`MF4-Record.MF4`)**:整轮查询 `0.097 → 0.073 us/query`
(一轮 198 次查询省 ≈4.8 μs)。
* **体积代价**:`788,480 → 794,112 B`(**+5,632 B**)。
* **结论:回退**。5.6 KB 的体积换 ≈5 μs/帧(约占 5 ms 整帧的 0.1%)不成立 ——
这与 §6.2 原本的判断一致,本轮把它从"估计"变成了**实测**。
⚠️ 同时修正了 §6.2 的量级:报告原估"288 信号时 ≈0.3 ms/帧",实测 198 状态整轮 **≈20 μs/帧**,
差一个数量级(线性扫描的常数很小)。
### T5 `EdgePanelLayoutFor` 表驱动化 —— ⏸ 只交付"可验证手段",**未重写**
* 第 33 轮已把**按种类分派**收敛成唯一派发点(`LanePanelEnabled` / `LanePanelStateOf` /
`LanePanelScrollOf`,且 `switch` 不写 `default:` 让 `-Wswitch` 兜底)。
* 剩余的重复是 957 行**几何算式本身**(每个面板的内容行高、表头、按钮、滚动区各不相同),
无法用"每面板一行元数据"表达。
* 本轮实测该函数 **1.9~2.5 μs/次**(2000 轮 × 7 种),按每帧约 14 次调用计约 **30 μs/帧**,
不是热点。
* ⇒ 重写的收益是"可读性",代价是 957 行几何公式的可回归风险。
**本轮交付的是让这类重写变得可验证的工具**:`-run panels -mode record/compare`
会逐面板比对**几何 + 像素哈希**(7 个面板、含内容区/滚动条/按钮)。
### T6 `wcstod` 自写替身(省 gdtoa)—— ⛔ **实测 0 字节收益**(建议 #4)
* 报告 §1.3 的估计是"≈20 KB"。本轮先做**最小实验**而不是先写代码:
在 `Makefile` 里加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 交给系统 UCRT),全量重编。
* **实测:产物仍然 788,480 B,一个字节都没变**;map 里 `mingw_wpformat.o` 依旧被链接。
* **原因(用 3 行最小程序确认)**:预编译的 **libstdc++ 自己引用 `__mingw_swprintf`** ——
只要工程里还调用 `std::to_wstring` / `std::to_string`(全仓约 80 处,其中若干处实参是浮点),
链接器就必须把 MinGW 的 printf 引擎(含 gdtoa)拉进来。
* ⇒ 真正的 33 KB 收益要以"替换全部 80 处 `std::to_*string`(含浮点格式语义)"为代价,
风险与收益不匹配,**不实施**。该实验已回退,只在 `Makefile` 里留下一段说明防止重复尝试。
* **顺带产出可复用的验收测试** `stdio_mingw_vs_ucrt_formats`:
同一批真实格式(`%.*f` / `%.6g` / `%g` / `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)
× 13.6 万次调用做**逐字符比对**,**0 处差异**。将来若真做迁移,这条测试就是验收标准。
### 本轮验证汇总
| 项 | 结果 |
| --- | --- |
| `make release`(删 `.o` 全量) | 零 error、零 warning |
| `make -C Tools` | 零 error、零 warning |
| `Tools/UnitTests/Build/UnitTests.exe` | `checks=219 failed=0 cases=18` |
| `Tools/OffscreenChecks/Build/OffscreenChecks.exe -all` | 5 个检查全 PASS,`checks=71 failed=0` |
| Release 产物 | **788,480 B**(与第 33 轮末一致;T4/T6 均回退) |
---
## 第 35 轮(2026-11-04):浮动面板架构评估 + 收敛 + 整数格式化
### 评估:浮动面板 / 二级菜单 / 右键菜单算不算"公共组件"?
先回答用户的问题(结论来自读码,不是印象):
| 对象 | 现状 | 是否已是"公共组件" |
| --- | --- | --- |
| **右键菜单 / 二级菜单** | `Components/Shared/PopupMenu`(+ `Layout` / `Paint` 两分片) | **是**。同一份实现服务绘图区网格菜单、信号菜单、分组菜单、标签页菜单、工具栏设置菜单;新增菜单只需描述条目数组 |
| **拾色器** | `Components/Shared/ColorPickerPopup` | **是**。独立的浮层组件,自带编辑框/滑块/色域/命中 |
| **实验室功能浮动面板** | 全部在 `PlotView` 内部(`EdgePanelLayoutFor` 算几何 + `PaintEdgeCursor` 画 + `OnLButtonDown/Move/Up` 命中),**共用一份"面板骨架"**(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕、可滚动内容区 + 滚动条、底部按钮行) | **骨架是公共的,但"按种类分支"的代码被抄了多份** |
**为什么不把面板做成 `UiWidget` 树节点(像 PopupMenu 那样)?**
三条硬约束决定了成本远大于收益:
1. 面板的**像素**要进 `m_panelDc` 图层缓存(键 = `PanelGlobalKey ⊕ PanelSlotKey`),
而 `UiWidget` 有自己的 dirty/capture 体系,两套缓存叠加会互相失效(历史教训:
`SignalPanel` 行位图缓存与控件树就被刻意分开维护)。
2. 面板几何**必须**由 `EdgePanelLayoutFor` 唯一给出(绘制 / 命中 / 光标 / 调试盒 / 缓存键
五处共用),改成控件树就要把这套"几何同源"拆掉。
3. 面板绘制要往 `m_panelDc` 里 `BitBlt` 抓像素、要 `AddOccluder` 挖洞、要按 `m_panelOrder`
逐层设置文本层号 —— 这些都是 `PlotView` 的内部协议。
⇒ **正确的公共化方向是把"按种类分支"收敛成唯一派发点,而不是把面板搬进控件树。**
### P20 面板「特征层」(提交 `abcf3e6`)
审计出同一套"按种类分支"被抄了多份:
| 重复项 | 位置 | 份数 |
| --- | --- | --- |
| 分类布尔量(`listStyle` / `countTable` / `hasButton` …) | `EdgePanelWidth` / `EdgePanelLayoutFor` / `PaintEdgeCursor` | 3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三分支 | 3 |
| `SetLane*Scroll` 实现 | `PlotViewFeatures.cpp` | 7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | `EdgePanelWidth` / `PaintEdgeCursor` | 2(**内容还不一致**) |
新增 `pvdetail::PanelStyle` + `PanelStyleOf(kind)`、`PanelScrollMetricsFor(...)`、
`PlotView::PanelTitle(kind)`,并让上面全部调用点改成读它们。
**顺带修掉一处潜伏错误**:`EdgePanelWidth` 的标题链漏了 MARK/CLIP(按"边沿计数"量宽)。
⚠️ 过程中踩到并修掉一个真实回归:把 `hasButton` 直接映射到 `PanelStyle::buildButton` 后,
「选区截取」的底部按钮行高归零 ⇒ 面板高度 124 → 87 px。像素基线**当场报 MISMATCH**。
修正方式:区分 `footerButtons()`(有按钮行)与 `buildButton`(是"构建为新信号")。
### P21 滚动收敛(提交 `abcf3e6`)
* 7 个 `SetLane*Scroll` → 一行转发到新的 `SetLanePanelScroll(laneIndex, kind, scroll)`。
* 滚轮 / 拖滚动条两条 `switch`/`if` 链 → 直接调它(**读与写从此同一个入口**)。
* `EdgePanelLayoutFor` 里 5 层滚动量三元式 → `LanePanelScrollOf(lane, kind)`
(该派发点第 33 轮就存在,布局一直没用它)。
新增离屏检查 `panelscroll`:7 种面板逐一验证"滚到底 / 滚回顶 / 夹取 / 内容区不动 /
不可滚动时无滚动条"。用 `MF4-Record.MF4`(566 信号并入同一网格)实测 **6/7 可滚**
(自由标记无固定点,不可滚是正确行为)。
### P22 整数格式化替换(提交 `34365b3`)
* 新增 `Utf8Util::FormatInteger` / `FormatIntegerUnsigned` / `FormatIntegerNarrow`
(手写十进制展开,不经 printf)。
* 全仓替换 **76 处** `std::to_wstring` + **2 处** `std::to_string`(20 个文件);
`MatLoader::TrimNumber` 的整数分支也改(它曾是最后一个引用 MinGW printf 的点)。
* **实测:−1,024 B**(787,968 → 786,944)。
* ⚠️ **gdtoa 那条 ≈33 KB 的链仍然去不掉**,原因已定位到具体文件行:
libstdc++ 的 `x86_64-w64-mingw32/bits/os_defines.h` **无条件**
`#undef __USE_MINGW_ANSI_STDIO` + `#define … 1`("libstdc++ depends on POSIX-definitions"),
`g++ -E -dM` 实测最终值恒为 1 ⇒ 任何 C++ TU 的 `swprintf`/`strtod` 必然是 MinGW 版本。
叠加 `-D__USE_MINGW_ANSI_STDIO=0` 实测 **0 字节**收益,已回退并在 Makefile 留下结论。
要真省这 33 KB 只能自己重写「浮点 → 十进制(正确舍入)」与浮点解析的特殊分支 ⇒ **不做**。
### P23 其余审查清理
* `PlotView::FreeMarkerValue` 的 `const Lane&` 形参从未使用(函数体里只有一句 `(void)lane;`)
⇒ 删除形参与该语句,更新 2 个调用点。
* 「选区截取是否就绪(双光标)」在布局里算了 3 遍 ⇒ 收敛到函数顶部一次
(它同时决定面板高度 footer、条目行、按钮几何,写错任一处就是"底部多一段空白"或"没双光标却能滚动")。
### 本轮验证
| 项 | 结果 |
| --- | --- |
| `make release`(删 `.o` 全量) | 零 error / 零 warning;产物 **786,944 B**(起点 787,968,−1,024) |
| `make -C Tools` | 零 error / 零 warning |
| `UnitTests` | `checks=260 failed=0 cases=19`(新增 `integer_replacement_matches_std`,41 条断言) |
| `OffscreenChecks -all` | 6 个检查全 PASS,`checks=87 failed=0`(新增 `panelscroll`) |
| `panels -mode compare` | 7 个面板**像素哈希 + 几何逐项不变**(三次重构全程守住) |
| `panelscroll`(MF4,566 信号) | 6/7 面板可滚,夹取与滑块位置全部符合预期 |
### 下一步建议(面板方向)
1. **`EdgePanelLayout` 的平行数组**:`rowHeaders` / `rowNameRects` / `rowTriangles` /
`rowChecks` / `rowPoints` / `rowIterations` / `rowStats` / `rowLevel*` / `rowIter*` /
`rowInfo*` / `rowIntervalField` / `rowFrequencyField` 共 17 个按行平行的 `vector`,
全仓引用 **314 处**。每加一种参数行都必须在每个分支里按**同一顺序** push 恰好 17 项,
漏一项 = 某列读到别人的矩形(这正是"改布局就出错"的机制)。
建议合并为 `struct PanelRow { … }` + `std::vector<PanelRow> rows`。
**本轮未做**:314 处引用、风险高,需要先补"逐行几何基线"检查。
2. **`PaintEdgeCursor` 拆分**:该函数约 1600 行,其中三段"内容绘制"(表格 / 列表 / 自由标记)
各 90–450 行,可机械提取为 3 个成员函数(`PaintPanelCountTableRows` /
`PaintPanelListRows` / `PaintPanelMarkerGroups`)。判据:`panels -mode compare` 全绿。
**本轮未做**:与 P22 抢时间,且属纯可读性收益。
3. **给平行数组补检查**:在 `panels` 里增加"每个 row 的 17 项长度一致"断言
(成本极低,可作为第 1 项的前置护栏)—— 已记入待办。
---
## 第 36 轮(2026-11-04):§12.7 两项建议落地
### P26 拆分 `PaintEdgeCursor` —— ✅ 完成(提交 `38fbd4b`)
* 新增 `Components/PlotView/Src/PlotViewPanelPaint.cpp`(1016 行,**机械搬移、零逻辑改动**):
* `PaintPanelCountTableRows` —— 边沿计数表格(固定表头 + 可滚动行);
* `PaintPanelListRows` —— 列表式(均值/中心轴/弱峰/重采样/截取)条目栏 + 参数行;
* `PaintPanelMarkerGroups` —— 自由标记(分组栏 + 表头 + 条目 + 吸顶)。
每个函数只接收它真正需要的参数(`layout` / `tabLane` / `kind` / `laneIndex`),
不再"整个函数都看得见"。
* `PlotViewPaint.cpp`:6554 → 5575 行(`PaintEdgeCursor` 由约 1900 行降到约 900 行);
三处块替换为一行调用,骨架(边框/标题栏/折起三角/GX 徽标/✕)与底部按钮仍在一处。
* 滚动条绘制抽为 `pvdetail::PaintPanelScrollBar(g, layout, theme)`。
* **顺带补上第 35 轮承诺未落地的一处**:`PaintEdgeCursor` 的标题仍是 7 层三元式
(与 `EdgePanelWidth` 里另一条链并存,后者漏了 MARK/CLIP)⇒ 统一为 `PanelTitle(kind)`。
**过程中"机械搬移"踩到的三个真实问题**(都已修)
1. 提取块里引用的 `paintCounts` / `paintPanelScrollBar` 是**局部 lambda** ⇒
前者改为新函数内的局部 lambda(只服务于该段)、后者提升为 pvdetail 内联函数。
2. 「标记透明度」滑块用到的 `panel` / `pad` 是外层局部变量 ⇒ 改为从 `layout.panel` 派生。
3. `tabLanes` 在 marker 段里是**被写**的(回写各列实测宽度)⇒ 参数必须非 const。
### P27 平行数组错位隐患 —— ✅ 用 `AppendRow()` 消除(提交 `85aaed5`)
审计发现 `EdgePanelLayout` 每行对应 **18 个平行 vector**,而**四个**布局分支各自手写 push:
* 边沿计数表格:13 次 push;
* 列表式基行:13 次 push;
* 「未勾选/收起」分支:又补 6(+重采样 4)次空 push;
* 「重采样展开」/「普通展开」分支:各自再 push 6(+4)次。
任何一处漏推 ⇒ 该列之后所有行读到**别人的**矩形。
改法:
* 新增 `EdgePanelLayout::AppendRow(ref, header, nameRect, triangle, check)` —— **唯一入口**,
18 个数组一次补齐(其余列为空矩形)。
* 三个 push 块 → 各 **1 次 `AppendRow`**;参数行/重采样行改为 `rowXxx.back() = …` **赋值**;
「未勾选/收起」分支的 6+4 次补空 push **整段删除**(AppendRow 已补过)。
* ⇒ "新增一列"只需改 `AppendRow` 一处;"新增一行"必须走它(结构上不会漏列)。
⚠️ **没做完整结构合并**(`std::vector<PanelRow>` + 列视图),理由:
列视图需要 `EdgePanelLayout` 自定义拷贝构造/赋值来重绑视图,而该类型是
`EdgePanelLayoutFor()` 的返回值、又被存进 `std::vector` —— 拷贝是热路径,
拷贝语义写错会产生**比原缺陷更隐蔽的悬垂视图**(原缺陷至少会被长度不变式抓到)。
先消除真实隐患,结构合并留待"逐行几何基线"检查就绪。
### P28 收紧不变式
`panels` / `panelscroll` 里的长度检查从"13 个必须等长 + 4 个可空"改为
**18 个全部必须等于行数** —— `AppendRow()` 成为唯一入口后,"某列短了"
就是"有人绕过它手写 push"的直接证据。
### 本轮验证
| 项 | 结果 |
| --- | --- |
| `make release`(全量) | 零 error / 零 warning;产物 786,432 → **785,408 B** |
| `make -C Tools` | 零 error / 零 warning |
| `UnitTests` | `checks=260 failed=0 cases=19` |
| `OffscreenChecks -all` | 6/6 PASS,`checks=101 failed=0`(断言合并后略减) |
| `panels -mode compare` | 7 个面板像素哈希 + 几何**逐项不变**(两次重构全程守住) |
| `panelscroll`(MF4,566 行) | 18 个行数组长度全部 = 566,行为不变 |
---
## 第 37 轮(2026-11-04):逐行几何基线(§13.6 第 1 项)
### P29 新增 `rowgeom` 检查 —— ✅ 完成
`Tools/OffscreenChecks/Src/Checks.cpp` 新增注册键 `rowgeom`,做法:
1. **场景 A(细节基线)**:切双光标 → 前 6 条信号 → `MERGE_LANES` → 7 个面板全开、
第 2 条信号勾选(同一面板里同时有"展开的参数行"与"未勾选的行")→ 加 3 个真实固定点
→ 导出 7 个面板 **展开态**全字段 → 全部折起再导一份 → 复原。
2. **场景 B(规模基线)**:全部信号合并成 1 个网格(默认数据 3 行;`-data MF4-Record.MF4` = 566 行)
→ 每面板一个 FNV-1a 摘要 + `rows` / `markerRows` 计数。
3. **导出格式**(纯文本、逐行 `键=值`,便于 `git diff` 与人工定位):
```text
mean.rowRefs.count=3
mean.rowNameRects[001]=1155.75,221.25,512.50,25.00
mark.markerGroupCount=2
mark.markerGroup[000].header=1155.75,171.25,512.50,27.50
mark.markerRow[000].item=0/1
mark.markerRowCount=3
```
⚠️ 因为 `rowRefs` 是**重载**的(见下),导出器按**每一列自己的长度**遍历,
不能"取 `rowRefs.size()` 当行数统一遍历全部 18 列"。
4. **`-mode record` / `-mode compare`**:比对双向(缺键 = MISSING,多键 = ADDED,值变 = CHANGED),
最多打印 20 条差异;差异数为 0 才算 PASS。
### 过程中两次崩溃(都已定位,结论写进 `agent_mem.md`)
| # | 现象 | 根因 | 处置 |
| --- | --- | --- | --- |
| 1 | `LaneCount()` 断言失败 | `SetMode(MERGED)` 只重建 `TabConfig` 的网格配置,`PlotView::m_lanes` 要等下一次布局/绘制才刷新 | `MERGE_LANES` 之后补 `SynchronizeView(true)` + `RenderFrame()` 再断言 |
| 2 | 0xC0000005(导出时越界) | 假设"18 个行数组等长",但 **`MARK` 面板复用 `rowRefs` 表示信号分组**,只填 `rowNameRects` / `rowNameCloses`,其余 14 列为空 ⇒ 按 `rowRefs.size()` 索引其它列直接越界 | 导出器改为**逐列按自身长度**遍历 |
### P30 长度不变式的判据修正 —— ✅ 完成
`panels` / `panelscroll` 里第 36 轮定的"18 列必须全部等于行数"对 `MARK` 面板**是错的**
(它对新结构是误判,只是既有场景恰好没触发)。改为按 `PanelStyleOf(kind).markerGroups` 分流:
* `markerGroups == true`:`rowNameRects` / `rowNameCloses` 等于分组数,
且 `rowHeaders` / `rowChecks` / `rowLevelTracks` / `rowIterTracks` **必须为空**;
* 其余面板:17 列(`rowRefs` + `rowHeaders` … `rowFrequencyField`)全部等于行数。
失败时打印 15 个计数字段,便于一眼看出是哪一列错了。
### 基线是否真的能抓回归(负向自检)
把基线副本里 `mark.markerRowCount=3` 改成 `2` 再跑 `-mode compare`:
```text
[rowgeom detail] CHANGED mark.markerRowCount: baseline=2 current=3
[FAIL] rowgeom detail geometry is unchanged
SUMMARY checks=32 failed=1 groups=1 result=FAIL
```
⇒ 检测器**不是空转**的。(正式基线所在目录始终是 `record` 生成的,未被改动。)
### 一个"顺手查清"的行为(不是缺陷)
`rowgeom` 第一次跑出 `mark.hash … rows=0`:场景 B 里固定点没了。追查结论:
`Session::ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
是 `RebuildLanes` 的**既有设计**(MERGED 分支的 `lanes.clear()`),
网格级状态(固定点 / 面板开关 / 滚动位置)随之清空 —— 与 `CLEAR_ALL` 行为一致,**不是新缺陷**。
因此场景 B 改为**重新播**固定点(`addFreeMarkers(2)` lambda),
场景 B 的 MARK 面板现在是 `rows=2 markerRows=3`,规模覆盖也补上了。
### 本轮验证
| 项 | 结果 |
| --- | --- |
| `make release`(全量) | 零 error / 零 warning;产物 **785,408 B 不变**(本轮只动工具与检查) |
| `make -C Tools` | 零 error / 零 warning |
| `UnitTests` | `checks=260 failed=0 cases=19` |
| `OffscreenChecks -all` | **7/7 PASS**,`checks=127 failed=0`(第 36 轮为 6/6、101) |
| `rowgeom -mode compare`(默认数据) | `detail 983 键 + scale 7 键`,0 差异 |
| `rowgeom -mode compare -data MF4-Record.MF4` | `detail 1304 键 + scale 7 键`(566 行),0 差异 |
| `panels -mode compare` | 7 个面板像素哈希 + 几何逐项不变 |