Commit Graph

3 Commits

Author SHA1 Message Date
d25770ea2f fix(欠账): Skip 进余额且条件必须是测量;豁免按文件+次数;三笔欠账合成一处可读余额
pi 2026-09-14 三条(他接受了我对"恒红=相位错"的反驳,但指出 Skip 带来的两处漏洞)。

1. **Skip 必须进余额、条件必须是测量**:
   - **条件**:跳过与否由 `measureMailStatusDebt` **实测**(详情路径的 status 是否真的
     等于按读者派生),不是常量、不是"我们还没迁完"这种没人会更新的事实;
   - **余额**:`docs/DEBTS.json` 是**唯一登记**,Go 侧判据 `TestDebtLedgerMatchesMeasurement`
     **自己测量**后与登记比对(第一版我让余额由另一条判据写入 ⇒ **排序依赖**,
     Go 同包内按源文件顺序跑,登记那条先跑就读到 0 —— 排序依赖是隐蔽的假绿,已抽成自足函数);
   - **可见性**:`go test` 跑通时**不打印包的输出**,我第一版把余额打在 TestMain 里,
     常态运行一个字都看不见 —— 正是 pi 说的"不显形"。所以常态可见的那份打在
     electron 套件的 RESULT 行:`RESULT phase=install static=5 debts=7
     (static-criteria:5,mails-status-derived:1,gesture-semantics:1) probe=ok`。

2. **豁免从"按文件"改成"按文件 + 次数"**:`migrate.go` 这类比较**上限 2 处**(附理由),
   多一处即红。我在读侧清册上自己修过这个洞,豁免那格却退了一格 —— pi 指出得对。

3. **三笔欠账合成一处**:原先各自表达(`RESULT static=5` / `t.Skip` 无余额 /
   文档里的到期前提无余额),**没有一处能一眼看全**。现在统一登记在 `docs/DEBTS.json`
   (id / 余额 / 到期前提 / 判据位置),两端读同一份:Go 侧比对实测,electron 侧打进 RESULT 行。
   还清那天:登记要跟着清 —— 不清则由 `TestDebtLedgerMatchesMeasurement` 报
   "**欠账已还清**,但登记还记着 N"(还清是可测事件,这正是那条判据存在的意义)。
2026-09-14 17:17:37 +08:00
eeb8f277fd test(mails.status): "正当"落成可判约束(只许比 archived);欠账补一条"还清即转绿"的判据
pi 2026-09-14 的三点接续。

1. **§1「正当」不能只是标注,要是可判的约束**:按文件+计数登记挡得住"新增一处",
   挡不住"**已登记的那一处改变性质**"。已判出来:**任何与 m.status 的比较只允许比
   'archived'**(全局属性),与 'read'/'unread' 比较 ⇒ 红(per-reader 属性必须走 mail_reads)。
   第一次跑就抓到第三处:`migrate.go` 的 `WHERE m.status = 'read'` ×2 —— 它是**回填**,
   职责就是读那个遗留值,所以按文件登记**窄豁免**(allowedCompareExempt,带理由),
   而不是"凡是迁移都放行"。

2. **§2 欠账要有"还清即转绿"的判据**:新增 `mail_status_derived_test.go`,三态形状
   (与套件探针同族)—— 欠账未清 ⇒ `t.Skip` 并写明还欠什么(**不假红也不假绿**);
   还清 ⇒ 开始实跑并断言"详情路径的 status == 按读者派生的值",**转绿即可测事件**;
   迁一半(只改 GetMailByID 漏 GetThread)⇒ 红。
   实测当前状态:`--- SKIP ... 详情路径返回行级 status="read",而按读者派生说 "unread"`。
   **没写成"现在就红"**:恒红的门 = 挂在错误相位的门(CRITERIA.md §11 已为它付过学费)。
   另按他的更正把 `migrate.go` 的理由写准:不是"跑过即结束",而是
   **"全新安装/空库路径必须存在"**(措辞会影响下一个人敢不敢删它)。

