Commit Graph
9 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash 35b67b216e test(tools): 默认数据/报告路径同时支持在仓库根目录运行
问题:`OffscreenChecks` 的默认 `-data` 只探测 `../TestData/...`,默认 `-report`
也固定写 `../Temp/tools/...`;在**仓库根目录**运行时会静默报
"no data file"(退出码 2),容易被误认为工具坏了(本轮实际踩到)。

改动:
* 默认数据文件候选同时包含 `../TestData/X` 与 `TestData/X`;
* 报告打不开时,若路径以 `../Temp/` 开头则去掉前缀重试一次;
* `Tools/README.md` 补充说明两种调用方式等价,显式 `-data/-out/-report`
  仍以当前工作目录为准。

验证:在仓库根目录执行 `Tools\OffscreenChecks\Build\OffscreenChecks.exe -all`
→ 6/6 检查 PASS,`checks=108 failed=0`;在 `Tools/` 下行为不变。
2026-11-04 14:39:27 +08:00
DeepSeek V4.1 Flash b58b427f62 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 个面板像素哈希与几何逐项不变。
2026-11-04 14:37:59 +08:00
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00
DeepSeek V4.1 Flash abcf3e6c44 refactor(panels): 浮动面板抽出唯一"面板特征层",消除 3 处分类/滚动几何/滚动设置的重复
背景(用户观察):7 个实验性功能都有自己的浮动面板,观感与骨架高度一致
(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕,内容区可滚动 + 滚动条,底部按钮行),
但每新增一种面板或改动布局,总会出"意料之外"的缺陷。

审计结论:骨架绘制本来只有一份,真正的问题是**同一套"按种类分支"的代码被抄了多份**:

| 重复项 | 位置 | 份数 |
| --- | --- | --- |
| 分类布尔量(listStyle / countTable / hasButton…) | EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor | 3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三个分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三个分支 | 3 |
| SetLane*Scroll 实现 | PlotViewFeatures.cpp | 7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | EdgePanelWidth 的三元式链 / 绘制里的三元式链 | 2(内容还不一致!) |

一、新增"面板特征层"(PlotViewInternal.hpp,pvdetail)
* `PanelStyle PanelStyleOf(EdgePanelKind)`:唯一分类来源,`listRows` / `countTable` /
  `markerGroups` + `buildButton` / `twoButtons` / `markerFooter`,`switch` 穷举不写 default
  (新增面板漏登记 ⇒ `-Wswitch` 编译报警)。
  其中专门区分 `footerButtons()`(底部有按钮行)与 `buildButton`(是"构建为新信号")——
  混为一谈会让「选区截取」底部少预留一行高度(本轮实测 124 → 87 px,被像素基线当场抓住)。
* `PanelScrollMetricsFor(...)`:滚动条轨道/滑块几何 + 滚动量夹取 + 内容宽度扣减的唯一实现。
* `PlotView::PanelTitle(EdgePanelKind)`:标题唯一来源。
  ⚠️ 修掉一处**潜伏**错误:`EdgePanelWidth` 的标题链漏了 MARK/CLIP(两者按"边沿计数"量宽),
  目前两种标题恰好等宽所以看不出来,但任何文案改动都会让宽度与标题不匹配。

二、收敛调用点
* `EdgePanelWidth` / `EdgePanelLayoutFor`(表格 / 列表 / 自由标记三分支)/ `PaintEdgeCursor`
  全部改用 `PanelStyleOf` 与 `PanelTitle`;滚动几何改用 `PanelScrollMetricsFor`。
* `EdgePanelLayoutFor` 的 5 层滚动量三元式 → `LanePanelScrollOf(lane, kind)`(该派发点第 33 轮
  就已存在,但布局这里一直没用它)。
* `PaintEdgeCursor` 的滚动条绘制抽成 `paintPanelScrollBar(layout)`,三处调用。
* 7 个 `SetLane*Scroll` 改为一行转发到新的 `SetLanePanelScroll(laneIndex, kind, scroll)`;
  滚轮与"拖滚动条"两条 `switch`/`if` 链改为直接调它(读与写从此同一个入口)。

