Selaa lähdekoodia

refactor(ac15): Δ detail 改打「before → after(Δ)」三数;§18.3 第 9 条改定性为「零区分力的第二种方向:假红」

## 一、Δ 类 detail 的形状变了(打印形状本身就是判据)

```
校区 balance:legacy NULL(NULL=true) → NULL(NULL=true)(Δ+0)|
              target NULL(NULL=true) → 50.00(NULL=false)(Δ+5000,期望 +5000)| m=5000
```

上一版只打 `Δ`,于是 `Δ+0` 看起来像"没加"。现在 `Δ` 前后各有一个数:

- `5000 → 5000(Δ+0)` ⇒ 读的人看到的是**"没变"**;
- `0 → 5000(Δ+5000)` ⇒ 看到的是**"加了"**;
- `NULL → NULL(Δ+0)` ⇒ 看到的是**"这一列从没写过"**(与 0.00 区分开)。

**只打 Δ 是一种自伤**:它把"哪两个数相减"这个信息藏起来,而这次 F25 假警报就死在这个信息缺失上。
配 `moneyOrNULL`:`isNull` 必须渲染成 `NULL`,不能退化成 `0.00`。

## 二、§18.3 第 9 条改定性(按 team-lead 的建议挂到 F7 框架上)

| | 形态 | 本质 |
|---|---|---|
| F7 | 恒真断言(空过) | 永远绿 ⇒ 零区分力 |
| **第 9 条** | **恒 0 断言(假红)** | **永远红 ⇒ 零区分力,反方向** |

标题改成「**零区分力的第二种方向:假红**」,并加两条判据:
①凡是减法先问两个操作数是否同源;②**报红之前先证明这条断言能"红得对"**(已知会变的构型必须非 0)。

这一点是对 F7 那句话的补充:F7 说"全绿不自动等于验过了",
第 9 条说"**红也不自动等于实现有问题** —— 红的第一个怀疑对象是断言自己有没有区分力"。

## 三、报数(粘贴;两个门禁分两行)

```
分母:DefaultCases=54 | 应跑 54 | 实跑 54
总览:54 个 case,54 PASS,0 FAIL
Skipped 98 | 业务 329 | 基建 113 | 未分类 0
代价 SQL 3202 次 / 19.896s / 59.3 次 per case / 368ms per case
grep "本块比对了" = 4
活动域 15(2/2/1/10)| 课酬 2(1/1)| 跨域 17 + 资金 37 = 54

go build OK | go vet OK | go test OK
```

未 push。
Q 8 tuntia sitten
vanhempi
sitoutus
60bf6ef084

+ 1 - 1
internal/academic/dualrun/stage2/README.md

@@ -1252,7 +1252,7 @@ $sql = "SELECT SUM(t1.discount_amount-t1.discount_amount_used) as amount FROM ..
 | **7** | **需要读库的断言写进"通用断言块"**(批 3c 真踩:精度口径那条要在原始 3 位精度上读一次种子行) | **红的是断言自己,而 case 是绿的。** 通用断言块跑在两侧各自 `Rollback()` **之后** ⇒ 种子行已随事务回滚,查回来 **0 行**、`SUM(...)` 返回 **NULL**(`sql: Scan error: converting NULL to int64 is unsupported`)。报告上是一条红,看着像"实现错了",实际是**断言读了一个已经不存在的数据** | 需要读库的断言只能放在 **per-side 快照**(事务内)或 **`xxxCompare` 阶段**(它拿到的是 `SideResult.Before/After` 快照)。本批的落地方式:在 `readArrears*` 的 SELECT 里**多读两列原始精度**(`ArrearsRow.RemainderRaw3/NetRaw3`,**不参与比对口径**),断言搬到 `arrearsCompare` |
 
 | **8** | **报数自伤:把两个门禁写成一句"全绿"**(批 3d 真踩:加了 case 却没改 `c2_refund_test.go` 的分母闸,`go test` FAIL,而提交信息写"53/53 PASS") | 双跑绿 + 单测红。**双跑红=实现有问题;单测红=分母闸在叫("分母变了就要重新核一遍")**。写成"全绿"等于**把一个还在叫的警报报成安静** —— 而这条闸存在的唯一目的,就是逼"加 case 的人解释一遍" | ①**任何数字一律从落盘输出 grep 出来再贴,不许回忆**;②**"全绿"这个词只允许用于刚刚贴出来的那段输出**;③提交信息里"go test"与"双跑"必须分两行写 |
