فهرست منبع

docs(ac15): 口径名改"SP 归因差异面" + Skipped 69 分解 + 活动链证据强度上限

只改文档,不动代码(go build ./... 无变化)。

一、§13.2 口径名与三个金额都订正
"差异面 88" → "**SP 归因**差异面 88"。那 11 个重合账户割接后结果**仍然不同**,
只是差异不再归因于 DECLARE 整数化、而归因于 3→2 位收敛。写成"差异面"会被读成
"这 11 个割接前后无差别",那是反的;归因不同决定了它是"我们不复刻"还是"迁移本来就要做"。

金额也订正(原来只给了 7 个的合计,且我口头说过"各 0.004"):
  7 个多扣侧逐个 = {0.001,0.001,0.002,0.002,0.004,0.004,0.004},合计 **0.018**(不是 0.028)
  4 个少扣侧逐个 = {0.004,0.004,0.002,0.004},合计 **0.014**
  |r2 − raw| 求和 = 0.032;带符号 SUM(r2 − raw) = **0.004**(两方向大部分抵消)
已核对:一条 SQL 出 11 行、0.032 / 0.004,随取随复现。
⚠ "7 个各 0.004" 又是一次"假设同类账户差额相同" —— 与本节开头的 4 位小数举例、
恒真判据同族,已写进该节的教训段。

二、§16 69 项 Skipped 的完整分解(各类相加 = 69)
total_recommended_amount 36 + C2 per-side 23 + 活动失效 8 + C1-5b 3 位原值 2。
每条都写"为什么在本 case 上无从验证",避免 69 被后来人读成"漏测 69 项"。

三、§17 活动链 9 支的证据强度上限(先天约束,三条实测事实)
1. target 活动域 schema 齐、数据空(5 张表全 0 行;legacy 90 / 1288)⇒ 活动侧只能靠种子
2. **9 支里只有 1 支碰资金表**(use_activity_recommend,14 处引用);其余 8 支资金表引用 = 0
   ⇒ 沿用 C1/C2 的 Δbalance/Δspecial/流水 那套机器对 8 支**完全不适用**,须新增活动域快照
3. 生产构型三档(PHP 仓 grep 调用点):A 档 5 支有调用、B 档 3 支封装方法 0 调用点、
   C 档 user_discount_usable 的 CALL 整行被注释(死代码,Helper:94)
   报告必须逐支标档位,不能笼统写"9 支全绿"。
Q 3 ساعت پیش
والد
کامیت
8ae4189e03
1فایلهای تغییر یافته به همراه109 افزوده شده و 19 حذف شده
  1. 109 19
      internal/academic/dualrun/stage2/README.md

+ 109 - 19
internal/academic/dualrun/stage2/README.md

@@ -834,41 +834,69 @@ _money   ← _balance
 166 + 95 + 37 + 268 = 566   ← 缺陷面
 ```
 
-再按**差异面**切一次(同一批账户,换一个判据):
+再按**「SP 归因差异面」**切一次(同一批账户,换一个判据):
 
-| 差异面 | 账户数 | 差额 | 说明 |
+> ⚠ **口径名必须写全:"SP 归因"差异面,不是"差异面"。**
+> 那 11 个"重合"账户**割接后结果仍然不同**,只是差异不再来自 `DECLARE` 整数化,
+> 而是来自"迁移把 3 位收敛成 2 位"。写成"差异面 88"会被读成"88 个账户割接后结果相同",
+> 那是**反的**:剩下 11 个也有差异,只是**归因不同**。归因不同决定了它是"我们不复刻"还是"迁移本来就要做"。
+
+| SP 归因差异面 | 账户数 | 差额合计 | 说明 |
 |---|---:|---:|---|
 | `r0 > r2`(legacy 比 target 多扣) | **88** | ¥21.130 | 95 减 7 |
 | `r0 < r2`(legacy 比 target 少扣) | **33** | ¥7.000 | 37 减 4 |
-| `r0 = r2` 但 `r0 ≠ raw`(**重合**) | **11** | — | `0.996 → 1.00` 这类 |
+| `r0 = r2` 但 `r0 ≠ raw`(**重合**:差异改归因给迁移) | **11** | 见下 | `0.996 → 1.00` 这类 |
 | `r0 = 0`(拒绝 vs 退款) | 166 | — | 差异是"退不出 vs 退得出",不是金额 |
+| `r0 = raw`(整数余额) | 268 | — | 无差异 |
 
 ```
