docs(欠账): build-stamp 因产物陈旧而长期红 —— 并修掉两处 JSON 里的坏字节

## 新增 `build-stamp-stale-artifact-blocks-verification`(debts 33 → 34)

`build-stamp` 断言产物自报的 `gitRev` 精确等于 HEAD。实测产物记 `87c55ac`,
而 HEAD 随本轮工作走到 `f1c74fc` ⇒ **无论谁提交什么,这条判据都不会自己
变绿**,只能被一次重构建救。

★ `BUILD_INFO.json` 经 `git ls-files` 确认**未被跟踪** ⇒ 缺的那次构建
  从来没人提交过,不是被谁回滚。
★ 已在**干净 HEAD** 上复现 ⇒ 不是本轮引入(`f1c74fc` 之前就红)。

## 为什么值得单独记一笔:它会**训练人忽略红色**

套件汇总里 `build-stamp` 与真缺陷并列显示。真实的危害不是这条判据本身,
是人一旦习惯「哦又是 build-stamp」,就会把**同一行里的真缺陷一起放过**。
这与本仓反复消的「看不到 ⇒ 绿」是同一族,方向相反:
**看到了 ⇒ 当没看见**。而 `d3a7873` 那笔 HIGH 记的「判据说干净而构建物
不干净」正是它的成因 —— 本条是那笔债的**日常形态**:不危险,但会钝化。

到期动作只认一个:**重跑构建**。判据自己的报错文案已警告不要去改
`gitRev`/`srcHash` 了事(那是把它废掉),本条认同并把正确修法写进 `due`。
**未做**:没触发构建、没改 `BUILD_INFO.json`、没加进任何跳过名单 ——
构建是部署动作,而本工作区正被多个会话并发提交(见下)。

## 顺带修掉两处 JSON 里的坏字节(U+FFFD)

写新条目时自查发现 `docs/DEBTS.json` 里有 6 个替换字符:
① **我这次笔误**:「无论谁提交什么,这条判据」被写成 3 个 `\ufffd`;
② **前人笔误**(HEAD 里就有,已用 `git show HEAD:` 确认):`harmony-system-back-key`
   的「正确的那一个钩子」同样烂了 3 个字节。
两处都已修,`replacement chars: 6 → 0`,JSON 复验合法。

★ 这与 `d3a7873` 里记的 `python-probe-shadowing-in-tmp` 同源:
  改机器可读文件必须**立刻回读验证**。这次是回读时**顺带**发现的,
  说明那条判据的价值不在"校验格式",在"逼你去看一眼内容"。

## 验证

· `node test/run-all.mjs`:files=35 ran=35 checks=582 pass=571 fail=2
  skip=9(**不是通过**)red=2。红的仍是那两条:本次故意红的
  `cross-bridge-prompt` 与本条新登记的 `build-stamp`。
  `RESULT … debts=37` 已把新条目计入(static-only=5==登记 ✓)。
· `debt-visibility` 1 pass / `criteria-hygiene` 10 pass / `commit-hygiene` 4 pass。
· go 侧:除故意红的 notify 外 16 包全绿(须 `GOCACHE=.tmp/gocache`)。

## 共享工作树实况(`shared-workspace-unserialized-deploy` 持续发生)

`server/internal/repo/` 下现有**四个不属于本会话**的未跟踪探针
(`zz_toctou_` / `zz_proposedfix_` / `zz_correctedshape_` / `zz_naivevscorrected_`),
本轮又多了两个 ⇒ **同一包内并发写入正在进行**。本 commit 只 stage
`docs/DEBTS.json`,四个探针原样留在工作树未动。
This commit is contained in:
2026-09-28 10:19:51 +08:00
parent e7d303f6f6
commit c25ee1e97e

View File