三、新增离屏检查 `panelscroll`(护栏)
* 对 7 种面板逐一:可滚动时滚到底 / 滚回顶,断言 `scrollOffset` 夹取到 `maxScroll`、
  滑块到达/返回轨道端点、内容区几何不随滚动改变;不可滚动时断言不出现滚动条。
* 用 `MF4-Record.MF4`(566 信号合并到同一网格)实测 6/7 面板可滚(自由标记无固定点,正确)。
  没有这条检查,本次收敛就等于"改完只能靠肉眼"。

验证(本机 GCC 15.2.0 UCRT / 125% DPI)
* `panels -mode compare`:7 个面板的**像素哈希与几何逐项不变**(零差异)。
  这条检查当场抓到了上面 `footerButtons` 的回归,证明基线有效。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `UnitTests`:`checks=219 failed=0`。
* release 全量零 error / 零 warning;产物 788,480 → **787,968 B**(−512)。
2026-11-04 14:20:57 +08:00
DeepSeek V4.1 Flash 5c48bf922e review(follow-ups): 落地上轮建议并给出实测结论(#2/#4 回退、#3 不重写)
一、Tools 完善
* OffscreenChecks `edgestats`:改为自动挑"最有起伏"的信号。旧实现固定取
  `signals[0]`,而 MF4 的第一条信号是常量(6000)⇒ 三个阈值的断言退化成
  "0 == 0" 的空转。现在扫描前 200 条挑 `max-min` 最大者,SCATTER_TEST 上
  三个阈值分别得到 (20,19)/(20,19)/(20,19),与独立参考实现逐位相同。
* UnitTests 新增 `stdio_mingw_vs_ucrt_formats`:把 MinGW ANSI stdio 与系统
  UCRT 的 printf 在**本程序实际用到的全部格式**(`%.*f` / `%.6g` / `%g` /
  `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)上做逐字符比对,
  13.6 万次调用 **0 处差异**(覆盖 1e-40~1e40)。它是"把 swprintf 让给 UCRT"
  这条体积优化的验收标准。
* Tools/README.md:同步新增用例与断言数量(219 项 / 18 用例)。

二、第 33 轮报告 §10 后续建议的结论(详细数字见 Progress 第 34 轮)

1. **`EdgeScanFor` 状态查找哈希化:实测后回退。**
   实现方式为 `deque` + `unordered_multimap<keyHash, EdgeScanState*>`(桶内仍
   逐字段比对,碰撞安全;新建/淘汰/定向作废/清空四处同步索引),功能与像素
   验证全绿。但 198 个状态的整轮查询只从 0.097 降到 0.073 us/query(一轮省
   ≈4.8 us),而产物体积 +5,632 B。⇒ 回退。
   同时修正报告的量级:原估"288 信号 ≈0.3ms/帧",实测 ≈20us/帧。

2. **`EdgePanelLayoutFor` 表驱动化:不做。** 实测 1.9~2.5 us/次(每帧约 14 次
   ≈30us),不是热点;第 33 轮已消除"按种类分派"的重复,剩余 957 行是几何算式
   本身,无法表驱动。本轮改为交付"让这类重写可验证"的手段:
   `-run panels -mode record/compare` 逐面板比对几何 + 像素哈希。

3. **`wcstod` 自写替身:实测 0 字节收益。** 只加
   `-D__USE_MINGW_ANSI_STDIO=0` 全量重编后产物仍是 788,480 B、`mingw_wpformat.o`
   仍被链接。原因是预编译 libstdc++ 自己引用 `__mingw_swprintf`:只要还调用
   `std::to_wstring` / `std::to_string`(全仓约 80 处,若干处实参是浮点),
   gdtoa 必然被链入。⇒ 回退,并在 Makefile 留下说明防止重复尝试。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §六(第 34 轮节点与验收标准)。
* `Documents/Code_Review_Progress.md` 新增第 34 轮逐节点记录(含全部实测数字)。
* `Documents/Code_Review_Report.md` 新增 §11(结论表 + 两处量级修正 + 更新的后续建议)。
* `Documents/agent_mem.md` 新增第 34 轮规则:Tools 约定、两个构建陷阱、
  两条"别再重复做"的实验结论。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **788,480 B**(与改动前一致)。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests.exe` → checks=219 failed=0 cases=18。
* `OffscreenChecks.exe -all` → 5/5 检查 PASS,checks=71 failed=0。
2026-11-04 11:52:10 +08:00
DeepSeek V4.1 Flash 97e9163314 test(tools): 重建单元测试与离屏验证工具(Tools/ 入库)
背景:第 33 轮 Code Review 的三项后续建议(EdgeScanFor 查找哈希化、
EdgePanelLayoutFor 表驱动化、wcstod 自写替身)都卡在"没有可自证的验证手段"上。
本轮先补齐基础设施,再动被建议的代码。

一、Tools/ 从忽略改为入库(只忽略各工具的 Build/ 与 Temp/ 临时产物)
* `.gitignore`:删除 `Tools/` 整目录忽略,改为 `Tools/**/Build/`;
  截图 / 基线 / 报告一律写 `Temp/tools/`(`Temp/` 早已忽略)。

二、新增 Tools/Makefile(统一构建入口,不碰应用本体)
* 目标:`make` 构建全部、`make UnitTests` / `make OffscreenChecks` 单个、
  `make clean` 清工具产物、`make help`。
* 工具链接应用本体 `../Build/Release/*.o`(排除入口 `Main.o`),
  因此 ABI 开关必须与本体一致:`-fno-exceptions` / `-fno-rtti` /
  链接 `-flto -flto-partition=one`。
* 工具源文件自身**不**编译成 LTO 字节码(否则 GCC 15.2.0 在
  `choose_baseaddr` 处 ICE),只在链接阶段带 LTO 参数。
* `-MMD -MP` + `-include`:改头文件 / 增删源文件自动重编重链。
* 修正一个构建陷阱:`$(foreach)` 生成的工具目标会早于 `all`,
  make 会把第一个显式目标当默认目标 ⇒ 显式 `.DEFAULT_GOAL := all`。

三、Tools/UnitTests:纯数据层单元测试(218 项断言,17 个用例)
* 极简自研框架(`Inc/UnitTestFramework.hpp`):无异常、无 RTTI、
  支持布尔 / 整数 / 近似 / **逐位相等** / 宽字符串断言。
* 覆盖:`TryParseDouble` 与 libc `strtod` 逐位一致性(6 万条随机串)、
  接受性语义、`FormatDecimal*` 永不科学计数法、`NiceStep/BuildTicks`、
  公式语法/语义/归一化、批量求值 `FormulaProgram` 与逐点求值逐位一致、
  `ApplyDrawTransform` 顺序、`TransformValueRange`、
  `ComputeMovingAverageIterated` 阶数等价性、
  `DataSet::FormulaTransformFor` 的值域与分组极值(逐点参考实现比对)。

四、Tools/OffscreenChecks:隐藏窗口离屏验证(5 个检查)
* 窗口 `SW_HIDE`;"鼠标"用 `Application::HandleMouseMove()` 合成注入;
  像素从后备缓冲区读取(不依赖窗口可见性),可保存 PNG 与计算区域 FNV 哈希。
* `smoke`:初始化 / 加载 / 渲染 / 落盘,并断言"同状态两帧逐像素相同"。
* `frames`:连续帧抓取(PNG + 哈希 + 耗时),用于问题分析。
* `panels`:7 个实验性面板全部展开后的几何不变式 + **像素基线 A/B**
  (`-mode record` / `-mode compare`)。
* `edgestats`:边沿统计与工具内**独立参考实现**逐位比对,
  覆盖阈值变化与重复查询稳定性。
* `perf`:整帧中位数、`EdgePanelLayoutFor` 单次耗时、
  198 个统计状态的整轮查询耗时(用于优化前后对比)。

五、文档
* `Tools/README.md`:目录结构、构建/运行方式、每个工具的作用、测试项与
  验收标准、临时产物位置、A/B 基线用法、扩展步骤与已知限制。

实测(本机 GCC 15.2.0 UCRT / 125% DPI):
* `UnitTests.exe` → `checks=218 failed=0`
* `OffscreenChecks -all` → 全部 PASS;`SCATTER_TEST.csv` 整帧 ~5ms;
  `MF4-Record.MF4`(283 信号)198 个统计状态整轮查询 0.097 us/query。
2026-11-04 11:41:08 +08:00
DeepSeek V4.1 Flash 2904699eae 修复五项交互缺陷:折起后滚动条 / 单点信号样式 / 线宽实时生效 / 网格菜单限高滚动 / 点选信号滚动绘图区
1) 分组折起后再展开,信号列表的滚动条消失(筛选态与常规态均可复现)
   - 根因:AdvanceGroupAnimations 只在“折起完成”时重建行集合;而“展开完成”那一帧
     m_contentHeight 从收起态跳回展开态,却没有任何路径重算滚动条可见性 ⇒ 滚动条停在隐藏。
   - 修法:任一方向动画结束都 m_rowsDirty = true + BuildRows(),并每帧 RefreshScrollBar(false)。
     ⚠️ 动画期间不夹取滚动偏移,避免打断与滚动同步的折起动画(scrollSynced)。

2) 单点信号(finiteCount == 1)默认绘制方式由「点线」改为「散点」
   - ApplySinglePointDefaultStyle → DrawMode::SCATTER(仍取最大线宽)。
   - 新增 SignalPanel::ModeOptionEnabled:单点信号的「线段」「点线」置灰、不可点、
     且不参与悬停高亮(避免“看着能点其实点不动”);样式区的绘制与命中都用它。
   - PlotView::UpdateAutoScatterStyles 跳过单点信号,否则“放大自动点线”会把它又改回「点线」。

