跨端: feat(预设): 静默兜底留一条可观测痕迹;"只写不读"在鸿蒙侧也补上判据(照 LEGACY_BACKUP_KEY 照搬);§10 出处落地

pi 2026-09-14 的三小条。

1. **§3 静默兜底要留可观测痕迹**:`normalizePreset` 把认不出的 id 换成 aurora 这件事,
   原先在真实环境里**不留任何痕迹** —— §10 的登记只防"被误报成 bug",防不住
   "没人知道它正在发生"。现在 `BackgroundPlan.presetSubstitutedFrom` 带出**原来那个 id**
   (换过非空、没换过空串),页面据此打一行日志 ⇒ 后果从"可能发生"变成"**可数**"。
   **痕迹记在返回值里而不是在这一层直接打日志**,理由写进代码:这一层是**纯逻辑**
   (无 `@ohos` 依赖 ⇒ 判据能用 node strip-types 直接跑它);为打一行日志引入 `@ohos.hilog`,
   等于把"能真跑的行为判据"换成"只能读源码的形态判据"——不划算的交易。
   **判据两条方向都钉**:替换必须留痕(且带出原 id);**没替换时必须为空**
   (痕迹退化成噪声就等于没有)。变异与正反例都在(5 条全绿)。

2. **§2 §10 的"谁批准"落成出处**:那一格原写"产品决定" —— 按 pi 的话这是**事后追认**。
   现在写的是 **「无人类批准:这是实现时的默认行为(随 `model/Wallpaper.ts` 引入,
   `git log --diff-filter=A` 可查出处,2e42aac),本行是补登记」**,并把**意图**与**批准**分开写
   (不拿意图冒充批准)。另两行也补了出处(`Appearance.ts:39,52,88,104,133` /
   `AppearanceStore.ets:142-195` / `backgroundStore.ts:200`)。

3. **§1 "可扫的字段清册"不必先造 —— 同形状已有一边是判据**:他说得对。
   WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读",鸿蒙的 `bgBlur` 只有文档
   ⇒ 差的是**同一个形状只有一边有判据**。已照搬:`harmony-appearance.test.mjs` 新增
   **"消费侧出现次数必须为 0"**;豁免**按文件登记 + 写理由**(域模型 / 状态同步 / 线上 DTO
   三处是搬运与传输,不是消费)——与 `mail_status_readers_test.go` 的豁免同一形状,
   登记表本身就是清册,不必另造一张。
   变异验证:让 MainPage 读一次 `bgBlur` → 判据红,红的信息写着"**停下:那时必须先补
   px ↔ 材质档位的映射判据**,而不是把登记值从 0 改成 1"。

SUITE 计数同步:harmony-presets 4→5、harmony-appearance 24→25。
This commit is contained in:
2026-09-14 17:13:19 +08:00
parent eeb8f277fd
commit f5c4f56682
5 changed files with 101 additions and 4 deletions

View File

@ -350,8 +350,8 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可
它的注释写明"省略或为空 = **不受限**",因为 17 个存量清单都没有这个字段,
若把空声明当最小权限,它们会全部静默失去 IO 注入与记忆读写。
| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | 产品决定:背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档** |
| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 核实于 2026-09-14:`Appearance.ts` clamp 存入、`AppearanceStore` 同步,**无页面读它**(材质档位是固定枚举)。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据 |
| **未知的预设 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` 那条同形 |
| 只写不读的字段: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` |

View File

@ -755,3 +755,55 @@ test('★ 遮盖色方向:`mask_*` 两套主题下都是**深色**(模态遮
assert.match(settings, /Theme\.overlay/, '自绘弹层的遮罩仍然用 mask(那里的语义是压暗背后)');
assert.ok(!/wallpaperScrim/.test(settings), '弹层不该用壁纸遮盖色(两个语义别混)');
});
/**
* ★ `bgBlur` 是"只写不读"的字段:**消费侧出现次数必须为 0**(pi 2026-09-14 指出同一形状只有一边有判据)。
*
* WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读,否则它会变成新的继承源";
* 而鸿蒙侧 `bgBlur`(`Appearance.ts` clamp 存入、`AppearanceStore` 同步)**只有文档**。
* 同一个形状只有一边有判据 ⇒ 这一边补上,不必等一张新的字段清册:
* **登记表本身就是清册**(与 `server/internal/repo/mail_status_readers_test.go` 的
* "文件 + 出现次数"同一个模板,只是这里的登记值是 0)。
*
* 消费侧一旦出现(有人按 px 选档位/设模糊半径)本条就红 —— 那是**必须停下来**的时刻:
* 那时要补的是"px ↔ 材质档位"的**映射判据**(见 CRITERIA.md §10 与计划文档 §7.12),
* 而不是把次数从 0 改成 1 了事。
*/
test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就得先补映射判据)', () => {
const files = [];
const walk = dir => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) { walk(p); continue; }
if (!/\.(ets|ts)$/.test(e.name)) continue;
files.push(p);
}
};
walk(HARMONY_ETS);
/*
* 豁免**按文件登记 + 写理由**(不是"凡是这几个目录都放行"):这三处是**搬运/传输**,
* 不是消费 —— 判据要挡的是"有人拿这个值去决定画什么"。
* 与 `server/internal/repo/mail_status_readers_test.go` 的豁免同一个形状。
*/
const plumbing = new Map([
['model/Appearance.ts', '域模型:clamp(0,40) 后存下 bgBlur(搬运)'],
['common/AppearanceStore.ets', '状态同步:与快照互转(搬运)'],
['api/AppearanceApi.ets:17', '线上 DTO 声明 bg_blur(传输格式,不是消费)']
]);
const consumers = [];
for (const f of files) {
const src = prose(f);
const rel = f.slice(HARMONY_ETS.length + 1);
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;
consumers.push(`${where} ${line.trim()}`);
});
}
assert.deepEqual(consumers, [],
`有人在消费 bgBlur 了 —— 停下:那时**必须**先补"px ↔ 材质档位"的映射判据` +
`(CRITERIA.md §10 / 计划文档 §7.12),而不是把登记值从 0 改成 1:\n ${consumers.join('\n ')}`);
});

