|
|
@@ -797,8 +797,8 @@ func DefaultCases(_ string) []Case {
|
|
|
// ⇒ **整个 CALL 中断**,`ac15-ra2-1` 一分钱没拿到(**部分写入**:ra2-0 的 +60.00 已落)。
|
|
|
// 即:legacy 不只"重复核销",它是**一行崩掉整个循环,后面的报课行全部漏核**。
|
|
|
// target 折平后正常核完两行(60 + 60 = 120.00),不报错。
|
|
|
- ArrearsLegacyNetDelta: map[string]int64{"ac15-ra2-0": 6000, "ac15-ra2-1": 0},
|
|
|
- ExpectTargetSuccess: true,
|
|
|
+ LegacyObservedNetDelta: map[string]int64{"ac15-ra2-0": 6000, "ac15-ra2-1": 0},
|
|
|
+ ExpectTargetSuccess: true,
|
|
|
// legacy 侧**必然失败**(1264),且这是"这条 case 要钉住的分歧"本身,不是偶发错误。
|
|
|
ExpectLegacySuccess: false,
|
|
|
IntentionalStrict: true,
|
|
|
@@ -1025,11 +1025,20 @@ func RunOne(ctx context.Context, cfg Config, c Case) (*CaseResult, error) {
|
|
|
}
|
|
|
if want := arrearsSnapshotRowsOrDefault(c); want > 0 {
|
|
|
n := minInt(len(lg.Before.Arrears), len(tg.Before.Arrears))
|
|
|
+ declared := arrearsDeclaredRowsOrDefault(c)
|
|
|
+ fanout := want - declared
|
|
|
+ // 三个数必须同时出现在 detail 里:**声明 M / 扇出 F / 物理期望 M+F**。
|
|
|
+ // 只打总和会让 "3 是哪来的" 变成魔数 —— 而"差 1 没有出处"正是 3a 那次
|
|
|
+ // 12+2+1+2=17 的同一个病:数本身不重要,**能闭合才重要**。
|
|
|
+ fanoutWhy := "无扇出"
|
|
|
+ if fanout > 0 {
|
|
|
+ fanoutWhy = "扇出来自 RA-2 的同一 (coursesid,classid) 两条排班(LEGACY 侧游标会把同一报课行取多次)"
|
|
|
+ }
|
|
|
res.Assertions = append(res.Assertions, mustCover(
|
|
|
"case 有效性前提:**课酬应收两侧都读到且条数与声明一致**(按核销 cursor 顺序)",
|
|
|
n, want,
|
|
|
- fmt.Sprintf("声明 %d 条报课行、期望快照 %d 行(含排班扇出),legacy 读到 %d、target 读到 %d",
|
|
|
- arrearsDeclaredRowsOrDefault(c), want, len(lg.Before.Arrears), len(tg.Before.Arrears))))
|
|
|
+ fmt.Sprintf("声明 %d 行报课 / 扇出 %d 行 / 物理期望 %d 行(%s)| legacy 读到 %d、target 读到 %d",
|
|
|
+ declared, fanout, want, fanoutWhy, len(lg.Before.Arrears), len(tg.Before.Arrears))))
|
|
|
}
|
|
|
|
|
|
// ①-d 精度口径的**业务断言**在 `arrearsCompare` 里(它已经拿到了 `Before` 快照的原始 3 位列)。
|
|
|
@@ -1950,11 +1959,27 @@ func Summarize(w io.Writer, results []*CaseResult) bool {
|
|
|
} else {
|
|
|
fmt.Fprintf(w, "未分类断言(Kind==0):0 条\n")
|
|
|
}
|
|
|
- // 活动域形态分布:**按唯一形态统计**,所以 Σ 必须等于该批条数。
|
|
|
+ // 形态分布:**按唯一形态统计**,所以每个域行自己的 Σ 必须等于该域条数。
|
|
|
// 这一行存在的理由就是 3a 那次人工加法算错(12+2+1+2=17,实际 15)——
|
|
|
// 报告自己给一个恒等式,比任何人再算一遍都可靠。
|
|
|
- if line, ok := activityShapeLine(results); ok {
|
|
|
- fmt.Fprintf(w, "活动域形态:%s\n", line)
|
|
|
+ //
|
|
|
+ // ⚠ **一个域一行,不许合并**:把课酬并进"活动域"会静默改掉 `9e2b0b7` 的口径
|
|
|
+ //("活动域 15 条"变成 17 条,而它本该是 15)。判据没变,只是多了个"谁来数它"。
|
|
|
+ actN, okAct := shapeLineOf(results, isActivityChainOp)
|
|
|
+ if okAct {
|
|
|
+ fmt.Fprintf(w, "活动域形态:%s\n", actN.line)
|
|
|
+ }
|
|
|
+ arrN, okArr := shapeLineOf(results, isArrearsOp)
|
|
|
+ if okArr {
|
|
|
+ fmt.Fprintf(w, "课酬域形态:%s\n", arrN.line)
|
|
|
+ }
|
|
|
+ // 跨域闭合:**域内 case 之和 + 资金域 case = 总 case 数**。
|
|
|
+ // 为什么要有这一行:域内恒等式(Σ 形态 == 该域条数)只保证"域内没漏",
|
|
|
+ // 不保证"域之间没重叠、没漏整批"。`52 这个数从哪来` 应该在报告里闭合,不该让人去减。
|
|
|
+ if okAct || okArr {
|
|
|
+ domainN := actN.n + arrN.n
|
|
|
+ fmt.Fprintf(w, "跨域合计:域内 case %d(活动 %d + 课酬 %d)| 资金域 %d | 合计 %d\n",
|
|
|
+ domainN, actN.n, arrN.n, len(results)-domainN, len(results))
|
|
|
}
|
|
|
for _, r := range results {
|
|
|
st := "PASS"
|
|
|
@@ -1977,24 +2002,36 @@ func sumBusiness(results []*CaseResult) int {
|
|
|
return n
|
|
|
}
|
|
|
|
|
|
-// activityShapeLine 生成活动域形态分布那一行;非活动域 case 为 0 条时返回 ok=false(不打印)。
|
|
|
+// shapeLineResult 是一行形态统计的返回值:`line` 是给人看的字符串,`n` 是该域 case 条数。
|
|
|
+//
|
|
|
+// `n` 单独返回(不从字符串里抠)是为了让"跨域合计"那一行**用数不用解析** ——
|
|
|
+// 从格式化字符串里反解数字是典型的"看起来能用、改一次文案就悄悄错"的写法。
|
|
|
+type shapeLineResult struct {
|
|
|
+ line string
|
|
|
+ n int
|
|
|
+}
|
|
|
+
|
|
|
+// shapeLineOf 按谓词挑出一批 case,生成形态分布那一行;该域 0 条时 ok=false(不打印)。
|
|
|
//
|
|
|
// 输出形如:`15 条:legacy-defect 4 | intentional-strict 2 | zero-write 6 | equality 3`
|
|
|
// **尾部再打一次总数**,让人不必自己加:两侧对不上就是记账错了,不是"少打印了一类"。
|
|
|
-func activityShapeLine(results []*CaseResult) (string, bool) {
|
|
|
- act := make([]Case, 0, len(results))
|
|
|
+func shapeLineOf(results []*CaseResult, pred func(Op) bool) (shapeLineResult, bool) {
|
|
|
+ picked := make([]Case, 0, len(results))
|
|
|
for _, r := range results {
|
|
|
- if isActivityChainOp(r.Case.Op) {
|
|
|
- act = append(act, r.Case)
|
|
|
+ if pred(r.Case.Op) {
|
|
|
+ picked = append(picked, r.Case)
|
|
|
}
|
|
|
}
|
|
|
- if len(act) == 0 {
|
|
|
- return "", false
|
|
|
+ if len(picked) == 0 {
|
|
|
+ return shapeLineResult{}, false
|
|
|
}
|
|
|
- tally := ActivityShapeTally(act)
|
|
|
+ tally := ActivityShapeTally(picked)
|
|
|
parts := make([]string, 0, len(tally))
|
|
|
for _, t := range tally {
|
|
|
parts = append(parts, fmt.Sprintf("%s %d", t.Shape.Label(), t.N))
|
|
|
}
|
|
|
- return fmt.Sprintf("%d 条:%s", len(act), strings.Join(parts, " | ")), true
|
|
|
+ return shapeLineResult{
|
|
|
+ line: fmt.Sprintf("%d 条:%s", len(picked), strings.Join(parts, " | ")),
|
|
|
+ n: len(picked),
|
|
|
+ }, true
|
|
|
}
|