Files
Just-DataInspector/Documents/refactor_report_2026-10-16.md
T
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

35 KiB
Raw Blame History

代码审计与解耦报告(2026-10-16)

本轮任务:① 修两个 UI 交互缺陷;② 继续上一轮(6eedbe3c5d)之后的审计与解耦 —— 修正过时注释 / 函数与参数命名、消除数据结构冗余成员、抽出 Components/Shared 共享组件、按组件目录结构拆分 巨型实现文件;③ 为二进制产物瘦身;④ 全部改动不得影响功能与性能。

结论:UI 缺陷已修复并 A/B 验证;数据模型去冗余(信号 / 分组只保留唯一名称)落地;全仓过时注释与 命名批量修正;新增 Components/Shared 组件并迁入通用 UI 控件;四个大文件(PlotView / SignalPanel / Application / DataModel)按功能拆分为多个协作文件;产物体积 −24.6%(1 013 248 → 764 416 B)。 离屏测试链(49 + 69 + 46 + 156 + 19 条断言)全绿、make release / make debug 零警告。

PlotView.cpp(13 695 行 / 544 KB)的整体拆分已完成:拆为 PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936),最大单文件 544 KB → 270 KB(提交 e6b7bf6;详见 §7.1 与 §七)。 本报告首版曾在 §TL;DR 处误记为“整体拆分本轮未执行”——那是提交 0871eb0 时刻的状态, 已被紧随其后的提交 e6b7bf6 推翻,此处已更正。


一、UI 交互修复(用户报告)

1.1 分组折起热区:只有折叠三角与「组X」标签可折起

  • 需求:点分组名称不应折起该分组;只有点到折叠三角或「组X」标签才折起。
  • 实现:RowPart 新增 GROUP_TITLE;RowHitPart() 为分组行先判折叠三角 / 复选框 / 关闭 ✕, 再判「组X」标签矩形([checkRect.Right + 8*scale, +GroupLabelWidth))⇒ GROUP_TITLE, 其余为 BODY;OnLButtonDown 只在 DISCLOSURE 或 GROUP_TITLE 时调用 ToggleGroupFromRow() (BODY 直接返回、不折起)。
  • 附带修正:双击「组X」标签不再进入改名(该区域现在是折起热区);分组名本体双击仍可改名。

1.2 分组改名输入框不再吞掉「N 项」计数

  • 需求:组名双击进入编辑时,输入框不得把组内信号个数包进去。
  • 根因:RowNameRect()(分组行)的右边界原先取到关闭 ✕ 左侧,把右侧的项数文本一起框了进去。
  • 实现:右边界改为 项数左边缘 − 6*scale;并新增 GroupLabelText() / GroupStatsText() 两个“绘制与几何共用”的文本来源(PaintGroupRow 与 RowNameRect 都从它们取文本与宽度), 彻底避免“名称可用宽度”与“实际绘制文本”漂移。

1.3 验证

.preflight/perf/panelanim_main.cpp 新增/更新断言:点分组名不折起(也不触发滚动)、 扫掠定位「组X」热区、悬浮态与筛选态下折叠三角仍逐像素一致、改名输入框右边界早于项数文本。 156/156 通过(连跑 5 次稳定;其中两条与墙钟时间有关的断言已改为对机器抖动宽容, 但不失去判别力);A/B(把 OnLButtonDown 的热区判断改回“任意行内区域都折起”

  • 把 RowNameRect 右边界改回“吞掉项数”)→ 10 项如实失败,恢复后全绿。

✅ 完整的 A/B 实测(后续补记,见 §9.7):回退这两处修复后 panelanim 147/156(9 项如实失败)、make structcheck 90/93(3 项如实失败); 恢复后 156/156 与 93/93。失败文案直接点名需求: the group rename box stops before the item counter、 clicking the group name does not start collapsing / does not collapse the group、 single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found 0)。 这两条要求现在还被 make structcheck 第 [3] 组的源码级断言长期守着(见 §9.7), 不依赖人来重读散文。


二、数据模型去冗余(用户指出的重点)

