Files
group_xinghuo_jinrong/docs/memory/2026-09-10.md
T
GaoYiYuan_0626 e80098b32f 文档订正:远程状态认知纠错(refs/remotes 写入不落盘)+ 交接清单过期基线
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 条可复现证据 + 三步对照实验 + 绕过方案 + 影响面。
2026-09-10 18:07:43 +08:00

433 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 行旧表述订正)· 本条。