-| **9** | **断言里自比较(`x − x`)恒为 0**(批 3d 真踩:`trc.Recommended - trc.Recommended` 被写成"target 校区 Δ",于是恒定 0) | **它看起来像"实现没加钱"**。一次就此报出了一个 P0 级的 `TARGET-DEFECT` 嫌疑("目标实现在建行路径上不记推荐金额"),并把 team-lead 引去读实现、找修法。实际目标实现是**对的**,错的是断言 | ①**差值的被减数与减数必须来自不同的快照**(before/after、legacy/target)—— 写 `A - A` 在语义上永远无意义;②同族自检:差值断言先在**已知会变**的构型上跑一次,若恒 0 立即怀疑自比较;③`Δ` 类量优先用**同一个 helper**(`delta(before, after)`)生成,别在 `fmt.Sprintf` 里手写 |
+| **9** | **零区分力的第二种方向:假红**(批 3d 真踩:`trc.Recommended - trc.Recommended` 被写成"target 校区 Δ",恒 0) | **恒真断言**(F7)永远绿;**恒假/恒 0 断言**永远红 —— 两者都是**零区分力**,只是方向相反。 这次的红伪装成了 `TARGET-DEFECT`:我据此报了 P0 嫌疑,team-lead 读了三层实现、我跑了一次 probe,全部作废 | ①**凡是减法,先问"两个操作数是不是同一个来源"**;②**报红之前先证明这条断言能"红得对"** —— 拿一个"已知会变"的构型跑一遍,必须非 0(`TestDeltaOrderAndSelfSubtraction`);③Δ 类 detail **必须同时打 before / after / Δ 三个数**(打印形状本身就是判据:`Δ+0` 若伴随 `5000 → 5000`,读的人看到的是"没变"而不是"没加") |
 
 | **10** | **用 `Δ = After − Before` 断言,却不断言 Before 就是声明的基线**(批 3d 的 W4-B 逼出来的) | **`Δ = 0` 有两种相反含义:「没改」与「本来就是那个值」。** 后者等于**这条 case 根本没测到东西**——它全绿,但什么都没验。同一形态还有第三种脏:残留行恰好也是 0,Δ 全对而"建行分支"从未执行 | 任何 Δ 断言都要**同时**断言 Before 的形状(行是否存在、各列值是否等于声明)。本批落地:①种子改成**按 user 全删**(不带 campus 条件,残留行 campus 不同就删不掉);②`run.go` 加前置断言「声明 `Absent` ⇒ 两侧 Before 必须**读不到行**」;③断言 detail 里打 `Before:…行=%v`,让"Δ=0"一眼能看出是哪种 |
 

+ 37 - 9
internal/academic/dualrun/stage2/recommend_compare.go