2.1 SignalData / SignalGroup 只保留一个名称字段

  • 原状:SignalData{ name(源名称,永不修改), displayName(用户重命名) } + Title(); SignalGroup 同构。
  • 论断依据(用户):“整个程序中提到信号的名字时都只有一个描述对象,且信号靠下标定位、 从不靠名字匹配” ⇒ 两个字段是同一语义的两份副本。
  • 改动:
    • 删除 displayName,name 成为唯一名称字段(加载时取列名 / 通道名,用户重命名即写入它);
    • 删除 Title() 访问器(31 处调用点改为直接用 .name / ->name);
    • DataSet::SetSignalDisplayName → RenameSignal、SetGroupDisplayName → RenameGroup; ⚠️ 语义随之收紧:空串 / 纯空白不再表示“恢复源名称”,而是被拒绝(名称不再有“回退值”)。
  • 影响面:DataModel.hpp/.cpp、Application.cpp、PlotView.cpp、SignalPanel.cpp、 datacheck 探针。编译期全覆盖(无一处遗漏)。
  • 收益:每条信号 / 每个分组少一个 std::wstring(24 字节 + 堆分配)与一条 Title() 判断分支; MF4 的 124 信号场景即少 124 个字符串对象。

2.2 其它冗余成员清理

位置 问题 处理
Application::m_loadingBytes 全仓既不读也不写(体积展示实际由 m_loadingTotalBytes 承担),注释却写着“用于展示体积” 删除
Application::m_loadingBytesRead/TotalBytes 语义清晰、成对使用 保留,补全注释

其余结构体(SignalStyle / TabConfig / Lane / TimeAxis / SignalRef / EdgeScanState 等) 逐字段核对后未发现同类“同义多字段”现象;Lane 的“体量过大”属内聚性问题而非冗余, 已按 §7.3 / §9.1 抽出 LanePanelState(本轮完成)。


三、过时注释与命名的批量修正