3) 拖动线宽滑块完全不生效(拖动时线宽不变、松手瞬间变了、再移动鼠标又变回去)
   - 根因一:AcquirePolyline 的折线缓存键没有 lineWidth,而 BuildPolyline 把线宽烘进
     包络带几何(每个像素列的上下包络各向外扩 max(1,线宽)*scale*0.5,「梳状」判定也依赖它)
     ⇒ 稠密信号(采样数 > 绘图区宽度)永远复用旧线宽的顶点与包络带。
   - 根因二:样式补丁只更新“静态场景缓存 + 后备缓冲区”,而每网格图层(宽基底)仍是旧样式像素,
     下一帧命中“宽基底快路径”时把新样式覆盖回旧像素。
   - 修法:PolylineCacheEntry 增加 lineWidth 并进比较;
     样式补丁之后 QueueLaneRebuild(受影响网格) + RenderPendingLaneRebuilds(1)(沿用每帧至多 1 个的预算)。

4) 「目标」行的 GX 标签复用「所属网格」菜单,并先给 PopupMenu 增加「限高 + 条目区滚动 + 尾部固定项」
   - MenuLayoutConstraint::maxHeight(OpenLaneMenu 传客户区高度的 62%);
   - 固定区 = 最后一个分隔线及其之后的条目(即「新建网格」)始终钉在菜单底部,其余条目可滚动;
   - 滚轮滚动条目区(Application 在菜单内滚动时不再关闭菜单,其余情形保持既有行为);
   - 键盘移动悬停项自动滚入可视区(EnsureItemVisible);ItemAtPoint 要求条目同时命中滚动视口;
   - 滚动区文本用 UiTextClipped(GDI 文本不遵循 GDI+ SetClip),并跳过完全不可见的条目(性能);
   - BuildLayout 不再无条件清零 m_scrollOffset(否则 RebuildLayout 会把滚轮位置吃掉);
   - 「目标」行 GX 标签点击**不参与**联动滚动(悬停联动保留)。

