{ "_": [ "欠账的**单一登记**(pi 2026-09-14 裁定 §3):三笔类型不同、但必须能一眼看全。", "为什么要一个文件:三笔原先各自表达(RESULT static=5 / t.Skip / 登记在文档里的到期前提),", "没有一处能看全 —— 而『欠账不显形,就等于没有』;分散在多处的登记,审计时只会被找到一处就当全部。", "两端都读这个文件:Go 侧判据断言自己的条目与**实测**一致(不许留一份手写的数字),", "electron 套件把它打进 RESULT 行(那是常态可见的位置)。", "已结算(2026-09-14):calendar-today-recompute —— P6 第 1 步(日历页 pages/CalendarPage.ets)落地,today 走 `@Prop @Watch('onVisibleChanged') visible` 在 pane 变可见时重算(另有 aboutToAppear 覆盖重新挂载),判据 test/harmony-calendar.test.mjs 的「★ today 在 pane **变可见时**重算」。结算即从此清单移除,余额里不再计这一笔。", "已结算(2026-09-19):gesture-semantics —— P6 第 3 步(左右滑动翻页)落地:鸿蒙侧 `CalendarPage.ets` 加了 `PanGesture`、判定逻辑在纯逻辑层 `model/Calendar.ts` 的 `judgeSwipe`。按该条自己的口径(「鸿蒙侧出现滑动手势代码时**立即建**」)同步建了语义契约判据 `test/cross-client-gesture.test.mjs`(8 条)—— 按 (b) 口径钉**语义**不钉数值:左滑=下一段/右滑=上一段/纵向优先/快滑窗口/手势与按钮共用同一翻页函数/无边界回弹/有意差异被记录/方向写反必红。另在 `harmony-logic.test.mjs` 加了行为判据(跑 `judgeSwipe` 的四道门)。结算即从此清单移除,余额里不再计这一笔。", "2026-09-19 新增 wide-breakpoint-divergence:两端断点不同且含义不同(审计发现,此前无记录)。它不是\"已确认的缺陷\",而是**未决的口径** —— 登记它是为了让「两者不同」这个事实本身可见,而不是让它藏在两处代码里。", "2026-09-19 新增 mail-list-attachment-count:邮件列表的附件数两端都拿不到(WebUI 那段是死代码)。鸿蒙侧只补了有真数据的「抄送 N」。", "2026-09-19 新增 harmony-maildetail-missing-three:邮件详情页缺三块功能(改名建议 / 转发 / 预算编辑),服务端都已支持。" ], "debts": [ { "id": "static-criteria", "count": 5, "due": "本工作区能装、能点设备(探针三值转 true 时自动变红)", "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(2026-09-19 起 harmony-admin 那条已升级为设备判据、移出名单 ⇒ 6 → 5:test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs、test/harmony-imageprep.test.mjs)", "kind": "scope", "note": "2026-09-19:`harmony-admin` 的条目**已升级**(管理页真的能打开、列表真的渲染出用户行)并移出 STATIC_ONLY ⇒ 本笔 6 → 5。剩下的五次升级按\"每条缺什么设备侧验证\"逐条来,不为了把数字消成 0 而凑 —— 凑出来的设备判据只是把\"没验\"换成\"假装验了\"。\n\n2026-09-19 再更新:`cross-client-theme` **升级了一半** —— 新增一条设备判据(读悬浮球像素:品牌色真的画成 `#2563EB`;且球上图标与底色 WCAG 对比度 ≥3:1),并顺手钉了一条静态防线(`Theme.surface` 不得当代的前景色)。**它仍留在 STATIC_ONLY 名单里**,因为 `.ets` 那半只有悬浮球这一处上了设备 —— 其余令牌仍是静态对齐。「一半」要写出来,不能让名单看起来像没动过。" }, { "id": "mails-status-derived", "count": 1, "due": "详情/线程改为按读者派生(readStateFor)之后 —— 那时 mail_status_derived_test.go 从 Skip 转实跑", "where": "server/internal/repo/mail_status_derived_test.go", "kind": "scope" }, { "id": "observability-output", "count": 1, "due": "页面层(MainPage.ets)接上「读 presetSubstitutedFrom 并打一行日志」时;那一步同时补判据『读侧恰好出现 1 次且在日志调用里』", "where": "尚无判据 —— 这正是欠账的一部分(P6 第 1、2 步动 MainPage.ets 时一起做);形态判据在位:test/harmony-appearance.test.mjs(bgBlur 消费侧计数)", "kind": "scope" }, { "id": "unknown-preset-approval", "count": 1, "due": "有人对上表那格**追认或驳回**「未知 id 显示 aurora 而不是空白」这个方向时(我作为实现者不能自己追认自己)", "where": "client/electron/test/CRITERIA.md §10 的『未知的预设 id』行(现为『无人类批准』)", "kind": "env" }, { "id": "overlay-follows-app-theme", "count": 1, "due": "上设备后**翻转一次 colorMode**(应用深色 / 系统浅色),断言**解析出的遮罩值跟着「应用」主题变、而不是跟「系统」**;真机若证伪,正确修法是「遮罩从应用主题派生」,不是回到双常量", "where": "client/harmony/entry/src/main/ets/common/Theme.ets:58-79 的注释(机制依据:AppearanceStore.applyTheme → app.setColorMode)——**注释不是判据,所以进余额**", "kind": "env" }, { "id": "nav-dark-route-b-unguarded", "count": 1, "due": "**引入深色主题(或第一次给导航组件加 `dark:` 变体)时**必须一并堵;堵法按**文件窄豁免**写,不许写成「导航目录不许出现 dark:」(`bg-chrome-600` plain 档徽标那个先例我踩过一次)", "where": "client/electron/test/background.test.mjs 两条反向断言 —— 这是**已知未覆盖的回滚路径**(不是未验的运行时性质):`.dark .nav-rail{}` 选择器作用域与组件 `dark:` 变体,变异确认过都会逃掉", "kind": "scope" }, { "id": "nav-blur-route-c-unguarded", "count": 1, "due": "**第一次给导航元素加工具类模糊(Tailwind `backdrop-blur-*`)时**堵", "where": "同上文件 —— **已知未覆盖的回滚路径**:元素级 `backdrop-blur-lg` 不在那条选择器下,变异确认过逃得掉", "kind": "scope" }, { "id": "boundary-vocabulary-incomplete", "count": 1, "due": "**由外部读者报告时**(自查机制对这一类结构性失明 —— 发现词表外说法的机制,正是看不见它的那个机制)。收到报告后:扩词表 + 登记该处 + 保留\"上一次是谁发现的\"。**没有内部触发器,这是这条递归的不动点**:无论词表多长、判据多严,总有一类盲区只能靠\"外面有人读了一遍\"", "where": "client/electron/test/debt-visibility.test.mjs(词表键控的盲区:**已知未覆盖**——词表是采样、不是完备)", "kind": "env" }, { "id": "redeploy-script-unguarded-steps", "count": 1, "kind": "scope", "due": "**下一次改 `deploy/` 下任一脚本时**必须一并堵(`redeploy-gateway.sh` 正在被另一条会话改 ⇒ 本条目就是给它接手时的入口)。堵法:给每个副作用步骤加 `|| { bad …; exit 2; }`,或在脚本上开 `set -e`;两者都要与既有的 2=环境 / 1=检查 约定对齐。", "where": "`deploy/redeploy-gateway.sh:84` 的 `run \"cp -r '$REPO/client/electron/dist/.' ...\"` —— 脚本只有 `set -uo pipefail`(**无 `-e`**),`run()` 内部 `eval` 的失败既不中断也不被调用点接收 ⇒ 前端产物没拷进去也继续往下走。同类已在 `deploy/redeploy-plugin.sh` 修掉(2026-09-14):那次的实测形状是 `mkdir`/`cp` 被拒后仍打出 `[ OK ] 已拷入 node_modules`,再打出 `[FAIL] staging 里没有入口` —— **一段输出里两个矛盾信号,且 OK 在前**。该文件现已在 `mkdir`/`cp`/`node_modules` 三处判失败并 exit 2。" }, { "id": "deploy-space-prefix-fs", "count": 1, "kind": "判据铺得不满(不是新列)", "due": "下一次因空间问题失败时;或有人愿意补一行 df 时", "where": "deploy/lib/env-defaults.sh ②b 只判了 $TMPDIR,没判 $PREFIX 所在的文件系统" }, { "id": "pi-bridge-adopt-cwd-mismatch", "count": 1, "kind": "沙箱 rw 与 worker 实际 cwd 的第三个来源未对齐(同类已修两处,剩接管路径)", "due": "下一次动 pi 桥的会话装载 / `session-scan.mjs` 时;或有人愿意把「这一轮用哪个 cwd」完全收成父进程一处决定时", "where": "`plugins/pi-mail-bridge/src/worker.mjs` 的**接管会话**分支:worker 用会话文件 header 里的 `info.cwd`(`resolveWorkspaceCwd(info.cwd || data.to_workspace, …)`),而父进程(`src/pool.mjs` → `src/turn-cwd.mjs`)只能从 `state` 里拿 `{sessionFile, cwd}`,**读不到 header** ⇒ 接管的首回合 rw 仍可能不含 worker 真正要写的目录。父进程要拿 header 得用 `src/session-scan.mjs`,但 `readHeader` 未导出、整表 `scan()` 在父进程里代价大(worker 里实测 1431ms / 堆瞬时 240MB,见 worker.mjs 里那段注释)。★ 这类错位的**特征是没有提示**:EACCES 落在「界内」,读日志的人会以为沙箱装错了(不像 `ask` 还有一次问)。修法方向:把「这次用哪个 cwd」收成父进程一处决定(它已有 `state.sessionFile`,header 也可读),worker 只消费、不再自己推导 —— 即 `src/turn-cwd.mjs` 头注释里写的「三来源变一来源」。" }, { "id": "deploy-interrupt-trap-other-scripts", "count": 1, "kind": "只在 redeploy-gateway.sh 做了,另两个部署脚本没做", "due": "下一次动 redeploy-plugin.sh / install.sh 时", "where": "只有 redeploy-gateway.sh 有 INT/TERM/HUP trap;install.sh 与 redeploy-plugin.sh 在写系统目录期间被打断同样会留半成品(它们没有\"服务停着\"那种后果,所以优先级低)" }, { "id": "harmony-p4c-boundary-decls", "count": 4, "due": "本工作区能装、能点设备 —— 那时这几条静态判据里被替代掉的那些断言换成真机断言,声明随之减少", "where": "client/electron/test/harmony-admin.test.mjs、client/electron/test/harmony-imageprep.test.mjs", "kind": "scope", "note": "2026-09-19 更新(设备可用了,开始逐条升级):① `harmony-admin` 的那条**已升级为设备判据**(管理页真的能打开、列表真的渲染出用户行)⇒ 从本组减 1。② `harmony-imageprep` **只做了一半**,如实记着:新加的设备判据用真实素材(用户真上传的那张壁纸 1402×1122 / 152570 字节,从 GET /me/appearance/image 取回)验了「设备上读到的尺寸/体积与压缩决策的输入对得上」;**未做**「应用真的用 image.createImagePacker() 压一次、产出字节落在预期区间」——那需要一个**用户选图**入口(DocumentViewPicker,要人操作系统选择器),自动化里没有稳定路径。编一个绕过选择器直接调 packJpeg 的测试专用入口,会是只有测试在用的代码 —— 那种代码不会被真实场景触到,验它等于验一个不存在的东西。⇒ 剩 4 条里这一条要等一个**真人操作**的验证窗口(或平台提供可注入的选择器)。" }, { "id": "deviceprobe-fixture-timing", "count": 1, "due": "把两份 fixture 变成**当场采集**(跑 hdc dump 取现场)而不是人工存文件时,这条就到期", "where": "client/electron/test/harmony-deviceprobe.test.mjs(两处提到「未验」的断言 + fixtures/aa-dump-l-*.txt)", "kind": "scope" }, { "id": "wide-breakpoint-divergence", "count": 1, "due": "决定是否统一:鸿蒙 768vp vs WebUI 1024px(**不是 bug,是未决的口径**)", "where": "client/harmony/entry/src/main/ets/pages/MainPage.ets(isWide 的 onAreaChange,>=768) 与 client/electron/src/hooks/useIsNarrow.ts(NARROW_QUERY = max-width: 1023px)", "kind": "scope", "note": "两端断点不同,且**含义也不同**,所以数值不同本身不算错:WebUI 的 1024 是「三栏(60 导航 + 320 列表 + >=520 详情 ≈ 900px,再加余量)放不下就退化单栏」,鸿蒙的 768 是「要不要显示侧栏」(鸿蒙没有并排的列表+详情三栏,它的内容区是一个窗格,所以 768 就够)。但**用户可见的后果**是:在 768–1023 宽(常见竖屏平板、窄窗口)下,WebUI 是单栏 + 底部导航,鸿蒙是侧栏 + 内容 —— 两台设备上同一宽度长得不一样。2026-09-19 由审计发现(此前**没有任何地方记录**这件事,连「两者不同」这个事实本身都没写下来,所以下一个人只会当成漏改)。待定:① 统一到 1024(鸿蒙跟 WebUI);② 统一到 768(WebUI 跟鸿蒙,但要重新论证三栏是否真能在 768 放下);③ 承认它们是两件事、把语义差异写进文档(那就该给两端各自的名字,而不是都叫 wide)。" }, { "id": "mail-list-attachment-count", "count": 0, "due": "已修(2026-09-20):字段确实返回,两端都可接", "where": "server/internal/handler(邮件列表的响应构造) —— 客户端侧见 client/harmony/entry/src/main/ets/pages/MainPage.ets 的 MailItem", "kind": "scope", "note": "★★ 2026-09-20 **更正**:当初这条的结论是**错的**,根因是它把一个`omitempty` 造成的**字段缺失**当成了「服务端不返回这个字段」。\n\n原文(留档,别再犯):实测 `GET /me/mail/inbox` 的回包字段列表里「既没有 `attachments` 也没有 `has_attachments`」⇒ `MailList.tsx:261` 的 `mail.attachments?.length ?? 0` 恒为 0、那个 📎 在 WebUI 上从不出现(死代码)。\n\n事实:服务端 `GetInbox`(`mail.go:586`)**明确调了 `fillAttachments`**,并写了理由:「Agent 靠收件箱列表得知有哪些附件可下载,否则它不知道该调 attachment_id」。而 `Mail.Attachments` 的 json tag 带 **`omitempty`** —— **没有附件的邮件根本不输出这个 key**。\n我当初是**只看了一封没附件的邮件**就下了全称结论。\n\n2026-09-20 实测(`limit=200`,96 封):带 `attachments` 的 **2 封**,都是真有附件的那两封。⇒ **WebUI 那个 📎 不是死代码**,鸿蒙当初「有意不抄」的前提不成立。\n\n★ 教训:`omitempty` 字段的**缺失**不等于「服务端不返回」。判「某字段有没有」必须拿**确实有值的那条**去验,而不是拿一条恰好为空的数据。这与同一天那个真崩溃(`session_workspace` 的 `omitempty` → 客户端 `undefined` → 白屏)是**同一个坑的两面**。" }, { "id": "harmony-maildetail-missing-three", "count": 0, "due": "鸿蒙详情页对齐 WebUI 时(三块功能,服务端都已支持)", "where": "client/harmony/entry/src/main/ets/pages/MailDetailPage.ets (对照 client/electron/src/components/MailView.tsx)", "kind": "scope", "note": "2026-09-19 审计发现:邮件详情页缺**三块 WebUI 有的功能**,而且**服务端三块都已支持**(不是做不了):① `RenameProposalBar`(MailView.tsx:324)—— Agent 建议改会话别名,人确认/驳回。服务端 `server/internal/handler/rename_proposal.go` 已在解析 `propose_alias` 标记并把提议从正文剥掉。**这条对寻址稳定性很重要**(WebUI 那里的注释:「Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效」)。② `ForwardBar`(MailView.tsx:378)—— 转发。服务端 `forward.go` 完整(含 `forwardSubject` 的 Fwd: 叠加处理)。③ `BudgetEditor`(MailView.tsx:235)—— 会话往返预算编辑(`max_rounds`,服务端 `forward.go:304` 有此字段)。三条都不是顺手能补的量级(各含交互 + 接口 + 状态),所以先登记,不在审计那一批里硬塞 —— 硬塞的结果是每条都半成品。\n\n2026-09-19 结算:三块**都做完了** ——\n① 转发(commit cc7ff25,`ForwardMailRequest` + 弹层;修了两个真 bug:两个动作球几乎完全重叠、弹层没高度导致键盘一弹按钮被顶出屏幕);\n② 会话改名建议(commit c26e857 接口 + fe4342a UI,端到端实测:服务端识别 rename_proposed → 建议条出现 → 点接受 → 库里别名真的改了);\n③ 往返预算(commit 4e84369,GET/PUT 契约用 curl 逐条实测)。\n★ 未验:预算条的**点击**在设备上没走通 —— 模拟器顶部 155px 是系统手势区,而折叠头部恰在其中(坐标式点击会被系统抢走)。数据链路已实测,观感没验。" } ] }