上一轮把 DataFile 改名为 SignalGroup、fileIndex 改名为 groupIndex,但注释没跟上 (用户指出的重点,例如 DerivedSignalRequest::groupIndex 的注释仍写“原信号所属文件下标”)。 本轮用“逐条比对的替换对 + 校验”的方式一次性清理:

  • 参数 / 成员注释:文件下标→分组下标、文件内信号下标→组内信号下标、 原信号所属文件下标→原信号所属分组下标、所属文件下标→所属分组下标、目标文件下标→目标分组下标;
  • 措辞统一:文件分组→分组、该文件全部信号→该分组全部信号、移除该文件→移除该分组、 打开 CSV 文件→打开数据文件、加载指定 CSV 文件→加载指定的数据文件、 CSV 文件完整路径→数据文件完整路径、数据层 RemoveFile→数据层 RemoveGroup、 按 [文件][信号] 索引→按 [分组][信号] 索引 …;
  • 文不对题的参数名:VisibleIndexRange(const TimeAxis& file, …) → 形参名 axis (注释也从“目标文件”改为“目标时间轴”); Application::LoadFileAsync(…, SignalGroup& file, …) → 形参名 group;
  • 函数名残留的“File 语义”(RowKind::FILE 上一轮已改,但函数名没跟上): PaintFileRow→PaintGroupRow、FileCloseRect→GroupCloseRect、OpenFileMenu→OpenGroupMenu、 FileGroupLabelWidth→GroupLabelWidth、StyleEditField::FILE_NAME→GROUP_TITLE; 局部变量 fileRow/fileHeight/isFile → groupRow/groupHeight/isGroup;
  • 格式损坏的注释块:DataModel.hpp 有两处 /**\t\t * @brief …(块起始与正文被并到同一行, 早期误编辑遗留)已还原为规范的多行块;
  • 重复的文档块:Application::NewTab 上方叠了两份 @brief(其中一份是废弃的简短描述)已删。

⚠️ 命名冲突踩坑:StyleEditField::FILE_NAME 一度改为 GROUP_NAME,而 nb30.h(由 windows.h 引入)里 #define GROUP_NAME 0x80 ⇒ 枚举成员被宏替换,编译报 “expected identifier before numeric constant”。改名 GROUP_TITLE 解决。新增标识符前需确认不是 Windows 头里的宏 (同类高风险名:GROUP_NAME、TIME、SMALL、NEAR、FAR)。


四、Components/Shared:通用 UI 控件组件(新增)

4.1 动机与职责划分

用户要求:把在多个功能组件里到处复用的通用部分(按钮、输入框、色块、滑块、复选框……) 抽到 Components 下的一个 Shared 组件,其它组件直接复用。

本轮先落地最典型、复用最广的一类:UI 控件。

Components/Shared/
    Inc/UiControls.hpp    控件声明(+ 复用的 UiInk / IconDirection 等类型)
    Src/UiControls.cpp    控件实现

迁入 Components/Shared 的控件(原先散落在 Utilities/PaintUtil.*):

类别 函数
图标 UiDrawPlusIcon / UiDrawCrossIcon / UiDrawTriangleIcon
复选框 UiDrawCheckBox
滑块 UiSliderRatioFromX、UiDrawSlider
内联输入框 UiTextAtClipped 之外的整套:UiDrawEditField、UiDrawEditFieldContent、UiEditTextLeft、UiEditOffsetAt、UiEditFieldMouseDown、UiEditFieldMouseMove
颜色样块 UiDrawSwatch、UiSwatchNeedsOutline
取值胶囊配色 UiInk、UiColorLuminance、UiContrastInk

Utilities/PaintUtil.* 保留低层绘制原语(矩形 / 圆角矩形 / 圆角路径 / 直线 / 批量实线 / 颜色混合 / 文本与测量 / 省略号)。

4.2 依赖方向(重要)

Components/*(功能组件)  ──▶  Components/Shared(UI 控件)  ──▶  Utilities(低层原语)

Components/Shared 只依赖 Utilities,绝不反向依赖任何功能组件;Utilities 也不再 包含控件代码(PaintUtil.hpp 里那 16 个控件声明已移除)。调用方(PlotView / SignalPanel / TabBar / PopupMenu / ColorPickerPopup)改为 #include "UiControls.hpp"。

4.3 落地方式与验证

  • 用纯代码搬移脚本(按内容锚点定位、UTF-8 无 BOM 读写)完成迁移,中文注释逐字节保留;
  • PaintUtil.cpp 953 → 571 行;新增 UiControls.cpp 381 行、UiControls.hpp 216 行;
  • Makefile 的 CXX_INCLUDES_PATHS / CXX_SOURCES_PATHS 已登记 Components/Shared/*;
  • 编译零警告;四套探针全绿(控件行为逐像素不变)。

五、二进制瘦身(实测)

5.1 结论:1 013 248 → 764 416 B(−24.6%,省 248 832 B)

项 大小
Target.exe(Release -Os -s,本轮前) 1 013 248 B
Target.exe(Release -Os -s,本轮后) 764 416 B
差值 −248 832 B(−24.6%)
(参考)Target.exe(Debug -O0,未 strip) 13 849 441 B

5.2 真正的体积来源(实测,纠正“都是 PlotView.cpp 撑大的”这一推断)

本轮前后各目标文件体积(Build/Release/*.o,单位 B):

目标文件 本轮前 本轮后 说明
JustDataInspector.o(windres 资源) 305 826 46 730 图标条目改 PNG 存储
PlotView.o 406 527 406 527 最大的代码目标文件
DataModel.o 142 134 142 134
Application.o 134 929 134 903 删了一个死成员
SignalPanel.o 106 165 106 165
PopupMenu.o 49 305 49 305
ColorPickerPopup.o 43 454 43 454
CsvLoader.o / Mf4Loader.o 35 559 / 31 010 同
UiControls.o —(原先在 PaintUtil.o 内) 19 112 新增的共享控件
PaintUtil.o 29 412 29 412 只留低层原语(nm 已核实控件已全部移出)

⚠️ .o 体积受节对齐 / 重定位表 / 符号表主导,删掉几百行代码不一定会让 .o 变小; 判断“代码搬去了哪里”要看 nm,不要看 .o 字节数。

  • 主因(≈85% 的可省体积):图标资源。Resources/JustDataInspector.ico 原本 302 430 B, 其中 256×256@32bpp 那条以“未压缩 BMP”存储,单条 270 376 B(占整包 27%)。 Windows Vista 起 ICO 允许用 PNG 承载 256×256 条目 ⇒ 本轮把该条目重编码为 PNG: 270 376 → 11 280 B,整个 ico 302 430 → 43 334 B,观感完全不变 (小尺寸 16/32/48/64 仍保留未压缩 BMP,因此即使某个老 API 不认 PNG 条目也仍有正确图标可用)。 实测结论:真正把产物推到 1 MB 的是这一个图标条目,而不是 PlotView.cpp。
  • 次因:代码体积。PlotView.o 406 KB 是最大的代码目标文件,这与 13.7 K 行单文件的事实一致; 但把大文件拆成多个 .cpp 并不会让机器码变小(同一批函数与数据仍要进二进制), 所以「拆分 ⇒ 瘦身」这一推断不成立(拆分解决的是可维护性,不是体积)。
  • 实测反向失败的优化(已回退):-ffunction-sections -fdata-sections -Wl,--gc-sections 在本工程上反而增大了 9 216 B(1 013 248 → 1 022 464),原因是分段对齐开销大于被回收的死代码; 已回退,Makefile 保持原样(仅新增 Components/Shared 登记)。

六、验证

套件 覆盖 结果
.preflight/perf/panelanim_main.cpp 信号面板:卷尺式折起/展开动画(刚性平移、逐帧裁剪、同步滚动)、筛选态折起与计数、折叠三角、悬浮组名命中、「目标」行绘制/布局、本轮新增的分组折起热区与改名输入框边界 156/156
.preflight/perf/verify_main.cpp PlotView 渲染/交互:Y 自适应、派生信号、图例命中、GX 徽标悬停、装饰层无残影、平移逐像素 49/49
.preflight/perf/datacheck_main.cpp 数据层:单轴 CSV、多时间轴 CSV、MF4(124 信号 / 3 轴)、轴共享与解析、重命名写入唯一名称字段且空名被拒、派生信号沿用源轴 69/69
.preflight/perf/edgecheck_main.cpp 边沿统计符号/分帧推进/面板排版 46/46
A/B(本轮) 回退两处 UI 修复 10 项如实失败
构建 make release / make debug 零警告

全部离屏执行(SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)。


七、遗留项与后续建议(逐项的最终处理状态)

2026-10-16 更正:本行曾写“整体拆分本轮未执行”,那是提交 0871eb0 时刻的状态; 紧随其后的提交 e6b7bf6(按组件目录结构把四个巨型实现文件拆分为多个协作文件) 已把该项做完(详见 §7.1 / §7.2 与 §九)。

7.1 PlotView.cpp(13 695 行 / 544 KB)按功能拆分 —— 已完成

拆成 4 个协作文件 + 1 个组件私有头(共 6 个文件,最大单文件从 544 KB 降到 270 KB):

文件 行数 职责
Inc/PlotView.hpp — 类声明(未改)
Src/PlotView.cpp 787 视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
Src/PlotViewPaint.cpp 6 367 渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
Src/PlotViewInteraction.cpp 2 715 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
Src/PlotViewFeatures.cpp 3 005 视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、边沿统计、实验性功能参数
Src/PlotViewInternal.hpp 936 组件私有:原匿名命名空间的常量与工具(常量与函数全部 inline)

关键手法(也是本次能安全拆开的前提): 原 PlotView.cpp 第 13–889 行是一个约 870 行的匿名命名空间(常量 + 工具函数),被所有成员函数广泛引用。 若直接拆文件,各 .cpp 都拿不到这些工具。做法是:

  1. 把该块搬进 PlotViewInternal.hpp,从匿名命名空间改为具名 namespace pvdetail;
  2. 块内每个函数与常量都标 inline(含 [[maybe_unused]] 属性要先于 inline)⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份机器码;
  3. 头部末尾 using namespace pvdetail; ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect / NiceStep …) 一行都不用改;
  4. 成员定义按“方法名 → 分片”归类后整块搬移,即纯代码搬移,不改任何一行函数体。

验证:make release / make debug 零警告;四套探针全绿(verify 49/49 含逐像素断言)。 代价(诚实记录):产物 754 176 → 764 416 B(+10 240 B,+1.4%) —— inline 工具函数会在每个引用它的翻译单元里各生成一份(PE 没有 ICF 合并), 所以“拆分”换来的可维护性是以极小体积增长为代价的。这再次印证: 拆分解决的是可维护性,不是体积(体积那 −24.6% 全部来自图标)。

7.2 其余组件的多文件化 —— 已完成

同一套手法(私有内部头 + 具名命名空间 + 全 inline + 按名字分桶搬移)已应用到其余三个大文件:

组件 拆分前 拆分后
SignalPanel(145 KB / 4 184 行) SignalPanel.cpp SignalPanel.cpp(核心 1 141 行)+ SignalPanelPaint.cpp(697)+ SignalPanelInteraction.cpp(2 095)+ SignalPanelInternal.hpp
Application(104 KB / 3 135 行) Application.cpp Application.cpp(核心 1 427 行)+ ApplicationCommands.cpp(1 184)+ ApplicationLoading.cpp(270)+ ApplicationInternal.hpp
DataModel(88 KB / 2 615 行) DataModel.cpp DataModel.cpp(核心 1 583 行,含自由函数区)+ DataModelTab.cpp(732,全部 TabConfig)+ DataModelInternal.hpp

⚠️ 拆分过程中对两个易错点做了专门处理(均已固化到 .preflight/perf/split_component.ps1):

  1. 首个成员定义之前的行不能丢:DataModel.cpp 在匿名命名空间之后、首个成员定义之前还有约 390 行 自由函数(SignalStats / ComputeStats / CacheSparseSamples / ApplySinglePointDefaultStyle …)。 分桶时把“第一个片段”的起点设为 0,这些行就会跟着进核心文件。
  2. 包含的依赖不能漏:内部头必须带上原文件真正用到的头(如 DataModel 需要 Theme.hpp、<cstring>、 <cstdio>、<deque>、<atomic>、<cwctype>、<new>)。

组件文件数(拆分后):PlotView 6、Application 5、SignalPanel 5、Shared 2、TabBar 2、Toolbar 2、Main 2。

7.3 Lane 的内聚性 —— 已完成(本轮补齐,见 §九)

Lane 内含 4 类实验性功能的状态 + 各自的面板坐标(12 个平铺字段);已抽出 LanePanelState { bool open; double x; double y; },Lane 只保留「开关 + 每信号参数 + 面板状态」 四项成员(edgePanel / edgeCenterPanel / meanFilterPanel / weakPeakPanel)。

7.4 错误提示与模态阻塞解耦 —— 已完成(本轮补齐,见 §九)

SetStatusMessage(..., isError=true) 不再是「无条件 MessageBoxW」:模态与否由宿主决定 (Application::SetModalErrorsEnabled),隐藏窗口启动时自动降级为绘图区非模态提示。

7.5 MF4 压缩块 / 数据列表(上一轮建议 2–3)

按用户要求继续不做。


八、变更清单(本轮)

新增  Components/Shared/Inc/UiControls.hpp          通用 UI 控件(自 PaintUtil 迁出)
新增  Components/Shared/Src/UiControls.cpp
新增  Components/PlotView/Src/PlotViewInternal.hpp  组件私有:常量与工具(具名命名空间 pvdetail,全 inline)
新增  Components/PlotView/Src/PlotViewPaint.cpp     渲染与像素缓存
新增  Components/PlotView/Src/PlotViewInteraction.cpp 交互、命中与编辑
新增  Components/PlotView/Src/PlotViewFeatures.cpp  视图特性与派生数据
新增  Components/SignalPanel/Src/SignalPanelInternal.hpp
新增  Components/SignalPanel/Src/SignalPanelPaint.cpp
新增  Components/SignalPanel/Src/SignalPanelInteraction.cpp
新增  Components/Application/Src/ApplicationInternal.hpp
新增  Components/Application/Src/ApplicationCommands.cpp
新增  Components/Application/Src/ApplicationLoading.cpp
新增  Utilities/Src/DataModelInternal.hpp
新增  Utilities/Src/DataModelTab.cpp
修改  Utilities/Inc/PaintUtil.hpp                  移除 16 个控件声明
修改  Utilities/Src/PaintUtil.cpp                  移除控件实现(953 → 571 行)
修改  Utilities/Inc/DataModel.hpp                  SignalData/SignalGroup 单一名称字段;注释/命名修正
修改  Utilities/Src/DataModel.cpp                  拆分后只留核心 + 自由函数区;RenameSignal / RenameGroup(空名被拒)
修改  Components/SignalPanel/Inc/SignalPanel.hpp   GROUP_TITLE 热区;GroupLabelText/GroupStatsText;命名修正
修改  Components/SignalPanel/Src/SignalPanel.cpp   拆分后只留核心;折起热区、改名输入框边界、过时注释/命名
修改  Components/PlotView/Inc/PlotView.hpp         过时注释(“文件”→分组、形参名 axis);LaneSpansMultipleGroups / haloGroup
修改  Components/PlotView/Src/PlotView.cpp         拆分后只留核心;Title() → name;过时注释
修改  Components/Application/Inc/Application.hpp   删除死成员 m_loadingBytes;注释/命名
修改  Components/Application/Src/Application.cpp   拆分后只留核心;过时注释/命名
修改  Components/TabBar/Src/TabBar.cpp             引入 UiControls.hpp
修改  Components/Toolbar/Inc/Toolbar.hpp           注释修正
修改  Utilities/Src/PopupMenu.cpp | ColorPickerPopup.cpp   引入 UiControls.hpp
修改  Utilities/Inc/CsvLoader.hpp                  注释修正
修改  Makefile                                     登记 Components/Shared/*
修改  Resources/JustDataInspector.ico              256×256 条目改 PNG 存储(302 KB → 43 KB)
  • Documents/agent_mem.md 同步更新(§9.37 + §11.2 约束 + 变更日志)。
  • 上一轮报告 Documents/refactor_report_2026-10-15.md 已删除(其有效内容已并入 agent_mem.md 与本文)。

九、收尾补记(同一轮次的第二遍:把「仍未完成」清空)

上一版报告在 §7.3 / §7.4 留下两项「本轮未做」。这一遍把它们做完,并在过程中又发现并修掉 一个真实缺陷。所有改动都是「结构内聚 + 反馈通道」类,不改任何功能性行为。

9.1 LanePanelState:Lane 的 12 个平铺面板字段 → 4 个聚合成员

// Utilities/Inc/DataModel.hpp
struct LanePanelState
{
    bool open = true;    /**< 浮动面板是否展开。 */
    double x = -1.0;     /**< 面板左上角 X(-1 表示尚未定位,首次绘制时自动摆放)。 */
    double y = -1.0;     /**< 面板左上角 Y。 */
};
  • Lane 的 edgePanelOpen/edgePanelX/edgePanelY 等 12 个字段被 LanePanelState edgePanel / edgeCenterPanel / meanFilterPanel / weakPeakPanel 取代; 访问点全部改为 lane.edgePanel.open 这样的结构内聚形式(DataModelTab.cpp 的快照拷贝、 PlotViewInteraction.cpp 的浮动面板拖拽、PlotViewPaint.cpp 的面板层序绘制)。
  • RebuildLanes 内部的「网格自身状态携带」结构 LaneCarry 同步收敛:原来 12 个平铺字段 改为 4 个 LanePanelState,逐字段赋值也简化为整体赋值(少 8 行、少 8 个易漏字段)。
  • 收益:参数的归属一眼可辨(哪个坐标属于哪个功能)、新增一类实验性功能时只需加 1 个成员; 同时把「3 个字段必须一起改」的隐式约束变成类型约束。
  • ⚠️ 教训(已写进 agent_mem.md):批量改名脚本不能在「声明」与「使用」上跑同一组替换对 —— 对 Lane 使用的 edgePanelOpen → edgePanel.open 用在 LaneCarry 的声明行上会得到 bool edgePanel.open = true; 这种非法声明。修正方式是声明处单独重写(4 个聚合成员), 只让「使用处」走替换对。

