Эх сурвалжийг харах

docs(ac15): §22 补 RS-5 / RS-6 覆盖与 `to < from` 实测

- 覆盖表加两行(RS-5 右端语义 25 条业务断言 / RS-6 空用户 23 条),
  并注明"表里没有 RS-4"的原因(A21 已撤销,指向未覆盖第 5 条)
- 未覆盖里把「右端语义没被区分」改成「已由 RS-5 补上(差 50.00)」
- 新增 `to < from` 一项:target `From.After(To)` ⇒ 2001;legacy 实测**不校验**,
  对 user 32596 返回 1 行(prev/cur −0.002、两个 give 170.223、其余 0.000,无报错)。
  做成 case 需骨架支持「期望 target 报错」,属能力扩展 ⇒ 停下报,未自行扩
- 标题从 RS-1~RS-3 更新为 RS-1~RS-6
Q 5 цаг өмнө
parent
commit
d7167a8715

+ 23 - 4
internal/academic/dualrun/stage2/README.md

@@ -1600,7 +1600,7 @@ target  MIN(start_time) 折平 ⇒ 60 + 60 = 120.00,两行都核完,不报
 > ⇒ **凡候选集由 repo 层过滤提供的 SP,双跑都缺一层"候选集非空/钱真的动了"的正向断言。**
 > 这是结构性缺口,不是 RA 两支的局部问题。
 
-## §22 只读结果集双跑(RS-1 ~ RS-3)
+## §22 只读结果集双跑(RS-1 ~ RS-6)
 
 ### 为什么另起一套机器
 
@@ -1628,13 +1628,18 @@ target  MIN(start_time) 折平 ⇒ 60 + 60 = 120.00,两行都核完,不报
    bill 是 `DECIMAL(10,3)`(3→2 收敛)、refundable 是 `FLOAT`(4 字节单精度)、
    teacher_detail 是 `float(10,1)`(1→2 **扩位**)——后两者各写一条自己的断言说明。
 
-### 三条 case 的实测覆盖
+### 五条 case 的实测覆盖
 
 | case | dump 列 | 认领 | 豁免 | 业务断言 | 基建 |
 | --- | --- | --- | --- | --- | --- |
 | RS-1 可退金额 | 1 | 1 | 0 | 3 | 4 |
 | RS-2 账单 | 9 | 9 | 0 | 24 | 4 |
 | RS-3 课酬明细 | 17 | 6 | 11 | 17 | 4 |
+| RS-5 账单右端语义 | 9 | 9 | 0 | 25 | 4 |
+| RS-6 账单空用户 | 9 | 9 | 0 | 23 | 4 |
+
+> 表里没有 RS-4:那是 A21 扇出,**已撤销**(target 的 `uk_user_activity` 让两侧同构种子不可能),
+> 见「未覆盖」第 5 条与「A21 的真实影响」一节。
 
 - RS-1 用六张工单各钉 SP 的一个 `WHERE` 条件(审批 / 类型 / 校区 / 赠送已用尽 / 一单两行扇出),
   期望 `min(800,1000) − 115 = 685.00` 由声明算出。
@@ -1646,6 +1651,14 @@ target  MIN(start_time) 折平 ⇒ 60 + 60 = 120.00,两行都核完,不报
 - RS-3 是**部分映射**:target 端点 59 只回 9 个派生字段,dump 回整行。
   11 个豁免列逐条带理由,分三类:命中判据(折成 `ruleTier` 下发)/ 身份归属(int vs ULID)/
   展示性文本。target 独有 3 个字段(`schemeCampusId` / `schemeScope` / `ruleTier`)单独对声明断言。
+- RS-5 钉**右端语义**:四条流水分别在窗口前 / 恰在 `from` / 窗口内 / 恰在 `to`。
+  期望 `Prev −10`、`End 60`、`Exclusive 70`、`Inclusive 120`,差 `50.00` = `to` 当天那笔;
+  恒等式 `期初 + Exclusive = 期末`(−10+70=60)两侧各自成立。
+  `t=to` 的流水故意取 `type=200`(不在消费 IN 列表):若取 100/101,dump 的 **VARCHAR 字符串比较**
+  (`'2026-03-31 00:00:00' <= '2026-03-31'` 为 false)会让当期发生额也差,掩盖右端语义本身。
+- RS-6 钉**空用户**:无任何流水时两侧都返回 1 行、9 列全 `0.00`。
+  期望写 `"0.00"` 而非"空" —— legacy 若返回 NULL,归一成 `"NULL"` 后逐列断言直接红;
+  另有具名断言「9 列没有一列是 NULL」,把 NULL 与 0 的差别归因到 NULL 而不是"算错了"。
 
 ### 有牙的证据(不是"这次数据没踩到")
 
@@ -1663,8 +1676,8 @@ target  MIN(start_time) 折平 ⇒ 60 + 60 = 120.00,两行都核完,不报
   这不是 target 单侧能修的。
 - **RS-2**:`inCampusids` 是**死参数**(dump 全程未引用)。流水表无 campus 列,
   所以「不过滤」这件事**无法用结果集证伪** —— 只能按契约声明它不过滤。
-- **RS-2**:`FlowInclusiveEnd` 与 `FlowExclusiveEnd` 在本种子下相等(没有 `t = to` 的流水),
-  「含右端 vs 不含右端」的差别**没有被区分**。要区分需再种一条 `create_time = end` 的流水。
+- **RS-2**:`FlowInclusiveEnd` 与 `FlowExclusiveEnd` 在 RS-2 的种子里相等(无 `t = to` 的流水);
+  **已由 RS-5 补上**(`to` 当天一笔 50.00,二者差 50.00)。
 - **RS-2**:§3.8-A21 的扇出缺陷**不能做成双跑 case,原因是结构性的**(不是"没做"):
   target 的 `t_courses_activity_signed` 有唯一键 `uk_user_activity(user_id, activity_id)`(`NON_UNIQUE=0`),
   写第二条同活动报名报 `1062 Duplicate entry '…-900011' for key 't_courses_activity_signed.uk_user_activity'`
@@ -1678,6 +1691,12 @@ target  MIN(start_time) 折平 ⇒ 60 + 60 = 120.00,两行都核完,不报
   **RS-4 撤销**(`baf1856` 已由 `dbb1f80` 整轮 revert),`Divergence` / `legacy-only`
   模式也一并撤掉 —— 不为一条回答不了真问题的 case 留投机性扩展。
 - **RS-2**:`type=108` 双来源「只归因不裁剪」未覆盖。
+- **RS-2**:`to < from` **未做成 case**:target 在 `GetBill` 里先校验 `From.After(To)` ⇒ 2001;
+  legacy **不校验**(实测 `CALL(...,'2026-03-31','2026-03-01')` 对 user 32596 返回 **1 行**:
+  `previousBalance=-0.002 / currentBalance=-0.002 / previousGiveCost=170.223 / currentGiveCost=170.223`,
+  其余 5 列 `0.000`,**无报错**)。
+  做成 case 需要骨架支持「期望 target 报错」(现有 runner 把 target 报错当成失败并直接返回),
+  属能力扩展 —— 按纪律停下报,未自行扩。
 - **RS-3**:`coursePaymentDetailid>1` 的**有意差异**(dump 是「≥2 才短路」⇒ id=1 的规则永不生效;
   Go 是 `>0` ⇒ 会生效)未覆盖 —— 本 case 规则 id=990002。要验必须专门造 id=1。
 - **RS-3**:持证档③、年级/科目通配档②、机构级方案回退(`campus_id=0`)、