T-8/T-9 提交时两次依据 git branch -r / branch -vv 判定「远程分支不见了」并向用户报警, 两次均为误报。本次深挖确认根因,并订正此前写入的错误表述。 - docs/memory/MEMORY.md 交接清单第 7 问重写: 本仓 .git/refs/remotes/** 写入不落盘 —— git update-ref refs/remotes/origin/X 返回 0、 reflog 也建了,但松散引用消失,且会把整个 refs/remotes/origin/ 目录删掉(含手工放进的文件); git fetch 报告 [new branch] ... -> origin/risk-control-agent 后同样不落盘。 故此前记的「stale tracking ref,git fetch 即恢复」是错的,fetch 并不修复。 对照证据:写 refs/heads/** 完全正常;同一 git(2.55.0.windows.3)在 /tmp 新建仓库里写 refs/remotes/** 完全正常 → 只此仓库特有;已排除外部进程删目录(marker 静置存活)、 重解析点(lstat FILE_ATTRIBUTE_REPARSE_POINT=False)、hooks / commondir / 浅克隆。 补绕过方案:packed-refs 是可靠存储,手工追加 <sha> refs/remotes/origin/<name> 后 git branch -r 立即正常(还原=删掉该行)。 补影响面:提交 / fetch 抓对象 / push 均不受影响(git push --dry-run 实测 fffb78a..d0097d6 成功),仅影响本地显示与上游跟踪。 顺手订正验证条目里过期的「516 passed / 3 skipped」→ 697,及第 73 行同款旧表述。 - docs/memory/2026-09-10.md 追加排查记录:7 条可复现证据 + 三步对照实验 + 绕过方案 + 影响面。
433 lines
30 KiB
Markdown
433 lines
30 KiB
Markdown
# 2026-09-10 工作日志
|
||
|
||
> 本日两条线:**架构改进线收尾闭环**(本文主体)+ 基金转换线**第 4 步开发计划待启动**。
|
||
|
||
---
|
||
|
||
## 一、接手与状态核对(会话起始)
|
||
|
||
读入:`docs/交接文档-基金转换.md`(v1.0)· `docs/memory/MEMORY.md §0` · `docs/交接文档-架构改进.md`(v1.1)
|
||
· `docs/项目框架设计/架构设计-基金转换交易.md §15/§15.1` · `docs/memory/{TODO,ITERATION}.md` · `AGENTS.md`。
|
||
|
||
**基线复核**:`python -m pytest -q`(系统 Python 3.13.14)→ **510 passed**(9.93s)。
|
||
|
||
**设计资产核对**:`tests/_ddl.py:50-54` 的 `core_holding` 仍为 `market_value`/`quantity` 且**无 PK** ——
|
||
与 T-0 描述**一致**,架构文档无误。
|
||
|
||
### ⚠️ 核对中发现的两处「文档与实况不符」(以实况为准)
|
||
|
||
1. **`037ce7e`「未 push」是过期表述。**
|
||
实测 `git ls-remote origin`:`refs/heads/risk-control-agent` = **`fffb78a`** = 本地 HEAD;
|
||
`git merge-base --is-ancestor 037ce7e fffb78a` → 成立。即**架构改进线全部提交(含其后 5 个文档提交)早已推送**。
|
||
本地 `git branch -vv` 显示 `origin/risk-control-agent: gone` **只是本地远程跟踪引用失效**,非远程分支被删。
|
||
2. **基金转换线 6 个设计文件全部 untracked**(未纳入版本库):`docs/PRD/PRD-基金转换交易.md`、
|
||
`docs/项目框架设计/架构设计-基金转换交易.md`、`基金转换-审查意见处置表.md`、
|
||
`评审待办-风控主架构与基金转换.md`、`docs/交接文档-基金转换.md`、`scripts/dev/calc_convert_demo.py`。
|
||
→ 用户拍板:**先 commit 入库(只 commit,不 push)**。
|
||
|
||
---
|
||
|
||
## 二、架构改进线 §7.2 手工冒烟(七项,全部代跑,2026-09-10)
|
||
|
||
用户授权「七项全部代跑」。所有临时改动**跑完即恢复**。
|
||
|
||
### 第 1 项 · 无 key 启动告警 ✅ PASS
|
||
|
||
- 本机 `.env` 的 `DEEPSEEK_API_KEY` **本就为空**(实测 `raw_value_len = 0`),故「清空」态即当前态。
|
||
- `uvicorn app.main:app`(端口 8123)→ **1 秒就绪**,`/health` → `200 {"status":"ok","env":"development"}`。
|
||
- 进程存活 → **告警不阻塞启动** ✓。
|
||
- 日志命中:`DEEPSEEK_API_KEY 未配置,对话将走降级回复(前缀 (LLM 未配置:请在 .env 设置 DEEPSEEK_API_KEY 后重启)),LLM 能力不可用`(`app/main.py:66-72`)。
|
||
|
||
### 第 2 项 · 恢复 key → 无告警 ✅ PASS(**口径替代,已留痕**)
|
||
|
||
- **替代说明**:本机无真实 DeepSeek key,「恢复 key」一支改用**非空占位值**(`sk-smoke-placeholder-not-a-real-key`)。
|
||
告警分支只判 `not settings.deepseek_api_key`,**不校验 key 有效性**,故该替代足以验证条件分支。
|
||
- 改后 `key_present: True` → 启动(端口 8124)→ `/health` 200 → 日志**未出现**该告警 ✓。
|
||
- 顺带验证 trace 中间件:响应头 `x-trace-id: trc-48349abc526e4055` / `x-request-id: req-612a3f2acb8349d6` ✓。
|
||
- **`.env` 已还原**,`key_present: False`(原状)✓。
|
||
|
||
### 第 3 项 · 交易阻断 + 放行各一次 ✅ PASS
|
||
|
||
| 场景 | 请求(debug 头 `X-Debug-Role: risk_demo` / `X-Debug-Actor: STAFF-DEMO`) | 响应 |
|
||
| --- | --- | --- |
|
||
| 阻断(SOP A-2) | CUST-4001 / PROD-161725 / subscribe / 20000 | `blocked=true`,`block_response_code=SUIT_AGE_CONFIRM`,`needs_branch_confirm=true`,`rule_refs=["FM-01"]` |
|
||
| 放行(SOP A-3) | CUST-3001 / PROD-510300 / subscribe / 500000 | `blocked=false`,`triggered_rules=["RISK-001","RISK-002"]`,`alert_ids=["ALT-20260910-7BCBBC94"]` |
|
||
|
||
**与 SOP 记录的预期逐字一致。** 库内核验:
|
||
|
||
- `core_trade`:阻断单 `TRD-20260910-C1D8AA26` **未落库** ✓;放行单 `TRD-20260910-FE545DF0` 已落库(`confirmed`, 500000.00)✓
|
||
- `risk_suitability_log`:CUST-4001 `is_blocked=1` ✓;`risk_alert` 出 `suitability` 单 `ALT-20260910-652280E1`(`pending_review`, score 90)✓
|
||
- `audit_log`:`suitability_block / suitability_alert_created`、`trade_request / suitability_blocked`(`rule_id=SUIT_AGE_CONFIRM`)、
|
||
`trade_request / trade_accepted`(放行)、`risk_judgement / alert_created` ✓
|
||
|
||
### 第 4 项 · 预警聚合行为不变 ✅ PASS
|
||
|
||
SOP A-4:CUST-9527 / PROD-510300 / subscribe / 1000 **连发 4 笔**:
|
||
|
||
| 笔次 | `triggered_rules` | `alert_ids` |
|
||
| --- | --- | --- |
|
||
| 1 | `[]` | `[]` |
|
||
| 2 | `[]` | `[]` |
|
||
| 3 | `["RISK-003"]` | `["ALT-20260910-C8600425"]` |
|
||
| 4 | `["RISK-003"]` | `["ALT-20260910-C8600425"]` ← **与第 3 笔同一张单(并入,非新建)** |
|
||
|
||
库内核验:
|
||
|
||
- `risk_alert` CUST-9527 当日 **`pending_review` 单数 = 1** ✓;`payload.events` **长度 = 2** ✓
|
||
- `audit_log` 呈 **`alert_created`(1184) → `alert_appended`(1187)`** 序列,聚合语义在审计层可证 ✓
|
||
- 4 笔交易全部落库(`TRD-20260910-B66CB0D8` / `B39E1456` / `83D58A41` / `79EDC16E`)✓
|
||
|
||
> 注:`risk_alert.status` 实际取值为 **`pending_review`**(文档口头简称「pending」)。首次核库我按 `status='pending'` 查得 0 条,
|
||
> 系**断言写错**而非系统问题,修正后为 1 ✓。
|
||
|
||
### 第 5 项 · Redis 不可用 → 退回进程内锁 ✅ PASS(**口径替代,已留痕**)
|
||
|
||
- **替代说明**:本机 Redis 为 `redis-server.exe`(PID 7716,Services 会话),**无文档化启动方式、`redis-server` 不在 PATH、
|
||
`sc.exe` 被本机安全策略拉黑**,杀掉后无法保证原样拉起(且 `FLOW.md` 明示其非权威数据源)。
|
||
改用 **`REDIS_URL=redis://127.0.0.1:6399/0`(死端口)**:应用侧抛 `ConnectionError`,与真宕机**同一条代码路径**
|
||
(`redis_gateway._ensure()` → 命令异常 → `locks._acquire_redis` 的 `except Exception` → `unavailable` → 进程内锁),且完全可逆。
|
||
- 结果:启动**不被阻塞**(1s,`/health` 200)→ 交易返回 `blocked=false`,`alert_ids=["ALT-20260910-7BCBBC94"]`,**业务照常完成** ✓
|
||
- 日志**明确出现 2 次** `Redis 锁不可用,退回进程内锁`:
|
||
`lock:agg:event:CUST-3001:2026-09-10`(`alert_service` 聚合锁)、`lock:l3:CUST-3001`(`profile_l3` L3 锁)✓
|
||
- 底层异常:`redis.exceptions.ConnectionError: Error 10061 connecting to 127.0.0.1:6399`,被正确吞噬未上抛 ✓
|
||
|
||
### 第 6 项 · Redis 恢复 → 行为一致 ✅ PASS
|
||
|
||
- `redis.Redis.from_url(settings.redis_url).ping()` → **True**(`redis://127.0.0.1:6379/0`)。
|
||
- 同笔交易 → `blocked=false`,`alert_ids=["ALT-20260910-7BCBBC94"]`(并入同单)✓
|
||
- 日志中 `退回进程内锁` **命中 0 行** → 确实走 Redis 锁路径,**未降级** ✓
|
||
- `triggered_rules` 由 `["RISK-001","RISK-002"]` 变为 `+RISK-003`:CUST-3001 当日累计至第 3 笔触发频次规则,
|
||
**属预期累积行为,非不一致**。
|
||
|
||
### 第 7 项 · 调换装饰器验证 T-202 守卫变红 ✅ PASS(本项价值最高)
|
||
|
||
守用例:`tests/test_audit_middleware.py::test_audit_middleware_runs_inside_trace_middleware:210`。
|
||
|
||
| 步骤 | 结果 |
|
||
| --- | --- |
|
||
| ① 基线 | `1 passed`(绿) |
|
||
| ② 调换 `main.py` 两装饰器(trace 先注册 → 变最内层;audit 后注册 → 变最外层) | 已调换,行序:`trace_middleware`@89 / `audit_middleware_entry`@117 |
|
||
| ③ 跑守卫 | **`1 failed`** ✓ 变红 |
|
||
| ④ 失败断言原文 | `AssertionError: audit 若先于 trace 执行,此处会静默为空` / `assert ''` @ `test_audit_middleware.py:221` |
|
||
| ⑤ 还原 | `sha256` 前后同为 `d9d7559445dffd2a4576cdc5454030d3acae5fd4c076de8daa5a35f53802510a` → **字节级一致** ✓ |
|
||
| ⑥ 再跑守卫 | `1 passed`(转绿)✓ |
|
||
| ⑦ 现场核对 | `git status --short app/main.py` 空、无残留 `*.smoke.bak` ✓ |
|
||
|
||
→ **守卫确实能抓住「audit 先于 trace 执行 → 全站审计静默丢 trace_id 且不报错」这一回归**。设计意图达成。
|
||
|
||
---
|
||
|
||
## 三、冒烟副产物与现场恢复
|
||
|
||
### 3.1 集成测试失败(防护行为,非回归)
|
||
|
||
冒烟后在真 MySQL 上跑全量:**507 passed / 3 failed**,全部落在 `tests/test_integration_risk.py`:
|
||
|
||
- `_assert_a3_clean` → `CUST-3001 今日已有 confirmed 交易(影响 RISK-002 累计/RISK-003 频次口径)`(我的 `TRD-20260910-FE545DF0`)
|
||
- `_assert_a4_clean` → `CUST-9527 今日已有 confirmed 交易(A-4 频次计数失真)`(我的 `TRD-20260910-B66CB0D8`)
|
||
- `test_a7_handle_state_machine_compliance_forbidden` → 级联 `KeyError: 'a3_alert_id'`
|
||
|
||
→ 这正是**核查单⑥「演示/测试同日交叉防护」前置断言**在按设计工作(交接文档已预警)。
|
||
|
||
### 3.2 演示库重灌(用户拍板:现在重灌)
|
||
|
||
按 `docs/项目框架设计/演示SOP-风控模块.md §2` 脚本化方式执行(`MYSQL_PWD` 传密码,避开 `reset.ps1` 交互 `-p` 卡死):
|
||
|
||
1. `DROP DATABASE IF EXISTS jinrong_core; DROP DATABASE IF EXISTS jinrong_agent;`
|
||
2. 依序灌 11 个 SQL:`scripts/core/00~06` → `表设计/01-mysql-共用底座.sql` → `表设计/02-mysql-agent专用.sql`
|
||
→ `scripts/agent/seed-aml-list.sql` → `scripts/demo/prepare_risk_demo.sql`
|
||
3. `python scripts/sync/sync_advisor_rel.py`
|
||
|
||
**完成标志三项全部吻合**:AML 名单 **8** 条 · `prepare_risk_demo` **7** 行测评(`expires_at` 剩余 **275** 天)·
|
||
`sync_advisor_rel` upserted **33** rows。
|
||
|
||
### 3.3 全量复跑(用户要求「改完后再跑一次」)
|
||
|
||
**`510 passed, 656 warnings in 9.52s`** → **复绿** ✓
|
||
|
||
现场状态:`.env` 已还原(key 为空、`REDIS_URL` 为 6379)✓ · `app/main.py` 字节级还原且 git 无改动 ✓ · 无残留 `.smoke.bak` ✓。
|
||
|
||
---
|
||
|
||
## 四、回写清单
|
||
|
||
| 文档 | 改动 |
|
||
| --- | --- |
|
||
| `docs/项目框架设计/TODO-架构改进.md` | T-107 手工冒烟勾选 + 完成判定块三勾 + 状态块改「全部闭环」+ push 状态纠正 |
|
||
| `docs/交接文档-架构改进.md` | **v1.1 → v1.2**;状态块重写(push 纠正 + 两处替代口径留痕);§7.3 全勾;§10 重写为结项 |
|
||
| `docs/交接文档-基金转换.md` | §9「另一条线的遗留」改为**已闭环**,纠正 push 表述 |
|
||
| `docs/memory/MEMORY.md` | §0 末尾「待办两条线」改为「架构改进线已结项」+ push 纠正常见坑 |
|
||
| `docs/memory/TODO.md` | 模块侧开放项「架构改进线 · 收尾」勾选并附七项证据摘要 |
|
||
| `docs/memory/ITERATION.md` | 新增 **2026-09-10 架构改进线收尾闭环** 迭代行 |
|
||
| `docs/memory/2026-09-10.md` | **本文件(新建)** |
|
||
|
||
---
|
||
|
||
## 五、下一步
|
||
|
||
**架构改进线:结项,无待办。**(可选:push 后打里程碑 tag,待用户拍板)
|
||
|
||
**基金转换线:第 4 步「产出开发计划」** —— 输入已齐备:
|
||
`docs/项目框架设计/架构设计-基金转换交易.md` §15(任务映射 T-0~T-13)+ §15.1(依赖拓扑与并行分组)+ §12(风险表);
|
||
产出落点 `docs/项目框架设计/开发计划-基金转换交易.md`(待创建)。
|
||
|
||
---
|
||
|
||
## 六、交接文档合并为单一入口 + 全仓指向统一(2026-09-10 晚)
|
||
|
||
### 6.1 用户指令与判定
|
||
|
||
- 指令:「由版本库的交接文档为主,这两个交接文档你合并成一个新的放到这次更新」+ 指向项目根 `交接文档.md`。
|
||
- 追加纠正:**「统一一下,不是早都说过交接文档是给你 AI 接手用的吗」** ——
|
||
**交接文档的定位 = AI 接手入口**,所以全仓指向必须**唯一**,且**绝不能指向过期文件**,否则新会话会被误导。
|
||
(`docs/` 下两份留档停在 v1.0 / v1.1,不含 T-2/T-2b、609 passed、D20 实施等进度 —— 是个真实陷阱。)
|
||
|
||
### 6.2 落地
|
||
|
||
**项目根 `交接文档.md` → v3.0(545 行)**,三线合并为单一入口:
|
||
**§0 公共层**(项目是什么 / 铁律与禁止事项 / 开发运行手册 / 已知坑 10 条 / 发现问题怎么办)·
|
||
**§A 风控模块主线**(原主干)· **§B 基金转换线**(含**工作区最新版 v1.4**)· **§C 架构改进线**(含**工作区最新版 v1.2**)。
|
||
最大价值:这两份工作区最新版**从未提交**,随时可能丢,现已完整固化。
|
||
|
||
**全仓 10 处指向统一** → `交接文档.md` §A/§B/§C:
|
||
`AGENTS.md`(含顶部新增「⚡ 需要接手/开工先读交接文档.md」)· `docs/memory/{MEMORY,TODO,FRAMEWORK}` ·
|
||
`docs/PRD/PRD-架构改进与稳定性加固.md` · `docs/项目框架设计/{开发计划-架构改进,TODO-架构改进,开发计划-基金转换交易}.md`。
|
||
|
||
**两份 `docs/交接文档-*.md` 保留为历史留档**,顶部加醒目横幅:**「本文件已废弃(2026-09-10)—— 请勿据此开工,
|
||
正确入口 `交接文档.md` §B/§C,内容已过期,勿读」** —— 不删除(保留仓库历史),但明确失效。
|
||
|
||
**未入库**:`交接文档.md` 在 `.gitignore:47` 内,用户确认「不进提交」。
|
||
|
||
### 6.3 ⚠️ 事故:`git rm` 静默抹除整个 `docs/`(复现 2 次,已零损失恢复)
|
||
|
||
- **现象**:`git rm`(带不带 `-f` 均如此)删除 `docs/` 下两个**中文名**文件 → **`docs/` 全部 54 个文件从工作区消失**,
|
||
命令**返回退出码 0 不报错**。
|
||
- **定位**:① `ls`/`find`/Read 三方确认真实删除;② `git checkout HEAD -- docs/` 完整还原 54 文件(索引与 HEAD blob 完好);
|
||
③ 无自定义 hooks(`core.hooksPath` 空);④ **同一中文路径 `ls` 完全正常** → **触发条件就是 `git rm` 本身**;
|
||
⑤ 换 **纯 `rm -f <精确路径>` → 正常**。
|
||
- **损失**:**零内容丢失**(HEAD blob 完好)。代价:本轮引用修改被连带还原 2 次(重做 3 次)。
|
||
- **纪律**(已写入 `交接文档.md` §0.4 第 10 条 + `.workbuddy/memory/MEMORY.md`):
|
||
- ❌ 本仓库**禁止对 `docs/` 下文件用 `git rm`**;
|
||
- ✅ 删文件用**纯 `rm -f <精确路径>`** + `git add -A` 记录删除;
|
||
- ✅ 批量删除后**立刻** `find <dir> -type f | wc -l` 复核,异常立即 `git checkout HEAD -- <dir>`。
|
||
- 根因未查明,**规避即可**。
|
||
|
||
---
|
||
|
||
## 七、设计方法论固化为 skill + 记忆精简(2026-09-10 晚)
|
||
|
||
**动机**:项目记忆 `MEMORY.md` 与交接文档膨胀——13 问 / AIcoding 六步 / 独立审查协议 / 写计划 4 法 等项目无关的方法论每个项目都重写一遍,且挤占了项目状态信息。
|
||
|
||
**落地**:
|
||
1. **新建用户级 skill `design-doc-selfcheck`**(`~/.workbuddy/skills/design-doc-selfcheck/SKILL.md`)——
|
||
固化:AIcoding 六步流程 · 设计自检 13 问(含 why/翻车案例)· PRD 独立 AI 审查协议(自包含包 + 只报告不改动 + 逐条判定)· 写开发计划 4 法 · 真实翻车案例库(4 处外部事实漏网 / 分类混用 / 公式副本 / 重置伴随数据 / 方言互斥)。
|
||
2. **精简项目 `MEMORY.md`**(205 行 → ~70 行):删除已入 skill 的方法论 verbatim,只留三条线状态、`git rm` 禁忌、交接文档定位、基金转换核心事实与业务口径、交易发起主体合规、D20 账号,并指向 skill。
|
||
3. **交接文档 §B 顶部加 skill 指针**:本线设计方法论已固化,避免重复维护。
|
||
|
||
**效果**:方法论跨项目可复用(新项目直接加载 skill);本项目记忆回到「状态 + 红线 + 口径」本职。
|
||
|
||
---
|
||
|
||
## 深夜 · 基金转换线 T-8(规则引擎改造)完成
|
||
|
||
**范围**:开发计划 §7.1 —— `rules._amount_view` + `engine.process_convert_event` + `alert_service.events`(并行组 B)。
|
||
|
||
### 改码三处(均在代码注释留痕)
|
||
|
||
- `app/service/risk/rules.py`:+`_amount_view(trades)`(同 `convert_group_id` 组内只留 `redeem`;**无 gid 恒等通过**;组内无 redeem 保首条);`run_rules` **双视图分流** —— `eligible`(全量)供 RISK-001/003/004,`_amount_view(eligible)` 供 RISK-002/005。
|
||
- `app/service/risk/alert_service.py`:+**公开** `build_trade_event(trade, hits)`(结构定义唯一副本);`record_trade_alerts(..., events=None)` 缺省退化为单条,既有调用零改动;新建单落 `payload.events` 全部,**聚合追加只追首条**(与架构 §5.4「只认转出端」同口径)。
|
||
- `app/service/risk/engine.py`:抽 `_run()` 共用实现;`process_trade_event` 变薄封装(**签名与行为不变**);新增 `process_convert_event(out_trade, in_trade, *, core_ro, risk_repo, thresholds, on_error_hook=None)` + `_notify_error_hook`(hook 自身异常吞掉、原始异常照常上抛)。
|
||
|
||
### ⭐ 实质影响:阶段 1.5 从「静默跳过」变「真跑」
|
||
|
||
T-7 落地时 `process_convert_event` 不存在 → `_run_engine` 走 `ImportError` 分支**跳过**;
|
||
**T-8 落地后同一笔转换会真实出单 + 写 `customer_profile_l3` + 落审计**。
|
||
→ `verify_convert_service.py` 的执行语义随之变化,复跑仍 **35/35 绿**(无连带破坏)。
|
||
|
||
### 验证
|
||
|
||
- 新增 `tests/test_convert_engine.py` **15 用例**;`test_convert_service.py` **+1 条接线回归**(大额转换真出单,防 `_run_engine` 退回跳过)。
|
||
- `pytest -q` → **672 passed / 3 skipped**(基线 656 **+16**,零回归)。
|
||
- **突变验证(防假绿)**:临时把 `amount_view = eligible` → **4 条变红**,其中 `detail` 直接暴露 `当日申赎累计 599000 元(共 2 笔)` 的翻倍错误;恢复后复绿。
|
||
- **新增 `scripts/dev/verify_convert_engine.py` 真库 31/31**(A 引擎真跑出单 / B **RISK-002 不翻倍夹逼**:单条 512000 < 800000 < 两条之和 1019975.33 / C 不删行 + DECIMAL 零漂移 / D 幂等重试不重复出单 / E 无命中只落 pass 审计 / F 六表零残留)。
|
||
|
||
### 真库脚本两条踩坑(与 T-7 同款 + 新一条,供后续 `verify_*.py` 参考)
|
||
|
||
1. **`id_factory` 必须注入**:默认生成 `CNV-<日期>-<uuid>`,与清理口径 `LIKE 'CNV-T8M%'` 不匹配 → 重跑遗留行撞 `uk_group`。注入 `_t8m_id` 并把清理条件补 `client_request_id LIKE 'T8M-%'` 兜底。
|
||
2. **`core_trade.trade_type` 在 MySQL 是 ENUM,`ORDER BY` 按定义序**(实测 `[subscribe, redeem]`)→ 断言改用 `{trade_type: row}` 字典定位,不依赖排序。
|
||
|
||
### 发现的既有行为(不在本任务范围,**未顺手改**)
|
||
|
||
> **更正(同日)**:初次记录误写成「RISK-001 实为当日累计口径」,**错误**。RISK-001 与 RISK-002
|
||
> 是两条独立规则——前者**单笔**(`rules.py:128-133`)、后者**当日累计**(`:137-145`)。T-8 只切后者。
|
||
|
||
真实根因在**引擎入参范围**:`process_trade_event` 拉**当日全量**流水重跑规则
|
||
(`engine.py` 的 `list_trades_range(customer, day_start, day_end)`)→ **触发这笔**与**命中那笔**
|
||
可以不是同一笔:真库 E 组实测,同客户同日先有一笔大额转换时,后续一笔 1000 元小额交易也带出 RISK-001。
|
||
既有聚合逻辑(同客户同日一张 pending 单)兜住了重复出单,属**既有设计**,T-8 不改变也不修。
|
||
|
||
### 文档回写
|
||
|
||
开发计划(§0 速览标 ✅ + 测试基线改 672 · **§7.1 DoD 全勾 + 新增执行记录 + 3 条实施级收敛 + 2 条脚本坑**)·
|
||
根 `交接文档.md` §B → **v1.9**(§0 导航 · §B 头部 · §B.1 状态与代码改动 · **新增 §B.6.1 T-8 小节** · §B.6 任务树与基线)·
|
||
`docs/memory/{MEMORY,TODO}` · 本条。
|
||
|
||
**下一步 = T-9(`api/simulate.py` 模型与错误码 + `trade_gateway` convert 分派 · 关键路径)/
|
||
T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。
|
||
|
||
---
|
||
|
||
## 深夜 · 基金转换线 T-9(API 模型 + 网关分派 · 关键路径)完成
|
||
|
||
**范围**:开发计划 §7.2 —— `api/simulate.py` 三型模型 + `trade_gateway` convert 分派 + 错误码映射。**至此 HTTP 层 convert 端到端走通**。
|
||
|
||
### 改码 5 处
|
||
|
||
- `app/utils/trace.py`:`_HEADER_ID_PATTERN` → **公开 `HEADER_ID_PATTERN`**(单点定义)。
|
||
执行期发现 `trace.py:16` 与 `main.py:44` **各有一份内容完全相同的正则** —— S4 要防的「白名单漂移」**其实已经发生**。
|
||
- `app/main.py`:删掉那份重复副本 + `import re`,改 import 上面的常量。
|
||
- `app/utils/response.py`:`_api_error_handler` 合入 `exc.extra`(`getattr` 取;既有 `ApiError` 无此属性 → 错误体逐字节不变)。
|
||
- `app/gateway/trade_gateway.py`:移除 convert 显式拒绝;新增 `_submit_convert`(只做参数映射 + 仓储装配,模块级符号作 monkeypatch 注入点)。
|
||
- `app/api/simulate.py`:`TradeRequest` 三型字段分池 + `@model_validator` 分支校验(`client_request_id` 复用同一白名单);
|
||
`model_dump(exclude_none=True)`;**`except LookupError` 收窄为 `except NotFoundError`**;`PROCESSING` → 202。
|
||
|
||
### ⚠️ 3 条与计划原文的出入(已写入开发计划 §7.2 执行记录)
|
||
|
||
1. **正则落点**:计划写「复用 `main.py:44`」,但 `app.main` → `app.api.simulate` 单向导入链,反向 import 成环
|
||
→ 上移 `trace.py`。连带突破 §12 **R14「本次不动 main.py」**(中间件注册顺序未动,守卫用例仍绿)。
|
||
2. **`gateway_repository.insert_trade` 加列:裁定不需要** —— convert 写路径在 `convert_core_repository.apply_convert`,该文件零改动。
|
||
3. **R7 归 T-10**;**R16 零改动通过**。
|
||
|
||
### 另一条实施级发现(**已上报,未顺手改**)
|
||
|
||
**幂等重放响应的数值位数与首次不一致**:首次走 `calc`(2 位)vs 重放 `_rebuild_quote` 直读 `DECIMAL(18,4)`(4 位)
|
||
→ `"53456.95"` vs `"53456.9500"`(**数值相等**)。违反 PRD「对外一律 2 位」展示契约,属 T-7 范畴。
|
||
集成测试已改为**比数值不比字符串**并在 docstring 钉住偏差。
|
||
|
||
### 验证
|
||
|
||
- 新增 **`tests/test_convert_integration.py`(7 条真 MySQL 集成)**:端到端与 PRD §5.3 逐项吻合(调生产纯函数算期望,禁手算)
|
||
· 两条流水同组同前缀 · 持仓与批次如实变动(转出归零保留行 / 转入新建)· 明细 completed + 审计
|
||
· 幂等重试不产生第二组 · 跨主体 400 · 未知类型 400。
|
||
- `test_trade_gateway.py` **+17**(11 条错误码映射全表参数化含 `extra` 展开 · 202 · 200 透传 · 不写 `trade_request` 审计 · 3 条 422 分支)。
|
||
- `test_integration_risk.py` **R15 处置**:端到端已迁入新文件;原槽位**未删除**,改造为
|
||
`test_invalid_type_400_and_no_new_trade_audit`(改用 `purchase` 触发),保住「校验失败不落审计」不变量。
|
||
- `pytest -q` → **696 passed / 3 skipped**(基线 672 **+24**,零回归)。
|
||
- **突变验证 3 组**:① 关掉 convert 分派 → **21 条红**;② 关掉错误体 `extra` 展开 → **精准 1 条**;
|
||
③ 关掉 `client_request_id` 正则 → **精准 1 条**。三处已恢复,`grep MUTATION-TEST app/` 为空。
|
||
- **真库验证载体 = 集成测试本身**(非脚本):T-9 **不新增 SQL、不涉方言语义**,故无需另写 `verify_convert_*.py`;
|
||
该文件经 `ensure_risk_demo_ready()` 在无 MySQL 环境整模块 skip,不炸 CI。
|
||
|
||
### 🔴 本日第二次误报「远程分支不见了」(教训)
|
||
|
||
提交 T-8 时依据 `git branch -vv` / `git branch -r` 判定远程 `risk-control-agent` gone、仅剩 `main`/`dev`,
|
||
据此向用户报警。**结论错误** —— `git ls-remote --heads origin` 实测远程 `risk-control-agent` = **`fffb78a`,健在**;
|
||
`git branch -r` 只剩两个分支纯属**本地远程跟踪引用 stale**。
|
||
**本日第 368-371 行已记录同款教训(`037ce7e` 误报),当日复发第二次** → 已升格为
|
||
`.workbuddy/memory/MEMORY.md` 铁律 + `docs/memory/MEMORY.md` 交接清单第 7 问。
|
||
|
||
### 文档回写
|
||
|
||
开发计划(§0 速览标 ✅ + 基线 696 · **§7.2 DoD 全勾 + 执行记录 + 4 条裁定 + 1 条发现 + 3 组突变** · §12 R15 补实际处置
|
||
并订正事件名 `trade_accepted`→`convert_accepted`)· 根 `交接文档.md` §B → **v2.0**(§0 导航 · §B 头部 · §B.1 状态与代码改动 ·
|
||
**新增 §B.6.2 T-9 小节** · §B.6 任务树与基线)· `docs/memory/{MEMORY,TODO}` · `.workbuddy/memory/{MEMORY,2026-09-10}` · 本条。
|
||
|
||
**下一步 = T-10(普通申赎批次维护 · 改 `trade_gateway` 主流程 · **回归风险最大,先跑基线再动**)/
|
||
T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。
|
||
|
||
---
|
||
|
||
## 深夜 · T-9 收尾:**展示位数口径修复**(用户「你先改问题,按照贴近现实业务改」)
|
||
|
||
**问题**:幂等重放响应的数值位数与首次不一致(首次 `"53456.95"` vs 重放 `"53456.9500"`,**数值相等、字符串不等**)。
|
||
|
||
### 根因不是 T-7 写错,是**契约缺位**
|
||
|
||
§2.5 只规定了「金额/份额 2 位」,**净值、费率、申请份额的回显位数根本没定义** →
|
||
实现只能把 `Decimal` 原样 `str()` 出网 → **位数随数据来源漂移**:
|
||
- 首次路径走 `calc` 纯函数(已 2 位量化)
|
||
- 重放路径由 `_rebuild_quote` 从 `core_trade`/`core_convert_lot_detail`(`DECIMAL(18,4)`)重建后直读
|
||
|
||
### 联网核验(7 家管理人公告,2026-09-10)
|
||
|
||
| 口径 | 依据 |
|
||
| --- | --- |
|
||
| 金额 2 位四舍五入 | 「转出金额以四舍五入的方式保留至小数点后两位」(中银/人保/浦银安盛/中欧/南方/申万菱信一致) |
|
||
| 份额 2 位四舍五入 | 「转入份额以四舍五入的方式保留至小数点后两位」;「**申请转换份额精确到小数点后两位**」(中银) |
|
||
| 净值 4 位 | 「份额净值保留 4 位、第 5 位四舍五入」(中欧/国泰公告:**由 3 位提高至 4 位**);巨额赎回极端可 8 位 |
|
||
| 费率 4 位 | 公告以百分比 2 位表示(`0.30%` ↔ `0.0030`) |
|
||
| ⚠️ **已知不统一** | **易方达(ETF 场外)份额取整数位**、**南方基金取截断**(非四舍五入)→ 本期取主流口径 + 记入 PRD §10 已知差异 |
|
||
|
||
### 修复(贴近现实业务)
|
||
|
||
`convert_service` 新增 **`_q(value, unit)` + `_D2`/`_D4` 规格常量**作**对外唯一出口**:
|
||
- **金额 / 份额 → 2 位**(`_D2`)
|
||
- **净值 / 费率 / 份额尾差 → 4 位**(`_D4`)
|
||
- 响应、审计 `summary`、异常日志**共用同一出口**;原 `_s()` 已全部替换(`grep _s(` 为空)
|
||
- 首次路径**幂等**:除 `requested_qty`/`actual_qty`/`lot[].qty` 由 4 位**补齐至 2 位**外逐字节不变
|
||
|
||
### 文档订正(用户「文档不准,你去联网查」)
|
||
|
||
- **PRD → v0.9.2**:§2.5 拆 **2.5.1 计算精度 / 2.5.2 展示位数**(新增分类规格表 + 外部依据 + 已知差异);
|
||
§5.3 示例 `requested_qty`/`actual_qty`/`lot_breakdown[].qty` **4 位 → 2 位**(原示例与 §2.5「对外展示按 2 位」**自相矛盾**,属漏改);
|
||
§5.3 字段类型约定补「位数不自由 + 两条产出路径必须逐字节一致」;新增 v0.9.1→v0.9.2 变更表与根因
|
||
- **架构 → v1.0.1**:§1 原则 11 补「`str()` 前必须按 §2.5.2 量化」
|
||
- 开发计划 §7.2「发现」条改为「**已修复**」并补完整证据链;交接文档 §B.6.2 同步
|
||
|
||
### 验证
|
||
|
||
- 集成测试改用**逐字段逐字节比对**(`REPLAY_IDENTICAL_FIELDS` + `lot_breakdown` 整体相等)
|
||
+ 新增**位数规格断言**(`test_convert_response_field_scales`,逐字段验 `split(".")[1]` 位数,并卡住 `requested_qty == "50000.00"`)
|
||
- `pytest -q` → **697 passed / 3 skipped**(零回归)
|
||
- **真库全复跑**:T-6 `verify_convert_apply` **24/24** · T-7 `verify_convert_service` **35/35** · T-8 `verify_convert_engine` **31/31**
|
||
- `calc_convert_demo.py` **15/15** 与 PRD §5.3 一致
|
||
- **突变验证**:把 `_q()` 的量化去掉 → **2 条红**,`assert '50000.0000' == '50000'` 直接复现修复前现象;已恢复,`grep MUTATION-TEST` 为空
|
||
|
||
### 教训已固化
|
||
|
||
skill `design-doc-selfcheck` → **v1.2.0**:新增 **「二补 · 格式契约最低三问」** + **铁律 6(格式契约铁律)**:
|
||
**凡 `str(Decimal)` 直接出网的字段,先问「这个字段的展示位数写在哪」—— 没答案就是契约缺位;
|
||
存储精度 ≠ 展示精度;多条产出路径必须共用一个格式出口 + 写逐字节相等断言。**
|
||
|
||
---
|
||
|
||
## 深夜 · 收尾:**`refs/remotes` 写入不落盘**根因排查(推翻此前「stale ref」结论)
|
||
|
||
**背景**:提交 T-8/T-9 时两次依据 `git branch -r` / `git branch -vv` 判定「远程分支不见了」并报警,
|
||
两次均**误报**(用户被迫去仓库核实两次)。用户指令:「你先改问题……改完确认没问题再提交」→ 先做根因排查。
|
||
|
||
### 已确认事实(可复现)
|
||
|
||
| # | 事实 | 证据 |
|
||
| --- | --- | --- |
|
||
| 1 | **远程健在** | `git ls-remote --heads origin` → `refs/heads/risk-control-agent` = `fffb78a`;另有 6 个分支 |
|
||
| 2 | **fetch 抓取正常,引用不落盘** | `git fetch origin` 报 `* [new branch] risk-control-agent -> origin/risk-control-agent`,`FETCH_HEAD` 写对;`.git/refs/remotes/` 仍为空 |
|
||
| 3 | **写 remotes 会删整个 `origin` 目录** | `git update-ref refs/remotes/origin/X <sha>` → **EXIT=0**、reflog 建了,但松散引用消失,**连手工 `echo >` 进去的文件一起被删** |
|
||
| 4 | **只 remotes namespace 有问题** | 写 `refs/heads/tmpX` 时,放在 `refs/remotes/origin/` 的 marker **存活**;写 remotes 才触发删除 |
|
||
| 5 | **只此仓库特有** | 同一 git(`2.55.0.windows.3`)在 `/tmp` 新建仓库写 `refs/remotes/origin/probeX` **完全正常** |
|
||
| 6 | **排除外部进程** | marker 静置 6 秒 + 跑只读 git 命令均存活,**只有写 remotes 的那一下才被删** |
|
||
| 7 | **排除目录属性** | Python `lstat` 查 `refs/remotes`:**非重解析点**(`FILE_ATTRIBUTE_REPARSE_POINT = False`);无 hooks、无 commondir、非浅克隆 |
|
||
|
||
**根因未定位**(倾向 git 写 remotes 引用时被本地环境干预)。**此前的记录写「stale ref,`git fetch` 即恢复」是错的** —— fetch 并不修复。
|
||
|
||
### ✅ 绕过方案(已验证)
|
||
|
||
`.git/packed-refs` 是**可靠存储**(`origin/dev`、`origin/main` 一直从这里读到)。
|
||
手工追加 `fffb78a11c7d4e05f2aae53c76ceec3ad5d7d33c refs/remotes/origin/risk-control-agent` 后:
|
||
`git branch -r` 立即显示 `origin/risk-control-agent`、`git rev-parse` 可解析。还原 = 删掉该行。
|
||
|
||
### 影响面(重要)
|
||
|
||
- **不受影响**:提交(`refs/heads/**` 正常)、`git fetch` 抓对象、**push**
|
||
(`git push --dry-run origin risk-control-agent:refs/heads/risk-control-agent` 实测 `fffb78a..d0097d6` 成功)。
|
||
- **受影响**:仅本地「显示 / 上游跟踪」——`git branch -r`、`git branch -vv`、`git pull`(无参) 的默认上游推断。
|
||
|
||
### 状态
|
||
|
||
本地 HEAD `d0097d6`(T-9)· 领先远程 `fffb78a` **9 个提交** · 工作区干净 · 探针与临时文件已全部清理。
|
||
|
||
### 文档回写
|
||
|
||
`.workbuddy/memory/MEMORY.md`(远程铁律条**重写**,含 7 条证据 + 绕过方案)·
|
||
`docs/memory/MEMORY.md`(交接清单**第 7 问重写** + 顺手订正验证条目里过期的「516 passed」→ **697** + 第 73 行旧表述订正)· 本条。
|