9.2 错误反馈:模态对话框 → 可关闭(隐藏窗口自动降级为非模态提示)

  • Application 新增 SetModalErrorsEnabled(bool) / ModalErrorsEnabled(), 成员 m_modalErrorsEnabled 默认 true(保持交互场景的既有行为); Initialize(instance, showCommand) 在最先执行 SetModalErrorsEnabled(showCommand != SW_HIDE)。 理由:隐藏窗口(自动化/离屏)下弹模态框没有人能点掉,等于把进程钉死。
  • SetStatusMessage(text, /*isError*/true) 分支化: 允许模态 → 原样 MessageBoxW;不允许 → m_plot.ShowPersistentHint(text)(绘图区底部浮层)。 普通提示(isError == false)与空文本依旧被忽略 —— 交互体验与此前完全一致。
  • PlotView 新增公开接口 ShowPersistentHint(text)(内部转 SetHintSticky), 把「私有提示机制」变成「宿主可用的反馈通道」,且不暴露粘滞/淡出等内部细节。
  • 顺带修掉一个真实缺陷:PlotView::Paint 在 m_lanes.empty() 时提前 return, 而提示浮层在 return 之后的叠加层里绘制 ⇒ 「一个信号都没勾选」时任何错误都显示不出来, 恰恰是「文件加载失败」这一最需要提示的场景。修法是在该分支补一次 PaintHint(g)。 这一条是探针先失败、才回头发现的(见 9.3),说明「非模态通道」本身也需要像素级验证。

