- dasha-unify: fix Ashtottari birth balance (BUG-1344) - rect-scoring-perf: server-side candidate cap 241 (BUG-1345) - node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346) - consult-prep-parallel: classifier/reserve concurrency declined - answer-effort-ab: model key supplied by product at acceptance - drop stricter-than-test line-count red lines (AGENTS.md section 6) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
198 lines
18 KiB
Markdown
198 lines
18 KiB
Markdown
# TASK · API 单体第一阶段:拆出不带 HTTP 的计算类,生产代码不再伪造 handler(issue #待定)
|
||
|
||
- 日期:2026-10-13
|
||
- 来源:2026-10-13 全仓屎山与性能排查 ·「`jyotish_api_server.py` 单体 / `JyotishAPIHandler.__new__` 伪造」条目
|
||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/api-server-service-extract-20261013`,只推这个分支
|
||
- 验收:Claude(Linux)
|
||
- BUG 编号:**无 BUG 编号**(结构改造,不是缺陷),记录写 PROGRESS(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
|
||
- **开工前置**:`dasha-unify`(BUG-1341)、`python-hygiene`(BUG-1340)两单已合入 `origin/staging`。两单都改 `scripts/jyotish_api_server.py`,本单整类重排,必须最后做。
|
||
- **主会话补充的串行链(2026-10-14)**:引擎/API 文件链按此顺序逐单合入后开工(`upstream-sync7` 也改 `jyotish_engine.py` / `jyotish_api_server.py`,若在途,后合一方 rebase):`api-disk-cache-retention` + `report-reference-perf` → `api-chart-subprocess-gate` → `pl9-render-split` → `python-hygiene` → `dasha-unify` → `api-server-service-extract`。本单的前置以这条链为准(与上一行不一致时以本行为准)。
|
||
|
||
## 1. 基线
|
||
|
||
- 基线 commit:`origin/staging` `77057a819`(写作时);开工以前置两单合入后的 `origin/staging` 为准,第 9 节命令重测全部数字。
|
||
- 前序任务书(必读):`docs/tasks/TASK-api-server-decomposition-20260916.md`(阶段 1 目标「`__new__` 归零」)、`docs/tasks/TASK-api-server-backdoor-close-20260916.md` 与 `docs/tasks/PROGRESS-api-server-backdoor-close-20260916.md`(mixin 方案 spike **红**、退回 A,scripts 侧伪造 4 → 2)。
|
||
|
||
## 2. 事故实证(`77057a819`)
|
||
|
||
### 2.1 现状数字(排查报告有两处要修正)
|
||
|
||
| 指标 | 排查报告 | 实测 | 口径 |
|
||
| --- | --- | --- | --- |
|
||
| 文件行数 | 10,970 | **10,970** | `wc -l` |
|
||
| `JyotishAPIHandler` 类体 | 7,751 行 / 215 方法 | **7,752 行 / 215 个类级方法**;合同口径(全文件 `^ def`)**218** | AST / `tests/test_api_server_growth_contract.py` |
|
||
| `JyotishAPIHandler.__new__` | 34 处、23 个测试文件 | **31 处**:`scripts/` 2(`consultation_workflow_service.py::execute_consultation_workflow`、`capture_report_blocked_repairs_golden.py::_handler`),`tests/` 29(**22** 个文件) | 合同口径(文本计数,不含合同文件本身) |
|
||
| 合同基线 | 31 | 行数 10,924 + 300;方法 218;伪造 31;scripts 伪造 2 | 合同文件常量 |
|
||
|
||
### 2.2 `execute_consultation_workflow` 不是 400 行的问题
|
||
|
||
排查报告建议「把 `execute_consultation_workflow`(400 行)及其依赖方法改成不依赖 self 的服务模块」。AST 求它的**传递闭包**(从模块级 `execute_consultation_workflow` 出发,沿 `handler.x` / `self.x` 与同文件模块级函数引用展开):
|
||
|
||
| 闭包成分 | 个数 | 行数 |
|
||
| --- | ---: | ---: |
|
||
| `JyotishAPIHandler` 方法 | **107** | **3,805** |
|
||
| 同文件模块级函数 | **62** | **1,958** |
|
||
| 用到的 mixin 方法 | 6 | (已在 `offline_compute_mixins.py`) |
|
||
| 引用 HTTP 成员(`headers` / `wfile` / `send_*` / `client_address` …) | **0** | — |
|
||
|
||
也就是说,「不依赖 self 的服务模块」= 搬 ~5,800 行,远超一轮可控的范围;而且 9 月 16 日的 spike 已证明这条路会撞墙:闭包里有 5 个模块级函数被测试**按 `jyotish_api_server` 模块属性打补丁**(`_load_local_module`、`_write_async_job_record`、`_async_job_dir`、`_sqlite_job_db_path`、`_attach_vedastro_main_entry_overview`),其中 `tests/test_api_server_security.py`(约第 2399 行 `_compute_consultation_workflow` 用例)与 `tests/test_consultation_consumer_context.py`(约第 211 行)直接走咨询链。函数搬到新模块后,查找走新模块 globals,补丁静默失效。
|
||
|
||
### 2.3 真正可以一刀切开的地方:HTTP 只占 15 个方法
|
||
|
||
对类内 215 个方法求「直接碰 HTTP 成员或覆写 `BaseHTTPRequestHandler`」及其**向上**调用者:
|
||
|
||
| 集合 | 方法 | 行数 |
|
||
| --- | ---: | ---: |
|
||
| 直接碰 HTTP | 9:`_json`、`_html`、`_send_cors_headers`、`_read_json_body`、`_enforce_request_security`、`_job_access_token`、`do_GET`、`do_POST`、`do_OPTIONS` | 456 |
|
||
| 调用上面任一的方法 | 6:`_error_json`、`_get_chart_job`、`_get_high_rigor_job`、`_require_dynamic_rectification_token`、`_compute_dynamic_rectification_score`、`_compute_dynamic_rectification_opportunities` | 119 |
|
||
| **合计 HTTP 侧** | **15** | **575** |
|
||
| 其余(纯计算) | 200 | ~7,170 |
|
||
|
||
另外核实:
|
||
|
||
- 类里**没有任何方法给 `self.xxx` 赋值**(无实例状态);唯一的动态读取是 `_enforce_request_security` 的 `getattr(self, 'client_address', …)`(属于 HTTP 侧)。
|
||
- 与 `BaseHTTPRequestHandler` 同名的类属性只有 `server_version`(必须留在 HTTP 类,否则 MRO 会让基类的值生效)。
|
||
- 咨询闭包 107 个方法与 HTTP 侧 15 个方法**交集为空**。
|
||
|
||
所以:把类拆成「纯计算基类 + HTTP 子类」,**两个类都留在同一个文件里**——模块 globals 不变,测试补丁落点不变,`self` 语义不变;纯计算类可以正常 `JyotishComputeService()` 实例化,不再需要绕过 `BaseHTTPRequestHandler.__init__`。
|
||
|
||
### 2.4 测试里的 29 处伪造
|
||
|
||
按「文件内是否调用 HTTP 侧方法或读写 `headers` / `wfile` / `client_address`」粗分(grep 启发式,执行方以实际运行为准):
|
||
|
||
| 类别 | 文件数 | 伪造处数 |
|
||
| --- | ---: | ---: |
|
||
| 需要 HTTP 侧(`test_api_server_security.py`、`test_api_async_job_contract.py`、`test_runtime_security_p0.py`、`test_dynamic_rectification_scoring.py`、`test_professional_report_reference_api.py`) | 5 | 5 |
|
||
| 只用计算方法 | 17 | 24 |
|
||
|
||
### 2.5 咨询工作流输出的确定性(golden 方法依据)
|
||
|
||
用 `scripts/consultation_workflow_service.execute_consultation_workflow`(MCP 入口)跑虚构盘(1990-01-01 12:00 新德里 28.6139/77.2090 +5.5,`theme=["career"]`,`defer_optional_external_evidence=True`,`today=2026-10-13`,`VEDASTRO_ENABLE_NETWORK=0`),约 1 秒、响应约 420 KB:
|
||
|
||
| 对比 | 结果 |
|
||
| --- | --- |
|
||
| 两次都清空排盘缓存(`scratch/local/api_chart_cache`) | 只差 2 个字段:`chart.runtime_cache.cache_created_at` / `cache_expires_at`(墙钟) |
|
||
| 换 `PYTHONHASHSEED` | 同上,与种子无关 |
|
||
| 第二次命中排盘缓存 | 另有 36 处差异:`chart.consultation_native_layers.argala.*` 里 `Sun` / `Saturn` 先后顺序互换(**附带发现**:缓存往返改变了 Argala 列表顺序,语义相同;本单不修,列入回报) |
|
||
|
||
结论:golden 比对必须「清空排盘缓存 + 去掉这两个墙钟字段」,其余逐字节相等。
|
||
|
||
## 3. 根因
|
||
|
||
业务逻辑长在 HTTP handler 类上,非 HTTP 调用方(MCP、golden 生成)没有合法实例可用,只能 `__new__` 伪造。9 月两单想把逻辑**搬出文件**,撞上「测试按模块属性打补丁」;本单换方向:**不搬出文件,先在文件内把 HTTP 与计算分成两个类**,伪造就失去存在理由。
|
||
|
||
## 4. 决策记录
|
||
|
||
- 2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行。
|
||
- **范围修正(主会话转交产品确认)**:排查报告的「第一阶段把 `execute_consultation_workflow` 及依赖改成独立服务模块」实测闭包约 5,800 行且会触发 9 月 spike 的补丁失效,**本单不做**;改为同文件内拆类(约 600 行纯移动),达成同一个可验证目标:**生产伪造 2 → 0**。
|
||
- 9 月 `TASK-api-server-backdoor-close` 红线「`tests/test_api_server_security.py` 一条断言不许改」**继续有效**:本单不改它的任何断言,也不改它的补丁落点。
|
||
- 本单允许改测试里的**构造语句**(`JyotishAPIHandler.__new__(JyotishAPIHandler)` → `JyotishComputeService()`),这不是断言;仍按三栏写进进度记录。`test_api_server_security.py` 里那 1 处伪造属于 HTTP 侧,不改。
|
||
- 增长合同基线**只许下调**:scripts 伪造 2 → 0;总伪造 31 → 实测;方法数与行数不变或下降。
|
||
- `consultation_workflow_service.py` 仍 import `jyotish_api_server`(依赖方向的根治属于后续阶段,需要拍板改补丁落点,见末尾)。
|
||
|
||
## 5. 硬红线
|
||
|
||
1. **纯移动**:方法体逐字不改;只允许把 15 个 HTTP 侧方法与 `server_version` 从原类剪到新 HTTP 子类、改类声明、调整两个调用方的构造语句。用 `git diff --color-moved=dimmed-zebra` 与 AST 方法体对比(第 6 节 T2)证明。
|
||
2. `tests/test_api_server_security.py` 一个字节不改且全绿。
|
||
3. `scripts/jyotish_api_server.py`:行数只按 `tests/test_api_server_growth_contract.py` 的粗护栏(AGENTS.md §6,开工实测基线 + 300);拆类新增的类声明与空行不需要抵消,**不得**为凑行数删代码、合并语句或删注释。顺带发现的零引用函数(写作时 `_strict_adjudication_bundle_from_contract`、`_high_rigor_job_path`)只记进 PROGRESS,不在本单删。
|
||
4. 类方法数(合同口径)不得增长;`JyotishAPIHandler.__new__` 计数只许降。
|
||
5. 不新增端点、不改任何端点请求 / 响应合同;HTTP 路由、安全检查、限流 429、异常映射行为不变。
|
||
6. 新计算类不得继承 `BaseHTTPRequestHandler`,其方法不得引用 HTTP 成员(加 AST 测试钉住)。
|
||
7. 不改 `.gitea/workflows/**`、不动表结构、不升级依赖、不碰 `skills/*/versions/**`。
|
||
8. golden 只能来自真实引擎输出;改既有断言写三栏(本单预期只有增长合同常量)。
|
||
|
||
## 6. 任务分解
|
||
|
||
### T1 · 搬前 golden(必做,先做)
|
||
|
||
- 写一个采集脚本(放 `scripts/research/` 或测试 helper,**不放进 `scripts/` 根目录**),在**基线工作树**上用第 2.5 节方法采集:
|
||
1. MCP 入口 `consultation_workflow_service.execute_consultation_workflow`:career、wealth、relationship 三题各一次;
|
||
2. HTTP 侧 handler 的 `_compute_consultation_workflow`(构造方式与现有测试相同)同三题;
|
||
3. `scripts/capture_report_blocked_repairs_golden.py` 现有产物;
|
||
4. `mcp_server.py` 里经由 `build_runtime_evidence_helpers` 的路径一次(9 月已有端到端测试,复用)。
|
||
- 去掉 `cache_created_at` / `cache_expires_at`,`json.dumps(sort_keys=True, ensure_ascii=False)` 落盘到 `tests/golden/`(体积过大则只存 sha256 + 关键键路径,fixture 写 `fixtureProvenance`:命令、基线 SHA、环境变量)。
|
||
- 验收:基线上连跑两次哈希一致(证明可复现)。
|
||
|
||
### T2 · 同文件拆类(必做)
|
||
|
||
- `class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryMixin)` 改为 `class JyotishComputeService(VedastroEvidenceMixin, SynastryMixin)`,保留 200 个计算方法原位不动。
|
||
- 在其后新增 `class JyotishAPIHandler(BaseHTTPRequestHandler, JyotishComputeService)`,放入 `server_version` 与第 2.3 节 15 个 HTTP 侧方法(开工按同口径重算名单,进度记录贴脚本输出)。`ThreadingHTTPServer((host, port), JyotishAPIHandler)` 不变。
|
||
- 类 docstring 一行说明两类分工即可(计入行数,用第 5 节第 3 条的死代码抵消)。
|
||
- 验收:
|
||
- AST 对比脚本:基线与本分支两个文件里,所有方法按名取 `ast.dump(body)`,逐个相等;方法总集合相等;只有 16 个成员换了所属类。
|
||
- `JyotishAPIHandler.__mro__` 中 `BaseHTTPRequestHandler` 先于 `JyotishComputeService`;`JyotishAPIHandler.server_version` 值不变。
|
||
- `tests/test_api_server_security.py` 全绿、零改动(`git diff --stat` 为空)。
|
||
- `tests/test_offline_compute_mixins.py` 第 85–86 行 `issubclass(JyotishAPIHandler, VedastroEvidenceMixin / SynastryMixin)` 仍成立(经由新基类)。
|
||
|
||
### T3 · 生产伪造归零(必做)
|
||
|
||
- `scripts/consultation_workflow_service.py::execute_consultation_workflow`:`JyotishAPIHandler.__new__(JyotishAPIHandler)` → `JyotishComputeService()`。
|
||
- `scripts/capture_report_blocked_repairs_golden.py::_handler`:同上(返回类型注解随之改)。
|
||
- 验收:
|
||
- T1 的 MCP 与 HTTP 两路 golden 逐字节一致(第 2.5 节方法);`capture_report_blocked_repairs_golden.py` 产物与 T1 一致。
|
||
- `grep -rn "JyotishAPIHandler.__new__" scripts` 零命中。
|
||
|
||
### T4 · 测试伪造下调(可砍)
|
||
|
||
- 对第 2.4 节「只用计算方法」的 17 个文件,把构造语句换成 `JyotishComputeService()`;逐文件跑,**红了就回退该文件**(说明它其实用到 HTTP 侧),回退名单写进进度记录。
|
||
- 每处改动按三栏记录(原值 `JyotishAPIHandler.__new__(JyotishAPIHandler)` / 新值 `JyotishComputeService()` / 原因:计算类可正常实例化,不再需要绕过 HTTP 构造)。
|
||
|
||
### T5 · 合同与守卫(必做)
|
||
|
||
- `tests/test_api_server_growth_contract.py`:
|
||
- `JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE` 2 → **0**;`JYOTISH_API_HANDLER_NEW_COUNT_BASELINE` 31 → 收尾实测;方法数基线若因删死代码下降同步下调;行数基线不动(只要不超)。三栏写在常量旁注释与进度记录。docstring 更新日期与说明。
|
||
- 新增两条:① `JyotishComputeService` 不是 `BaseHTTPRequestHandler` 子类;② AST 检查 `JyotishComputeService` 的方法体不引用 `self.headers` / `wfile` / `rfile` / `client_address` / `send_response` / `send_header` / `end_headers` / `path` / `command`,也不调用第 2.3 节 HTTP 侧方法名。
|
||
- 反向验证:在计算类里临时加一行 `self.headers`,第 ② 条变红;在 scripts 里临时加一处 `JyotishAPIHandler.__new__`,scripts 伪造条变红。输出贴进度记录后撤回。
|
||
- `AGENTS.md` §6 措辞若提到伪造基线数字,同步数字(纯文档)。
|
||
|
||
### T6 · 记录(必做)
|
||
|
||
- `docs/tasks/PROGRESS-api-server-service-extract-20261013.md`:第 2 节全部数字的开工重测值、HTTP 侧名单脚本输出、AST 方法体对比结果、golden 对照、T4 回退名单、反向验证输出、搬前 / 搬后行数与方法数。
|
||
- 不写 BUG_HISTORY;不写 CHANGELOG(无用户可感知变化);不改 `frontend/DESIGN.md`。
|
||
|
||
## 7. 让步顺序
|
||
|
||
时间不够按此顺序砍:**T4 → T5 第 ② 条 AST 守卫(保留 ① 与基线下调)**。T1、T2、T3、T5 基线下调、T6 不可砍。若 T2 让 `test_api_server_security.py` 任何一条变红且不是搬错所致,**停下回报,不得改测试**。
|
||
|
||
## 8. 验收命令分工
|
||
|
||
| 谁 | 命令 |
|
||
| --- | --- |
|
||
| Grok 自跑 | `python -m pytest tests/test_api_server_security.py tests/test_api_server_growth_contract.py tests/test_offline_compute_mixins.py tests/test_consultation_workflow_domains.py tests/test_consultation_consumer_context.py tests/test_consultation_plan_contract.py tests/test_commercial_domain_calculation_contract.py tests/test_calculation_p0_regressions.py -q` + T4 改到的每个测试文件 + T1/T3 golden 对照 + AST 方法体对比。快速门、全量 pytest **不跑**,以 Claude 为准 |
|
||
| Claude 验收 | 上述 + 快速门 + 全量 `pytest` 与基线逐名比对 + 本机起 API 服务做冒烟:未登录 `/api/health` 200、`/api/chart` 虚构盘 200、超大请求体与非 JSON 的错误码与基线相同 |
|
||
| 不需要 | `test:db`、前端 |
|
||
|
||
## 9. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git log --oneline origin/staging | grep -i "dasha-unify\|python-hygiene" # 两单都已合入
|
||
git worktree add -b codex/api-server-service-extract-20261013 .worktrees/api-server-service-extract-20261013 origin/staging
|
||
cd .worktrees/api-server-service-extract-20261013
|
||
wc -l scripts/jyotish_api_server.py
|
||
grep -c "^ def \|^ async def " scripts/jyotish_api_server.py
|
||
grep -rn "JyotishAPIHandler.__new__" --include=*.py scripts tests | grep -v test_api_server_growth_contract | wc -l
|
||
grep -rn "JyotishAPIHandler" --include=*.py scripts tests mcp_server.py | grep -v "__new__" | head -40 # 其他按类名的用法(issubclass、注解、import)
|
||
python -m pytest tests/test_api_server_security.py -q | tail -3
|
||
python -m pytest tests -q -p no:cacheprovider > /tmp/baseline-full.txt 2>&1; tail -3 /tmp/baseline-full.txt
|
||
```
|
||
|
||
## 10. BUG 编号起点
|
||
|
||
无(结构改造)。若途中发现缺陷,按开工时 `docs/BUG_HISTORY.md` 最大号 +1 另记,并在回报里说明。
|
||
|
||
## 11. 交付物清单
|
||
|
||
- 代码:`scripts/jyotish_api_server.py`(拆类、删两处死代码)、`scripts/consultation_workflow_service.py`、`scripts/capture_report_blocked_repairs_golden.py`
|
||
- 测试:`tests/test_api_server_growth_contract.py`(基线下调 + 两条新守卫)、T1 golden 与对照测试、T4 改到的测试文件
|
||
- 文档:`docs/tasks/PROGRESS-api-server-service-extract-20261013.md`;(可能)`AGENTS.md` §6 数字
|
||
- 不改:`frontend/**`、`frontend/DESIGN.md`、`CHANGELOG.md`、`docs/BUG_HISTORY.md`
|
||
|
||
## 后续阶段(本单不做)
|
||
|
||
1. **把咨询闭包搬出文件**(107 方法 / 3,805 行 + 62 函数 / 1,958 行):前提是先解决 5 个被按模块属性打补丁的函数。可行做法是把 `_load_local_module` 与异步任务记录层抽成独立模块、两边都按「模块属性」调用,并把测试补丁落点改到新模块——**会改 `test_api_server_security.py` 的补丁行,需产品拍板**(9 月红线)。之后 `consultation_workflow_service.py` 才能不再 import 单体。
|
||
2. 按入口继续切计算类:图表计算、报告、校正、外部证据各成模块(每次一个,增长合同只许降)。
|
||
3. `do_POST` / `do_GET` 改路由表(9 月 decomposition 阶段 3,仍未做)。
|
||
4. 测试里剩下的 5 处 HTTP 侧伪造:给 HTTP 子类一个测试用构造工厂(放 `tests/` 辅助模块),伪造计数归零。
|
||
5. 附带发现:排盘缓存命中时 Argala 列表顺序与未命中不同(第 2.5 节),另开单。
|