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

+36
View File
@@ -271,3 +271,39 @@
本轮先用 `AppendRow()` 把**真实隐患**("某分支漏推一列")消除,
完整结构合并留待"逐行几何基线"检查就绪后再做。
---
## 九、第 37 轮追加(2026-11-04):逐行几何基线(§13.6 第 1 项)
> 触发:用户要求「按照你的建议继续推进」。§13.6 第 1 项 = 逐行几何基线,
> 它是 §13.3「完整结构合并」与「重写布局算式」的前置护栏。
| 节点 | 内容 | 验收标准 |
| --- | --- | --- |
| **P29** | 新增 `OffscreenChecks -run rowgeom`:把 7 个面板**每个矩形的每个字段**导出为文本基线,逐项比对 | `-mode record`/`-mode compare` 无差异;**篡改基线必须报错**(负向自检) |
| **P30** | 修正长度不变式的判据:从"18 列全等"改为**按排版类别**判定 | `panels` / `panelscroll` 仍全绿,且对 `MARK` 面板不再误判 |
### 9.1 基线的两层覆盖
| 场景 | 数据 | 记录内容 | 用途 |
| --- | --- | --- | --- |
| **A:细节** | 前 6 条信号合并成 1 个网格,7 个面板全开、参数行展开、3 个固定点、折起/展开各一份 | 每行每列的精确矩形(`Temp/tools/rows-baseline/rowgeom-baseline.txt`,983 条键) | 定位级:一眼看出"哪一行哪一列偏了几个像素" |
| **B:规模** | **全部**信号合并成 1 个网格(MF4 = 566 行) | 每个面板一个 FNV-1a 摘要哈希 + 行数/固定点行数 | 覆盖级:任何一行变动都会改哈希(不产生几十万行基线) |
### 9.2 本轮新增硬约束
1. **改面板几何(宽高算式 / 行距 / 内边距 / 折起规则)前后必须跑**
`OffscreenChecks -run rowgeom -mode record|compare`。
基线是**检测器**不是黄金图:变化是**预期**时要重新 record,并在提交信息里说明原因。
2. **`rowRefs` 是重载的**:「列表式面板」用 `rowRefs` 表示"每个信号一行",
而「自由标记面板」用它表示"每个信号一个分组",只填 `rowNameRects` / `rowNameCloses`,
其余 14 列**必须为空**(真正的条目行在 `markerRows`)。因此任何"平行数组长度"不变式
**必须按 `PanelStyle` 分类判定**,不能写"全部 18 列全等"。
3. **`ExecuteToolbarAction(MERGE_LANES / SPLIT_LANES)` 之后、读 `LaneCount()` 之前,
必须先 `SynchronizeView(true)`**:`SetMode()` 只重建 `TabConfig` 的网格配置,
`PlotView::m_lanes` 要等下一次布局/绘制才刷新。
4. 检查里**不要依赖"上一个场景的网格级状态还在"**:`Session::ShowSignals()` 先
`SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"是既有设计
(`RebuildLanes` 的 MERGED 分支),固定点/面板开关会随之清空 —— 该场景要**重新播**。