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

+81
View File
@@ -410,3 +410,84 @@ Makefile 按目录通配收集源文件,**无需登记新文件**。
| `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 个面板像素哈希 + 几何逐项不变 |