Bläddra i källkod

refactor(ac15): Δ 断言三件套 —— delta() + Before 形态入 detail + 种子按 user 全删(§18.3 第 10 条)

`26535f9` 已把 F25 撤销(错在断言 `x−x`)。这一轮把**防止它再发生**的三件事落地。

## 一、`delta(before, after)` + 单测

手写 `a - b` 什么都不问;`delta(before, after)` 的参数**有名字**,逼着人回答
"这两个是不是不同来源"。单测 `TestDeltaOrderAndSelfSubtraction` 同时钉住:
① 参数顺序(`delta(100,250)==150`,反了就是 −150);② **自减恒 0** ——
把那个坑的**表现形式**固定下来,让人一眼认出"报告里那个 +0 可能就是自减"。

## 二、Before 的**形态**打进 detail

`Δ = 0` 有两种相反含义:「没改」与「本来就是那个值」。断言 detail 现在先打:

```
Before:legacy 全局行=false 校区行=false | target 全局行=false 校区行=false | …
```

有没有行**比数值更重要** —— 残留行恰好是 0 时 Δ 全对,而"建行分支"从未执行过。

## 三、种子改成**按 user 全删**(不带 campus 条件)

原来按 `(user_id, institution_id, campus_id)` 删:残留行 campus 不同就删不掉 ⇒
"Absent 变成假的",而症状是某条业务断言的 Δ=0(看起来像实现没加钱),**不是"前提不成立"**。
全删之后由前置断言兜底:`run.go` 里「声明 `Absent` ⇒ 两侧 Before 必须**读不到行**」两条
(全局 + 校区各一条,detail 打 `读到=%v`)。**删干净这件事本身必须有断言看着**,否则只是运气。

## 四、README §18.3 第 10 条(清单 9 → 11 种)

> **用 `Δ = After − Before` 断言,却不断言 Before 就是声明的基线** ⇒ `Δ=0` 的两种含义无法区分。
> 判据:**先证明"起点是对的",再谈"走了多远"。**
> 这条覆盖面最广 —— 对所有基于 Δ 的 case 成立,不只 W4-B。

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

```
分母:DefaultCases=54 | 应跑 54 | 实跑 54
总览:54 个 case,54 PASS,0 FAIL
Skipped 98 | 业务 329 | 基建 113 | 未分类 0
代价 SQL 3202 次 / 20.642s / 59.3 次 per case / 382ms 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 timmar sedan
förälder
incheckning
cc20e1caf8

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

@@ -1236,7 +1236,7 @@ $sql = "SELECT SUM(t1.discount_amount-t1.discount_amount_used) as amount FROM ..
 反过来,三处**故意选严**的:`toleranceCents = 0`(容差永不放宽)、
 哨兵的 `nonzeroSamples > 0`(非空自检)、`未分类断言数 == 0`。
 
-### 18.3 「自伤清单」:新基建最容易把旧判据悄悄弄坏的十种方式
+### 18.3 「自伤清单」:新基建最容易把旧判据悄悄弄坏的十一种方式
 
 每一条都**不产生明显症状**,只让断言变松或整块消失。加基建/加 case 时逐条对照。
 
@@ -1254,6 +1254,11 @@ $sql = "SELECT SUM(t1.discount_amount-t1.discount_amount_used) as amount FROM ..
 | **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` 里手写 |
 
+| **10** | **用 `Δ = After − Before` 断言,却不断言 Before 就是声明的基线**(批 3d 的 W4-B 逼出来的) | **`Δ = 0` 有两种相反含义:「没改」与「本来就是那个值」。** 后者等于**这条 case 根本没测到东西**——它全绿,但什么都没验。同一形态还有第三种脏:残留行恰好也是 0,Δ 全对而"建行分支"从未执行 | 任何 Δ 断言都要**同时**断言 Before 的形状(行是否存在、各列值是否等于声明)。本批落地:①种子改成**按 user 全删**(不带 campus 条件,残留行 campus 不同就删不掉);②`run.go` 加前置断言「声明 `Absent` ⇒ 两侧 Before 必须**读不到行**」;③断言 detail 里打 `Before:…行=%v`,让"Δ=0"一眼能看出是哪种 |
+
+第 10 条是本清单里**覆盖面最广**的一条:它对**所有**基于 Δ 的 case 成立,不只 W4-B。
+判据可以压成一句:**先证明"起点是对的",再谈"走了多远"。**
+
 第 9 条比前八条都更值得记一笔:它是**唯一一条"断言自称在比、其实什么也没比"**的形态,
 而且**它的红看起来完全像实现缺陷**。判据可以压成一句话:**凡是减法,先问"这两个操作数是不是同一个来源"**。
 