View File

@ -105,3 +105,29 @@ test('深色档必须与浅色档不同("深色没换色" → 红)', () => {
`${id} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`);
}
});
/*
* ★ 静默兜底必须留下**可观测的痕迹**(pi 2026-09-14)。
*
* §10 登记了"未知 id 静默换成 aurora"这条缺省语义,但**登记只防"被误报成 bug"**,
* 防不住"没人知道它正在发生" —— 真实环境里它不留任何痕迹。所以返回值里带
* `presetSubstitutedFrom`:换过就是原来那个 id,没换过是空串,页面据此打一行日志。
*
* 这条判据同时钉住两件事:① 替换**必须**留痕;② 没替换时**不许**留痕
* (否则痕迹会退化成噪声,等于没有)。
*/
test('★ 静默兜底留痕:换过要带出原 id,没换过必须是空串', () => {
const unknown = 'ocean-v2-这个档还不存在';
const bad = W.resolveBackground('preset', unknown, 0.2, false, false);
assert.equal(bad.presetId, 'aurora', `自检:未知 id 应当被换成默认档(实际 ${bad.presetId})`);
assert.equal(bad.presetSubstitutedFrom, unknown,
'认不出的 id 被静默替换了,却没留下痕迹 ⇒ 版本偏移时无法确认它在发生(只能猜)');
for (const id of IDS) {
for (const dark of THEMES) {
const plan = W.resolveBackground('preset', id, 0.2, false, dark);
assert.equal(plan.presetSubstitutedFrom, '',
`${id}(${themeName(dark)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`);
}
}
});

View File

@ -66,11 +66,11 @@ const SUITE = [
['test/cross-client-theme.test.mjs', [], 15],
// 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。
// 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。
['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4],
['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 5],
['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'], 24],
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 25],
// P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位
['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6],
// 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号

View File

@ -220,6 +220,23 @@ export class BackgroundPlan {
/** 用的是深色那套色板吗(跟着主题走,不是"未验") */
dark: boolean = false;
presetId: string = '';
/**
* **静默兜底的痕迹**(pi 2026-09-14 裁定):服务端/WebUI 先加了新档、而这台设备是旧构建时,
* `normalizePreset` 会把认不出的 id **静默换成 aurora** —— 用户以为选的是新档。
* 这条缺省语义已登记在 `CRITERIA.md` §10,但登记只防"被误报成 bug",
* **防不住"没人知道它正在发生"**(真实环境里不会留下任何痕迹)。
*
* 所以把"发生过替换"记在这里:换过就是**原来那个 id**(非空),没换过就是空串。
* 页面拿到非空值时**打一行日志**(`hilog`/`console.info` 皆可)——
* 于是这个后果从"可能发生"变成"**可数**":出问题时能立刻确认是版本偏移,而不是猜。
*
* ⚠️ 为什么痕迹记在**返回值**里、而不在这一层直接打日志:这一层是**纯逻辑**
* (无 `@ohos` 依赖 ⇒ 判据能用 node `--experimental-strip-types` 直接跑它,
* 见 `harmony-presets.test.mjs`)。为了打一行日志而引入 `@ohos.hilog`,
* 会把"能真跑的行为判据"换成"只能读源码的形态判据" —— 那是不划算的交易。
* 记事实在这一层(可判),打日志在页面层(可观测)。
*/
presetSubstitutedFrom: string = '';
layers: PresetLayer[] = [];
/**
* 遮盖浓度(0~1):**两档都用**(预设档也压),用系统遮罩色刷一层。
@ -251,6 +268,8 @@ export function resolveBackground(bgKind: string, presetId: string, scrim: numbe
if (bgKind === 'preset') {
plan.kind = 'preset';
plan.presetId = normalizePreset(presetId);
// 留痕:认不出的 id 被静默替换时,把**原来那个 id**带出去(页面据此打一行日志)
plan.presetSubstitutedFrom = plan.presetId === presetId ? '' : presetId;
plan.layers = layersFor(plan.presetId, dark);
// 预设档**也**套遮罩:与 WebUI 的 `--bg-dim` 一致(它不区分档位),服务的是可读性
plan.scrim = scrim;