-88 + 33 + 11 + 166 + 268 = 566   ← 差异面,同样必须闭合
+88 + 33 + 11 + 166 + 268 = 566   ← 加法必须闭合
 95 − 88 = 7,37 − 33 = 4,7 + 4 = 11   ← 两个口径的差恰好是"重合"那 11 个
 ```
 
-那 **11 个重合**是什么:3 位小数**向上进位恰好跨过整数**,2 位迁移后 `r2` 就等于 legacy 算出来的
-`r0`,于是两侧扣的一样多。真实样本(已列出,可直接复跑):
+那 **11 个重合**是什么:3 位小数**向上/向下进位恰好落在整数上**,2 位迁移后 `r2` 就等于 legacy
+算出来的 `r0`,于是**退的金额一样**、但**期末余额不同**(差额来自迁移,不来自 SP)。
+真实样本(已列出,可直接复跑):
+
+| userid | raw | `r2` | `r0` | 期末余额差 | 归因 |
+|---|---:|---:|---:|---:|---|
+| 48510 | 0.996 | 1.00 | 1 | 0.004 | 迁移(3→2 位) |
+| 66449 | 0.999 | 1.00 | 1 | 0.001 | 迁移 |
+| 66743 | 0.998 | 1.00 | 1 | 0.002 | 迁移 |
+| 27676 | 1.999 | 2.00 | 2 | 0.001 | 迁移 |
+| 67983 | 7.998 | 8.00 | 8 | 0.002 | 迁移 |
+| 68584 | 99.996 | 100.00 | 100 | 0.004 | 迁移 |
+| 47613 | 301.996 | 302.00 | 302 | 0.004 | 迁移 |
+| **合计 7 个** | | | | **0.018** | |
+| 65321 | 1100.004 | 1100.00 | 1100 | 0.004 | 迁移(反方向) |
+| 66688 | 1.004 | 1.00 | 1 | 0.004 | 迁移 |
+| 67617 | 400.002 | 400.00 | 400 | 0.002 | 迁移 |
+| 68184 | 200.004 | 200.00 | 200 | 0.004 | 迁移 |
+| **合计 4 个** | | | | **0.014** | |
+
+```
+11 个的 |r2 − raw| 求和 = 0.018 + 0.014 = 0.032
+带符号求和 SUM(r2 − raw)      = 0.018 − 0.014 = 0.004   ← 两个方向大部分互相抵消
+```
 
-| userid | raw | `r2` | `r0` |
-|---|---:|---:|---:|
-| 48510 | 0.996 | 1.00 | 1 |
-| 66449 | 0.999 | 1.00 | 1 |
-| 66743 | 0.998 | 1.00 | 1 |
-| 27676 | 1.999 | 2.00 | 2 |
-| 67983 | 7.998 | 8.00 | 8 |
-| 68584 | 99.996 | 100.00 | 100 |
-| 47613 | 301.996 | 302.00 | 302 |
+> ⚠ 这三个数(0.018 / 0.014 / 0.004)**必须分开说**。把它们当成"每个 0.004 × 7 = 0.028"是错的:
+> 7 个的差额是 `{0.001, 0.001, 0.002, 0.002, 0.004, 0.004, 0.004}`,**不同质**;
+> 而且反方向还有 4 个。**假设"同一类账户的差额相同"就是又一次"用了一个真实数据里不存在的值域"** ——
+> 与 §13.2 开头那两处错是同一族。
 
 复跑判据(**注意不是** `ROUND(balance,0) <> ROUND(ROUND(balance,2),0)`,那个在全库是 0 行):
 
 ```sql
--- 多扣侧的重合:r0 > raw(缺陷面里)但 r0 = r2(差异面里没有)
-SELECT userid, balance FROM t_user_expense_school
+-- 11 个重合账户(7 多扣侧 + 4 少扣侧),一条 SQL 全出
+SELECT userid, balance,
+       ROUND(balance,2)        AS r2,
+       ROUND(balance,0)        AS r0,
+       ROUND(balance,2) - balance AS balance_diff
+  FROM t_user_expense_school
  WHERE balance > 0
