文档:T-7 确认段落地回写(开发计划 DoD 全勾 + 执行记录 / 交接文档 §B.11 / TODO / 两处 MEMORY)

- 开发计划 T-7:DoD 9 项勾选 + 新增「执行记录」小节,逐条留痕 6 处与计划的差异裁定:
  ① 确认段不做最低持有重判(计划缺口,强制全转属受理段决策,受理单 remark 传承);
  ② 新增 format/audit/engine_call 三共用模块(后者顺带完成 T-8 的「引擎时机改」);
  ③ nav.py 未改(确认段直调 core_ro.get_nav_on,取 T−1 路径随 v1.0 convert_fund 一并删);
  ④ test_convert_integration 改写推迟到 T-9(需端到端真接口);
  ⑤ 受理单置 confirmed 并入 apply_convert 同事务(防「批次已扣、状态未置」中间态);
  ⑥ MySQL rowcount=changed rows 陷阱(nav_pending 重试不得复用冲突判定)。
- 交接文档 §B.11:T-6→T-7 完成、基线 797→827、真库 36/36→37/37 与 83/83、
  新增 T-7 实施期发现段、下一步 T-8/T-9(并注明 T-8 范围已缩小)。
- docs/memory/TODO.md:进度行 + T-6 真库数 + T-7 条目勾选与详述。
- .workbuddy/memory/MEMORY.md 与 docs/memory/MEMORY.md:进度与关键裁定同步;
  当日工作日志补「续 3:T-7 确认段落地」。