+ 26 - 0
internal/academic/dualrun/stage2/delta_test.go

@@ -0,0 +1,26 @@
+package stage2
+
+import "testing"
+
+// TestDeltaOrderAndSelfSubtraction 锁住 §18.3 第 9 条的两种形态。
+//
+// 为什么需要这个单测:批 3d 真踩过一次 —— 手写 `trc.Recommended - trc.Recommended`
+// 恒为 0,打印成 `+0`,看起来完全像"实现没给这一列加钱",据此报了一个假的 P0 缺陷。
+// 函数本身挡不住"传同一个变量",所以这里把"什么都不比"的那个**形状**显式钉住:
+// **自减必须得 0** —— 一旦某条断言用了自减,它打印出来的 0 就是这个测试描述的东西。
+func TestDeltaOrderAndSelfSubtraction(t *testing.T) {
+	if got := delta(100, 250); got != 150 {
+		t.Fatalf("delta(before, after) 应为 after−before=150,实得 %d(参数顺序反了?)", got)
+	}
+	if got := delta(250, 100); got != -150 {
+		t.Fatalf("delta(250,100) 应为 −150,实得 %d", got)
+	}
+	// 自减恒 0:这条不是"应该这样用",而是**把这个形状固定下来**,
+	// 让人一眼认出"报告里那个 +0 可能就是自减"(§18.3 第 9 条)。
+	if got := delta(5000, 5000); got != 0 {
+		t.Fatalf("自减必须得 0(这正是那个坑的表现形式),实得 %d", got)
+	}
+	if got := delta(0, 0); got != 0 {
+		t.Fatalf("delta(0,0) 应为 0,实得 %d", got)
+	}
+}

+ 8 - 4
internal/academic/dualrun/stage2/recommend_accounts.go