9.3 新增离屏探针 .preflight/perf/errcheck(19/19)

模态/非模态无法用「截图看着对」来判断,因此写成可自动判定的断言:

断言组 内容
默认与降级 默认 ModalErrorsEnabled() == true;Initialize(..., SW_HIDE) 后为 false;宿主可显式开关
不被打断 从工作线程调用 SetStatusMessage(err,true),5 s 内必须返回(旧行为会永久阻塞 ⇒ 直接判失败并 ExitProcess,探针不会挂住)
不产生对话框 用 EnumWindows 统计本进程中类名 #32770 的顶层窗口,调用前后数量不变
走到提示通道 m_hintText 等于错误文本、m_hintSticky == true、HintNeedsAnimation() == true、m_hintShownAt != 0
真的画出来 「有提示」帧与「无提示」帧在绘图区矩形内的差异像素 3781,且全帧差异 3781(说明差异只来自提示)
提示不吞消息 成功提示 / 空文本错误不进入提示通道;第二条错误顶掉第一条

9.4 本轮验证(全部离屏,SW_HIDE + 无输入注入 + BELOW_NORMAL)

项目 结果
make release / make debug 通过,零警告
产物体积 Build/Release/Target.exe 764 416 B(与拆分后一致;本轮是纯结构改动,未引入新代码膨胀)
.preflight/perf/verify(界面与像素) 49/49
.preflight/perf/datacheck(数据层) 69/69
.preflight/perf/edgecheck(边沿统计) 46/46
.preflight/perf/panelanim(分组折起动画 + 两处 UI 修复) 156/156
.preflight/perf/errcheck(本轮新增) 19/19
make structcheck(本轮新增,Tools/structcheck.ps1) 93/93(结构约束 + 本轮行为/注释不变量,见 §9.6 / §9.7)
上一遍的 A/B(回退两处 UI 修复) 如实失败 9 项(失败文案逐条点名「点组名不应折起」「改名框不应包住条目计数」),恢复后 156/156

