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
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 15:49:25 +08:00
1 parent 221f9d2410
commit 353c553721
6 files changed
+821 -48

No files matched your search

+55
View File
@@ -555,3 +555,58 @@ Tools/README.md 目录结构、作用、测试项、验收标准、A/B
可在 `AppendRow` 里保留但标注"仅为兼容保留",或在下一轮与文档一起清掉。
3. **`PaintPanelListRows` 仍约 470 行**:可再按"条目栏 / 参数行 / 重采样行"拆三段,
但当前已不在"改一次就出错"的路径上,优先级低于第 1 项。
---
## 十四、第 37 轮:逐行几何基线(§13.6 第 1 项)
### 14.1 结论
| §13.6 建议 | 结果 |
| --- | --- |
| 1. 逐行几何基线 | ✅ 新增 `OffscreenChecks -run rowgeom`(`record` / `compare`),并做了**负向自检**证明它能抓回归 |
| 2. `rowPoints` / `rowIterations` 清理 | ⏸ 未做(本轮与该建议无耦合,留给下一轮) |
| 3. `PaintPanelListRows` 再拆 | ⏸ 未做(纯可读性,优先级低) |
`-all` 从 6 个检查 / 101 条断言 → **7 个检查 / 127 条断言**,全绿。
### 14.2 基线的两层设计(为什么不是"一行一个文件")
| 层 | 场景 | 记录 | 键数 | 用途 |
| --- | --- | --- | --- | --- |
| 细节 | 6 信号 1 网格、7 面板全开、参数行展开、3 个固定点、折起/展开各一份 | 每个矩形的每个字段 | 983(MF4 上 1304) | **定位**:直接看出哪一行哪一列偏了几像素 |
| 规模 | 全部信号 1 网格(MF4 = 566 行) | 每面板一个 FNV-1a 摘要 + 行数 | 7 | **覆盖**:任何一行变动都改哈希,且不产生几十万行基线 |
两层互补:哈希层保证"改动一定被看见",明细层保证"看见了能定位"。
### 14.3 这次护栏暴露的两个结构事实
1. **`rowRefs` 是重载的字段**。
「列表式」面板里 `rowRefs[i]` = 第 i 行的信号;
「自由标记」面板里 `rowRefs[i]` = 第 i 个**信号分组**,而它真正的条目行在 `markerRows`,
且只填 `rowNameRects` / `rowNameCloses`,另外 14 列**必须为空**。
⇒ 第 36 轮定的"18 列全等"不变式对 `MARK` 是**错的**,本轮改为按 `PanelStyle` 分流判定。
同一个"字段重载"也让导出器必须**逐列按自身长度**遍历(否则越界崩溃 —— 这正是本轮第一次
`0xC0000005` 的来源)。
2. **`SetMode()` 不同步 `m_lanes`**。`ExecuteToolbarAction(MERGE_LANES/SPLIT_LANES)` 之后
`TabConfig` 的网格配置已是新的,但 `PlotView::m_lanes` 要等下一次布局/绘制才刷新 ——
任何"合并后立刻读 `LaneCount()`"的代码都必须先 `SynchronizeView(true)`。
### 14.4 一个被误判成缺陷的既有行为
场景 B 一开始 `mark` 面板是空行:`Session::ShowSignals()` **先** `SetAllVisible(false)`,
而"全部不可见 ⇒ 丢弃网格"是 `RebuildLanes` 的既有设计(与工具栏「清空」一致),
网格级状态(固定点 / 面板开关 / 滚动位置)随之清空。⇒ 不是新缺陷;
检查改为在该场景**重新播**固定点,顺带把规模覆盖补全(`rows=2 markerRows=3`)。
### 14.5 下一步(按价值排序)
1. **`rowPoints` / `rowIterations` 清理**(确认已无绘制方使用,仅 `BuildBoxes` / 命中键残留):
这是当前唯一"写了没人读"的列,删掉能减少每次 `AppendRow` 的 2 次 push 与 2 个 vector。
判据:`rowgeom -mode compare`(细节基线会逐项证明几何没变)+ `panels -mode compare`。
2. **完整结构合并(`std::vector<PanelRow>` + 列视图)**:前置护栏(本轮)已就绪,
但 §13.3 的**失败可见性**问题仍未解决 —— 列视图拷贝后忘记重绑是静默悬垂。
若要做,必须同时给 `EdgePanelLayout` 加"视图自检"(例如保存 `rows` 容量并与视图长度比对),
否则不建议动。
3. `PaintPanelListRows` 再拆三段(纯可读性)。