5) 点选信号名 ⇒ 绘图区自动把目标网格滚动到可视区内(上百个独立网格时旧有高亮无法定位)
   - SignalPanel::RequestPlotLaneScroll / TakePlotLaneScrollRequest;
     Application::ApplyPendingPlotLaneScroll(WM_LBUTTONDOWN 的面板分支);
     PlotView::ScrollLaneIntoView(在可视区上方就上滚、在下方就下滚、已完整可见则不动)。
   - 只有“点击信号名”触发;悬停绝不改变视图;勾选框 / GX 徽标 / 移除 ✕ 也不触发。

验证(全部离屏执行,窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- 新增 .preflight/check(不入库)68/68 通过;
- A/B:五处修复全部回退 ⇒ 17 项如实失败(B 4 / C 4 / D 7 / E 2);“折起后滚动条”需要
  逐帧重绘驱动动画才能复现,单独回退时 ⇒ 5 项如实失败;恢复后 68/68;
- make release / make debug 零警告。

其它:
- 按要求删除工作区 Tools/(structcheck 脚本)并移除 Makefile 的 structcheck 目标;
- Documents/agent_mem.md 同步:新增 §9.38、§11.2 第 97–103 条与变更日志一行。
2026-10-16 11:06:31 +08:00
DeepSeek V4.1 Flash 9cf6b2fedc 把三处功能要求也变成可复跑的检查(structcheck 93 条 + A/B 反证)
背景
----
「点组名不折起」「组名改名框不吞项数」「SignalData 去掉 displayName」
三处用户要求此前只有散文描述与已压缩的历史记录。本提交给它们补上
(a) 源码级断言、(b) A/B 反证、(c) 运行时断言三类证据。

新增 / 修改
-----------
- Tools/structcheck.ps1 新增第 [3] 组「行为与注释不变量」(31 条断言):
  * 折起热区:RowHitPart 产出专用 RowPart::GROUP_TITLE,其矩形正好是「组N」标签矩形;
    单击门限 `if (part != RowPart::DISCLOSURE && part != RowPart::GROUP_TITLE)` 全文件恰好 1 处;
    ToggleGroupFromRow 恰好 2 个入口(单击门限 + 双击折叠三角);
    双击非名称本体(`if (part != RowPart::BODY) return false;`)不进入改名。
  * 改名框:右边界必须是 `statsRight - statsWidth - 6*scale`,且全文件不得出现 `right = statsRight;`;
    项数宽度必须来自 GroupStatsText。
  * 数据模型:SignalData / SignalGroup 各只声明 1 个字符串字段且名为 name;
    RenameSignal / RenameGroup 存在;全仓 displayName / Title() / Set*DisplayName 残留 0 处。
  * 注释核验:过时措辞(文件下标 / 所属文件下标 / 原信号所属文件下标 / 文件分组 / 原文件下标 /
    文件序号)全仓 0 处;已修正注释确实存在(例如 DerivedSignalRequest::groupIndex →
    「原信号所属分组下标」)。
- Tools/structcheck_phrases.txt(新增,UTF-8):中文短语表,供 .ps1 以 UTF8Encoding($false)
  读入后逐条断言(forbid / require)。⚠️ 中文不能直接写进 .ps1 —— PowerShell 5.1 按 ANSI 读脚本。
- 断言总数 62 → 93;退出码仍为 0/1,可直接接 CI。

A/B 反证(判别力)
------------------
回退两处 UI 修复(`if (part != DISCLOSURE && part != GROUP_TITLE)` → `if (false)`;
`right = statsRight - statsWidth - 6*scale;` → `right = statsRight;`):
  * panelanim.exe   147/156(9 项如实失败)→ 恢复后 156/156
    失败文案直接点名需求:the group rename box stops before the item counter /
    clicking the group name does not start collapsing / does not collapse the group ...
  * structcheck     90/93(3 项如实失败)→ 恢复后 93/93
    single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found 0) /
    rename box right edge excludes the item counter / rename box never extends over the counter