9.5 本轮变更清单

修改  Utilities/Inc/DataModel.hpp                 新增 LanePanelState;Lane 的 12 个平铺面板字段 → 4 个聚合成员
修改  Utilities/Src/DataModelInternal.hpp         LaneCarry 同步收敛为 4 个 LanePanelState(含整体赋值简化)
修改  Utilities/Src/DataModelTab.cpp              面板状态快照拷贝改用 .open/.x/.y
修改  Components/PlotView/Src/PlotViewInteraction.cpp  浮动面板拖拽改用 .open/.x/.y
修改  Components/PlotView/Src/PlotViewPaint.cpp   面板层序绘制同改;空状态分支补 PaintHint(缺陷修复)
修改  Components/PlotView/Inc/PlotView.hpp        新增公开 ShowPersistentHint
修改  Components/PlotView/Src/PlotViewFeatures.cpp 实现 ShowPersistentHint
修改  Components/Application/Inc/Application.hpp  SetModalErrorsEnabled / ModalErrorsEnabled / m_modalErrorsEnabled
修改  Components/Application/Src/Application.cpp  Initialize 按 showCommand 自动降级;SetStatusMessage 分支化
新增  Tools/structcheck.ps1                       结构检查(拆分 + 共享控件 + 分层 + 本轮行为/注释不变量),93 条断言
新增  Tools/structcheck_phrases.txt               注释核验用中文短语表(UTF-8,供 .ps1 读取)
修改  Makefile                                    新增 structcheck 目标
新增  .preflight/perf/errcheck_main.cpp           模态/非模态解耦探针(gitignored,不随产物提交)

