diff --git a/AGENTS.md b/AGENTS.md index b07492d..fe3548b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ - 技术选型 / JWT:`docs/项目框架设计/技术选型和版本/` - Core 模拟底座:`docs/项目框架设计/Core模拟底座/` - 业务记忆分层:`docs/业务记忆管理/业务记忆管理手册.md` -- **基金转换线(第 5 步 todo 开发进行中 · T+1 受理/确认分离模型)**:**T-1 ~ T-8 ✅ / T-9 ~ T-17 ⬜**(当前基线 **829 passed / 10 skipped** 实测;真库 T-6 `verify_convert_accept.py` **37/37**、T-7 `verify_convert_confirm.py` **83/83**、T-8 `verify_convert_engine.py` **39/39**,退出码均 0;下一步 **T-9** = api 层:redeem 份额申报 + 撤单/确认/查询接口 + 网关分派);开工入口 **`交接文档.md` §B**(§B.11 下一条立项指引 = 最新状态)· `docs/PRD/PRD-基金转换交易.md`(**v1.1**)· `docs/项目框架设计/架构设计-基金转换交易.md`(**v2.1**)· `docs/项目框架设计/开发计划-基金转换交易.md`(**v2.0** · T-1~T-17 逐任务改法 + DoD) +- **基金转换线(第 5 步 todo 开发进行中 · T+1 受理/确认分离模型)**:**T-1 ~ T-9 ✅ / T-10 ~ T-17 ⬜**(当前基线 **834 passed / 10 skipped** 实测;真库 T-6 `verify_convert_accept.py` **37/37**、T-7 `verify_convert_confirm.py` **83/83**、T-8 `verify_convert_engine.py` **39/39**、T-9 `verify_convert_api.py` **88/88**,退出码均 0;下一步 **T-10** = 普通申赎批次维护 + T+2 校验 + rebuild_lots);开工入口 **`交接文档.md` §B**(§B.11 下一条立项指引 = 最新状态)· `docs/PRD/PRD-基金转换交易.md`(**v1.1**)· `docs/项目框架设计/架构设计-基金转换交易.md`(**v2.1**)· `docs/项目框架设计/开发计划-基金转换交易.md`(**v2.0** · T-1~T-17 逐任务改法 + DoD) - **架构改进线(✅ 2026-09-10 已结项,无待办)**:开工入口 **`交接文档.md` §C** - **风控模块主线**:**`交接文档.md` §A** diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 802cb8c..3d21d79 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -13,7 +13,7 @@ **⚡ 基金转换(convert)线 — 两条子线**: - **(一)v0.x 两阶段实时模型(已结项 2026-09-10)**:PRD **v0.9.3** + 架构 **v1.0.1** + 开发计划 **v1.0**,T-0 ~ T-13 全部完成(第 5+6 步闭环,最终 **739 passed / 10 skipped**,`CONVERT_STRESS=1` 并发 9/9,含死锁重试补丁 `43cf2a1`);**该模型已被下一线取代,其 PRD/架构/开发计划作废,代码保留作过渡基线**。 -- **(二)下一线 · T+1 受理/确认分离模型(进行中 · 2026-09-11)**:入口 **交接文档.md §B.11**(10 项真实业务拍板)· PRD **v1.1** + 架构 **v2.1** 已整体重写并经独立 AI 联网审查通过(0 阻断级),**已定稿**(2026-09-11 用户批准);**第 4 步开发计划 v2.0 已整体重做**(`docs/项目框架设计/开发计划-基金转换交易.md`,797 行:T-0~T-17 按架构 §15 重排 + §1.4 代码事实核对表 F-1~F-25 + §1.5 实现级裁定 R-1~R-15 + §5 十六条回归面 + §6 验收 31 条 + FR-C29/C30 全映射),**经三轮独立审核闭环(第一轮挑毛病 2 阻断+5 重要+3 可选 / 第二轮验证型 12 项零残留 / 第三轮白纸重审 C1~C5+O1~O4 + 验证型复核 22/22 落位),已定稿(2026-09-11 用户批准)**。**核心认知:不新增冻结列,在途占用由受理单推导;紧池 80% 病根在 T+1 串行确认下自动消失。** 下一步:**第 5 步 todo 开发进行中(T-1~T-8 ✅,**829 passed / 10 skipped** 零失败;T-6 真库 verify **37/37**、T-7 真库 verify **83/83**、T-8 真库 verify **39/39**,退出码均 0;下一步 T-9 api 层)**。**T-7 关键裁定:强制全转属受理段决策** —— 受理由 `plan_lots` 判定并把 `qty` 收敛为实际全转量 + 受理单 `remark='full_transfer'`,确认段**只继承不重判**(`plan_lots` 不传 `min_hold_qty`);重判会因 T→T+1 可用份额变化(部分成交/他人申赎/产品参数改)得出与受理承诺不一致的结论(擅自扩大客户指令)。**T-8 关键裁定**:引擎时机(R-7)的实现部分在 T-7 已完成,T-8 补的是**守护与验证**(AST 守护唯一调用点 / 受理不触引擎 / 确认链路真引擎正证+反证);⚠️ **T-8 不动去重口径** —— `amount_view` 三处消费方原样不动(属 T-11);`convert_fund` 的阶段 1.5 调用点随 T-9 删函数一并清除(只摘调用会让 v1.0 过渡函数「扣份额但不出预警单」,风控缺失比时机错更危险)。撤单范围:PRD §5.5 原表述与实现(仅 `accepted`)不矛盾、无需回填(第三轮 C1 订正)。已就绪:`fetch_nav.py` / `06-seed-nav.sql`(60 个真实净值日)/ `gen_trade_calendar.py` / `10-seed-trade-calendar.sql`(503 交易日)。 +- **(二)下一线 · T+1 受理/确认分离模型(进行中 · 2026-09-11)**:入口 **交接文档.md §B.11**(10 项真实业务拍板)· PRD **v1.1** + 架构 **v2.1** 已整体重写并经独立 AI 联网审查通过(0 阻断级),**已定稿**(2026-09-11 用户批准);**第 4 步开发计划 v2.0 已整体重做**(`docs/项目框架设计/开发计划-基金转换交易.md`,797 行:T-0~T-17 按架构 §15 重排 + §1.4 代码事实核对表 F-1~F-25 + §1.5 实现级裁定 R-1~R-15 + §5 十六条回归面 + §6 验收 31 条 + FR-C29/C30 全映射),**经三轮独立审核闭环(第一轮挑毛病 2 阻断+5 重要+3 可选 / 第二轮验证型 12 项零残留 / 第三轮白纸重审 C1~C5+O1~O4 + 验证型复核 22/22 落位),已定稿(2026-09-11 用户批准)**。**核心认知:不新增冻结列,在途占用由受理单推导;紧池 80% 病根在 T+1 串行确认下自动消失。** 下一步:**第 5 步 todo 开发进行中(T-1~T-9 ✅,**834 passed / 10 skipped** 零失败;T-6 真库 verify **37/37**、T-7 真库 verify **83/83**、T-8 真库 verify **39/39**、T-9 真库 verify_convert_api **88/88**,退出码均 0;下一步 T-10 普通申赎批次维护)**。**T-7 关键裁定:强制全转属受理段决策** —— 受理由 `plan_lots` 判定并把 `qty` 收敛为实际全转量 + 受理单 `remark='full_transfer'`,确认段**只继承不重判**(`plan_lots` 不传 `min_hold_qty`);重判会因 T→T+1 可用份额变化(部分成交/他人申赎/产品参数改)得出与受理承诺不一致的结论(擅自扩大客户指令)。**T-8 关键裁定**:引擎时机(R-7)的实现部分在 T-7 已完成,T-8 补的是**守护与验证**(AST 守护唯一调用点 / 受理不触引擎 / 确认链路真引擎正证+反证);⚠️ **T-8 不动去重口径** —— `amount_view` 三处消费方原样不动(属 T-11);`convert_fund` 的阶段 1.5 调用点随 T-9 删函数一并清除(只摘调用会让 v1.0 过渡函数「扣份额但不出预警单」,风控缺失比时机错更危险)。撤单范围:PRD §5.5 原表述与实现(仅 `accepted`)不矛盾、无需回填(第三轮 C1 订正)。已就绪:`fetch_nav.py` / `06-seed-nav.sql`(60 个真实净值日)/ `gen_trade_calendar.py` / `10-seed-trade-calendar.sql`(503 交易日)。 **仓库地图:** | 路径 | 状态 | 说明 | @@ -201,7 +201,7 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md 3. 是否需 customer_id 归属与 JWT RBAC? 4. Core 是模拟库只读还是 agent 库读写? 5. 如何验证?(`python -m pytest` 全量(当前 **739 passed / 10 skipped**,基线 510)· uvicorn 启动 + /health · SQL / sync 脚本 · 对照 REQUIREMENTS 验收列) -6. **当前有哪两条并行线?**(① 风控/架构改进线:**已结项**(510 基线绿、§7.2 七项冒烟 7/7 PASS、`037ce7e` 已核实早已推送);② **基金转换线**:v0.x 实时模型已结项(739 绿,**已被 T+1 模型取代**);**下一线 T+1 受理/确认分离模型进行中(2026-09-11)**:PRD v1.1 / 架构 v2.1 / 开发计划 v2.0 重写完毕并经独立审查收敛,**已定稿**(2026-09-11 用户批准),批准后进第 5 步 todo 开发(T-1~T-17;**当前进度 T-1~T-8 ✅,829 绿**))——动代码前先确认自己属于哪条线,别混淆前置条件。 +6. **当前有哪两条并行线?**(① 风控/架构改进线:**已结项**(510 基线绿、§7.2 七项冒烟 7/7 PASS、`037ce7e` 已核实早已推送);② **基金转换线**:v0.x 实时模型已结项(739 绿,**已被 T+1 模型取代**);**下一线 T+1 受理/确认分离模型进行中(2026-09-11)**:PRD v1.1 / 架构 v2.1 / 开发计划 v2.0 重写完毕并经独立审查收敛,**已定稿**(2026-09-11 用户批准),第 5 步 todo 开发进行中(T-1~T-17;**当前进度 T-1~T-9 ✅,834 绿**))——动代码前先确认自己属于哪条线,别混淆前置条件。 7. **远程分支到底还在不在?**(**在**。`git ls-remote --heads origin` 实测 `refs/heads/risk-control-agent` = `fffb78a`。⚠️ **判断远程存亡只能用 `git ls-remote`** —— 2026-09-10 **同日误报两次**,勿再踩。) **本仓特有异常(2026-09-10 深挖确认,别误判为 stale ref)**:`.git/refs/remotes/**` **写入不落盘** —— `git update-ref refs/remotes/origin/X ` 返回 0,但松散引用消失,**且整个 `refs/remotes/origin/` 目录被删** diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index c759b2f..226dbaa 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -9,7 +9,7 @@ **⚡ 并行新线 · 基金转换(convert)**—— **两条子线**: - **(一)v0.x 两阶段实时模型(已结项 2026-09-10)**:PRD **v0.9.3** + 架构 **v1.0.1** + 开发计划 **v1.0**,**T-0 ~ T-13 全部完成**(第 5 步 + 第 6 步闭环,**736 passed / 10 skipped**;7 个 convert 真库脚本复跑零回归;压测修复 2 处并发缺陷 + 死锁重试后补 `43cf2a1` 最终 **739 passed / 10 skipped**)。**注:该模型已被下一线取代(按下述),其设计文档作废,代码仍保留作过渡基线**。 -- **(二)下一线 · T+1 受理/确认分离模型(进行中 · 2026-09-11)**:入口 **交接文档.md §B.11** · PRD **v1.1** + 架构 **v2.1** 均已整体重写并经独立 AI 联网审查通过(0 阻断级),**已定稿**(2026-09-11 用户批准);**第 4 步开发计划 v2.0 已整体重做(`docs/项目框架设计/开发计划-基金转换交易.md`,T-1~T-17 按架构 §15 重排 + 代码事实 F-1~F-25 + 裁定 R-1~R-15 + 16 条回归面 + 验收 31 条 + FR-C29/C30 全映射),经三轮独立审核闭环(挑毛病 / 验证型 / 白纸重审 + 验证型复核 22/22 落位),已定稿(2026-09-11 用户批准)**。**第 5 步 todo 开发已开工(2026-09-11):T-1 ~ T-8 完成(全量 pytest 829 passed / 10 skipped 零失败;T-6 真库 verify 37/37、T-7 真库 verify 83/83、T-8 真库 verify 39/39,退出码均 0),下一步 T-9(api 层:redeem 份额申报 + 撤单/确认/查询接口 + 网关分派)**。撤单范围:PRD §5.5 原表述与实现(仅 `accepted`)不矛盾、无需回填(第三轮 C1 订正)。 +- **(二)下一线 · T+1 受理/确认分离模型(进行中 · 2026-09-11)**:入口 **交接文档.md §B.11** · PRD **v1.1** + 架构 **v2.1** 均已整体重写并经独立 AI 联网审查通过(0 阻断级),**已定稿**(2026-09-11 用户批准);**第 4 步开发计划 v2.0 已整体重做(`docs/项目框架设计/开发计划-基金转换交易.md`,T-1~T-17 按架构 §15 重排 + 代码事实 F-1~F-25 + 裁定 R-1~R-15 + 16 条回归面 + 验收 31 条 + FR-C29/C30 全映射),经三轮独立审核闭环(挑毛病 / 验证型 / 白纸重审 + 验证型复核 22/22 落位),已定稿(2026-09-11 用户批准)**。**第 5 步 todo 开发已开工(2026-09-11):T-1 ~ T-9 完成(全量 pytest 834 passed / 10 skipped 零失败;T-6 真库 verify 37/37、T-7 真库 verify 83/83、T-8 真库 verify 39/39、T-9 真库 verify_convert_api 88/88,退出码均 0),下一步 T-10(普通申赎批次维护 + T+2 校验 + rebuild_lots)**。撤单范围:PRD §5.5 原表述与实现(仅 `accepted`)不矛盾、无需回填(第三轮 C1 订正)。 ### 基金转换线待办(推荐顺序) @@ -35,7 +35,7 @@ - [x] **T-6 受理事务 ✅**:`accept_convert` 八步落地(幂等前置 → 产品/份额/适当性 → 受理日顺延 R-5 → 可用份额 R-3 → 落 `core_convert_request(accepted)` → 镜像 + 审计 → 202 回执);新增 `tests/test_convert_accept.py`;**真库 `verify_convert_accept.py` 37/37 一致(退出码 0)** - [x] **T-7 三段编排 + `confirm_service`(核心重写)✅(2026-09-11,提交 `048f1a9`)**:新增 `confirm_service.py`(`confirm_one` 8 步:读单+状态闸门 → T 日净值精确匹配(缺→`nav_pending`)→ 产品复核 + T+1 适当性复核 → 部分成交 R-10 → FIFO 扣批 + 折算 → 确认事务(扣批次 + 2 流水 + 转入批次 + 两端持仓 + 明细 + 受理单置 `confirmed`,同事务,`rowcount!=1` → `ConfirmConflict` 整体回滚)→ 引擎恰好一次(commit 后,异常不阻断)→ 镜像 + 审计;`confirm_batch`:整批锁 + 业务日捞单窗口 + 串行 + 单笔容错);抽出 `format.py`/`audit.py`/`engine_call.py` 三个共用出口;`convert_service` 加 `cancel_convert`(T 日撤单两道闸门)+ `_t1_t2_dates`,受理响应改 PRD §5.3.1 字段;`convert_core_repository.ConvertApplyInput` 加 `convert_request_id`/`diff_fee`/`request_remark` 并内置 `_confirm_request` 状态守卫;`convert_request_repository` 加 `_end_of_day` 闭区间 + `reject()`。**单测 `tests/test_convert_confirm.py`(24 例)+ 真库 `verify_convert_confirm.py` 83/83 一致(含 PRD §5.3.2 示例真库逐字节重放)**;全量 **827 passed / 10 skipped**。**关键裁定(计划缺口,已回写开发计划)**:强制全转是受理段决策,受理单 `remark='full_transfer'` 传承,确认段**只继承不重判**(`plan_lots` 不传 `min_hold_qty`)—— 重判会因 T→T+1 可用份额变化擅自扩大客户指令。 - [x] **T-8 引擎时机收口 ✅(2026-09-11,提交 `57f16a5`)**:**R-7 的实现部分在 T-7 已完成**(`engine_call.run_convert_engine` 是引擎唯一出口、位于确认事务 commit 之后;`accept_convert` 自实现起不含引擎调用)。本轮补**守护与端到端验证**:① `test_process_convert_event_has_single_call_site` —— 用 **AST** 统计 `app/` 下 `process_convert_event(` 的 Call 节点**恰 1 处**且宿主为 `run_convert_engine`(字符串匹配会被 `engine_call.py` 说明段误判);② 受理无副作用用例补「不触引擎」断言(受理后 `risk_alert` 0 行、无 `pass`/`engine_error` 审计,验收 29 前半);③ `test_real_engine_on_confirm_dedupes_daily_total` —— 确认链路接**真引擎**(不注入 hook)的正证 + 反证(阈值夹逼 → RISK-002 不命中 / 阈值下调 → 命中),并断言一张单 + `payload.events` 两条 + 去重不删行(验收 5/6/7)。两条新用例**均做突变验证**(加第二处调用 / 禁用引擎调用 → 变红 → 还原)。真库 `verify_convert_engine.py` **整体切换 T+1 链路**(此前走 v1.0 `convert_fund`):受理 → 确认,新增 A0 组「受理不触引擎」,折算期望改用 T 日净值,**真库 39/39 一致**。**订正**:原记「T-8 剩余工作仅为 `rules.amount_view`」有误 —— F-10 明确 T-8 **不动**去重口径(属 T-11 范围),T-8 剩余的是**验证面**。 -- [ ] T-9 ~ T-17(API 受理接口 / 撤单 / 调换 / 工具 / 补偿 / 回归 / D27 产品级舍入 / D28 巨额赎回 / D29 资金流中转 / D30 A/C 互转) +- [ ] T-10 ~ T-17(普通申赎批次维护 + T+2 校验 / 汇总去重 / 补偿改写 / 全量回归 / D27 产品级舍入 / D28 巨额赎回 / D29 资金流中转 / D30 A/C 互转) > **第 0~2 批结果(2026-09-10)**:基线 **510 passed** → 批 0 后 **516 passed / 3 skipped**(+3 T-0 用例 +3 T-0b 引擎用例)→ 批 1(T-1)后**仍 516 passed / 3 skipped**(只加表与种子,未加用例 → **零回归**);**T-1 数据层断言 8/8 PASS**(含新增断言 ⑧:费率档 ↔ `product_type` 匹配,越档即 FAIL)→ 批 2(T-2 + T-2b)后 **609 passed / 3 skipped**(**+93 纯函数用例**,零回归);T-2b 实算脚本 15/15 与 PRD §5.3 一致(退出码 0)。 > ⚠️ **以下四条引用块均为 v0.x 两阶段实时线记录,其 T 编号与新线(T-1~T-17)无关、勿混**:新线 T-11 亦涉 `amount_view`,与 v0.x T-11「提升为公开函数」是两回事。 diff --git a/docs/项目框架设计/开发计划-基金转换交易.md b/docs/项目框架设计/开发计划-基金转换交易.md index 59841f8..439edcc 100644 --- a/docs/项目框架设计/开发计划-基金转换交易.md +++ b/docs/项目框架设计/开发计划-基金转换交易.md @@ -170,6 +170,8 @@ | 8 | 引擎时机 | FR-C28 确认后跑 | §15 T-8 | **以 PRD 为准**:T-8 改时机 | 无冲突 | | 9 | 撤单范围 | 状态机表「`cancelled` ↔ accepted / nav_pending」(PRD :178/:181)与 §5.5 撤单接口(PRD :626:「T 日 15:00 前且状态 accepted / nav_pending → 200 置 cancelled」)**口径一致**——两者都允许 nav_pending 撤单。但 nav_pending 只在 T+1 确认时缺净值才出现,彼时 T 日 15:00 撤单窗口已过 | 架构任务 T-9(对接 R-9) | **裁定:仅 `accepted` 可撤(R-9)**;nav_pending 出现于 T+1,撤单窗口(T 日 15:00 前)早已关闭,PRD §5.5 所列条件在业务时序上不可达,保留 PRD 原表述但实现按 R-9 收紧为 `accepted` | 功能结果与「T 日 15:00 前」语义一致;第一轮审核曾误判「PRD 内部矛盾」(本行已订正:**矛盾不存在**,仅实现侧按窗口语义收紧) | | 10 | 15:00 截点 | PRD §2.6.1「`convert_cutoff_time` 为配置项(默认 15:00),**不得硬编码**」 | 架构任务 R-5(R-5 初稿写死 `before_cutoff(dt, 15:00)`) | **以 PRD 为准**:T-1 settings 新增 `convert_cutoff_time="15:00"`;TradingCalendar.before_cutoff 读 settings,禁硬编码 | 审核 B1 | +| 11 | **T-9 三个接口路径** | §5.5 撤单 `POST /api/simulate/trade/convert/{convert_group_id}/cancel`;§5.7 确认 `POST /api/admin/convert/confirm?accept_date=…`、查询 `GET /api/simulate/trade/convert/{convert_group_id}` | 架构 §15 T-9 **未给路径**;**本计划 T-9 初稿自拟** `POST /api/convert/{gid}/cancel` / `POST /api/convert/confirm` / `GET /api/convert/{gid}` | **以 PRD 为准**(2026-09-11 用户拍板):T-9 按 PRD 三条路径实施,本计划初稿的自拟路径**作废** | PRD 是对外需求契约;其路径延续 `/api/simulate/trade/...` 与 `/api/admin/...` 既有风格,而初稿的 `/api/convert/...` 是新的顶格前缀、与全仓路由风格(`/api/risk`·`/api/simulate`·`/api/admin`·`/api/chat`)不一致。**根因留痕(审查为何未发现)**:计划的三轮独立审查均未发现本差异 —— §9 审核清单 9 条只对「架构 §15 / 代码事实行号 / tests 回归面 / PRD §9 **验收条目**」四类,**无一条对「对外契约逐字」**;且 §1.4 代码事实核对对的是**现有代码**、§1.3 差异裁定对的是「PRD **vs 架构**」,**都不覆盖「PRD vs 本计划自身」**。审查实际做的是「计划有没有**覆盖** PRD 的要求」,而非「计划有没有**擅自改** PRD 的承诺」—— 接口路径属后者,落在盲区。已补 §9 第 10 条 + 本表第 11/12 条 | +| 12 | **确认接口参数 `accept_date` 的名与义** | §5.7 代码块注释写「按**受理日**批量确认」、参数名 `accept_date`;但同节表格口径写「可指定**业务日**」—— **PRD 内部字面冲突** | R-1:`confirm_batch(as_of)` 的 `as_of` 是**业务确认日(T+1)**,捞单窗口 = `[as_of 上推 sla_days 个交易日, as_of]` 的**受理日** | **裁定(2026-09-11)**:参数名**保留 PRD 的 `accept_date`**(对外契约字面不动),**语义取同节表格口径「业务日」**,直接映射 `confirm_batch(as_of=accept_date)`;接口响应回显实际生效业务日 | PRD 表格口径与 R-1 实现一致(业务日);若按参数名取「受理日」语义,需给 `confirm_batch` 增加「精确单日受理」能力 —— 会改动 T-7 已验收的捞单窗口逻辑,超出 T-9 范围,且无业务必要(窗口上推 SLA 本就是为消化净值晚公告造成的积压)。**遗留(已在接口 docstring 标注)**:窗口是「上推 SLA」的**批量**语义,非「精确某受理日」 | ### 1.4 开工前代码事实核对表(§2 产物 · 全部带 文件:行号) @@ -572,10 +574,10 @@ **改法**: 1. `simulate.py` convert 分支:返回改 `202 + {convert_group_id, status: 'accepted', requested_at}`(F-5 语义升级:202 从「未抢到执行权」改为「受理成功」;「未抢到执行权」仍 202 但并存 status 区分——**两类 202 需分辨**:`status='accepted'`=受理成功 / `status='processing'`=并发执行中,PRD §8.3) -2. **新增 `app/api/convert_admin.py`**: - - `POST /api/convert/{group_id}/cancel`:撤单(R-9;accepted + 窗口内 → cancelled;否则 409 `CANCEL_NOT_ALLOWED`);鉴权:**本人可撤(交易 owner 闸门,红线 7)** - - `POST /api/convert/confirm`(body: `as_of`):触发批处理(R-1 → `confirm_service.confirm_batch(as_of)`;运维接口,鉴权同 admin 级) - - `GET /api/convert/{group_id}`:查询状态 + 折算结果(确认后才有折算金额,Q2;未确认 → status + 无金额);鉴权:本人/代理人可查(查询 scope 闸门) +2. **新增 `app/api/convert_admin.py`**(**路径以 PRD 为准**,见 §1.3 裁定 11/12): + - `POST /api/simulate/trade/convert/{convert_group_id}/cancel`(PRD §5.5):撤单(R-9;accepted + 窗口内 → cancelled;否则 409 `CANCEL_NOT_ALLOWED`);鉴权:**本人可撤(交易 owner 闸门,红线 7)** + - `POST /api/admin/convert/confirm?accept_date=<业务日>`(PRD §5.7):触发批处理(R-1 → `confirm_service.confirm_batch(as_of=accept_date)`;运维接口,鉴权同 admin 级);**参数名沿用 PRD `accept_date`、语义取 PRD §5.7 表格口径「业务日」**(裁定 12) + - `GET /api/simulate/trade/convert/{convert_group_id}`(PRD §5.7):查询状态 + 折算结果(确认后才有折算金额,Q2;未确认 → status + 无金额);鉴权:本人/代理人可查(查询 scope 闸门) 3. `trade_gateway.py`:**`SUPPORTED_TRADE_TYPES` 不动(F-25/O2)**——convert 走既有独立 `_submit_convert` 分派(trade_gateway.py:105),把内部实时确认改调 `accept_convert`(受理),**不把 CONVERT 加进支持元组**(避免改变申赎路径语义) 4. `errors.py`:新增 `CANCEL_NOT_ALLOWED`(409)/ `CONCURRENT_CONFLICT`(锁冲突);既有错误码保留 5. redeem 改份额申报(D26 · R-6):`TradeRequest` redeem 分支 `qty` 入参(替代 amount 反算);网关 redeem 调 `_maintain_lots` 新签名 @@ -583,13 +585,15 @@ **依赖**:T-7 **DoD**: -- [ ] API 契约用例:受理 → 202 + accepted;**撤单**窗口内成功(cancelled + 占用释放)、窗口外 409(验收 23);确认后撤 → 409 -- [ ] 查询接口:确认前无折算金额字段、确认后有(Q2) -- [ ] 两类 202 分辨用例存在 -- [ ] **`SUPPORTED_TRADE_TYPES` 维持 2 类型(SUBSCRIBE/REDEEM,O2)**;convert 走独立分派不再 400(验收 1) -- [ ] redeem 份额申报用例:`qty` 入参与 `amount` 反算路径断言的既有用例**改写后**绿(回归面 F-25→R-回归 1) -- [ ] 撤单/查询接口鉴权用例:本人可撤/可查、代理人可查不可撤、他人 403(红线 7) -- [ ] pytest 全量绿(test_integration_risk 中旧 400 断言已改为受理 202;test_risk_api 无 convert 断言、零改动,C4) +- [x] API 契约用例:受理 → 202 + accepted;**撤单**窗口内成功(cancelled + 占用释放)、窗口外 409(验收 23);确认后撤 → 409 +- [x] 查询接口:确认前无折算金额字段、确认后有(Q2) +- [x] 两类 202 分辨用例存在 +- [x] **`SUPPORTED_TRADE_TYPES` 维持 2 类型(SUBSCRIBE/REDEEM,O2)**;convert 走独立分派不再 400(验收 1) +- [x] redeem 份额申报用例:`qty` 入参与 `amount` 反算路径断言的既有用例**改写后**绿(回归面 F-25→R-回归 1) +- [x] 撤单/查询接口鉴权用例:本人可撤/可查、代理人可查不可撤、他人 403(红线 7) +- [x] pytest 全量绿(test_integration_risk 中旧 400 断言已改为受理 202;test_risk_api 无 convert 断言、零改动,C4) + +**✅ T-9 完成(2026-09-11)**:`app/api/convert_admin.py`(**路径以 PRD 为准**,裁定 11/12)三接口 + `simulate.py` convert 分支 202 + `trade_gateway` 分派改调 `accept_convert`(`SUPPORTED_TRADE_TYPES` 维持 2 类型)+ redeem 份额申报 + 错误码新增 `CANCEL_NOT_ALLOWED`(409)/`CONCURRENT_CONFLICT`(409)。**验证**:① 端到端 `tests/test_convert_integration.py` 重写为 T+1 两段链路 + 新增 API 契约/鉴权用例(**13 passed**);② 全量 pytest **834 passed / 10 skipped**(基线 829 + 新增 5);③ 真库新脚本 `verify_convert_api.py`(HTTP 全链路)**88/88**,T-6/T-7/T-8/v1.0 四脚本复跑**零回归**(37/37 · 83/83 · 39/39 · 35/35);④ **3 组突变验证**(受理回执/未确认查询泄漏折算字段、顾问误放行撤单)**全部命中后还原**。**执行期发现并修正 2 处口径缺陷**:① `_rebuild_quote` 的 `out_nav` 原从「金额÷份额」反推(0.005 档 HALF_UP 舍入致 1.3604 → 1.3600 漂移)→ 新增 `_out_nav_rebuild` 优先直读 `core_convert_lot_detail.nav`(与确认段首次折算同源,缺失才回退反推);② `_accept_idempotent` 字段面扩展与受理回执对齐(补 `requested_qty/qty/forced_full_transfer/accept_date/confirm_date/available_date/cancel_deadline/actual_qty`,原仅 qty/actual_qty/requested_at → 幂等重试客户端否则拿不到预计确认日)。 **风险**:契约变更打穿 test_integration_risk 的 convert→400 断言 → 回归面 §5 处置(test_risk_api 无 convert 断言、非风险项,C4) **回滚点**:API 加字段不删旧字段(兼容期);撤单接口新增无破坏 @@ -900,4 +904,8 @@ 6. **依赖排序是否与架构 §15 冲突**(P1 已剔除 T-8;T-16/T-17 依赖已改 T-7/T-6) 7. **验收 31 条是否无落空**(对照 §6 表逐条;**C2b:FR-C29/C30 已补独立行 32/33**) 8. **R-b / 红线 1/2/3 是否被无意违反**(trade_type 仍是 redeem/subscribe;amount_view 三处同步;_q 单点量化) -9. **B1/B2 是否已闭环**(`convert_cutoff_time` 配置全链读 settings;确认段净值判定用 `get_nav_on` 精确匹配、禁 `get_nav_as_of` 回退) \ No newline at end of file +9. **B1/B2 是否已闭环**(`convert_cutoff_time` 配置全链读 settings;确认段净值判定用 `get_nav_on` 精确匹配、禁 `get_nav_as_of` 回退) +10. **对外契约是否逐条与 PRD 比对**(**T-9 实测补** · 2026-09-11):凡本计划中**新出现或改写**的对外契约 —— **API 路径 / 参数名 / 响应字段名 / HTTP 状态码 / 错误码** —— 必须回到 PRD 原文逐条对齐,并注明 PRD 出处(章节或行号)。 + - **为什么单列一条**:T-9 初稿自拟的三个接口路径与 PRD §5.5/§5.7 给出的路径**完全不同**,而**三轮独立审查(挑毛病 / 验证型 / 白纸重审)全部未发现** —— 原因是本清单前 9 条对的是「架构 §15 / 代码事实行号 / tests 回归面 / PRD §9 验收**条目**」,**没有一条对接口契约本身**;审查做的是「计划是否**覆盖** PRD 的要求」,而非「计划是否**擅自改** PRD 的承诺」,契约改写恰好落在盲区。 + - **做法**:对本计划所有 `/api/...` 字符串、`FIELD_NAME` 常量、`409`/`422` 等状态码做一次 `grep`,逐条问「PRD 原文怎么写的?出处是哪一节?」,**自拟而无出处的必须标出并请用户裁定**。 + - **同源规律(与铁律 10 一致)**:**带「文件:行号」的断言会被逐条验;不带出处的自由书写没有锚点 → 必漏**。 \ No newline at end of file