运行时断言(数据层,datacheck 69/69)
-------------------------------------
  Check(set.RenameSignal(groupIndex, 0, L"改名") && set.Resolve(SignalRef{0,0})->name == L"改名",
        "signal rename writes the single name field");
  Check(set.RenameGroup(groupIndex, L"我的组") && set.GroupAt(0)->name == L"我的组",
        "group rename writes the single name field");
  Check(!set.RenameSignal(groupIndex, 0, L"") && ..., "empty rename is rejected (name is never blanked)");
即重命名只写一个 name 字段(编译期已无 displayName 可写),空名被拒。

文档
----
- Documents/refactor_report_2026-10-16.md:新增 §9.7(三处功能要求的源码断言 + A/B + 运行时断言);
  §1.3 补完整 A/B 实测;§9.4 验证表与 §9.5 变更清单同步(62 → 93)。
- Documents/agent_mem.md:§9.37 开头补可复跑证据块;§11.2 第 96 条扩写为三组 + 中文短语数据文件的做法;
  变更日志 d2e6fdd 行的结构检查描述更新为 93 条与 A/B 数据。

验证
----
make release 零警告,Target.exe 764 416 B;
structcheck 93/93;verify 49/49;datacheck 69/69;edgecheck 46/46;
panelanim 156/156;errcheck 19/19。
2026-10-16 04:56:35 +08:00
DeepSeek V4.1 Flash 695b73a366 结构证据落库:新增 Tools/structcheck.ps1 + make structcheck(62/62)
背景
----
上一轮报告对「巨型实现文件按职责拆分」与「Components/Shared 共享控件」两件事只有散文描述,
缺少可复跑的证据。本提交把这两项结构约束写成入库脚本,任何人(或 CI)都能一键核验。