@ -222,7 +222,7 @@
{
"id": "harmony-system-back-key",
"count": 2,
"due": "**真机**(或能观察到 Navigation 返回事件分发的环境)上验清楚:系统返回键在 `Navigation` 内部的消费点在哪一层,以及正确的那一<EFBFBD><EFBFBD><EFBFBD>钩子是什么",
"due": "**真机**(或能观察到 Navigation 返回事件分发的环境)上验清楚:系统返回键在 `Navigation` 内部的消费点在哪一层,以及正确的那一个钩子是什么",
"where": "client/harmony/entry/src/main/ets/pages/MainPage.ets(约 1700 行的 PopIntent 监听者)+ EntryAbility(无 onBackPressed)",
"kind": "bug",
"note": "2026-09-26 审查发现并**尝试修复失败**,整块撤回。\n\n## 未修好的两个具体问题\n\n① `KEY_COMM_STACK_DEPTH` 停在旧值:退到列表后,下一层\"返回\"输入被吞。\n② `KEY_OPEN_MAIL_ID` 留着上一封:回到列表按回车时以为\"还在详情\",会开**回复**而不是新邮件。\n (连带 `MainPage.closeDetail()` 是死代码 —— 它只被 Esc 那条路调。)\n\n## 为什么没修\n\n2026-09-26 在 `EntryAbility` 加了 `UIAbility.onBackPressed()` → `PopIntent.request()`。\n模拟器(HarmonyOS 6.1.1,HarmonyPhone)实测:\n\n- 应用在前台时按**一次**系统返回键 ⇒ **UIAbility 被销毁**(hilog 有 `HandleAppDied`,\n `aa dump -a` 里 EntryAbility 消失),`harmony-admin` 设备判据因此挂住不返回。\n- 对照实验:把 EntryAbility + ComposeIntent + MainPage 三处 stash 掉重编重装,\n 同一条判据 **31/31 通过**。\n\n⇒ `UIAbility.onBackPressed()` 在这条链上**没被调用到**(`Navigation` 自己先消费了\n返回事件),而改动 `PopIntent` 的回调签名又改变了判据侧的行为。\n\n## 为什么不在模拟器上继续查\n\n要验的是\"框架内部返回事件的分发顺序\"这类**框架行为**,而本机模拟器这一条\n又不能代表真机。与其赌一个**会关掉应用**的半成品,不如整块撤回、留成有\n复现步骤的账。\n\n## 试过并被否掉的三种接法(都写在代码注释里)\n\n1. `Navigation.onPop` —— **不存在**,编译报\n `Property 'onPop' does not exist on type 'NavigationAttribute'`。\n2. `pushPath` 第三个参数 —— **不接受**(只接受 1-2 个)。\n3. `NavPathInfo.onPop` —— 存在,但官方文档写明**只在 `pop()` 带了 result 时才触发**,\n 而系统返回键**不走** `pop(result)` ⇒ 照样漏。\n\n## 建议的下一步\n\n在 `NavDestination` 上挂 `onWillDisappear`(或 `onHidden`),在它里面统一做\n\"重发层数 + 栈空时清发布键\";这条是**每个详情页自己**的出栈时机,\n不依赖谁来分发返回事件 ⇒ 系统返回键、Esc、手势三条路都会经过它。\n需真机确认 `onWillDisappear` 在系统返回键下确实触发。"
@ -266,6 +266,14 @@
"due": "给 `inboundHeadline` / `replyInstruction` 的 `inReplyTo` 加上方向判据(父邮件的 from == 本方),或让服务端只在父邮件确由收件方发出时才填 `in_reply_to` 时。★ 到期动作不是'在提示词里写清楚'——本次已证明写清楚没用:四封通知的正文里已经逐字写明'回的是你那封:<id>',模型照样每封都去核一遍,然后照样被误导。",
"where": "`plugins/*-mail-bridge/lib/relay-policy.js:105-111`(`inboundHeadline`:`if (inReplyTo)` 直接出'你上一封信的回复到了');`plugins/zcode-mail-bridge/src/prompt.mjs:141`(`if (data?.in_reply_to) lines.push('回的是你那封:…')`,无方向判断);服务端 `server/internal/notify/mail.go:202` 把 `ParentMailID` 原样透传;实测线索 `stress-thread-21863-15348`(session aa2a600d,8 封全为 opencode→pi,层号跳过 3/6/9)",
"note": "★★ 2026-09-28 登记(pi 实测,opencode 复核)。\n\n## 形状:**单向 8 封被读成双向 4 轮**\n\n压测线索 `stress-thread-21863-15348` 里 8 封全是 `opencode → pi`,\n`read_thread` 逐层确认,pi 侧一封未发(唯一一次 `send_mail` 被\n'Agent 互发 8 封上限'拦下)。但四封投递通知各自宣称\n'回的是你那封:<上一封的 mail_id>' ——\n\n| 通知 | 宣称的父邮件 | 实际 |\n|---|---|---|\n| 层1 de4e212f | 6298f78a | 6298f78a 是 **opencode 自己的**信 |\n| 层2 e0e8b3c7 | de4e212f | 同上,仍是 opencode 的 |\n| 层4 6358cfc7 | e0e8b3c7 | 同上 |\n\n⇒ 通知里**不存在**一封是 pi 发出的。\n\n## 根因不是'通知乱序',是**方向被省略**\n\n`resolveTarget` 里 `reply_to` 被解析后 `parentMailID = &replyID`\n(`server/internal/handler/mail.go:88`),notify 原样透传(`notify/mail.go:202`)。\n**模型是对的**:父邮件 = 上一封 = 我上一封收到的。\n\n漏掉的是**方向判据**:`inboundHeadline` 只问'有没有父邮件',\n不问'这封父邮件是不是我发的'。单向续信也满足'有父邮件',\n于是一条纯单向的压测链被逐封判定成'对方在回我'。\n\n## 为什么第一封没被骗\n\n6298f78a 走的是真·新会话路径,`parentMailID == nil`\n⇒ `in_reply_to` 为空 ⇒ `inboundHeadline` 落到默认分支\n⇒ 显示'你收到一封新邮件'。**四个桥的同名字符串都在\n`relay-policy.js:111` 这一行**,改动会同时影响 dsh/zcode/opencode/pi。\n\n## 后果:撞 hop 上限,掩盖真实缺陷\n\n每被误判一轮,模型就'处理'一次并回一封,客套到上限被拦\n(生产实测 6 轮)。这次 8 封单向压测消耗的正是这份额度,\n把真正的缺陷挤出了视野。\n\n## 我做过的核对(避免重蹈 aab92f17 的归因错误)\n\n· 4 次 `read_inbox` 全空(unread 与 all 都空,`all` 返回的 8 封全标 `[archived]`);\n· `read_thread` 三次均为 8 封、全 `opencode → pi`、无任何 pi→opencode;\n· 逐封 `read_mail` 核对发件人与正文(正文是'层 N 的正文'占位);\n· 层号跳过 3/6/9,但 mail_id 序列连续 ⇒ **发送侧跳号,不是丢信**;\n· 本条根因定位所依据的行号均已回读原文确认。\n\n## 修法的一处取舍(尚未做)\n\n最小改动是在 `relay-policy.js` 加 `inReplyToFromMe` 判据,但那需要\n父邮件的发件人信息 —— 当前 payload **没有**这个字段(只有 `from_name`\n即本封发件人)。所以两个选项:服务端补一个 `parent_from` 字段,\n或插件侧用 `read_mail(parent_id)` 查一次。**前者更便宜且不用多一次往返**。\n\n★ 顺带记一笔:另有两个观察(`read_inbox` unread 视图与通知不一致、\n`session_participants` 回显把 path 段吞掉)**可能同源** ——\n都指向'通知/展示层没有回读真实数据',但**我没有查证**,不并入本条。\n\n## 2026-09-28 校正(pi自查,上条那节写得太快)\n\n上次写「修法:服务端补 `parent_from` 字段更便宜」——**方向对,但我没查证\n就下了结论**。现已回读确认,且**比原先想的更便宜**:\n\n`resolveTarget` 的 `reply_to` 分支(`server/internal/handler/mail.go:80-85`)\n**已经把父邮件整行 `repo.GetMailByID` 读进内存**了,只用了它的 `SessionID`\n就把 `mail` 丢掉;而 `models.Mail` 上就有 `FromName`\n(`server/internal/models/models.go:142`)—— 即判据需要的方向信息\n**在函数里现成,不需要任何一次额外查询或新 join**。\n\n⇒ 修法应当是:`resolveTarget` 一并返回父邮件发件人,notify 载荷带出去。\n**这不是\"补一个字段的成本\",是\"别把已经在手的数据扔掉\"。**\n上次那句把它说成新增成本,是我把话说满了。\n\n判据已就位:`client/electron/test/cross-bridge-prompt.test.mjs` 第 5 条\n(当前**故意红**,即本条债的判据)。"
},
{
"id": "build-stamp-stale-artifact-blocks-verification",
"count": 1,
"kind": "**产物自证长期红**:`build-stamp` 要求 `BUILD_INFO.json` 的 `gitRev` 精确等于 HEAD,而该文件**未被 git 跟踪**、内容长期停在某次旧构建上 ⇒ 这条判据只能靠「重构建」恢复,**任何代码改动都无法让它转绿**",
"due": "有人重跑一次客户端构建(产出新的 `BUILD_INFO.json`)时。★ 到期动作**不是**改 `BUILD_INFO.json` 里的 `gitRev`/`srcHash` —— 判据自己的报错文案就写明那样做等于把它废掉;也不是把它加进跳过名单;唯一正确的动作是重跑构建。",
"where": "`client/electron/test/build-stamp.test.mjs:93`(`★ 产物必须自报来源:BUILD_INFO 精确比对`);产物记录在 `client/electron/BUILD_INFO.json`(★ **未被 git 跟踪**);实测 2026-09-28 产物记 `87c55ac`、HEAD 已走到 `f1c74fc`",
"note": "★★ 2026-09-28 登记(pi 实测)。\n\n## 形状:**一条判据长期红,且与被测代码无关**\n\n`build-stamp` 断言产物自报来源必须精确等于当前 HEAD。实测:\n产物记 `87c55ac`,HEAD 当时是 `359cb43`、随后因本轮工作走到 `f1c74fc`\n⇒ **无论谁提交什么,这条判据都不会自己变绿** —— 它只能被一次重构建救。\n\n## 为什么值得单独记一笔:它是**共享工作树债的直接产物**\n\n同一个 `HEAD` 上(`shared-workspace-unserialized-deploy`):产物落后于源码,\n因为多个会话连续提交而**没人重跑构建**。`d3a7873` 那笔 HIGH 记的正是\n「判据说干净而构建物不干净」;本条是它的**日常形态** —— 不危险,\n但它会长期挂在套件的红名单上,让「红了」这件事开始钝化。\n\n★ 真正的危害不是这条判据本身,是**它会训练人忽略红色**:\n套件汇总里 `build-stamp` 与另两条真缺陷并列显示,人一旦习惯\n「哦又是 build-stamp」,就会把同一行里的真缺陷一起放过。\n这与本仓反复消的「看不到 ⇒ 绿」是同一族,方向相反:**看到了 ⇒ 当没看见**。\n\n## 已做的核对(避免把别人的问题算成新的)\n\n· 提交 `f1c74fc` 之前就在**干净 HEAD** 上复现过 ⇒ 不是本轮引入;\n· `BUILD_INFO.json` 经 `git ls-files` 确认**未被跟踪** ⇒ 缺的那次构建\n 没人提交过,不是被谁回滚;\n· 判据报错文案本身已警告「别去改 gitRev/srcHash 了事」 —— 本条认同,\n 并把那个正确修法(重跑构建)写进 `due`。\n\n## 未做\n\n没有触发构建、没有改 `BUILD_INFO.json`、也没有把它加进任何跳过名单。\n构建是部署动作,而本工作区正在被多个会话并发提交(已有两个\n`server/internal/repo/zz_*_probe_test.go` 不属于本会话)—— 由人决定何时构建。"
}
]
}