把三处功能要求也变成可复跑的检查(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。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 04:56:35 +08:00
1 parent 695b73a366
commit 9cf6b2fedc
4 files changed
+181 -13

No files matched your search

+31 -10
View File
@@ -2137,6 +2137,16 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
> `Documents/refactor_report_2026-10-16.md`(已删除上一轮的 `refactor_report_2026-10-15.md`)。
> 离屏验证:`panelanim` **156/156**、`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;
> A/B **10 项如实失败**;`make release` / `make debug` 零警告。
>
> ✅ **可复跑证据(后续补上,见 §11.2 第 96 条)**:本节第一/二/三节的三项要求已写成
> `make structcheck` 的第 **[3] 组源码级断言**(折起门限只接受 `DISCLOSURE`/`GROUP_TITLE` 且全文件恰好 1 处、
> `ToggleGroupFromRow` 恰好 2 个入口、改名框右边界 `statsRight - statsWidth - 6*scale` 且不存在
> `right = statsRight;`、`SignalData`/`SignalGroup` 各只 1 个字符串字段且名为 `name`、
> 全仓 `displayName`/`Title()`/`Set*DisplayName` 0 处、过时措辞 0 处、已修正注释确实存在)。
> **判别力**:回退这两处 UI 修复 ⇒ `panelanim` **147/156**(9 项如实失败,文案点名
> “group rename box stops before the item counter”“clicking the group name does not collapse the group”)、
> `structcheck` **90/93**;恢复后 **156/156** 与 **93/93**。数据层另有 `datacheck` 运行时断言:
> `RenameSignal`/`RenameGroup` **只写一个 `name` 字段**、空名被拒(`69/69`)。
#### 一、分组折起热区:只有折叠三角与「组X」标签可折起
@@ -2328,7 +2338,7 @@ Components/Shared/Src/UiControls.cpp
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **分组折起/展开动画 + 筛选态可折起(项数按筛选计数)+ 悬浮组名可交互与"先滚动再折起" + 「适应视图」与「Y 轴自适应」彻底同源(详见 §9.35)**。① **折起动画**:`SignalPanel::m_groupAnims` + `GroupRowScale()` —— 每行速度倍率 `1 + 1.2×ordinal/(行数−1)`(**离组名越远越快收尽**)叠加 ease-out cubic(末段减速),总时长 `clamp(115+14×行数, 140, 300)` ms;`BuildRows` 输出**非整行高**,低于 0.75 逻辑 px 的行不再产生;宿主 `WM_TIMER` 同时驱动滚动与折起动画(`Render` 判定改为二者取或);新增 `SetRowClip/ClearRowClip` 把行内容裁到本行(否则垂直居中文本溢出到相邻行)。② **筛选态折起**:筛选模式改用**独立**的 `m_filterCollapsed`(默认展开,不污染普通列表),`ListRow::displayCount` 承载"该组内符合筛选条件的个数",筛选文本/清空/数据变化即清空。③ **悬浮组名可交互**:根因是 `HitRow` 只按滚动偏移映射行 ⇒ 点击穿透到下层;修法:`HitRow` 先用 `StickyGroupRowIndex()`+`StickyGroupRowTop()` 判定悬浮行矩形并临时用 `m_rowYOverrideActive` + 新的 `RowHitPart()` 算行内区域,并跳过高度为 0 的行;新增 `ToggleGroupFromRow()`:正悬浮时 `StartScrollAnimationTo(行内容偏移, groupIndex)`(挂起请求 `m_pendingGroupToggle`),滚动到位后才启动折起动画。④ **单点信号上两种自适应不一致**:`FitSinglePointCluster` 曾另算"半径留白 + 跨度 1"且把 `fitYToSelection` 置回 false ⇒ 点「适应视图」既改区间又**关掉 Y 轴自适应**;修法:单点簇只负责时间轴(居中 + 一大格 1s),**逐网格幅值轴与常规路径/Y 轴自适应完全同源**(`fitYToSelection` → `AutoRangeLaneToView`,否则 `autoRange=true`,再接 `ApplySinglePointValueMargins`),且**绝不改写** `fitYToSelection`。验证:新增离屏探针 `.preflight/perf/panelanim_main.cpp`(真实 `Application` + `SW_HIDE`)**94/94 通过**(连跑 3 次稳定;覆盖折起/展开的分帧过渡、单调收敛、越靠下越矮、末态精确无残留、筛选计数与独立折起状态、悬浮四个可交互区域全部命中且下层行仍可点、点击后"先滚动到位再折起"、真实 MF4 单点信号两种自适应逐位一致/往返稳定);**A/B** 三组(回退绘图区修复 / 禁用悬浮命中优先 / 动画时长置 0)分别 **4 / 13 / 8 项如实失败**;既有 `verify` **49/49**(顺带修正 4 条与远端新规则不符的过期断言)、`datacheck` **69/69**、`edgecheck` **46/46**;`make release` 零警告。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **动画改「抽拉卷尺」式刚性平移 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正(详见 §9.36)**。① **动画重做**(推翻上一轮的“按行缩放”):整组信号作为**刚性整体**平移,**行高永不变**、只维护一个逐行平移量 `m_rowShift` ⇒ 动画期间**无需重建行集合**(流畅关键);收起时逐条被组名栏吞掉、展开时逐条冒出;中途反向点击**从当前位移续接**不跳变;时长 `clamp(115+6×行数, 150, 320)` ms。② **藏进组名栏的裁剪**:信号行越过组名的**绘制**下边界即裁掉,且除 GDI 裁剪外**必须用 `g.SetClip(..., CombineModeIntersect)` 同时收紧 GDI+ 裁剪**(否则行内色块/复选框/徽标画到组名栏上)。③ **悬浮组名折起改为“一边滚动一边收起”**(推翻“先滚动到位再折起”):`scrollSynced` 让滚动与平移按同一进度推进 ⇒ 组名屏幕位置恒定;结束时滚动偏移恰好落在“组名正好在最上方”,并且**无跳变**;旧 `m_pendingGroupToggle` 机制删除。④ **筛选态折叠三角**:`PaintFileRow` 改读 `IsGroupCollapsed()`(筛选模式用的是独立的 `m_filterCollapsed`),修复“三角维持筛选前状态”。⑤ **「目标」行**:`PaintTargetRow` 误用列表裁剪版 `PaintRowText` ⇒ 名称与 GX 文本被完全裁掉(改用 `UiText`);GX 标签改为**右对齐到内容列右边缘**、名称无标签时占满内容列;`EditFieldRect()` 对目标行只返回名称矩形(否则改名输入框把 GX 标签包进去)。验证:离屏探针 `.preflight/perf/panelanim_main.cpp` **147/147 通过**(连跑 3 次稳定;含逐帧断言组名栏像素零变化、`m_rowsDirty` 恒 false、滚动单调且恰好落位、筛选/普通态折叠三角逐像素 0 差异、目标行文本像素存在与两端对齐);**A/B** 五组(回退筛选三角+非刚性平移 / 回退目标行绘制与布局 / 关闭同步滚动 / 去掉 GDI+ 收紧裁剪 / 不续接位移)分别 **4 / 6 / 6 / 2 / 1 项如实失败**;`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;`make clean && make all` 零警告。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 `Components/Shared` 组件 + 过时注释/命名大清理 + 二进制瘦身 24.6% 并完成多文件拆分(详见 §9.37 与本轮报告 `Documents/refactor_report_2026-10-16.md`)**。① **折起热区**(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**(新增 `RowPart::GROUP_TITLE`)才折起。② **分组改名输入框不再吞掉「N 项」**:`RowNameRect` 右边界改为项数左缘;新增 `GroupLabelText`/`GroupStatsText` 作为文本与宽度的唯一来源。③ **`SignalData`/`SignalGroup` 只留一个名称字段**:删除 `displayName` 与 `Title()`(31 处改用 `name`),`SetSignalDisplayName`/`SetGroupDisplayName` → **`RenameSignal`/`RenameGroup`**(空名一律拒绝);删除死成员 `Application::m_loadingBytes`。④ **新增 `Components/Shared`**(用户要求):从 `PaintUtil` 迁出全部**控件**(图标/复选框/滑块/内联输入框/颜色样块/取值胶囊配色)到 `UiControls.hpp/.cpp`,`PaintUtil` 只留低层原语;依赖方向固定 `Components/* → Components/Shared → Utilities`。⑤ **过时注释/命名**:`文件下标`→`分组下标`、`文件分组`→`分组` 等一批;形参名 `file`→`axis`/`group`;`PaintFileRow`→`PaintGroupRow`、`FileCloseRect`→`GroupCloseRect`、`OpenFileMenu`→`OpenGroupMenu`、`FileGroupLabelWidth`→`GroupLabelWidth`、`StyleEditField::FILE_NAME`→`GROUP_TITLE`;修复 `DataModel.hpp` 两处被压扁的注释块与 `NewTab` 重复文档块。⑥ **多文件拆分(本轮完成)**:`PlotView.cpp`(13 695 行)+ `SignalPanel.cpp` + `Application.cpp` + `DataModel.cpp` 四个大文件全部按功能拆成多个协作文件(私有内部头 + 具名命名空间 + 全 `inline` + 按名字分桶),最大单文件 544 KB → 276 KB。⑦ **二进制瘦身**:真因是 **ICO 里 256×256 未压缩 BMP 条目(270 KB,占整包 27%)**,改为 **PNG 存储**(观感零变化)⇒ `Target.exe` **1 013 248 → 764 416 B(−24.6%)**;实测“拆分不会减小机器码,反而 +1.4%”、`--gc-sections` 反而 +9 KB(已回退)。验证:`panelanim` **156/156**、`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;**A/B** 回退两处 UI 修复 **10 项如实失败**;`make release`/`make debug` 零警告。⚠️ 该提交当时的待办(`PlotView.cpp` 等大文件多文件化拆分、`LanePanelState` 抽取、模态提示解耦)**已全部在紧随其后的两个提交里完成**:拆分 → 提交 **`e6b7bf6`**(详见 §9.37 第七节),`LanePanelState` + 模态解耦 → 提交 **`d2e6fdd`**(详见 §9.37 第八节)。**本行不再表示任何未完成项。** |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **收尾:`LanePanelState` 抽取 + 错误提示非模态化(把上一行的「未完成」清零,详见 §9.37 第八节与报告 `Documents/refactor_report_2026-10-16.md` §九)**。① **`Lane` 的 12 个平铺面板字段**(`edgePanelOpen/X/Y` × 4 类实验性功能)→ **4 个 `LanePanelState`**(`struct LanePanelState { bool open=true; double x=-1; double y=-1; }`),访问点全部改为 `lane.edgePanel.open` 形式;`RebuildLanes` 的 `LaneCarry` 同步收敛为 4 个 `LanePanelState`(逐字段赋值 → 整体赋值)。⚠️ 教训:**批量改名脚本不能对「声明」与「使用」跑同一组替换对**(会生成 `bool edgePanel.open = true;` 非法声明),声明处须单独重写。② **错误提示与模态阻塞解耦**:新增 `Application::SetModalErrorsEnabled(bool)`/`ModalErrorsEnabled()`(成员默认 `true`),`Initialize` **最先**执行 `SetModalErrorsEnabled(showCommand != SW_HIDE)`(隐藏窗口弹模态框 = 进程被钉死);`SetStatusMessage(err,true)` 允许模态时原样 `MessageBoxW`,否则走 `m_plot.ShowPersistentHint(text)`(绘图区底部非模态浮层);`PlotView` 新增公开 `ShowPersistentHint`。普通提示/空文本仍被忽略 ⇒ **交互行为不变**。③ **顺带修掉真实缺陷**:`PlotView::Paint` 在 `m_lanes.empty()` 时提前 `return`,提示浮层在其后绘制 ⇒ **未勾选任何信号时错误永远不可见**(正是"加载失败"场景),该分支补一次 `PaintHint(g)`。④ **新增探针 `.preflight/perf/errcheck` 19/19**:默认值/隐藏降级/宿主开关、**工作线程 + 5s 超时**判"不阻塞"(旧行为会永久阻塞 ⇒ 判失败并 `ExitProcess`,探针不挂)、`EnumWindows` 数本进程 `#32770` 顶层窗口数不变、提示文本/粘滞/需重绘/起始时刻、**像素级**"有/无提示"差异 3781 且全帧差异同为 3781。验证:`make release`/`make debug` 零警告,`Target.exe` **764 416 B**;`verify` 49/49、`datacheck` 69/69、`edgecheck` 46/46、`panelanim` 156/156。⑤ **新增结构检查** `Tools/structcheck.ps1` + `make structcheck`(**62/62**):从磁盘与 `nm` 核验「四个巨型文件已按职责拆分(`PlotView.cpp` 13 695 行 → 705 行,绘制符号只在 `PlotViewPaint.o`)」与「`Components/Shared` 16 个控件全部由 `UiControls.o` 定义、69 处调用跨 ≥3 个组件」以及分层方向。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **收尾:`LanePanelState` 抽取 + 错误提示非模态化(把上一行的「未完成」清零,详见 §9.37 第八节与报告 `Documents/refactor_report_2026-10-16.md` §九)**。① **`Lane` 的 12 个平铺面板字段**(`edgePanelOpen/X/Y` × 4 类实验性功能)→ **4 个 `LanePanelState`**(`struct LanePanelState { bool open=true; double x=-1; double y=-1; }`),访问点全部改为 `lane.edgePanel.open` 形式;`RebuildLanes` 的 `LaneCarry` 同步收敛为 4 个 `LanePanelState`(逐字段赋值 → 整体赋值)。⚠️ 教训:**批量改名脚本不能对「声明」与「使用」跑同一组替换对**(会生成 `bool edgePanel.open = true;` 非法声明),声明处须单独重写。② **错误提示与模态阻塞解耦**:新增 `Application::SetModalErrorsEnabled(bool)`/`ModalErrorsEnabled()`(成员默认 `true`),`Initialize` **最先**执行 `SetModalErrorsEnabled(showCommand != SW_HIDE)`(隐藏窗口弹模态框 = 进程被钉死);`SetStatusMessage(err,true)` 允许模态时原样 `MessageBoxW`,否则走 `m_plot.ShowPersistentHint(text)`(绘图区底部非模态浮层);`PlotView` 新增公开 `ShowPersistentHint`。普通提示/空文本仍被忽略 ⇒ **交互行为不变**。③ **顺带修掉真实缺陷**:`PlotView::Paint` 在 `m_lanes.empty()` 时提前 `return`,提示浮层在其后绘制 ⇒ **未勾选任何信号时错误永远不可见**(正是"加载失败"场景),该分支补一次 `PaintHint(g)`。④ **新增探针 `.preflight/perf/errcheck` 19/19**:默认值/隐藏降级/宿主开关、**工作线程 + 5s 超时**判"不阻塞"(旧行为会永久阻塞 ⇒ 判失败并 `ExitProcess`,探针不挂)、`EnumWindows` 数本进程 `#32770` 顶层窗口数不变、提示文本/粘滞/需重绘/起始时刻、**像素级**"有/无提示"差异 3781 且全帧差异同为 3781。验证:`make release`/`make debug` 零警告,`Target.exe` **764 416 B**;`verify` 49/49、`datacheck` 69/69、`edgecheck` 46/46、`panelanim` 156/156。⑤ **新增结构检查** `Tools/structcheck.ps1` + `make structcheck`(**93/93**):[1] 四个巨型文件已按职责拆分(`PlotView.cpp` 13 695 行 → 705 行,`nm`:绘制符号只在 `PlotViewPaint.o`);[2] `Components/Shared` 16 个控件全部由 `UiControls.o` 定义、69 处调用跨 ≥3 个组件 + 分层方向;[3] 本轮三项用户要求的源码级断言(折起热区、改名框不吞项数、`SignalData` 单一 `name` 字段 + 注释核验)。**判别力 A/B**:回退两处 UI 修复 ⇒ `panelanim` 147/156(9 项如实失败,文案点名“group rename box stops before the item counter”“clicking the group name does not collapse the group”)、`structcheck` 90/93;恢复后 156/156 与 93/93。 |
---
@@ -2746,15 +2756,26 @@ Components/Shared/Src/UiControls.cpp
`Initialize(instance, showCommand)` 按 `showCommand != SW_HIDE` **自动降级**为
`m_plot.ShowPersistentHint(text)`;隐藏窗口下弹模态框没人能点掉 = 进程被钉死。
96. **结构约束有脚本可验**:`make structcheck`(脚本 `Tools/structcheck.ps1`,入库、纯 ASCII、
退出码 0/1)核验「巨型文件按职责拆分」与「`Components/Shared` 共享控件」两类不变量:
文件存在与行数、职责函数是否**只**定义在预期文件、内部头的具名命名空间 + `inline` 计数、
`Build/Release/*.o` 是否齐全、**`nm` 断言绘制符号只在 `PlotViewPaint.o`**、
每个实现目录都在 `Makefile` 里、`UiControls.hpp` 声明的每个控件**是否只由 `UiControls.o` 定义**、
调用点是否真的跨 ≥3 个组件。⚠️ 判定控件要用「`UiControls.hpp` **声明**的名字」而不是「所有 `Ui*` 函数」
(`UiFillRect`/`UiText`/`UiMeasureTextWidth` 是底层原语,本就住在 `Utilities/PaintUtil`)。
⚠️ 精确分层不变量:**`Components/Shared` 不依赖任何组件;底层 `PaintUtil` 不依赖 `Shared`;
`Utilities` 不包含任何功能组件头**;`Utilities/{PopupMenu,ColorPickerPopup}` 是历史上放在 `Utilities/`
下的自绘 UI 小部件,**允许**消费 `Shared`(方向同组件),不要把它们判成违规。
退出码 0/1)**93 条断言**,共三组:
* **[1] 拆分**:文件存在与行数、职责函数是否**只**定义在预期文件、内部头的具名命名空间 + `inline` 计数、
`Build/Release/*.o` 是否齐全、**`nm` 断言绘制符号只在 `PlotViewPaint.o`**、每个实现目录都在 `Makefile` 里。
* **[2] 共享控件**:`UiControls.hpp` 声明的**每一个**控件是否**只由 `UiControls.o` 定义**(`nm`)、
调用点是否真的跨 ≥3 个组件、`Shared` 是否不依赖任何组件、底层 `PaintUtil` 是否不依赖 `Shared`。
⚠️ 判定控件要用「`UiControls.hpp` **声明**的名字」而不是「所有 `Ui*` 函数」
(`UiFillRect`/`UiText`/`UiMeasureTextWidth` 是底层原语,本就住在 `Utilities/PaintUtil`)。
⚠️ `Utilities/{PopupMenu,ColorPickerPopup}` 是历史上放在 `Utilities/` 下的自绘 UI 小部件,
**允许**消费 `Shared`(方向同组件),不要把它们判成违规。
* **[3] 本轮行为/注释不变量**(用户 2026-10-16 的三项要求):折起门限只接受
`DISCLOSURE`/`GROUP_TITLE`(全文件恰好 1 处)+ `ToggleGroupFromRow` 恰好 2 个入口 +
双击非名称本体 `return false`;改名框右边界 `statsRight - statsWidth - 6*scale` 且**不存在**
`right = statsRight;`;`SignalData`/`SignalGroup` **各只1个字符串字段且名为 `name`**、
全仓 `displayName`/`Title()`/`Set*DisplayName` **0 处**、过时措辞(`文件下标`/`所属文件下标`/
`文件分组`/`原文件下标`/`文件序号`)**0 处**、已修正注释(如 `DerivedSignalRequest::groupIndex`
→ 「原信号所属分组下标」)**确实存在**。
⚠️ 中文短语不能写在 `.ps1` 里(PowerShell 5.1 按 ANSI 读脚本)⇒ 放在 UTF-8 数据文件
`Tools/structcheck_phrases.txt`,由脚本以 `UTF8Encoding($false)` 读入后逐条断言(`forbid`/`require`)。
⚠️ **判别力已用 A/B 反证**:回退两处 UI 修复后 `panelanim` **147/156(9 项如实失败)**、
`structcheck` **90/93(3 项如实失败)**;恢复后 **156/156** 与 **93/93**。
71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**:
否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号,
`Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上,
+63 -2
View File
@@ -44,6 +44,15 @@
但不失去判别力);**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),
> 不依赖人来重读散文。
---
## 二、数据模型去冗余(用户指出的重点)
@@ -400,7 +409,7 @@ struct LanePanelState
| `.preflight/perf/edgecheck`(边沿统计) | **46/46** |
| `.preflight/perf/panelanim`(分组折起动画 + 两处 UI 修复) | **156/156** |
| `.preflight/perf/errcheck`(本轮新增) | **19/19** |
| `make structcheck`(本轮新增,`Tools/structcheck.ps1`) | **62/62**(结构约束:拆分 + 共享控件 + 分层,见 §9.6) |
| `make structcheck`(本轮新增,`Tools/structcheck.ps1`) | **93/93**(结构约束 + 本轮行为/注释不变量,见 §9.6 / §9.7) |
| 上一遍的 A/B(回退两处 UI 修复) | 如实失败 **9** 项(失败文案逐条点名「点组名不应折起」「改名框不应包住条目计数」),恢复后 156/156 |
### 9.5 本轮变更清单
@@ -415,7 +424,8 @@ struct LanePanelState
修改 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 结构检查(拆分 + 共享控件 + 分层),62 条断言
新增 Tools/structcheck.ps1 结构检查(拆分 + 共享控件 + 分层 + 本轮行为/注释不变量),93 条断言
新增 Tools/structcheck_phrases.txt 注释核验用中文短语表(UTF-8,供 .ps1 读取)
修改 Makefile 新增 structcheck 目标
新增 .preflight/perf/errcheck_main.cpp 模态/非模态解耦探针(gitignored,不随产物提交)
```
@@ -445,3 +455,54 @@ make structcheck # = powershell -File Tools/structcheck.ps1
> 不变量是「**`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` 可写),空名被拒。
+74 -1
View File
@@ -195,8 +195,81 @@ Check ($paintUtilIncludes.Count -eq 0) "Utilities/PaintUtil (base layer) does no
$badDir = @(Select-String -Path "Utilities/Src/*.cpp", "Utilities/Inc/*.hpp" -Pattern '#include "(PlotView|SignalPanel|Toolbar|TabBar)' -ErrorAction SilentlyContinue)
Check ($badDir.Count -eq 0) "Utilities never includes a feature component"
Write-Host ""
Write-Host "== [3] user-requested behaviour invariants (source level) =="
$utf8 = New-Object System.Text.UTF8Encoding($false)
$panelFile = "Components/SignalPanel/Src/SignalPanel.cpp"
$panelIntFile = "Components/SignalPanel/Src/SignalPanelInteraction.cpp"
$modelFile = "Utilities/Inc/DataModel.hpp"
$panelText = [System.IO.File]::ReadAllText($panelFile, $utf8)
$panelIntText = [System.IO.File]::ReadAllText($panelIntFile, $utf8)
$modelText = [System.IO.File]::ReadAllText($modelFile, $utf8)
# 3a. collapsing a group needs the disclosure triangle OR the "Group N" label -- clicking the name
# itself must not collapse it. (User 2026-10-16.)
Check ($panelText -match "return RowPart::GROUP_TITLE;") "hit test yields a dedicated GROUP_TITLE part"
Check ($panelText -match "const gd::RectF labelRect\(labelLeft, rect\.Y, GroupLabelWidth\(row\), rect\.Height\);") "GROUP_TITLE hot zone is exactly the Group-N label rect"
$gate = @(Select-String -Path $panelIntFile -Pattern "^\s*if \(part != RowPart::DISCLOSURE && part != RowPart::GROUP_TITLE\)")
Check ($gate.Count -eq 1) ("single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found " + $gate.Count + ")")
$toggles = @(Select-String -Path $panelIntFile -Pattern "^\s*ToggleGroupFromRow\(item\.groupIndex, row\);")
Check ($toggles.Count -eq 2) ("ToggleGroupFromRow has exactly 2 entry points, single click + double click (found " + $toggles.Count + ")")
Check ($panelIntText -match "(?s)if \(part == RowPart::DISCLOSURE\)\s*\{\s*//[^\n]*\n(\s*//[^\n]*\n)*\s*ToggleGroupFromRow") "double click on the triangle still collapses"
Check ($panelIntText -match "(?s)if \(part != RowPart::BODY\)\s*\{\s*return false;\s*\}") "double click outside the name body never starts a rename"
# 3b. the group rename box must stop before the "N items" counter. (User 2026-10-16.)
Check ($panelText -match "right = statsRight - statsWidth - 6\.0f \* m_scale;") "rename box right edge excludes the item counter"
Check ($panelText -notmatch "right = statsRight;") "rename box never extends over the counter"
Check ($panelText -match "const float statsWidth = UiMeasureTextWidth\(m_fonts->uiSmall, GroupStatsText\(row\)\);") "counter width comes from GroupStatsText"
# 3c. SignalData / SignalGroup keep exactly ONE name field. (User 2026-10-16: drop displayName.)
$data = [regex]::Match($modelText, "(?s)struct SignalData\s*\{(.*?)\n\t\};")
Check ($data.Success) "SignalData struct found in the data model"
if ($data.Success) {
$body = $data.Groups[1].Value
$fields = @([regex]::Matches($body, "std::wstring\s+\w+;"))
Check ($fields.Count -eq 1) ("SignalData declares exactly one string field (found " + $fields.Count + ")")
Check ($body -match "std::wstring name;") "that single field is name"
}
$group = [regex]::Match($modelText, "(?s)struct SignalGroup\s*\{(.*?)\n\t\};")
Check ($group.Success) "SignalGroup struct found in the data model"
if ($group.Success) {
$gbody = $group.Groups[1].Value
$gfields = @([regex]::Matches($gbody, "std::wstring\s+\w+;"))
Check ($gfields.Count -eq 1) ("SignalGroup declares exactly one string field (found " + $gfields.Count + ")")
}
Check ($modelText -match "bool RenameSignal\(") "DataSet::RenameSignal replaces the old SetSignalDisplayName"
Check ($modelText -match "bool RenameGroup\(") "DataSet::RenameGroup replaces the old SetGroupDisplayName"
$staleNames = @(Select-String -Path "Components/*/Src/*.cpp","Components/*/Inc/*.hpp","Components/*/Src/*.hpp",
"Utilities/Src/*.cpp","Utilities/Inc/*.hpp" -Pattern "displayName|SetSignalDisplayName|SetGroupDisplayName|\.Title\(\)|->Title\(\)")
Check ($staleNames.Count -eq 0) ("no displayName / Title() left in any source (found " + $staleNames.Count + ")")
# 3d. comment hygiene, driven by a UTF-8 data file (the .ps1 itself must stay ASCII for PowerShell 5.1)
$phraseFile = "Tools/structcheck_phrases.txt"
Check (Test-Path $phraseFile) "comment-hygiene phrase list exists"
if (Test-Path $phraseFile) {
$scanFiles = @(Get-ChildItem "Components/*/Src/*.cpp","Components/*/Src/*.hpp","Components/*/Inc/*.hpp",
"Utilities/Src/*.cpp","Utilities/Inc/*.hpp" | ForEach-Object { $_.FullName })
$scanText = @{}
foreach ($f in $scanFiles) { $scanText[$f] = [System.IO.File]::ReadAllText($f, $utf8) }
foreach ($line in [System.IO.File]::ReadAllLines($phraseFile, $utf8)) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split "`t"
$mode = $parts[0]
if ($mode -eq "forbid") {
$phrase = $parts[1]
$offenders = @($scanFiles | Where-Object { $scanText[$_].Contains($phrase) })
Check ($offenders.Count -eq 0) ("stale wording absent everywhere: " + $phrase + " (found in " + $offenders.Count + " files)")
} elseif ($mode -eq "require") {
$target = $parts[1]
$phrase = $parts[2]
Check ((Test-Path $target) -and ([System.IO.File]::ReadAllText($target, $utf8).Contains($phrase))) ("corrected comment present in " + $target + ": " + $phrase)
}
}
}
Write-Host ""
Write-Host ("structcheck: " + $pass + " passed, " + $fail + " failed")
if ($fail -ne 0) { exit 1 }
exit 0
+13
View File
@@ -0,0 +1,13 @@
forbid 文件下标
forbid 所属文件下标
forbid 原信号所属文件下标
forbid 文件分组
forbid 原文件下标
forbid 文件序号
forbid displayName
forbid SetSignalDisplayName
forbid SetGroupDisplayName
require Components/Application/Inc/Application.hpp 原信号所属分组下标
require Utilities/Inc/DataModel.hpp 信号名称(加载时取列名 / 通道名;用户可在界面上重命名)
require Utilities/Inc/DataModel.hpp 分组名(加载时取源文件名、含扩展名;用户可重命名)
require Utilities/Inc/DataModel.hpp 与磁盘上的文件**没有任何关系**