@@ -96,9 +96,10 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
 		asserts = append(asserts, Assertion{Kind: AssertionBusiness,
 			Name: "W4-B① 全局 `balance`:**两侧都 +m**(`t_user_expense.balance` 有 `DEFAULT 0.000`,建行后有值)",
 			Pass: delta(lb.Balance, lr.Balance) == want && delta(tb.Balance, tr.Balance) == want,
-			Detail: beforeShape + fmt.Sprintf(" | legacy %+d(期望 %+d)| target %+d(期望 %+d)| m=%d。"+
+			Detail: beforeShape + fmt.Sprintf(" | 全局 balance:legacy %s → %s(Δ%+d,期望 %+d)| target %s → %s(Δ%+d,期望 %+d)| m=%d。"+
 				"这一条是**唯一两侧一致**的那句 —— 也是为什么只看全局会漏掉整个缺陷",
-				delta(lb.Balance, lr.Balance), want, delta(tb.Balance, tr.Balance), want, want),
+				centsOf2(lb.Balance), centsOf2(lr.Balance), delta(lb.Balance, lr.Balance), want,
+				centsOf2(tb.Balance), centsOf2(tr.Balance), delta(tb.Balance, tr.Balance), want, want),
 		})
 
 		add("W4-B·推荐人 校区 Δbalance", fmt.Sprintf("%+d(NULL=%v)", delta(lbc.Balance, lrc.Balance), lrc.Nulls.Balance),
@@ -107,9 +108,13 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
 		asserts = append(asserts, Assertion{Kind: AssertionBusiness,
 			Name: "W4-B② 校区 `balance`:legacy **仍是 NULL(没到账)** / target **+m 且有值**",
 			Pass: lrc.Nulls.Balance && delta(lbc.Balance, lrc.Balance) == 0 && !trc.Nulls.Balance && delta(tbc.Balance, trc.Balance) == want,
-			Detail: beforeShape + fmt.Sprintf(" | legacy Δ=%+d NULL=%v | target Δ=%+d NULL=%v | m=%d。"+
+			Detail: beforeShape + fmt.Sprintf(" | 校区 balance:legacy %s(NULL=%v) → %s(NULL=%v)(Δ%+d)| "+
+				"target %s(NULL=%v) → %s(NULL=%v)(Δ%+d,期望 %+d)| m=%d。"+
 				"legacy 侧 `SET balance=balance+m` 在 NULL 上运算仍是 NULL ⇒ **校区一分没记**",
-				delta(lbc.Balance, lrc.Balance), lrc.Nulls.Balance, delta(tbc.Balance, trc.Balance), trc.Nulls.Balance, want),
+				moneyOrNULL(lbc.Balance, lbc.Nulls.Balance), lbc.Nulls.Balance,
+				moneyOrNULL(lrc.Balance, lrc.Nulls.Balance), lrc.Nulls.Balance, delta(lbc.Balance, lrc.Balance),
+				moneyOrNULL(tbc.Balance, tbc.Nulls.Balance), tbc.Nulls.Balance,
+				moneyOrNULL(trc.Balance, trc.Nulls.Balance), trc.Nulls.Balance, delta(tbc.Balance, trc.Balance), want, want),
 		})
 
 		add("W4-B·推荐人 Δrecommended(全局/校区)",
@@ -124,13 +129,16 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
 				delta(lb.Recommended, lr.Recommended) == 0 && delta(lbc.Recommended, lrc.Recommended) == 0 &&
 				!tr.Nulls.Recommended && !trc.Nulls.Recommended &&
 				delta(tb.Recommended, tr.Recommended) == want && delta(tbc.Recommended, trc.Recommended) == want,
-			Detail: beforeShape + fmt.Sprintf(" | legacy 全局 %+d NULL=%v、校区 %+d NULL=%v | target 全局 %+d NULL=%v、校区 %+d NULL=%v。"+
+			Detail: beforeShape + fmt.Sprintf(" | 全局:legacy %s(NULL=%v) → %s(NULL=%v)(Δ%+d)| target %s → %s(Δ%+d,期望 %+d);"+
+				"校区:legacy %s(NULL=%v) → %s(NULL=%v)(Δ%+d)| target %s → %s(Δ%+d,期望 %+d)。"+
 				"⚠ **NULL 不是 0**:它表示「累计推荐金额从未被写成功过」,与「推荐金额确实是 0」在账上完全不同;"+
 				"按 Q1 口径 target 应为 0.00 的地方这里是 +m —— 这就是缺陷本身",
-				delta(lb.Recommended, lr.Recommended), lr.Nulls.Recommended,
-				delta(lbc.Recommended, lrc.Recommended), lrc.Nulls.Recommended,
-				delta(tb.Recommended, tr.Recommended), tr.Nulls.Recommended,
-				delta(tbc.Recommended, trc.Recommended), trc.Nulls.Recommended),
+				moneyOrNULL(lb.Recommended, lb.Nulls.Recommended), lb.Nulls.Recommended,
+				moneyOrNULL(lr.Recommended, lr.Nulls.Recommended), lr.Nulls.Recommended, delta(lb.Recommended, lr.Recommended),
+				centsOf2(tb.Recommended), centsOf2(tr.Recommended), delta(tb.Recommended, tr.Recommended), want,
+				moneyOrNULL(lbc.Recommended, lbc.Nulls.Recommended), lbc.Nulls.Recommended,
+				moneyOrNULL(lrc.Recommended, lrc.Nulls.Recommended), lrc.Nulls.Recommended, delta(lbc.Recommended, lrc.Recommended),
+				centsOf2(tbc.Recommended), centsOf2(trc.Recommended), delta(tbc.Recommended, trc.Recommended), want),
 		})
 	}
 
@@ -282,3 +290,23 @@ func signedStatus(s *SideResult, activityID int64) int32 {
 //	① 单测 `TestDeltaOrderAndSelfSubtraction`(传同一个值必须得 0,且必须有别处断言非 0);
 //	② 任何基于 Δ 的断言,同时断言 **Before 就是声明的基线**(§18.3 第 10 条)。
 func delta(before, after int64) int64 { return after - before }
+
+// centsOf2 把分渲染成两位小数字符串(报告里给人读的"值",不是 Δ)。
+func centsOf2(c int64) string {
+	sign := ""
+	if c < 0 {
+		sign, c = "-", -c
+	}
+	return fmt.Sprintf("%s%d.%02d", sign, c/100, c%100)
+}
+
+// moneyOrNULL 渲染三态金额:isNull ⇒ "NULL"(**不是 0.00**)。
+//
+// 为什么必须区分:这一批里"NULL"与"0.00"是**两件不同的事** ——
+// NULL = 从没写过;0.00 = 写过且是 0。混在一起打印,读的人会把缺陷看成正常。
+func moneyOrNULL(c int64, isNull bool) string {
+	if isNull {
+		return "NULL"
+	}
+	return centsOf2(c)
+}