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 分支),固定点/面板开关会随之清空 —— 该场景要**重新播**。
+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 个面板像素哈希 + 几何逐项不变 |
+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` 再拆三段(纯可读性)。
+74
View File
@@ -10564,3 +10564,77 @@ release/debug **零警告**;Release `TargetCheck.exe` **890,880 B**(< 1 MB
* **逐行几何基线**(导出每行 18 个矩形做逐项比对)—— 完整结构合并与布局算式重写的前置护栏。
* `rowPoints` / `rowIterations` 已无绘制方使用(旧式输入框排版早已删除),可标注/清理。
* `PaintPanelListRows` 仍约 470 行,可再拆三段,但已不在"改一次就出错"的路径上。
---
## 2026-11-04 · 第 37 轮(`Code_Review` 分支):逐行几何基线(§13.6 第 1 项)
> 提交:见本轮提交信息;规则原文 `Documents/Code_Review_Report.md` §14、`Code_Review_Plan.md` §九、
> `Code_Review_Progress.md` 第 37 轮、`Tools/README.md` §4.1/§4.3/§4.4。
### 1. ✅ 新增 `OffscreenChecks -run rowgeom`(`record` / `compare`)
两层覆盖:
* **细节层**(6 信号 1 网格、7 面板全开、参数行展开、3 个固定点、折起+展开各一份):
每个矩形的每个字段一条键,`Temp/tools/rows-baseline/rowgeom-baseline.txt` 共 **983** 条
(`-data MF4-Record.MF4` 时 1304 条)。
* **规模层**(全部信号 1 网格,MF4 = 566 行):每面板一个 FNV-1a 摘要 + 行数,7 条键。
键格式:`<panel>.<field>` / `<panel>.<列名>[NNN]=x,y,w,h` / `mark.markerGroup[NNN].header=…`。
比对双向(MISSING / ADDED / CHANGED),差异 0 才 PASS。
### 2. 🔴 新硬规则一:`rowRefs` 是**重载字段**,长度不变式必须按类别判定
| 面板类别 | `rowRefs` 含义 | 真正的行在 |
| --- | --- | --- |
| 列表式(mean/center/peak/resample/clip) | 每个信号一行 | `rowRefs` 自身 + 其余 15 列 |
| 边沿计数(count) | 每个信号一行(表格行) | 同上 |
| 「自由标记」(mark) | 每个**信号分组**一项 | `markerRows` / `markerGroups` |
MARK 面板**只填** `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空**。
⇒ 第 36 轮定的"18 列全部等于行数"对 MARK 是**错的**,本轮已改为:
* `PanelStyle::markerGroups == true` ⇒ `rowNameRects`/`rowNameCloses` 等于分组数,
且 `rowHeaders`/`rowChecks`/`rowLevelTracks`/`rowIterTracks` 必须为空;
* 其余面板 ⇒ `rowRefs` + 16 列全部等于行数。
任何"遍历 18 列"的代码(导出器、不变式、批量处理)**都必须逐列按自身长度**遍历,
不能拿 `rowRefs.size()` 当统一行数(否则 MARK 面板直接越界 —— 本轮第一次 `0xC0000005`)。
### 3. 🔴 新硬规则二:`SetMode()` 不同步 `m_lanes`
`ExecuteToolbarAction(MERGE_LANES / SPLIT_LANES)`(以及 `TabConfig::SetMode`)只重建
`TabConfig` 的网格配置,`PlotView::m_lanes` 要等下一次布局/绘制才刷新。
⇒ **合并/拆分后、读 `LaneCount()` 或 `EdgePanelLayoutFor()` 之前,必须先
`app.SynchronizeView(true)`(必要时再 `RenderFrame()`)**。
### 4. ⚠️ 已知行为(不是缺陷):全部不可见 ⇒ 丢弃网格 ⇒ 网格级状态清空
`Session::ShowSignals()` 先 `SetAllVisible(false)`;`RebuildLanes` 的 MERGED 分支在
"无可见信号"时 `lanes.clear()`(与工具栏「清空」一致)。
⇒ 固定点 / 面板开关 / 面板滚动位置 / 面板显隐 **全部丢失**,不是新 BUG。
检查里跨场景依赖这些状态时,必须**重新播**(`rowgeom` 用 `addFreeMarkers(wanted)` lambda)。
### 5. 基线的定位
**检测器,不是黄金图。** 预期内的几何变化要重新 `-mode record`,并在提交信息里写明原因。
负向自检已做:篡改基线中 `mark.markerRowCount=3 → 2`,`-mode compare` 会
`CHANGED … mismatches=1` 并 FAIL ⇒ 护栏不是空转。
### 6. 本轮数字
* Release **785,408 B 不变**(本轮只动工具/检查与文档);零 warning。
* `UnitTests` 260 断言 / 19 用例;`OffscreenChecks -all` **7 检查 / 127 断言**(原 6/101)。
* `rowgeom -mode compare`:默认数据 983+7 键、MF4 1304+7 键,0 差异。
* `panels -mode compare` 7 个面板像素哈希 + 几何逐项不变。
### 7. 遗留(下一步候选,按价值)
1. **`rowPoints` / `rowIterations` 清理**(确认已无绘制方使用,仅 `BuildBoxes` / 命中键残留)——
当前唯一"写了没人读"的列,删掉可省每次 `AppendRow` 的 2 次 push。
判据:`rowgeom -mode compare` + `panels -mode compare`。
2. **完整结构合并(`std::vector<PanelRow>` + 列视图)**:护栏已就绪,但 §13.3 的
**失败可见性**问题未解(拷贝后忘重绑 = 静默悬垂)。若要做,必须同时给
`EdgePanelLayout` 加"视图自检"(比对 `rows` 容量与视图长度)。
3. `PaintPanelListRows` 再拆三段(纯可读性)。