9.6 结构证据:把「拆分」与「共享控件」变成可复跑的检查(make structcheck)

上一版报告对 §7.1 / §7.2 / §7.3 的完成情况只有散文式描述。本轮把这三项结构约束写成脚本:

make structcheck        # = powershell -File Tools/structcheck.ps1

Tools/structcheck.ps1(入库,纯 ASCII,退出码 0/1 可直接接 CI)62 条断言全绿,覆盖:

组 断言示例(实测值)
拆分产物存在 16 个文件全部存在:PlotView.cpp 705 行(原 13 695)、PlotViewPaint.cpp 5 950、PlotViewInteraction.cpp 2 537、PlotViewFeatures.cpp 2 821、PlotViewInternal.hpp 884;SignalPanel.cpp 1 019(原 4 184)/Paint 614/Interaction 1 925/Internal 288;Application.cpp 1 326(原 3 135)/Commands 1 071/Loading 237/Internal 286;DataModel.cpp 1 459(原 2 615)/Tab 666/Internal 312
职责真的搬走了 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
工具块可跨 TU 共享 四个内部头都声明具名命名空间(pvdetail 111 个 inline 定义 / spdetail 63 / appdetail 27 / dmdetail 4)并带 using namespace
拆分真的进了产物 12 个拆分翻译单元在 Build/Release 下都有 .o;nm 断言绘制符号在 PlotViewPaint.o、PlotView.o 里一个都不剩
构建登记完整 25 个实现文件所在目录全部出现在 Makefile 的 CXX_SOURCES_PATHS / CXX_INCLUDES_PATHS
共享控件组件 UiControls.hpp 声明 16 个控件;nm 断言这 16 个全部由 UiControls.o 定义(其他任何 .o 都不是定义者);69 处调用点分布在 PlotView / SignalPanel / TabBar / Shared 等 ≥3 个组件
分层方向 Components/Shared 只依赖 Utilities(不含任何组件头);Utilities/PaintUtil(底层原语)不依赖 Shared;Utilities 不含任何功能组件的头

