|
|
11 小时之前 | |
|---|---|---|
| .. | ||
| README.md | 12 小时之前 | |
| attribution.go | 11 小时之前 | |
| cases.go | 12 小时之前 | |
| guard.go | 12 小时之前 | |
| model.go | 11 小时之前 | |
| oracle.go | 11 小时之前 | |
| oracle_probe.php | 11 小时之前 | |
| report.go | 12 小时之前 | |
| run.go | 12 小时之前 | |
| target.go | 11 小时之前 | |
internal/academic/dualrun —— AC-15 双跑 harness(阶段 1:课酬链)本包只做一条链路的双跑比对:
| 侧 | 实现 | 怎么跑 |
|---|---|---|
| oracle | legacy PHP(CoursesRecordHelper::settlementExpenses) |
真跑:起一个 PHP 进程,事务内结算、finally 回滚,读异常前已写入的库值 |
| target | Go 纯函数(service.computeRecordReward + distributeAmount) |
只算不写:输入取自 oracle 侧同一份观测,不连 yuansiwei_2026 |
阶段 2~4 的写 SP、活动链、分账链不在本包范围。
这不是"两边各跑一次入库":
理由是课酬属于"由输入算出一个金额"的逻辑,纯函数足以判定;target 库也没有对应历史数据, 真跑只会引入无关变量。报告里必须原样保留这段说明,否则读的人会以为两侧都真跑了。
guard.go)两道条件,缺一不可:
127.0.0.1 / localhost / ::1SELECT DATABASE() 必须 ∈ {finance_bk, legacy_finance, legacy_oracle, yuansiwei_2026}违反时 panic(不返回 error)——这类错误一旦被忽略,会以"连到错误实例后的正常结果"形式污染整轮结论。
写库另有更严的允许表(阶段 1 只允许 legacy_oracle / legacy_finance)。
allowed-list 而非 deny-list:代码里不出现任何具体远端地址,日志里的 host 一律脱敏(127.0.***.**)。
AssertNoSysdate:目标库所有 routine 定义里 sysdate( 出现数必须为 0,否则拒绝运行。
依据:SET SESSION timestamp 冻结 NOW() 但不冻结 SYSDATE()(P0 §6 组 1)。
一旦有 SP 用 SYSDATE(),冻结时钟会静默失效而结果看起来正常。
oracle 侧每个 case 起一个新 PHP 进程(= 新连接)。依据(P0 §8.1 Q5):
MEMORY 引擎临时表的行不参与 ROLLBACK,只靠回滚清不掉;sp_user_expense_get_bill 正是 12 张 MEMORY 临时表。
进程隔离同时解决三件事:临时表残留、表结构残留、会话变量(timestamp / time_zone)污染。
| 类 | 判据 | 重置方式 |
|---|---|---|
| A | 只有 DML,或只用 TEMPORARY 表 |
START TRANSACTION + ROLLBACK |
| B | 含 TRUNCATE / 持久表 DDL / LOCK TABLES |
快照恢复 |
阶段 1 的两个入口(sp_course_payment_detail_get_teacher_detail、settlementExpenses)都是 A 类,
当前由 PHP 探针内的事务 + finally 回滚实现;B 类接口位置见 Case.Mutations 的扩展点。
attribution.go,顺序不可换)| 步 | 比什么 | 不等时 |
|---|---|---|
| 1 | income:legacy round(Σcost,3) vs 迁移后 Σ ROUND(cost,2) |
精度收敛,记 Δincome,继续第 2 步 |
| 2 | 规则:oracle 实返 detail_id vs runner 复刻的兜底判定 |
真缺陷(规则选错 / 层级错) |
| 3 | teacher_reward |
Δreward ≈ Δincome × pct/100 ⇒ 精度收敛;不成比例 ⇒ 真缺陷 |
第 3 步的成比例判据是关键:没有它,所有带 3 位小数的记录都会红,然后有人会去改一个本来正确的算法。
每条 case 记录:detail_id 是否命中、命中层级(teacher-classify / teacher-grade-subject /
cert-tier / generic-tier)、是否退化、isSupplement 取值、rewardType。
数值相等但路径不同 ⇒ PATH-DIVERGENCE,交人工判。R4 就是"数值相等掩盖路径不同"的实例。
Truncate(time.Second);SET SESSION timestamp=<整秒> + time_zone='+08:00'time() 的结果 —— A22:PHP 时钟不受 SET timestamp 影响,不假装能冻MATCH / PRECISION-CONVERGENCE / DEFECT / ORACLE-DEGRADED / PATH-DIVERGENCE / ENV-FAILURE
汇总必须给出六类计数,不能只给"通过率"。
dualrun stage1 --case-set real100
dualrun stage1 --case-set synthetic
dualrun stage1 --all --out report.json --md report.md
dualrun stage1 --dry-run
dualrun stage1 --all --idempotency
环境变量(口令只经环境变量,不落盘):AC15_DB_HOST / AC15_DB_PORT / AC15_DB_NAME /
AC15_DB_USER / AC15_DB_PASS / AC15_LEGACY_REPO / AC15_PHP_BIN。
退出码:0 跑完(无论结论);2 参数错误;3 环境层面失败(守卫 / 断言 / 连接)。
| 类别 | 数量 | 说明 |
|---|---|---|
real100 |
100 | status=2 + income>0 + 有规则命中;按 percentage 区间 × 人数区间分层抽样(不是取前 100 条) |
synthetic |
6 类 × 5 | 四级兜底(classify / cert 必须造数)、income 三位小数、income=0 夹下限、isSupplement 两值、人数不整除、rewardType=2 查表 |
不做:getTeacherPaymentDetail 无命中的记录(占 6.25%)—— 只标 ORACLE-DEGRADED,不判定。
internal/academic/service/dualrun_shim.go —— 两个只做转发、不含逻辑的导出函数
(ComputeRecordRewardForDualRun / DistributeRecordRewardForDualRun)。
为什么需要它:computeRecordReward / distributeAmount 是包内私有,harness 在另一个包。
为什么不能让 harness 自己再写一份公式:那会变成"用第二份实现校验第一份",
一旦两份都错、错法一致,双跑会给出全绿结论 —— 这正是要避免的自证。