@@ -54,8 +54,12 @@ func seedLegacyRecommend(ctx context.Context, tx *gorm.DB, c Case) error {
 		// 先无条件清干净:不能假设库里有/没有。target 库整库为空,legacy 侧则
 		// 可能有 `INSERT…SELECT FROM finance_bk` 搬进来的真实行 —— 不清就会
 		// 把"不建行"那条 case 种成"行还在",而它在报告上只表现为"断言不成立"。
-		if err := tx.Exec(`DELETE FROM t_user_expense_school WHERE userid=? AND schoolid=? AND campusid=?`,
-			u.legacyID, c.InstitutionID, c.CampusID).Error; err != nil {
+		// ⚠ **按 user 全删,不带 campus 条件**。
+		// 带 campus 条件会留一种脏法:残留行的 campus_id 与本次不同 ⇒ 删不掉 ⇒
+		// "Absent"变成假的,而症状是某条业务断言的 Δ=0(看起来像实现没加钱),
+		// 不是"前提不成立"。全删之后由下面的**前置断言**兜底(见 `verifyLegacyRecommendSeed`
+		// 与 run.go 里"声明 Absent ⇒ 两侧都读不到"那两条)。
+		if err := tx.Exec(`DELETE FROM t_user_expense_school WHERE userid=?`, u.legacyID).Error; err != nil {
 			return fmt.Errorf("清 legacy %s 校区账户失败:%w", u.role, err)
 		}
 		if err := tx.Exec(`DELETE FROM t_user_expense WHERE userid=?`, u.legacyID).Error; err != nil {
@@ -101,8 +105,8 @@ func seedTargetRecommend(ctx context.Context, tx *gorm.DB, c Case) error {
 		{c.RecommendUserID, c.Recommend.Referrer, "推荐人"},
 	} {
 		uid := fixture.FixtureCanonicalUserID(u.legacyID)
-		if err := tx.Exec(`DELETE FROM t_user_expense_school WHERE user_id=? AND institution_id=? AND campus_id=?`,
-			uid, c.InstitutionID, c.CampusID).Error; err != nil {
+		// 同上:按 user 全删,理由见 legacy 侧那段注释。
+		if err := tx.Exec(`DELETE FROM t_user_expense_school WHERE user_id=?`, uid).Error; err != nil {
 			return fmt.Errorf("清 target %s 校区账户失败:%w", u.role, err)
 		}
 		if err := tx.Exec(`DELETE FROM t_user_expense WHERE user_id=?`, uid).Error; err != nil {

+ 42 - 19
internal/academic/dualrun/stage2/recommend_compare.go

@@ -83,48 +83,54 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
 		lrc, trc := lg.After.RecommenderCampus, tg.After.RecommenderCampus
 		lb, tb := lg.Before.Recommender, tg.Before.Recommender
 		lbc, tbc := lg.Before.RecommenderCampus, tg.Before.RecommenderCampus
+		// Before 的**形态**也要打出来:Δ 只在"Before == 声明的基线"时才有意义。
+		// `Δ=0` 有两种相反含义:「没改」与「本来就是那个值」,而后者等于这条 case 没测到东西。
+		// 本 case 声明的基线是 Absent ⇒ Before 必须**没有行**(这一条另由 run.go 的前置断言钉住)。
+		beforeShape := fmt.Sprintf("Before:legacy 全局行=%v 校区行=%v | target 全局行=%v 校区行=%v",
+			lg.Before.HasRecommender, lg.Before.HasRecommenderCampus,
+			tg.Before.HasRecommender, tg.Before.HasRecommenderCampus)
 
-		add("W4-B·推荐人 全局 Δbalance", fmt.Sprintf("%+d", lr.Balance-lb.Balance),
-			fmt.Sprintf("%+d", tr.Balance-tb.Balance), lr.Balance-lb.Balance == tr.Balance-tb.Balance,
+		add("W4-B·推荐人 全局 Δbalance", fmt.Sprintf("%+d", delta(lb.Balance, lr.Balance)),
+			fmt.Sprintf("%+d", delta(tb.Balance, tr.Balance)), delta(lb.Balance, lr.Balance) == delta(tb.Balance, tr.Balance),
 			"两侧都应 +m(全局列有 DEFAULT 0.000)")
 		asserts = append(asserts, Assertion{Kind: AssertionBusiness,
 			Name: "W4-B① 全局 `balance`:**两侧都 +m**(`t_user_expense.balance` 有 `DEFAULT 0.000`,建行后有值)",
-			Pass: lr.Balance-lb.Balance == want && tr.Balance-tb.Balance == want,
-			Detail: fmt.Sprintf("legacy %+d(期望 %+d)| target %+d(期望 %+d)| m=%d。"+
+			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。"+
 				"这一条是**唯一两侧一致**的那句 —— 也是为什么只看全局会漏掉整个缺陷",
-				lr.Balance-lb.Balance, want, tr.Balance-tb.Balance, want, want),
+				delta(lb.Balance, lr.Balance), want, delta(tb.Balance, tr.Balance), want, want),
 		})
 
-		add("W4-B·推荐人 校区 Δbalance", fmt.Sprintf("%+d(NULL=%v)", lrc.Balance-lbc.Balance, lrc.Nulls.Balance),
-			fmt.Sprintf("%+d(NULL=%v)", trc.Balance-tbc.Balance, trc.Nulls.Balance),
+		add("W4-B·推荐人 校区 Δbalance", fmt.Sprintf("%+d(NULL=%v)", delta(lbc.Balance, lrc.Balance), lrc.Nulls.Balance),
+			fmt.Sprintf("%+d(NULL=%v)", delta(tbc.Balance, trc.Balance), trc.Nulls.Balance),
 			c.IntentionalStrict, "legacy 建行只写 3 列 ⇒ 该列无 DEFAULT ⇒ NULL")
 		asserts = append(asserts, Assertion{Kind: AssertionBusiness,
 			Name: "W4-B② 校区 `balance`:legacy **仍是 NULL(没到账)** / target **+m 且有值**",
-			Pass: lrc.Nulls.Balance && lrc.Balance-lbc.Balance == 0 && !trc.Nulls.Balance && trc.Balance-tbc.Balance == want,
-			Detail: fmt.Sprintf("legacy Δ=%+d NULL=%v | target Δ=%+d NULL=%v | m=%d。"+
+			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。"+
 				"legacy 侧 `SET balance=balance+m` 在 NULL 上运算仍是 NULL ⇒ **校区一分没记**",
-				lrc.Balance-lbc.Balance, lrc.Nulls.Balance, trc.Balance-tbc.Balance, trc.Nulls.Balance, want),
+				delta(lbc.Balance, lrc.Balance), lrc.Nulls.Balance, delta(tbc.Balance, trc.Balance), trc.Nulls.Balance, want),
 		})
 
 		add("W4-B·推荐人 Δrecommended(全局/校区)",
-			fmt.Sprintf("%+d/%+d (NULL %v/%v)", lr.Recommended-lb.Recommended, lrc.Recommended-lbc.Recommended,
+			fmt.Sprintf("%+d/%+d (NULL %v/%v)", delta(lb.Recommended, lr.Recommended), delta(lbc.Recommended, lrc.Recommended),
 				lr.Nulls.Recommended, lrc.Nulls.Recommended),
-			fmt.Sprintf("%+d/%+d (NULL %v/%v)", tr.Recommended-tb.Recommended, trc.Recommended-tbc.Recommended,
+			fmt.Sprintf("%+d/%+d (NULL %v/%v)", delta(tb.Recommended, tr.Recommended), delta(tbc.Recommended, trc.Recommended),
 				tr.Nulls.Recommended, trc.Nulls.Recommended),
 			c.IntentionalStrict, "两张表这一列都无 DEFAULT")
 		asserts = append(asserts, Assertion{Kind: AssertionBusiness,
 			Name: "W4-B③ `total_recommended_amount`:legacy **全局与校区都是 NULL(两边都没记)** / target 两处都 +m",
 			Pass: lr.Nulls.Recommended && lrc.Nulls.Recommended &&
-				lr.Recommended-lb.Recommended == 0 && lrc.Recommended-lbc.Recommended == 0 &&
+				delta(lb.Recommended, lr.Recommended) == 0 && delta(lbc.Recommended, lrc.Recommended) == 0 &&
 				!tr.Nulls.Recommended && !trc.Nulls.Recommended &&
-				tr.Recommended-tb.Recommended == want && trc.Recommended-tbc.Recommended == want,
-			Detail: fmt.Sprintf("legacy 全局 %+d NULL=%v、校区 %+d NULL=%v | target 全局 %+d NULL=%v、校区 %+d NULL=%v。"+
+				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。"+
 				"⚠ **NULL 不是 0**:它表示「累计推荐金额从未被写成功过」,与「推荐金额确实是 0」在账上完全不同;"+
 				"按 Q1 口径 target 应为 0.00 的地方这里是 +m —— 这就是缺陷本身",
-				lr.Recommended-lb.Recommended, lr.Nulls.Recommended,
-				lrc.Recommended-lbc.Recommended, lrc.Nulls.Recommended,
-				tr.Recommended-tb.Recommended, tr.Nulls.Recommended,
-				trc.Recommended-tbc.Recommended, trc.Nulls.Recommended),
+				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),
 		})
 	}
 
@@ -259,3 +265,20 @@ func signedStatus(s *SideResult, activityID int64) int32 {
 	}
 	return -1
 }
+
+// delta 返回 `after − before`(单位:分)。
+//
+// 为什么要有这个函数,而不是各处直接写 `a - b`:
+// 批 3d 真踩过一次 —— 手写时把被减数与减数写成了**同一个快照**
+// (`trc.Recommended - trc.Recommended`),恒等于 0,打印成 `+0`,
+// 看起来就像"目标实现没给这一列加钱",我据此报了一个 P0 级的 TARGET-DEFECT 嫌疑。
+// 实际实现是对的,错的是断言。见 §18.3 第 9 条。
+//
+// 用函数的价值不在计算,在**参数有名字**:调用处必须写 `delta(before, after)`,
+// 名字逼着人回答"这两个是不同来源吗";而 `a - b` 什么都不问。
+//
+// ⚠ 它挡不住"两个参数传了同一个变量",所以还要配合两条:
+//
+//	① 单测 `TestDeltaOrderAndSelfSubtraction`(传同一个值必须得 0,且必须有别处断言非 0);
+//	② 任何基于 Δ 的断言,同时断言 **Before 就是声明的基线**(§18.3 第 10 条)。
+func delta(before, after int64) int64 { return after - before }