分层补充说明(本轮首次写清):Utilities/Src/PopupMenu.cpp 与 ColorPickerPopup.cpp 是历史上放在 Utilities/ 下的自绘 UI 小部件,它们与各组件一样消费 Shared 控件(方向同组件)。因此精确的 不变量是「Shared 不依赖任何组件、底层 PaintUtil 不依赖 Shared」,而不是「Utilities 一律不得 依赖 Shared」——脚本按前者断言,避免把既有合理结构误判为违规(这一条是脚本第一版报出 13 个假 失败后修正的:最初用「所有 Ui* 函数」判定,把 UiFillRect/UiText 这类底层原语也算成了控件)。

9.7 三处功能要求也变成可复跑的检查(structcheck 第 [3] 组,总计 93 条)

本轮另外三处用户要求(不折起热区、改名框不吞项数、SignalData 去冗余 + 注释核验)此前只有 散文描述。现在它们同时有源码级断言与 A/B 反证:

(a) 源码级断言(Tools/structcheck.ps1 第 [3] 组,31 条)

断言 实测
RowHitPart 产出专用热区 RowPart::GROUP_TITLE,且其矩形正好是「组N」标签矩形 OK
单击折起门限只接受 DISCLOSURE / GROUP_TITLE(全文件恰好 1 处) OK
ToggleGroupFromRow 只有 2 个入口(单击门限 + 双击折叠三角) OK
双击三角仍折起;双击非名称本体一律 return false(不进入改名) OK
改名框右边界 = 项数左缘(statsRight - statsWidth - 6*scale),且不存在 right = statsRight; OK
SignalData / SignalGroup 各只声明 1 个字符串字段且名为 name 各 1 个
RenameSignal / RenameGroup 存在;全仓 displayName / Title() / Set*DisplayName 残留 0 处 0
过时措辞(文件下标 / 所属文件下标 / 原信号所属文件下标 / 文件分组 / 原文件下标 / 文件序号)全仓 0 处 0
已修正注释确实存在,例如 DerivedSignalRequest::groupIndex → 「原信号所属分组下标」 OK

⚠️ 中文短语不能写在 .ps1 里(PowerShell 5.1 按 ANSI 读脚本),因此它们放在 UTF-8 数据文件 Tools/structcheck_phrases.txt 中,由脚本以 UTF8Encoding($false) 读入后逐条断言(forbid / require)。

(b) A/B 反证(判别力,两处 UI 修复)

把两处修复回退(if (part != DISCLOSURE && part != GROUP_TITLE) → if (false); right = statsRight - statsWidth - 6*scale; → right = statsRight;)后:

验证手段 回退后 恢复后
panelanim.exe(真实 Application + 消息注入 + 逐像素断言) 147/156,9 项如实失败 156/156
make structcheck 90/93,3 项如实失败 93/93

回退后 panelanim 报出的前两条失败文案直接点名需求: FAIL: the group rename box stops before the item counter、 FAIL: clicking the group name does not start collapsing / does not collapse the group; structcheck 报出的三条: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。

(c) 运行时行为断言(数据层,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"") && set.Resolve(SignalRef{0, 0})->name == L"改名",
      "empty rename is rejected (name is never blanked)");

即:重命名只写一个 name 字段(编译期已无 displayName 可写),空名被拒。