From 839089064674a8f41d3bf14e70e0da873a64ec8f Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 17:46:20 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20feat(=E5=AF=B9=E9=BD=90):?= =?UTF-8?q?=20=E5=8F=82=E7=85=A7=E7=89=A9=E7=89=88=E6=9C=AC=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=EF=BC=88CalendarView.tsx=20@d78f19f=EF=BC=8C=E5=8F=98?= =?UTF-8?q?=E4=BA=86=E5=8D=B3=E7=BA=A2=EF=BC=89+=20=E5=9C=86=E8=A7=92?= =?UTF-8?q?=E6=8C=89=E8=AF=AD=E4=B9=89=E9=85=8D=E5=AF=B9=EF=BC=88=E6=95=B0?= =?UTF-8?q?=E5=80=BC=E6=9D=A5=E6=BA=90=E4=B8=8D=E5=90=8C=E5=8F=A6=E8=A1=8C?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-14 骨架开工前两件。 1. **对齐参照物要有版本号**:WebUI `CalendarView.tsx` 可能同时在动(gui-lab 有未合入改动), 照工作副本画完之后参照物一变,这版就成了"照一份没人认领的草案对齐的",而**没人能判它对不对**。 新增 `docs/ALIGN-REFS.json`(blob 哈希 + 登记于哪个 commit + "以哪次为准") + 判据 `test/align-refs.test.mjs`:哈希变了即红,报错按 §14 写明 「正确修法 = 读差异→判断骨架要不要改→再更新登记」与 「最常见的错误修法 = 把新哈希抄进去(那是把闸门降级成状态记录)」。变异确认会红。 当前登记:`d78f19f`「日历页面补上圆角」,工作副本干净 —— 若另有未合入的,合入后再对一次。 2. **圆角必须走令牌,且跨端按语义配对**:鸿蒙侧已有 `Theme.ets:105/107` 的 `radiusCard`/`radiusControl`(**系统**资源),WebUI 是 `--radius-card: 0.875rem` / `--radius-control: 0.5rem`。⇒ 端**语义对齐、数值来源不同**,所以登记写成"按语义配对", 并且判据要求每条都写明"按语义还是按数值"(否则下一个人会直接去比数字)。 数值差异本身按形态差异进余额:`radius-card-numeric-divergence`(等设备并排看再决定以谁为准)。 鸿蒙侧不许把 14 / 0.875 抄成裸数字 —— 与当初 14 处 Material 调色板清零同一形态,量纲换成长度。 --- client/electron/test/align-refs.test.mjs | 66 ++++++++++++++++++++++++ client/electron/test/run-all.mjs | 1 + docs/ALIGN-REFS.json | 33 ++++++++++++ docs/DEBTS.json | 7 +++ docs/DEV-TOOLING.md | 43 ++++++++++++++- 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 client/electron/test/align-refs.test.mjs create mode 100644 docs/ALIGN-REFS.json diff --git a/client/electron/test/align-refs.test.mjs b/client/electron/test/align-refs.test.mjs new file mode 100644 index 0000000..dbd2b0f --- /dev/null +++ b/client/electron/test/align-refs.test.mjs @@ -0,0 +1,66 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { prose } from './lib/read.mjs'; + +/* +★ 对齐参照物必须有**版本号**(pi 2026-09-14 §1)。 + +WebUI 的 `CalendarView.tsx` 可能同时在动(gui-lab 有未合入的改动)。照工作副本画完之后, +参照物一变,这一版就成了"照一份没人认领的草案对齐的" —— 而且**没人能判它对不对, +因为参照物没有版本号**。所以把参照物的 blob 哈希登记在 `docs/ALIGN-REFS.json`, +**它一变就红**,逼一次"重新核对再更新登记"。 + +报错按 §14 写"正确修法 + 最常见的错误修法":正确修法是**重新核对对齐**(读差异、改骨架、再更新登记), +**不是**把新哈希抄进去 —— 那是把这条判据降级成"记录当前状态",它就不再是闸门了。 +*/ + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); + +test('★ 对齐参照物的版本必须与登记一致(变了就重新核对,不是抄新哈希)', () => { + const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); + assert.ok(Array.isArray(reg.refs) && reg.refs.length > 0, '登记里至少要有一个参照物'); + + const drifted = []; + for (const r of reg.refs) { + const res = spawnSync('git', ['hash-object', r.path], { cwd: ROOT, encoding: 'utf8' }); + assert.equal(res.status, 0, `要能算 ${r.path} 的 blob 哈希:${res.stderr}`); + const now = res.stdout.trim(); + if (now !== r.blob) drifted.push({ id: r.id, path: r.path, registered: r.blob.slice(0, 12), now: now.slice(0, 12), at: r.atCommit }); + } + assert.deepEqual(drifted, [], + `对齐参照物变了,登记还是旧的:\n ${drifted.map(d => `${d.id}(${d.path}):登记 ${d.registered}(@${d.at})→ 现在 ${d.now}`).join('\n ')}\n` + + `**正确修法**:读一遍参照物的差异 → 判断骨架要不要跟着改(并说明改了哪里)→ 再把登记的 blob/commit 更新。\n` + + `**最常见的错误修法**:把新哈希抄进去就完事 —— 那是把这条判据降级成"记录当前状态",\n` + + `它就不再是闸门,"照一份没人认领的草案对齐"这件事会静默发生。`); + + // 每一条都要有出处(哪个 commit)与它对齐的是什么,否则"版本号"只是个数字 + for (const r of reg.refs) { + assert.ok(r.atCommit && r.atCommitSubject, `参照物 ${r.id} 必须写明登记于哪个 commit(否则无人能判断对齐目标)`); + assert.ok(r.note && r.note.trim(), `参照物 ${r.id} 必须写明"以哪次为准"`); + } +}); + +/* +★ 圆角也是"裸值"那一族:从 `index.css` 抄一个数字进 `.ets`,今天看起来一样, +等 WebUI 改一次半径,鸿蒙这边**没有任何东西会红**(与当初 14 处 Material 调色板同一形态, +量纲从颜色换成长度)。所以:**鸿蒙侧圆角必须来自 Theme.ets 令牌**, +且语义配对登记在 ALIGN-REFS.json 里(数值来源不同属于形态差异,另行登记)。 +*/ +test('★ 圆角走令牌:鸿蒙侧必须有 radius 令牌,且每一条语义都在登记里配了对', () => { + const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); + const theme = prose(join(ROOT, 'client', 'harmony', 'entry', 'src', 'main', 'ets', 'common', 'Theme.ets')); + assert.match(theme, /radiusCard/, 'Theme.ets 必须有卡片圆角令牌(不许在页面里写裸数字)'); + assert.match(theme, /radiusControl/, 'Theme.ets 必须有控件圆角令牌'); + + assert.ok(Array.isArray(reg.radius) && reg.radius.length >= 2, '圆角的语义配对要登记'); + for (const r of reg.radius) { + assert.ok(r.semantic && r.webui && r.harmony, `圆角条目必须写明语义、WebUI 侧与鸿蒙侧出处`); + assert.match(r.webui, /--radius-/, `WebUI 侧要指向变量(不是抄来的数字):${r.semantic}`); + assert.match(r.harmony, /Theme\.ets/, `鸿蒙侧要指向令牌出处:${r.semantic}`); + assert.ok(r.judgement && r.judgement.includes('语义'), `${r.semantic} 必须写明按语义对齐还是按数值对齐(否则下一个人会去比数字)`); + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index a6cc36d..52a4f28 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -77,6 +77,7 @@ const SUITE = [ ['test/appearance-defaults.test.mjs', [], 4], ['test/build-stamp.test.mjs', [], 7], ['test/packaging.test.mjs', [], 5], + ['test/align-refs.test.mjs', [], 2], ['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 10], ['test/debt-visibility.test.mjs', [], 1], ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2], diff --git a/docs/ALIGN-REFS.json b/docs/ALIGN-REFS.json new file mode 100644 index 0000000..5b81775 --- /dev/null +++ b/docs/ALIGN-REFS.json @@ -0,0 +1,33 @@ +{ + "_": [ + "对齐参照物的**版本登记**(pi 2026-09-14:对齐本身要有版本号)。", + "为什么需要:WebUI 那边同一个文件可能同时在动(gui-lab 有未合入的改动),", + "照工作副本画完之后,参照物一变,这一版就成了「照一份没人认领的草案对齐的」,", + "而且**没人能判它对不对 —— 因为参照物没有版本号**。", + "所以:把参照物的 blob 哈希登记在这里,**它一变就红**,逼一次「重新核对再更新登记」。" + ], + "refs": [ + { + "id": "calendar-view", + "path": "client/electron/src/components/CalendarView.tsx", + "blob": "03388112c4b5e114472f516380bc0b2238e6a9d9", + "atCommit": "d78f19f", + "atCommitSubject": "fix(webui): 日历页面补上圆角(圆角要给到「有底色的那一层」)", + "note": "P6 日历骨架的对齐目标。当前工作副本干净、最后一次改动就是上面那笔日历圆角修复(很可能就是 gui-lab 那笔)。**若另有未合入的改动**,以合入后再对一次为准 —— 那时本条必须重新登记(哈希变了就会红)。" + } + ], + "radius": [ + { + "semantic": "卡片圆角", + "webui": "--radius-card: 0.875rem(=14px,client/electron/src/index.css:278)", + "harmony": "Theme.ets:105 radiusCard = $r('sys.float.ohos_id_corner_radius_card')", + "judgement": "**按语义对齐,不按数值对齐**:两端都表示「卡片圆角」,但取值来源不同(WebUI 自定 rem,鸿蒙用**系统**资源)。数值是否一致属于形态差异,等设备上并排看再定 —— 见 docs/DEBTS.json 的 radius-card-numeric-divergence。**鸿蒙侧不许把 0.875rem/14 抄成裸数字**(那正是当初 14 处 Material 调色板被清零的同一形态,量纲从颜色换成长度)。" + }, + { + "semantic": "控件圆角", + "webui": "--radius-control: 0.5rem(=8px,client/electron/src/index.css:279)", + "harmony": "Theme.ets:107 radiusControl = $r('sys.float.ohos_id_corner_radius_button')", + "judgement": "同「卡片圆角」:语义对齐、数值来源不同。" + } + ] +} diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 08cba1f..311a6c6 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -76,6 +76,13 @@ "kind": "scope", "due": "**日历页面骨架写完后立刻**:`today` 必须在 pane **变为可见时**重算(Tabs 切换回调 / 页面可见回调,API 以 ArkTS 规范为准),**不许**只在 aboutToAppear 取一次。判据形式:同一页面实例推进一天后重新求值,`today` 必须跟着变", "where": "client/harmony/entry/src/main/ets/model/Calendar.ts 的纯逻辑判据(test/harmony-calendar.test.mjs)只钉到「喂进去的值对」;**喂进去的值会不会过期是调用侧的事**" + }, + { + "id": "radius-card-numeric-divergence", + "count": 1, + "kind": "env", + "due": "上设备并排看时:WebUI `--radius-card`=14px 与鸿蒙系统卡片圆角**是否视觉等价**;不等则决定「以谁为准」并登记成决定 —— 在此之前它是**已知的形态差异**,不是缺陷", + "where": "docs/ALIGN-REFS.json 的 radius 段(语义对齐、数值来源不同)" } ] } diff --git a/docs/DEV-TOOLING.md b/docs/DEV-TOOLING.md index 4f5fa71..9e756ff 100644 --- a/docs/DEV-TOOLING.md +++ b/docs/DEV-TOOLING.md @@ -77,9 +77,48 @@ pi-lens 在编辑文件后会走 smart-default 回退选择格式化器(见它 ```bash bash deploy/prune-deploy-artifacts.sh # 干跑,只报告 bash deploy/prune-deploy-artifacts.sh --apply # 真删(默认:网关留 3 份、每插件留 3 份快照) +bash deploy/prune-deploy-artifacts.sh --self-check # 判据自检(16 项,两侧都验) ``` -两条硬规矩:**保留回滚窗口**(发布纪律要求有回滚目标,所以不是全清);**绝不删正在使用的 -快照**(扫 /proc 的 cmdline 与 cwd,命中就跳过 —— 删掉它进程一重启就找不到自己的代码)。 +三条硬规矩:**保留回滚窗口**(发布纪律要求有回滚目标,所以不是全清);**绝不删正在使用的 +快照**(扫 /proc 的 cmdline 与 cwd,命中就跳过 —— 删掉它进程一重启就找不到自己的代码); +**在线数据库永不入列**。 + 注意那个"在用"判断必须排除**本进程及其祖先链**:调用方常把路径写在命令行里, 不排除就会出现"永远判为在用"(与 `pkill -f` 杀掉自己那条命令同一个坑,已写进脚本注释)。 + +### 四类残留与各自的窗口 + +| 类别 | 来源 | 窗口 | +|---|---|---| +| 网关旧二进制 `agentmail-gateway.bak-` | `redeploy-gateway.sh` | 保留最新 3 份 | +| 插件快照 `/` | `redeploy-plugin.sh` | 每插件保留最新 3 份,`current` 永远保留 | +| 数据库/附件**备份集** `backups/agentmail-.db` + `attachments-.tar.gz`、`data/agentmail.db.bak-` | `reset-demo.sh` 与早期手工留档 | 保留最新 1 集 | +| `/tmp/agentmail-pre-deploy-*.db`、`/tmp/agentmail-pre-prune-*.db` | `redeploy-gateway.sh`、`prune-test-sessions.sh` | 各保留最新 2 份 | + +同一个 `` 的库与附件包算**一个备份集**,一起进出窗口 —— 拆开留没有意义。排序按 +**文件名里的时间戳**而不按 mtime:09-02 的两份备份被 09-08 的一次"打开看一眼"改了 mtime, +按 mtime 排会把最老的判成最新的。**没有时间戳的文件一律不碰**(判定不了就不删)。 +`pre-prune` 那份是会话归档(不可逆操作)唯一的回滚点,所以它单独一条窗口、报告里也写明身份。 + +### 三条判据,以及它们各自防的那个错 + +1. **在线库不入删除清单**(`del()` 里的 `readlink -f` 比对):备份删错能重建,在线库删错回不来。 +2. **每个插件至少留一份"上一版"**:只有 `current` 时脚本报 `⚠ 无回滚目标`。它只报告、 + **不造快照** —— 造不出来的东西不该假装有。(2026-09-14 那次事故就是这么暴露的: + 四个插件各只剩 `current`,回滚目标没了。) +3. **`--self-check` 两侧都验**:干净样本该删的删、该留的留、在线库不动必须是绿的; + 把窗口外的备份换成**指向在线库的符号链接**,脚本必须**拒跑**(退出码 1), + 不是"删了才发现"。最后一条是"自检没有碰生产根":比对前后 `/opt/agentmail` 的清单指纹。 + +### 事故记录:自检把生产当成了沙箱(2026-09-14) + +`--self-check` 第一版用 `ROOT="$t" … bash "$0"` 传假根。脚本读的是 `AGENTMAIL_ROOT`, +于是这个前缀赋值被静默忽略,自检的 `--apply` 打在了**生产根**上,删掉一批回滚备份 +(3 个旧网关二进制、8 个插件快照、09-02 的两组备份集)。 +**为什么没被发现**:`PRUNE_TMP_DIR` 那一路的变量名是对的,所以输出的 `/tmp` 段看着"确实是假根", +干跑那一轮的报告也像模像样 —— 半对的状态比全错更难认。是 `bash -x` 里那行 +`ls -1t /opt/agentmail/…` 露的马脚。 +**改法不是"下次小心"**:① 传对变量名;② 自检的根目录必须在临时区,否则拒跑; +③ "自检不碰生产"进判据(前后指纹比对)。这条与 `pkill -f` 杀自己同类: +**安全装置自己出错时,产出的是一份看着正常的报告**。