feat(app-iced): M101 r2 设置页状态漏斗 + pages/mod.rs 拆分 + r1 复审订正

r1 复审的 P1:消息漏斗只解决了「加一支消息」,一组要画真值仍要改 `settings/mod.rs`
(`Group.body` 与 `view` 只认 `appearance::State`)。本 commit 补齐与消息漏斗同构的状态漏斗:

- 新增 `pages/settings/state.rs`:`Groups`(一个组一个字段,字段类型在组自己文件里)+
  `States`(按值传给渲染器的只读视图:外观状态 + 组状态)+ `demo_groups()`(harness 的
  `'static` 默认状态)。
- `Group.body` 签名改成 `for<'a> fn(&'a Ui, States<'a>) -> Option<Element>`:每组自己从
  `States` 取自己要的数据,「没有可画的状态」返回 `None` 由 `form` 退回演示行。
  `appearance` 组照旧接 `pages/appearance/section.rs`(M93 路径行为不变)。
- `App.settings: pages::settings::Groups`(`app/state.rs` 字段列表末尾)+ `app/view.rs` 构造
  `States` 走新入口 `pages::page_with_settings`。加/改某一组的字段只改那一组的文件。
- 四个组各自的 `State` 落在各自文件;`java::Message` 留一支只有测试构造的 `Ping`,配
  `app/update/settings/tests.rs` 的 `a_group_message_reaches_its_handler_through_the_funnel`
  ——消息漏斗第一次有**运行时**证据。

r1 其余条目:
- P2-1 删掉 `only_the_appearance_group_carries_a_real_control_body`(被四条 per-group 断言蕴含),
  并在同处写明删掉之后「唯一性」没人守;三份 section 文档从「改 3 个文件」订正为 2 个。
- P2-2 `pub mod settings` → `pub(crate) mod settings`(四个消息口模块 `pub(crate)`),
  并订正两处理由(lint 按类型自身可见性判,不是可达性)。
- P2-3 `pages/mod.rs` 397 行 → 拆出 `pages/message.rs` / `pages/screen.rs`(纯搬移,`pub use`
  保证 `pages::Message` / `Action` / `Screen` 路径不变),并删掉 M101 自己那 18 行重复文档。
- P3-1 订正共享点清单(`Key` 由宏从域文件累积生成,加 key 不改 `catalog.rs`)。
- P3-2 守门改成「除已知臂外任何含 `=>` 的行都算兜底」(`other =>` 也拦),并加「恰好四臂」断言。
- P3-3 `ai.rs` → `ai_about.rs` 单独一条 R100 改名提交,`git log --follow` 不断链。
- P3-4 `message.rs` 的「加一支消息改 1 个文件」订正为 2 个。
This commit is contained in:
AskaEth committed 2026-11-03 20:57:08 +08:00
1 parent 423ea77758
commit 029ece3f17
25 files changed
+1004 -240

No files matched your search

