review(panels+size): 面板结构收敛收尾、审查清理、新增平行数组护栏

一、审查清理
* `PlotView::FreeMarkerValue` 的 `const Lane&` 形参从未使用(函数体里只有一句
  `(void)lane;`)⇒ 删除形参与该语句,更新 2 个调用点。
* `EdgePanelLayoutFor` 里「选区截取是否就绪(双光标)」算了 3 遍 ⇒ 收敛为函数顶部
  一次 `clipReady`。它同时决定面板高度 footer、条目行可见性、按钮几何,
  三处各算一遍时只要有一处条件写错,就是"底部多一段空白"或"没双光标却能滚动"。

二、新增护栏(离屏检查)
* `panels`:新增「平行数组长度一致」不变式 —— `EdgePanelLayout` 的 13 个按行平行
  vector 必须与行数一致;4 个「重采样」专属数组允许为空、非空则必须对齐。
  这是"某列读到别人矩形"这类错位缺陷的唯一低成本护栏(O(1))。
* `panelscroll`:同样加一份(大文件、566 行时才会暴露的问题)。
* `panels` 输出增加 rows 数,便于定位。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §七(第 35 轮节点与硬约束)。
* `Documents/Code_Review_Progress.md` 新增第 35 轮(面板架构评估 + 三次重构记录)。
* `Documents/Code_Review_Report.md` 新增 §12(评估结论 + gdtoa 最终结论 + 护栏价值)。
* `Documents/agent_mem.md` 新增第 35 轮六条规则。
* `Tools/README.md`:补 `panelscroll`、`integer_replacement_matches_std`,
  断言数更新为 260 / 108,最小集表补充面板相关条目。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **786,944 B**。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests` → checks=260 failed=0 cases=19。
* `OffscreenChecks -all` → 6/6 检查 PASS,checks=108 failed=0。
* `panels -mode compare` → 7 个面板像素哈希与几何逐项不变。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 14:37:59 +08:00
1 parent 34365b3c1c
commit b58b427f62
8 files changed
+408 -15

No files matched your search

+93 -3
View File
@@ -417,6 +417,69 @@ namespace jdi
rect.y + rect.height <= plotRect.y + plotRect.height + 1;
Expect(inside, std::string("panel inside plot area: ") + PanelKindName(kind));
// 不变式:`EdgePanelLayout` 的**平行数组长度必须全部等于行数**。
// 这 13 个按行平行的 vector 每个分支都要按同一顺序 push 恰好 13 项,
// 漏一项就会让某一列读到别的行的矩形("改布局就出错"的典型机制)。
// ⚠️ 「重采样」专属的 4 个数组(最大/最小间隔、间隔/频率输入框)在**其它**
// 面板里允许为空(没人读),但只要非空就必须同样与行数对齐。
// 成本极低(O(1) 比长度),但能挡住整类错位缺陷。
const std::size_t rowCount = layout.rowRefs.size();
const std::size_t coreSizes[] = {
layout.rowRefs.size(), layout.rowHeaders.size(),
layout.rowNameRects.size(), layout.rowTriangles.size(),
layout.rowChecks.size(), layout.rowPoints.size(),
layout.rowIterations.size(), layout.rowStats.size(),
layout.rowLevelTracks.size(), layout.rowLevelValues.size(),
layout.rowLevelResets.size(), layout.rowIterTracks.size(),
layout.rowIterValues.size(), layout.rowIterResets.size()};
bool parallel = true;
for (const std::size_t size : coreSizes)
{
if (size != rowCount)
{
parallel = false;
break;
}
}
const std::size_t resampleSizes[] = {
layout.rowInfoMaxInterval.size(), layout.rowInfoMinInterval.size(),
layout.rowIntervalField.size(), layout.rowFrequencyField.size()};
bool resampleParallel = true;
for (const std::size_t size : resampleSizes)
{
if (size != 0 && size != rowCount)
{
resampleParallel = false;
break;
}
}
if (!parallel || !resampleParallel)
{
EmitLine("[panels] %s row arrays: refs=%d hdr=%d name=%d tri=%d chk=%d pts=%d it=%d "
"stat=%d lvl=%d/%d/%d iter=%d/%d/%d info=%d/%d fld=%d/%d\n",
PanelKindName(kind), static_cast<int>(layout.rowRefs.size()),
static_cast<int>(layout.rowHeaders.size()),
static_cast<int>(layout.rowNameRects.size()),
static_cast<int>(layout.rowTriangles.size()),
static_cast<int>(layout.rowChecks.size()),
static_cast<int>(layout.rowPoints.size()),
static_cast<int>(layout.rowIterations.size()),
static_cast<int>(layout.rowStats.size()),
static_cast<int>(layout.rowLevelTracks.size()),
static_cast<int>(layout.rowLevelValues.size()),
static_cast<int>(layout.rowLevelResets.size()),
static_cast<int>(layout.rowIterTracks.size()),
static_cast<int>(layout.rowIterValues.size()),
static_cast<int>(layout.rowIterResets.size()),
static_cast<int>(layout.rowInfoMaxInterval.size()),
static_cast<int>(layout.rowInfoMinInterval.size()),
static_cast<int>(layout.rowIntervalField.size()),
static_cast<int>(layout.rowFrequencyField.size()));
}
Expect(parallel, std::string("row arrays have identical lengths: ") + PanelKindName(kind));
Expect(resampleParallel,
std::string("resample row arrays empty-or-aligned: ") + PanelKindName(kind));
char buffer[160] = {};
std::snprintf(buffer, sizeof(buffer), "%s=%016llX,%d,%d,%d,%d\n", PanelKindName(kind),
static_cast<unsigned long long>(HashRegion(session.LastFrame(), rect)), rect.x,
@@ -537,9 +600,36 @@ namespace jdi
const PlotView::EdgePanelLayout initial = plot.EdgePanelLayoutFor(0, kind);
const float maxScroll =
std::max(0.0f, initial.markerContentHeight - initial.content.Height);
EmitLine("[panelscroll] %-8s content=%.1f view=%.1f maxScroll=%.1f scrollable=%d\n", name,
initial.markerContentHeight, initial.content.Height, maxScroll,
initial.scrollTrack.Width > 0.0f ? 1 : 0);
EmitLine("[panelscroll] %-8s content=%.1f view=%.1f maxScroll=%.1f scrollable=%d rows=%d\n",
name, initial.markerContentHeight, initial.content.Height, maxScroll,
initial.scrollTrack.Width > 0.0f ? 1 : 0,
static_cast<int>(initial.rowRefs.size()));
// 多信号场景下额外守住"平行数组长度一致"(本轮新增,成本 O(1))。
{
const std::size_t rows = initial.rowRefs.size();
const std::size_t sizes[] = {initial.rowHeaders.size(), initial.rowNameRects.size(),
initial.rowTriangles.size(), initial.rowChecks.size(),
initial.rowPoints.size(), initial.rowIterations.size(),
initial.rowStats.size(), initial.rowLevelTracks.size(),
initial.rowLevelValues.size(), initial.rowLevelResets.size(),
initial.rowIterTracks.size(), initial.rowIterValues.size(),
initial.rowIterResets.size(), initial.rowInfoMaxInterval.size(),
initial.rowInfoMinInterval.size(),
initial.rowIntervalField.size(),
initial.rowFrequencyField.size()};
bool aligned = true;
for (const std::size_t size : sizes)
{
// 「重采样」专属数组在其它面板允许为空(见 `panels` 检查的说明)。
if (size != rows && size != 0)
{
aligned = false;
break;
}
}
Expect(aligned, std::string("row arrays aligned with rows: ") + name);
}
if (maxScroll <= 1.0f)
{
+7 -4
View File
@@ -124,12 +124,13 @@ SUMMARY checks=<n> failed=<m> cases=<k> result=<PASS|FAIL>
| `filter_moving_average_equivalence` | 1 阶 ≡ 单次、2 阶 ≡ 链式、常数信号不变 | 均值滤波多阶 |
| `dataset_group_access` | `AddGroup` / `GroupAt` / `Style(ref)` 读写 | 数据层入口 |
| `dataset_formula_transform_cache` | 值域 + 逐分组极值 vs 逐点参考**逐位相同**;命中同指针;样式变化失效 | 第 32 轮核心缓存的正确性 |
| `stdio_mingw_vs_ucrt_formats` | MinGW ANSI stdio 与系统 UCRT 的 printf 在**本程序用到的全部格式**上逐字符比对(13.6 万次调用) | 评估“把 `swprintf` 交给 UCRT”是否可行(当前 0 差异);将来真做迁移就是验收标准 |
| `stdio_mingw_vs_ucrt_formats` | MinGW ANSI stdio 与系统 UCRT 的 printf 在**本程序用到的全部格式**上逐字符比对(13.6 万次调用) | 评估“把 `swprintf` 交给 UCRT”是否可行(结论:0 字节,libstdc++ 强制开 MinGW 版本) |
| `integer_replacement_matches_std` | `FormatInteger*` 与 `std::to_wstring` / `std::to_string` 逐字符对照(含 LLONG_MIN/MAX、ULLONG_MAX) | 全仓 78 处整数转换的语义护栏(它们已替代标准库,以去掉 printf 依赖) |
### 3.2 验收标准
* **零失败**(`result=PASS`)才算通过;
* 断言数量只增不减(当前 **219** 项 / 18 个用例):删断言必须说明理由;
* 断言数量只增不减(当前 **260** 项 / 19 个用例):删断言必须说明理由;
* 任何"为了让它过"而放宽的断言,必须在代码注释里写清依据。
---
@@ -162,7 +163,8 @@ Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
| --- | --- | --- |
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**平行数组长度与行数一致**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住平行数组一致性 |
| `edgestats` | 3 个阈值下的边沿计数(普通计数)+ 中心轴计数 | 与 `Checks.cpp` 内的**独立参考实现**(朴素逐点)**逐位相等**;同参数重复查询结果稳定 |
| `perf` | 整帧中位数、`EdgePanelLayoutFor` 单次耗时、198 个统计状态的整轮查询耗时 | 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比 |
@@ -199,9 +201,10 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
| --- | --- |
| 任何数据层 / 解析 / 公式 / 变换 | `UnitTests.exe` |
| 绘图、面板、布局、缓存键 | `-run smoke -run panels`(+ 需要时 `-mode compare`) |
| 面板内容滚动 / 面板几何 / 新增面板种类 | `-run panels -run panelscroll`(大文件加 `-data ..\TestData\MF4-Record.MF4`) |
| 边沿统计 / 中心轴 / 扫描预算 | `-run edgestats -run perf` |
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
| 完全不确定影响面 | `-run -all`(约几秒到几十秒)+ `UnitTests.exe` |
| 完全不确定影响面 | `-all`(约几秒到几十秒)+ `UnitTests.exe` |
> 用户明确要求:**除非要求"全测一遍",否则只跑针对性检查**。