This commit is contained in:
2026-09-11 19:37:30 +08:00
parent 048f1a9e1e
commit 0e27fec5cb
4 changed files with 67 additions and 14 deletions
@@ -453,19 +453,58 @@
**依赖**:T-2 ~ T-6
**DoD**:真库 verify 脚本 `verify_convert_confirm.py`:
- [ ] 确认:`core_share_lot.remain_qty` 扣减、`core_trade` 恰 2 行同 gid、两端 `core_holding` 更新、状态 `confirmed`(验收 2/10/24)
- [ ] 重复触发 confirm_batch 同 as_of:第二次 0 处理,流水不重复(验收 24 后半 / 15)
- [ ] 缺 T 日净值:状态 `nav_pending`、无流水、`remain_qty` 不变;补 nav 后重跑 → `confirmed`(验收 25)
- [ ] T+1 复核不通过 → `rejected` + 占用释放(验收 26)
- [ ] 部分成交:占用被抢 → `actual_qty < qty`、remark='partial'、未确认部分占用释放(验收 30)
- [ ] 跨批次:各批按持有期计费、明细落 `core_convert_lot_detail`(验收 8)
- [ ] 确认事务内**不写** `core_cash_flow`(资金流写入唯一归属 T-16,反双重归属;若 T-16 未落地此断言恒守住)
- [ ] 引擎恰好跑一次:`risk_alert` 1 条、RISK-002 不翻倍(验收 5/7/29)
- [ ] pytest 重写用例全绿;**全量回归 739+(含 test_convert_integration 改写)**
- [x] 确认:`core_share_lot.remain_qty` 扣减、`core_trade` 恰 2 行同 gid、两端 `core_holding` 更新、状态 `confirmed`(验收 2/10/24)
- [x] 重复触发 confirm_batch 同 as_of:第二次 0 处理,流水不重复(验收 24 后半 / 15)
- [x] 缺 T 日净值:状态 `nav_pending`、无流水、`remain_qty` 不变;补 nav 后重跑 → `confirmed`(验收 25)
- [x] T+1 复核不通过 → `rejected` + 占用释放(验收 26)
- [x] 部分成交:占用被抢 → `actual_qty < qty`、remark='partial'、未确认部分占用释放(验收 30)
- [x] 跨批次:各批按持有期计费、明细落 `core_convert_lot_detail`(验收 8)
- [x] 确认事务内**不写** `core_cash_flow`(资金流写入唯一归属 T-16,反双重归属;若 T-16 未落地此断言恒守住)
- [x] 引擎恰好跑一次:`risk_alert` 1 条、RISK-002 不翻倍(验收 5/7/29)
- [x] pytest 重写用例全绿;**全量回归 739+(含 test_convert_integration 改写)**
**风险**:确认事务变长(扣批次 + 流水 + 持仓 + 资金流)→ 死锁概率↑(沿用 1213 重试外壳 43cf2a1);nav_pending 无限挂起(SLA 兜底 T-12);并发双跑(锁 + 条件 UPDATE 双守卫)
**回滚点**:受理单可撤(确认前);本任务改动面大,先基线快照再改
**执行记录(2026-09-11 · 提交 `048f1a9`)**
真库 `verify_convert_confirm.py` **83 项一致 / 0 不一致**;全量 pytest **827 passed / 10 skipped**(T-6 基线 798)。
基准日取日历中位开市日(本次实跑 T=2027-01-14 / T+1=2027-01-15),并用 PRD §5.3.2 示例输入在真库上
**逐字节重放**:`68020.00 / 612.18 / 67407.82 / 333.36 / 67074.46 / 34945.54 / -0.0049`。
与计划的差异与裁定(**均已在代码注释与本节留痕**):
1. **确认段不做最低持有重判(计划缺口 · 本次新发现)**。计划 ①-⑧ 未指明确认段是否重跑
`plan_lots` 的最低持有判定。实测暴露:受理段已把 `qty` **收敛为实际全转量**(申请 48000
→ 落库 50000),确认段若重判会因 `leftover = 0` 得出 `forced_full_transfer = False`(漏报);
更危险的是 T→T+1 之间可用份额变化(部分成交 / 他人申赎 / 产品参数改)会让重判得出与
**受理承诺不一致**的结论 —— 例如可用涨到 60000 后反被判「留 12000 < 新阈值」而擅自
把客户指令扩大成全转。**裁定:强制全转是受理段决策**,受理段把结论落受理单
`remark='full_transfer'`(新增常量 `REMARK_FULL_TRANSFER`),确认段**只继承、不重判**
(`plan_lots` 不传 `min_hold_qty`)。`remark` 支持多标记 `;` 连接,
故部分成交的强制全转单记 `full_transfer;partial`。用例守护:
`test_confirm_does_not_promote_to_full_transfer`。
2. **新增三个共用模块**(计划未列):`format.py`(`D2/D4/new_id/q` 展示规格唯一出口)、
`audit.py`(`write_convert_audit` 唯一审计写入点)、`engine_call.py`
(`run_convert_engine` 引擎唯一调用点)。理由:受理段与确认段共用同一套量化 / 审计 /
引擎口径,若两段各写一份必然漂移。`engine_call.py` 的引入**同时完成了 T-8 的「引擎时机改」**
(调用点已迁到确认事务 commit 之后,且全仓恰 1 处调用)—— T-8 剩余工作仅为
`rules.amount_view` 与三处消费方同步。
3. **`nav.py` 未改**:确认段直接调 `core_ro.get_nav_on(product, T)`(T-3 已新增的精确匹配口径),
无需经 `nav.py`。计划第 4 条要删的「取 T−1 路径」(`_nav_as_of`)属 v1.0 `convert_fund`,
该函数已标 `⛔ Deprecated`,随 T-9 一并删除。
4. **`test_convert_integration.py` 未改写**:其现状依赖 v1.0 `convert_fund` 全链路;
T-7 另起 `tests/test_convert_confirm.py`(24 例)覆盖确认段。integration 改写与本计划
「test_convert_service 重写」一并在 **T-9(api 层落地后)** 做,届时端到端才有真接口可打。
5. **确认事务原子性口径收窄**:计划 ⑧「条件 UPDATE 置 confirmed」在实现中**并入
`apply_convert` 同一事务**(`ConvertApplyInput.convert_request_id` + gateway 内
`_confirm_request`),`rowcount != 1` → 抛 `ConfirmConflict` 整事务回滚。若按计划放在
事务外,会出现「批次已扣、受理单未置位」的中间态(超扣风险)。
6. **MySQL `rowcount` = changed rows 陷阱**:`nav_pending` 重试时
`SET status='nav_pending' WHERE status='nav_pending'` 返回 0,直接复用 `transition_status`
的冲突判定会把**正常重试误判为并发冲突**。已修为「状态未变则不迁移」+ 返回 `transitioned`
标志(sqlite 返回 1、MySQL 返回 0 的方言差异由此抹平)。
---
### T-8 · 引擎时机改(确认后跑一次)