@@ -1,8 +1,8 @@
//! 「Downloads」分组自己的断言(M100 **新增**:`settings/tests.rs` 那 7 条全是跨 section 的,
//! 一条 section 断言都没有;这 3 条是新增,不是搬移)。
//!
//! **归 download 线**。接真控件那天要改**三个**文件:本文件 `the_downloads_group_is_still_demo_rows`
//! (`body.is_none()` 绊线)+ `settings/download.rs` + `tests/mod.rs` 的跨 section 绊线。
//! **归 download 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨 section 绊线):
//! `settings/download.rs` + 本文件 `the_downloads_group_is_still_demo_rows`。
//!
//! 别把它和下载页(`pages/download*`)搞混:那是「装哪个版本」,这一组是「怎么下」的默认值。
@@ -38,8 +38,8 @@ fn the_downloads_group_keeps_its_icon() {
/// 抓得住:download 线接上了 `body` 却没更新这里的期望(那会让本文件静默变成假绿)。
/// 抓不住:界面上真控件长什么样。
///
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**;再去 `tests/mod.rs` 的
/// `only_the_appearance_group_carries_a_real_control_body` 那条绊线上手工确认一次(那条是四组共用的)。
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**(M101 r2 起不必再去
/// `tests/mod.rs` 报到:那条跨 section 聚合绊线已删)。
#[test]
fn the_downloads_group_is_still_demo_rows() {
assert!(
@@ -1,9 +1,8 @@
//! 「General」分组自己的断言(M100 **新增**:`settings/tests.rs` 那 7 条全是跨 section 的,
//! 一条 section 断言都没有;这 3 条是新增,不是搬移)。
//!
//! **归 language+appearance 线**。接真控件那天要改**三个**文件:本文件
//! `the_general_group_is_still_demo_rows`(`body.is_none()` 绊线)+ `settings/general.rs` +
//! `tests/mod.rs` 的跨 section 绊线。
//! **归 language+appearance 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨
//! section 绊线):`settings/general.rs` + 本文件 `the_general_group_is_still_demo_rows`。
use super::super::general;
use unlucky_design::icons::IconKind;
@@ -38,8 +37,8 @@ fn the_general_group_keeps_its_icon() {
/// 抓得住:language+appearance 线接上了 `body` 却没更新这里的期望(那会让本文件静默变成假绿)。
/// 抓不住:界面上真控件长什么样。
///
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**;再去 `tests/mod.rs` 的
/// `only_the_appearance_group_carries_a_real_control_body` 那条绊线上手工确认一次(那条是四组共用的)。
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**(M101 r2 起不必再去
/// `tests/mod.rs` 报到:那条跨 section 聚合绊线已删)。
#[test]
fn the_general_group_is_still_demo_rows() {
assert!(
@@ -1,10 +1,10 @@
//! 「Java」分组自己的断言(M100 **新增**:`settings/tests.rs` 里一条 section 断言都没有,
//! 那 7 条全是跨 section 的;所以这 3 条是新增,不是搬移)。
//!
//! **归 java 线**。接真控件那天要改**三个**文件:
//! 1. 本文件 `the_java_group_is_still_demo_rows`(它断言 `body.is_none()`,接上就红);
//! 2. `settings/java.rs`(写 `body = Some(..)`);
//! 3. `tests/mod.rs` 的 `only_the_appearance_group_carries_a_real_control_body`(跨 section 绊线)。
//! **归 java 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨 section 绊线,
//! 见 `tests/mod.rs` 的 `every_line_can_be_wired_alone` 说明):
//! 1. `settings/java.rs`(`body = Some(java::body)` + 渲染器);
//! 2. 本文件 `the_java_group_is_still_demo_rows`(把 `is_none()` 改成 `is_some()`)。
//!
//! download / general 同型。
//!
@@ -46,8 +46,8 @@ fn the_java_group_keeps_the_folder_icon_from_the_visual_review() {
/// 抓得住:java 线接上了 `body` 却没更新这里的期望(那会让本文件静默变成假绿)。
/// 抓不住:界面上真控件长什么样。
///
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**;再去 `tests/mod.rs` 的
/// `only_the_appearance_group_carries_a_real_control_body` 那条绊线上手工确认一次(那条是四组共用的)。
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**(M101 r2 起不必再去
/// `tests/mod.rs` 报到:那条跨 section 聚合绊线已删,见那里的 `every_line_can_be_wired_alone`)。
#[test]
fn the_java_group_is_still_demo_rows() {
assert!(
+54 -25
View File
@@ -130,33 +130,62 @@ fn the_split_kept_the_left_column_order() {
assert_eq!(names, vec!["General", "Appearance", "Java", "Downloads"]);
}
/// M99:「外观」是**当前唯一**接了真控件的那一组。
/// M101 r2:`settings/mod.rs` 里**不许出现任何一组的 `State` 类型**(r1 复审 P1 的判据钉在这里)。
///
/// 抓得住:拆分时把 `body: Some(…)` 搬丢 / 搬到了别的组(那会让真控件消失,退回演示行——
/// 而截图对这种「只在有外观状态时出现」的差别不一定抓得住,见报告「没验的部分」);
/// 也抓「别组偷偷接了 body 却没人知道」。
/// 抓不住:`body` 指向的那个渲染器画得对不对(那是 `pages::appearance` 的事)。
///
/// **为什么不能下放**(任务书点名要求写清):这条断言的全部内容是**四组的 body 现状对比**
/// ——「只有第 2 组是 Some,其余三组都是 None」。放进 `appearance_section.rs` 它就得替 java /
/// download / general 三组作证(那三条线一接真控件,本文件以外的地方无缘无故变红,且责任方
/// 不是改代码的人);拆成四份「我这组是 None」又会丢掉「唯一」这个要点。
/// 它是**绊线**:哪条线接上真控件,就必须来本文件手工改这一条(T1:绊线保持字面量,
/// 动态取活值等于把绊线剪了)。
/// 抓得住:谁把状态漏斗拆回去——让 `view` / `form` 直接收 `appearance::State` 或某一组的 `State`
/// (那正是四条线「加一个字段就要改本文件」的形状,本次复审要求修掉的就是它)。
/// 抓不住:各组的**字段**(那些活在各组自己的文件里,本文件本来就不该知道,也就没有可断言的东西)。
#[test]
fn only_the_appearance_group_carries_a_real_control_body() {
fn the_settings_module_does_not_name_any_group_state() {
let source = include_str!("../mod.rs");
let forbidden = [
"appearance::State",
"java::State",
"download::State",
"language_appearance::State",
"ai_about::State",
];
let hits: Vec<&str> = forbidden
.into_iter()
.filter(|needle| source.contains(needle))
.collect();
assert!(
GROUPS[APPEARANCE_GROUP].body.is_some(),
"外观分组必须是接了真控件的那一组"
hits.is_empty(),
"`settings/mod.rs` 里出现了具体的组状态类型 {hits:?}:状态漏斗被打回去了——\
四条填页线又要改本文件。状态应该只在 `settings/state.rs` 的 `Groups` 与各组自己的\
`State` 定义里出现,本文件只转发 `States`"
);
for (index, group) in GROUPS.iter().enumerate() {
if index == APPEARANCE_GROUP {
continue;
}
assert!(
group.body.is_none(),
"{} 现在应该还是演示行(真控件由它自己那条 section 线接)",
group.name
);
}
}
///
/// 为什么能删:`tests/appearance_section.rs` 断言外观组 `body.is_some()`,
/// `tests/{general,java,download}_section.rs` 各自断言本组 `body.is_none()`——四条合起来与
/// 原来那条聚合断言等价(成员与顺序另有 [`the_split_kept_the_left_column_order`] 钉住)。
/// 删掉之后每条线「接真控件」只需改自己那 2 个文件,不必来本文件报到。
///
/// ## 删掉之后**抓不住什么**(tower 要求写明)
///
/// 那四条都只看**本组**,没有任何一条再断言「唯一性」——「四组里恰好一组挂了真控件」这件事
/// **现在没有守卫**。具体漏法:
///
/// | 情形 | 还红吗 |
/// |---|---|
/// | 某条线接上 `body` 却忘了翻自己那条 `is_none()` | **红**(本组断言还在) |
/// | 两条线的断言被同时改成 `is_some()`,页面上两组同时长真控件 | **绿着过去**(没人指认「唯一」) |
/// | 谁把 `body: Some(..)` 挂到一个不该有的组、又顺手改了它那条断言 | **绿着过去** |
///
/// 接受这个损失的理由:接真控件是**每组各自的一次性动作**,谁做谁翻自己那一条;而「唯一」在
/// 四条线并行的阶段是最容易假红的约束(一条线接上就替另外三条红)。想要回这条守卫,等四条线
/// 都落地后在这里加一条**计数**断言(`GROUPS.iter().filter(|g| g.body.is_some()).count()`),
/// 而不是现在钉死某个下标。
///
/// 这条 `#[test]` 本体已删;保留这段文档是为了让后来的人知道「这里原本有一条绊线」。
#[test]
fn every_line_can_be_wired_alone() {
// 判据是「每条线只改自己的文件」,不是任何一条具体断言:这里只钉住架子本身的两件事——
// ① 每组都有 `GROUP`(成员齐);② 演示下标合法。真正的 per-group 断言在各自文件里。
assert_eq!(GROUPS.len(), 4, "四条线各一个分组");
assert!(SELECTED < GROUPS.len(), "演示下标仍在范围内");
}