新增
----
- Tools/structcheck.ps1(入库,纯 ASCII,退出码 0/1):
  [1] 拆分
      * 16 个拆分产物必须存在且非空,并断言核心文件已显著变小
        (PlotView.cpp 13 695 → 705 行;SignalPanel.cpp 4 184 → 1 019;
          Application.cpp 3 135 → 1 326;DataModel.cpp 2 615 → 1 459);
      * 12 组职责函数必须「只」定义在预期文件里
        (PlotView::Paint/PaintLane/PaintHint → PlotViewPaint.cpp;
          LaneAtY/CancelDrag → PlotViewInteraction.cpp;FitAll/SetHint → PlotViewFeatures.cpp;
          Layout/BuildLaneVisuals → PlotView.cpp;SignalPanel::Paint/OnLButtonDown/EnsureRows → paint/interaction/core);
      * 四个内部头必须声明具名命名空间并带 using-declaration,且把共享工具标 inline
        (pvdetail 111 / spdetail 63 / appdetail 27 / dmdetail 4 个 inline 定义);
      * 12 个拆分翻译单元必须真的被编译(Build/Release/*.o 齐全),并用 nm 断言
        「绘制符号在 PlotViewPaint.o、PlotView.o 里一个都不剩」;
      * 25 个实现文件所在目录必须全部登记在 Makefile;
  [2] Components/Shared
      * UiControls.hpp 声明 16 个控件,nm 断言这 16 个全部由 UiControls.o 定义(其他 .o 不是定义者);
      * 69 处调用点必须跨 ≥3 个组件(实测 PlotView / SignalPanel / TabBar / Shared);
      * 分层:Components/Shared 不含任何组件头;底层 Utilities/PaintUtil 不依赖 Shared;
        Utilities 不含任何功能组件头。
- Makefile:新增 structcheck 目标(make structcheck)。

说明(脚本第一版的两个假失败,已修正判定)
------------------------------------------
1. 判定「控件定义」必须用 UiControls.hpp 里**声明**的名字,而不是「所有 Ui* 函数」:
   UiFillRect / UiText / UiMeasureTextWidth 等是底层原语,本就住在 Utilities/PaintUtil。
2. 精确分层不变量是「Shared 不依赖任何组件;底层 PaintUtil 不依赖 Shared;
   Utilities 不含功能组件头」,而不是「Utilities 一律不得依赖 Shared」:
   Utilities/Src/{PopupMenu,ColorPickerPopup}.cpp 是历史上放在 Utilities 下的自绘 UI 小部件,
   允许消费 Shared 控件(方向与各组件一致)。

文档
----
- Documents/refactor_report_2026-10-16.md:
  * TL;DR 处更正首版「PlotView.cpp 整体拆分本轮未执行」的错误结论(那是提交 0871eb0 时刻的状态,
    已被 e6b7bf6 推翻);
  * §七 标题改为「遗留项与后续建议(逐项的最终处理状态)」;
  * §9.4 验证表新增 make structcheck 62/62;
  * 新增 §9.6「结构证据:把拆分与共享控件变成可复跑的检查」;
  * §9.5 变更清单补充 Tools/structcheck.ps1 与 Makefile。
- Documents/agent_mem.md:
  * §9.37 第七节补上可复跑证据与实测行数;
  * §11.2 新增第 96 条(结构检查脚本的用法、两个易错判定、精确分层不变量);
  * 变更日志 d2e6fdd 行补充 structcheck;
  * 0871eb0 行的「未完成(已如实记录)」改为明确的「已由后续两个提交完成」。

验证
----
make release 零警告,Target.exe 764 416 B;
make structcheck 62/62;verify 49/49;datacheck 69/69;edgecheck 46/46;
panelanim 156/156;errcheck 19/19。
2026-10-16 04:48:11 +08:00