3. **§3 (b) 的语义契约补归属**:归属 = dsh(鸿蒙侧实现者),WebUI 侧不需改动;
   落点 = 跨端对齐判据那一族(形状与"预设清单逐项一致"同构,比语义不比数值);
   **如实标注当前未建**及原因(鸿蒙侧还没有日历页/手势代码 ⇒ 此时"两端逐项相同"只有一端存在,
   建出来就是假判据);到期前提 = P6 第 3 步出现手势代码时立即建,此前是**登记的欠账**。
2026-09-14 17:11:36 +08:00
b7f624bc45 fix(已读/手势): 读侧也拒绝空读者;mails.status 读者/写者登记成"新增即红";阈值契约显式选 (b)
pi 2026-09-14 的三条裁定,逐条落地。

1. **§2 读侧守卫**(他只钉了写侧,读侧是另一半,而且更隐蔽):`reader` 在查询里是**过滤条件**,
   空串不写坏数据也不报错,只会**算出一个错误的数** —— `unreadFor('')` 的
   `NOT EXISTS(... reader_name = '')` 恒真 ⇒ `CountUnread(ctx,"")` 把**所有**邮件算成未读
   (用户看到"全都没读"),`ListInbox(...,"read")` 恒空。
   二选一里选 **①报错**(当前没有任何调用方需要"汇总"语义;选②就要立刻定义"汇总"是什么,
   而那是个还不存在的需求 —— 将来要就新增一个名字里带汇总的函数,别让空串偷偷兼职)。
   `requireReader` 装到 `ListInboxScoped`/`CountUnread`/`CountUnreadInSession`,
   判据两条:空 reader 三个入口都**必须报错**且**不返回数**;正例防止写成"一律拒绝"。
   变异验证:撤掉 `CountUnread` 的守卫 → `CountUnread("") 必须报错,实际返回 0`。

2. **§1 `mails.status` 收口**:先做他要求的第 1 步(枚举读者,他没有 shell)。
   枚举结果:**没有**"零功能读者"那条路 —— 行级值仍会进邮件 JSON(`GetMailByID`、
   `GetThread` 都 select 它),`migrate.go` 的两处是**一次性回填**(正当),
   `unreadFor`/`readStateFor` 只用它判 `archived`(正当,且已注明"不再作为未读判据")。
   所以走第三步:**登记欠账 + 增量判据**。
   - 「只为兼容保留」**没有**当结语:`mail_status_readers_test.go` 是一张**机器检查的清册**,
     按**文件 + 次数**登记(repo.go 13 / thread.go 1 / migrate.go 4),
     **新增一处读取就红**并强制当场回答"这处合不合规";写入点同样登记
     (多一处 `UPDATE mails SET status` 就红)。
   - **第一次跑就抓到第二处写入**:`MarkAllInboxReadForSession`(整会话批量已读)里还有一句
     `UPDATE mails SET status='read'`,我原先只看到 `MarkMailRead` 那一处 ——
     这正是"新增即红"的价值:靠人 grep 会漏,靠判据不会。
   - 收口路径(写进判据注释):行级 `'read'` 迁到 `markReadFor`;
     但在"详情/线程仍返回行级 status"两处读者迁移**之前**不能只删写入
     —— 那会让 JSON 里的 status 永远是 unread,是另一种错误事实。
   变异验证:在已允许的文件里新增一处读取 → 清册对不上,红。

3. **§3 阈值契约显式选 (b)**:不要求数值一致(`40px`/`600ms` 在触摸屏与鼠标、手机与大屏上
   的人体工学合理值天然不同,强行同值会两边都不舒服),契约改钉**语义层**(左滑/右滑=什么、
   边界是否回弹、"快滑"是感知档)。并写清推论:选了 (b) ⇒ **鸿蒙不得引用 WebUI 的三个数**
   (引了就等于偷偷选了 (a),还是"我抄的那一份"那种)。
2026-09-14 17:08:31 +08:00