-   AND ROUND(balance,0) > balance                 -- raw 口径:整数化后更大
-   AND ROUND(balance,0) = ROUND(balance,2);       -- 但 2 位口径下已重合
+   AND ROUND(balance,0) > 0          -- 不是"整数化后归 0"那 166 个
+   AND ROUND(balance,0) = ROUND(balance,2)  -- 2 位口径下已与整数重合
+   AND ROUND(balance,0) <> balance;         -- 但 raw 不是整数
 ```
 
 > ⚠ 两个坑,都是我自己踩过的:
@@ -1010,3 +1038,65 @@ hash / DefaultCases / 实跑 / PASS / FAIL / 崩溃 / Skipped
 这个泄漏就被永久掩埋了:以后每加一条 case 都要重新踩一次,而且每次都看起来像"量的刻度没选好"。
 
 修法是给每个打开点加 `defer closeDB(...)`,并在 `closeDB` 的注释里记下这个坑。
+
+## 16. 69 项 `Skipped` 的分解("结构性不适用"总量,不含任何"没测到")
+
+报数第七个数是 `Skipped`。它必须是**可解释的**:一个孤零零的 69 会被后来人当成"漏测了 69 项"。
+以下是当前 35 条 case 的完整分解(`grep '^    — ' 报告` 可直接复核,各类相加 = 69):
+
+| 类 | 条数 | 为什么是"结构性不适用" |
+|---|---:|---|
+| `全局 Δtotal_recommended_amount` | 17 | 当前所有 Op 都**不写**该列(唯一会写的是老带新 `use_activity_recommend`,尚未进批次)。§9 已说明。 |
+| `转出校区 Δtotal_recommended_amount` | 17 | 同上,校区维度。 |
+| `转入校区 Δtotal_recommended_amount` | 2 | 同上,仅转结那两条有第二校区。 |
+| `余额(调用后,校区)` | 9 | C2 的 per-side 比对:两侧算法不同 ⇒ **两侧之间不比**。各自的结果已由 Δ 行按其自身期望断言。 |
+| `【不适用】legacy 本条未被拒绝 ⇒ 拒绝契约无从验证` | 8 | C2 里未被前置校验拒绝的 case:没有 `success=0` 可查。 |
+| `legacy 专属不变量:108.money == round0(...)` | 4 | C2 里走 ELSE 分支的 case:**没有 108 流水**可查。 |
+| `共同不变量:207.money / Δtotal_income` | 2 | C2-2(两侧都被拒)与另一条:**没有 207 可查**,恒等式无从验证。 |
+| `活动失效(t_courses_activity_signed)` | 8 | C2 里不种活动的 case:快照为空,`{} == {}` 不构成验证。 |
+| `legacy 校区余额 / 全局余额(原值 3 位)` | 2 | C1-5b:target 侧只有 2 位存储,**没有可比的值**。 |
+
+```
+17 + 17 + 2 + 9 + 8 + 4 + 2 + 8 + 2 = 69
+```
+
+两条纪律:
+
+1. **`Skipped` 不等于"测不了"**。上表每一行都写清了"为什么这一项在本 case 上无从验证",
+   而不是"实测和我预期不一样所以跳过"(见 §14 的警告)。
+2. **`Skipped` 不是免检通道**。`EnsureNotAllSkipped` 保证每条 case 至少有 1 条真断言(§14.1),
+   所以"69 项 Skipped"配的是"35 条 case 每条都有实质比对",不是空壳。
+
+## 17. 活动链 9 支的**证据强度上限**(写在这里,因为它是这个批次的先天约束)
+
+三条实测事实(`spdump sql` / `spdump routines` / PHP 仓 `grep`,都可复跑):
+
+1. **target 侧活动域 schema 齐、数据空**:
+   `t_courses_activity` / `_signed` / `_used_log` / `_user_discount` / `_user_discount_log` 全部 **0 行**;
+   legacy 侧 `t_courses_activity` = 90、`t_courses_activity_signed` = 1288。
+   ⇒ **任何真实报名/真实赠送额度都只有 legacy 有,target 看不到。活动侧只能靠种子。**
+2. **9 支里只有 1 支碰资金表**:
+   `sp_courses_activity_use_activity_recommend`(`t_user_expense` / `t_user_expense_school` /
+   `t_user_expense_log` type=107 / `total_recommended_amount`,共 14 处引用)。
+   **其余 8 支的资金表引用数 = 0** —— 它们在 SP 层面根本不写资金,资金变动在 PHP 应用层。
+   ⇒ 沿用 C1/C2 那套"Δbalance / Δspecial / Δincome / 流水 type"的比对机器对 8 支**完全不适用**,
+   必须新增**活动域快照**(`t_courses_activity` / `_signed` / `_used_log` / `_user_discount` / `_user_discount_log`)。
+3. **生产构型分三档**(PHP 仓 `grep '<method>('` 去掉定义行):
+
+   | 档 | SP | 外部调用点 | 含义 |
+   |---|---:|---:|---|
+   | A 有生产调用 | `signed_add`(4) / `signed_cancel`(4) / `use_activity_recharge`(3) / `refund_activity_recharge`(4) / `use_activity_recommend`(1) | ≥1 | 线上真的会走,能构造真实业务链 |
+   | B 封装方法 0 调用点 | `user_discount_use` / `user_discount_refund` / `user_discount_refund_user_activity` | **0** | 只有"把 dump 正文实现对了"这一层 |
+   | C SP 的 `CALL` 被注释掉 | `user_discount_usable` | **0**(`CoursesActivityUserDiscountHelper.php:94` 整行被 `//`) | **死代码**:连"照 dump 实现"的驱动力都没有 |
+
+   ⇒ 活动链批次的**证据强度上限**:
+
+   | 支 | 两侧数据 | 能证明什么 |
+   |---|---|---|
+   | 5 支 A 档(其中 4 支无资金写) | 活动侧种子 + 资金侧 n/a | "照 dump 正文实现对了" + "种下去的活动状态演化对" |
+   | `use_activity_recommend`(A 档唯一碰资金的) | 活动侧种子 + **资金侧可真实**(用户与推荐人都是真人真账户) | 上面两条 + 双表双列 `balance`/`total_recommended_amount` 同额 + 107 流水双发 |
+   | 3 支 B 档 | 两侧都是合成 | **只有**"照 dump 正文实现对了" |
+   | `user_discount_usable`(C 档) | 只读、无写 | 只有"返回值口径对了";且它是**死代码**,建议不进 `DefaultCases` |
+
+**报告里必须逐支标注上表的档位,不能笼统写"9 支全绿"** ——
+B/C 档的绿与 A 档的绿不是同一种绿,混在一张总表里就是把"没验证过生产构型"读成"生产上验证过了"。