diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index b50012b..e44d7bc 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -350,8 +350,8 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可 它的注释写明"省略或为空 = **不受限**",因为 17 个存量清单都没有这个字段, 若把空声明当最小权限,它们会全部静默失去 IO 注入与记忆读写。 -| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | **无人类批准**:这是实现时的默认行为(`client/harmony/entry/src/main/ets/model/Wallpaper.ts` 随该文件一起引入,`git log --diff-filter=A` 可查出处),**本行是补登记**(pi 2026-09-14:写成"产品决定"就是事后追认)。意图(写在这里,不冒充批准):背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档**;替换发生时会留下痕迹(`BackgroundPlan.presetSubstitutedFrom`,页面据此打日志,判据见 `harmony-presets.test.mjs`) | -| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 出处:`model/Appearance.ts:39,52,88,104,133`(clamp + 合并)+`common/AppearanceStore.ets:142-195`(同步);**消费侧 0 处**。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据。**该侧现在也有判据**(`harmony-appearance.test.mjs`:消费侧出现次数必须为 0),与 WebUI `LEGACY_BACKUP_KEY` 那条同形 | +| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | **待批准**(不是"无人类批准"就收尾):这是实现时的默认行为(`model/Wallpaper.ts` 随该文件引入,`git log --diff-filter=A` 可查出处),本行是补登记。**已进欠账余额**:`docs/DEBTS.json` 的 `unknown-preset-approval` —— 到期前提是"有人追认或驳回『未知 id 显示 aurora 而不是空白』这个方向",**我作为实现者不能自己追认自己**(pi 2026-09-14:登记要落到"谁负责"或"什么时候还",不能停在"我们知道")。意图(写在这里,不冒充批准):背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档**;替换发生时会留下痕迹(`BackgroundPlan.presetSubstitutedFrom`,页面据此打日志,判据见 `harmony-presets.test.mjs`) | +| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它、映射函数也写好了,但没有任何消费点**(映射本身**已判**) | 无(当前无效果) | 出处:`model/Appearance.ts:39,52,88,104,133`(clamp + 合并)+`common/AppearanceStore.ets:142-195`(同步);**消费侧 0 处**。⇒ **这里的结论更正过一次**:我原先写"不存在映射表 ⇒ 钉映射判据是假判据",**那是错的** —— 映射表 `blurStyleFor(bgBlur)` **早就在 `model/Appearance.ts` 里**(文件注释还写着「判据可以直接跑它」),只是**没有任何调用点**。发现方式值得记下来:pi 要求把豁免从"按文件"改成"**文件 + 次数**",逐处核对出现次数时它才被翻出来。**现在的登记**:映射表存在且**已有行为判据**(分档边界 0/8/20、单调性、NaN 不许落到最厚档);"缺的是调用点"这件事由消费侧计数判据管(登记值 0),两个问题分开判 | | 只写不读的字段:WebUI `LEGACY_BACKUP_KEY` | 删旧值前**另存一份**,没有任何代码读它 | 无(有意为之) | `backgroundStore.ts:200` 的注释写明目的(否则它会变成新的继承源)。**登记在此是为了审计能一次找全** | | 邮件的 `permission_mode` | **不写、不改档**(不是写默认档) | 窄(fail-closed) | 本仓 2026-09-14:缺字段被 `\|\| 'workspace'` 兜成窄档,等于"一个 bug 以正常形态活着";守卫见 `plugins/*/lib/permission-mode.js` 的 `modeForStateWrite` | | HomeAgent `plugin.json` 的 `sdk` | **内核不读**(字段只对人有效) | 无(不是语义,是文档) | 内核 `internal/plugin/manifest.go` 的结构体里没有该字段;`registry.go` 的 `loadOne` 只用 `NameZh/NameEn` | @@ -429,3 +429,19 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可 **同族**:这条与"两张表各缺一半时必须按 id 联接,不能按相邻关系配对"、 "'与 X 一致'必须先确认 X 侧有判据钉住"是同一族 —— **比较/引用的一端必须是权威来源, 不是手边那份可能过期的副本。** + +## 14. 登记/清册类判据的报错,要同时写"正确修法"和"**最常见的错误修法**" + +**规则**(pi 2026-09-14 提出):凡是"登记 / 清册 / 次数"类的判据,红的时候读的人**第一反应 +通常是改那个数字**。所以报错信息必须把两件事都写上: + +1. **正确修法**(该去补什么); +2. **最常见的错误修法**(别做什么,以及为什么那等于把判据废掉)。 + +例(`harmony-appearance.test.mjs` 的 `bgBlur` 那条): + +> 停下:那时**必须**先补"px ↔ 材质档位"的映射判据(§10 / 计划文档 §7.12), +> **而不是把登记值从 0 改成 1**。 + +这与"清册条目的『允许』要给出一条能判的约束"(§11 族)是同一条纪律的两半: +前一半保证**判据有分辨力**,这一半保证**分辨力不会被"改数字"抹掉**。 diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index fa78515..798b434 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -786,11 +786,17 @@ test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就 * 不是消费 —— 判据要挡的是"有人拿这个值去决定画什么"。 * 与 `server/internal/repo/mail_status_readers_test.go` 的豁免同一个形状。 */ + /* + * 豁免必须**按文件 + 次数**(pi 2026-09-14 交叉提醒,与 migrate.go 那处同一条): + * 只按文件放行 ⇒ "在已允许的文件里顺手再读一下 bgBlur 做别的事"会被静默吞掉 + * (例如有人在 DTO 文件里拿它算点别的)。次数写死在这里,多一次即红。 + */ const plumbing = new Map([ - ['model/Appearance.ts', '域模型:clamp(0,40) 后存下 bgBlur(搬运)'], - ['common/AppearanceStore.ets', '状态同步:与快照互转(搬运)'], - ['api/AppearanceApi.ets:17', '线上 DTO 声明 bg_blur(传输格式,不是消费)'] + ['model/Appearance.ts', { max: 11, why: '域模型:声明 + clamp + 合并 + 映射函数 blurStyleFor(px → 材质档,见下一条判据)—— 搬运与映射,都不是消费' }], + ['common/AppearanceStore.ets', { max: 4, why: '状态同步:与快照互转(搬运)' }], + ['api/AppearanceApi.ets', { max: 1, why: '线上 DTO 声明 bg_blur(传输格式,不是消费)' }] ]); + const plumbingSeen = new Map(); const consumers = []; for (const f of files) { @@ -799,7 +805,14 @@ test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就 src.split('\n').forEach((line, i) => { if (!/\bbgBlur\b|\bbg_blur\b/.test(line)) return; const where = rel + ':' + (i + 1); - if (plumbing.has(rel) || plumbing.has(where)) return; + const rule = plumbing.get(rel); + if (rule) { + const n = (plumbingSeen.get(rel) || 0) + 1; + plumbingSeen.set(rel, n); + if (n <= rule.max) return; + consumers.push(`${where} **超出豁免上限**(${rel} 上限 ${rule.max} 处,理由:${rule.why})—— ${line.trim()}`); + return; + } consumers.push(`${where} ${line.trim()}`); }); } @@ -807,3 +820,38 @@ test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就 `有人在消费 bgBlur 了 —— 停下:那时**必须**先补"px ↔ 材质档位"的映射判据` + `(CRITERIA.md §10 / 计划文档 §7.12),而不是把登记值从 0 改成 1:\n ${consumers.join('\n ')}`); }); + +/** + * ★ `blurStyleFor`:**px → 系统材质档** 的映射是**行为**,不是注释(可以真跑)。 + * + * 这条的来历值得记:我先前把"鸿蒙没有消费点"登记成"**不存在映射表 ⇒ 钉映射判据是假判据**", + * 那个结论**是错的** —— 正是 pi 要求把豁免改成"文件 + 次数"之后,逐处核对出现次数 + * 才把 `model/Appearance.ts` 里的 `blurStyleFor` 翻出来:**映射表早就写了** + * (文件里的注释还写着「判据可以直接跑它」),只是**没有任何调用点**。 + * + * 所以正确的登记是:**映射表存在且可判(本判据);缺的是调用点**。 + * "有没有人用它"是另一件事,由上面那条消费侧计数判据管(登记值为 0)。 + */ +test('★ blurStyleFor:分档边界、单调性、NaN 都是行为(0/8/20 是契约的一部分)', async () => { + const { pathToFileURL } = await import('node:url'); + const A = await import(pathToFileURL(join(HARMONY_ETS, 'model', 'Appearance.ts')).href); + assert.equal(typeof A.blurStyleFor, 'function', 'blurStyleFor 必须存在且可跑'); + + const cases = [[0, 'NONE'], [-5, 'NONE'], [1, 'COMPONENT_THIN'], [8, 'COMPONENT_THIN'], + [9, 'COMPONENT_REGULAR'], [20, 'COMPONENT_REGULAR'], [21, 'COMPONENT_THICK'], + [40, 'COMPONENT_THICK'], [999, 'COMPONENT_THICK']]; + for (const [px, want] of cases) { + assert.equal(A.blurStyleFor(px), want, `bg_blur=${px} 应当映射到 ${want}`); + } + // 单调性:px 变大,档次不许倒退(分档写反是最容易犯的错) + const order = ['NONE', 'COMPONENT_THIN', 'COMPONENT_REGULAR', 'COMPONENT_THICK']; + let last = -1; + for (let px = 0; px <= 40; px++) { + const i = order.indexOf(A.blurStyleFor(px)); + assert.ok(i >= last, `bg_blur=${px} 的档次倒退了(${order[last]} → ${order[i]})`); + last = i; + } + // NaN 不许落到最厚那一档:它只能来自坏数据,不该被解释成"最模糊" + assert.notEqual(A.blurStyleFor(Number.NaN), 'COMPONENT_THICK', + 'NaN 不许落到最厚那一档(比较全 false 时掉到最后一档 —— 那是最坏的方向)'); +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index e516216..dd39228 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -70,7 +70,7 @@ const SUITE = [ ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], ['test/harmony-system-api.test.mjs', [], 5], // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types - ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 25], + ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 26], // P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位 ['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6], // 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号 diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 1e5e32c..c2c9525 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -24,6 +24,18 @@ "count": 1, "due": "P6 第 3 步:鸿蒙侧出现滑动手势代码时立即建(此前建 = 只有一端存在的假判据)", "where": "docs/HARMONY-ALIGN-PLAN.md P6 段" + }, + { + "id": "observability-output", + "count": 1, + "due": "页面层(MainPage.ets)接上「读 presetSubstitutedFrom 并打一行日志」时;那一步同时补判据『读侧恰好出现 1 次且在日志调用里』", + "where": "尚无判据 —— 这正是欠账的一部分(P6 第 1、2 步动 MainPage.ets 时一起做)" + }, + { + "id": "unknown-preset-approval", + "count": 1, + "due": "有人对上表那格**追认或驳回**「未知 id 显示 aurora 而不是空白」这个方向时(我作为实现者不能自己追认自己)", + "where": "client/electron/test/CRITERIA.md §10 的『未知的预设 id』行(现为『无人类批准』)" } ] } diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index fa5f688..4e58a91 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -588,7 +588,7 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 | 圆角 | 自声明 `--radius-card: 0.875rem` | 系统 `sys.float.ohos_id_corner_radius_card/button` | 系统圆角会随设备/主题/无障碍设置变;跟着系统才是"系统方案" | | 材质(玻璃) | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter` | 系统 `backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` | 系统材质自带深浅两套颜色与模糊半径,手写 alpha 跟不了深色 | | **未知预设 id(版本偏移)** | WebUI/服务端先加第 7 档、鸿蒙还是旧构建 ⇒ 旧端认不出新 id | 按缺省语义**静默替换为 aurora**(`normalizePreset`) | **这不是 bug,是批准的缺省语义的可见后果**:用户以为选的是新档,实际看到的是 aurora。方向是"宽(静默)"——不会看到错误,但会看到**别人的档**。行为判据用"不存在的 id 当哨兵"正好钉住这条(`harmony-presets.test.mjs`) | -| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有被消费**:栏上的系统**材质档位**(`BlurStyle.COMPONENT_THICK`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量,而且这边根本没有映射表**。2026-09-14 核实:`bg_blur` 在鸿蒙侧只有"搬运"没有"消费" —— `Appearance.ts` 把它 clamp 到 0~40 存进 `bgBlur`、`AppearanceStore` 同步它,但**没有任何页面/组件读它**(材质档位是固定枚举)。所以不存在"px ↔ 档位"的值映射可钉,也就**没有可判的性质**(pi 问的"映射表有没有判据":没有,且当前不该有 —— 钉一张不存在的表是假判据)。**改一端的含义**:改 WebUI 的 `bg_blur` 语义**不影响鸿蒙**;若将来鸿蒙开始消费它(例如按 px 选不同 `BlurStyle`),**那时**必须补一条映射判据,并更新本行 —— 这是"人工约定、当前无判据"的登记处(CRITERIA.md §12 的同一族)。 | +| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有被消费**:栏上的系统**材质档位**(`BlurStyle.COMPONENT_THICK`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量;映射表存在但无调用点**。2026-09-14 核实:`bg_blur` 在鸿蒙侧只有"搬运"没有"消费" —— `Appearance.ts` 把它 clamp 到 0~40 存进 `bgBlur`、`AppearanceStore` 同步它,但**没有任何页面/组件读它**(材质档位是固定枚举)。(**此处结论已更正**:`blurStyleFor(bgBlur)` 这张映射表**是存在的**,且**已有行为判据**(分档边界 0/8/20、单调性、NaN 不许落到最厚档);缺的是**调用点**,由消费侧计数判据管。我先前的"不存在映射表 ⇒ 钉它是假判据"是错的,纠正过程见 CRITERIA.md §10。)**改一端的含义**:改 WebUI 的 `bg_blur` 语义**不影响鸿蒙**;若将来鸿蒙开始消费它(例如按 px 选不同 `BlurStyle`),**那时**必须补一条映射判据,并更新本行 —— 这是"人工约定、当前无判据"的登记处(CRITERIA.md §12 的同一族)。 | | 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") | | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 |