# TASK-api-server-decomposition-20260916 · 把业务逻辑搬出 HTTP handler ## 基线 - 代码基线:`origin/staging` = **`2d7698ea`**(文档树 `6f74aa67`)。开工时以最新 `origin/staging` 为准。 - **串行依赖**:本单必须等 `TASK-qizheng-native-chart-20260915` **合入 staging 之后**才开工。那一单独占 `scripts/jyotish_api_server.py` 的写权,并行等于自找冲突。 - **2026-09-15 新增串行依赖**:还要等 `TASK-consultation-external-evidence-cache-20260915`(C1)合入。产品拍板 C1 先做——它动的是模块级函数(`execute_consultation_workflow`、`_join_foreground_vedastro`、三个前台 VedAstro 常量),本单动的是 `JyotishAPIHandler` **类**,重叠不大;C1 的用户价值(每轮省掉一次外网等待)高于一次纯搬运。**本单要吸收 C1 的 diff**:开工时 `execute_consultation_workflow` 里会多出走缓存模块的分支,照常搬运即可,不得把它改回去。 - **2026-09-15 新增前置**:`TASK-freeze-metric-change-20260915` 决定了阶段 4 的收尾口径(见下方阶段 4 的改写),本单收尾前它必须已合入。 - 本单不改任何行为,不新增功能,不动前端。 ## 为什么要做:不是因为文件长,是因为它成了事实上的服务层 `scripts/jyotish_api_server.py` 现在 **11,334 行**(立单时 11,314,2026-09-15 实测已涨到 11,334),契约上限 11,363(`tests/test_api_server_growth_contract.py`:baseline 11,063 + 300 bugfix 余量)——**只剩 29 行**。 但真正的问题不是行数,是这个: ```python # scripts/consultation_workflow_service.py,全文只有 32 行 def execute_consultation_workflow(body, *, surface="api"): from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow as _execute handler = JyotishAPIHandler.__new__(JyotishAPIHandler) # ← 伪造一个从未初始化的 handler return _execute(handler, body=body, surface=surface) def build_runtime_evidence_helpers(chart): handler = JyotishAPIHandler.__new__(JyotishAPIHandler) # ← 又一个 return { "vedastro_official": handler._high_rigor_vedastro_official_summary(chart), "vedastro_archive_manifest": handler._compute_vedastro_gateway_archives(), "interpretation_coverage": handler._interpretation_source_runtime_coverage(chart), } ``` 那个声称是「shared consultation workflow boundary for API and MCP callers」的模块,**反过来依赖单体文件**,并且靠 `__new__` 绕过 `BaseHTTPRequestHandler.__init__` 造一个空壳 handler,只为借用它身上的方法。 全仓 **3 个文件、4 处调用**这样做(下表第一行含两处;收尾断言按 `grep -c` 的 **4** 计,不要按文件数的 3 计): | 位置 | 用途 | | --- | --- | | `scripts/consultation_workflow_service.py:20` / `:27` | 给 `mcp_server.py:741` 与 `:4749` 供咨询链与证据助手 | | `scripts/capture_report_blocked_repairs_golden.py:59` | 生成 golden | | `scripts/local_accuracy_report.py:141` | 离线准确率报告 | **这就是这个文件长不下去的根因**:业务逻辑长在 HTTP handler 类上,非 HTTP 的调用方(MCP、离线脚本、golden 生成)没有别的入口,只能伪造 handler。于是新逻辑也只能继续往这个类里长。 这个后门还很脆:空壳 handler 没有 `self.headers` / `self.wfile` / `self.client_address`,今天能跑只是因为被借用的那几个方法碰巧没碰它们。任何人往里面加一行 `self.headers.get(...)` 就会在 MCP 侧炸。 **所以本单的目标是:让 `JyotishAPIHandler.__new__` 这个后门消失。行数下降是副产品。** ### 现状量化(AST 口径,立单时实测) | 指标 | 值 | | --- | ---: | | 总行数 | 11,314 | | `class JyotishAPIHandler` | 8,070 行 / 222 方法 | | 路由分支 | 72 个,其中 **62 个已经 ≤4 行**(薄注册模式一直在用) | | `_load_local_module(...)` 调用 | 83 次 | | 方法长度 <20 行 | 110 个 | | 方法长度 ≥150 行 | **7 个,合计 2,073 行** | | 方法长度 100–150 行 | 11 个,合计 1,321 行 | 结论:**分派已经很薄,体积集中在少数几个长方法上。** 因此按"业务横切成 chart.py / dasha.py / consult.py"收益小、diff 大、冲突面广,**本单不采用**。 ## 决策记录 产品负责人在 2026-09-15 的对话中授权: 1. 拆 `scripts/jyotish_api_server.py`,**按体积与依赖方向抽取,不按业务横切**。 2. 本单只做阶段 1–3;把 222 个方法拆成 mixin 类的「第三刀」**先不做**,等前三阶段落地后复盘再定。 3. 收尾时**重新冻结行数 baseline**,并把 bugfix 余量从 300 收到 **50**——否则文件会慢慢涨回去。 4. 全程**纯搬运**:不改行为、不改签名、不顺手优化。 ## 硬红线 1. **纯搬运。** 不得修改任何被搬运代码的逻辑、参数、返回结构、异常类型。允许的改动只有:函数从方法变成模块级函数(`self` 参数消失或显式传入)、import 调整、调用点改成薄转发。 2. **`tests/test_api_server_security.py`(3,841 行)是本单唯一的安全网,必须逐条通过,一个断言都不许改。** 若某条断言因搬运而失败,说明搬错了,改代码不改断言。 3. **一轮搬一个模块。** 每搬完一个模块提交一次,并跑 `.venv/bin/python scripts/run_quality_gate.py --profile quick`。不得把多个模块攒成一个大提交。 4. **不得改 `skills/jyotish-vedic-astrology/versions/**` 下的任何文件**——那是冻结的历史 skill 版本副本,里面有同名脚本,改了等于篡改历史版本。 5. 不得改 `.gitea/workflows/**`、不得改 `deploy/gated-paths.txt` 的既有条目、不得提升 `main`。 6. 不得新增任何端点、不得改任何端点的请求/响应合同。 7. 不改前端、不改数据库结构。 8. 每个新模块必须**不 import `jyotish_api_server`**。依赖方向只能是 `jyotish_api_server → 新模块`,反向即为不通过。 ## 任务分解 ### 阶段 1 · 拆掉 `__new__` 后门(本单的核心,必做) 把三处伪造 handler 所借用的逻辑变成真正的模块级函数。 涉及的四个东西: | 现状 | 行数 | 去向 | | --- | ---: | --- | | 模块级 `execute_consultation_workflow(handler, ...)`(`:2160-2560`) | 401 | 改成不需要 handler 的纯函数 | | `JyotishAPIHandler._high_rigor_vedastro_official_summary`(`:4884-5103`) | 220 | 新模块 `scripts/vedastro_runtime_evidence.py` | | `JyotishAPIHandler._compute_vedastro_gateway_archives` | — | 同上 | | `JyotishAPIHandler._interpretation_source_runtime_coverage` | — | 同上 | 做法: 1. 先读清楚这四个函数**实际用到了 handler 的哪些成员**。如果只用到别的纯函数/模块,那 `self` 就是纯装饰,直接去掉。 2. 搬到新模块后,`JyotishAPIHandler` 上留**薄转发方法**(每个 ≤3 行),保证 HTTP 侧行为零变化。 3. 改写 `scripts/consultation_workflow_service.py`:直接 import 新模块,**删掉两处 `JyotishAPIHandler.__new__`**,并去掉对 `jyotish_api_server` 的 import。 4. 同样修 `scripts/capture_report_blocked_repairs_golden.py:59` 与 `scripts/local_accuracy_report.py:141`。 **验收标准** - 全仓 `grep -rn "JyotishAPIHandler.__new__" --include=*.py .`(排除 `skills/*/versions/**`)**零命中**。 - `scripts/consultation_workflow_service.py` 不再 import `jyotish_api_server`。 - `.venv/bin/python -m pytest tests/test_api_server_security.py` 全绿,断言未改。 - MCP 侧:`mcp_server.py:741` 与 `:4749` 两条路径各跑一次,输出与搬运前逐字节一致(把对照写进进度记录)。 - `scripts/local_accuracy_report.py` 与 `scripts/capture_report_blocked_repairs_golden.py` 各跑一次,产物与搬运前一致。 - 新增 `tests/test_api_server_service_boundary.py`:断言新模块不 import `jyotish_api_server`(用 `ast` 静态检查,不靠约定)。 ### 阶段 2 · 抽走其余 ≥150 行的业务方法(必做) | 方法 | 行数 | 去向 | | --- | ---: | --- | | `_attach_local_consultation_layers`(`:1496-1833`,模块级) | 338 | `scripts/consultation_local_layers.py` | | `_compute_active_rectification_events`(`:8951-9254`) | 304 | 推回**已存在**的 `scripts/active_rectification_event_engine.py` | | `_build_chart_prompt_pack`(`:7181-7469`) | 289 | `scripts/chart_prompt_pack.py` | | `_compute_chart_sync`(`:6814-7093`) | 280 | `scripts/chart_compute_service.py` | 每搬一个,handler 上留 ≤3 行薄转发。 **验收标准** - 每个模块一次提交,每次提交后 `run_quality_gate.py --profile quick` 通过。 - `tests/test_api_server_security.py` 每次都全绿。 - `tests/test_calculation_p0_regressions.py` 全绿(`docs/research/pre_work_error_ledger.md` ERR-042 的防复发项:domain / CLI / REST 必须共享 `domain_calculation_service.py`、effective parameters 与 `result_hash`)。 - 每个新模块配一个 golden 单测,fixture 来自**真实引擎响应**,不得手造形状。 ### 阶段 3 · `do_POST` / `do_GET` 改路由表(必做) 现状:`do_POST` 241 行(`:3475-3715`)+ `do_GET` 104 行(`:3370-3473`),共 72 个 `elif path == ...` 分支。**每加一个端点要吃 3–4 行预算。** - 改成 `{path: bound_handler}` 字典分派,保留现有的 `_enforce_request_security` / `_read_json_body` / `acquire_heavy_compute_slot` / 异常映射的统一包装。 - 表化后每个端点 **1 行**。 - 顺带必须搬走的:`/api/location/resolve` 分支把城市别名字典 `{'beijing': '北京', ...}` **直接写在 `do_POST` 里**(`:3485` 一带),搬进对应模块。 **验收标准** - 72 条路径的请求/响应与改动前逐条一致(用 `test_api_server_security.py` 覆盖 + 一份路径清单对照写进进度记录)。 - 404 行为、方法不允许、限流 429、异常映射全部不变。 - `do_POST` + `do_GET` 合计 ≤ 90 行。 ### 阶段 4 · 收尾重新冻结(必做,收尾)—— 2026-09-15 改写 **原方案(行数 baseline 重设 + 余量 300→50)已作废。** 产品 2026-09-15 拍板换口径:余量收到 50 行只是把今天的问题推到三个月后,下一次 bugfix 又会立刻撞墙。新口径由 `TASK-freeze-metric-change-20260915` 落地,本单收尾时按它已经立好的尺子填新基线: | 门 | 收尾时要达到 | | --- | --- | | 主门 · `JyotishAPIHandler.__new__` 全仓计数 | **必须为 0**(今天 4)——这是「拆干净了」唯一不可伪造的证据 | | 主门 · `JyotishAPIHandler` 类方法数 | 显著低于 225,并把新值写成不得增长的新基线 | | 粗护栏 · 文件行数 | baseline 重设为收尾实际行数,余量保持 **300**(不再收到 50) | - 同步更新 `tests/test_api_server_growth_contract.py` 顶部 docstring 的日期与说明。 - **不得**在本单之外的任何轮次里调高任何一条基线。 **验收标准** - `grep -rn "JyotishAPIHandler.__new__" --include=*.py .`(排除 `skills/*/versions/**`)**零命中**,且该断言已经写进合同测试。 - 类方法数新基线已写入合同测试,且人为加一个类方法能让它变红(贴反向验证)。 - `AGENTS.md` §6 的措辞与新口径一致(措辞由 freeze-metric-change 单先行落地,本单只填数)。 ## 预期收益(按 AST 实测推算,允许 ±10%) | 阶段 | 预计减少 | 累计行数 | | --- | ---: | ---: | | 起点 | — | 11,314 | | 阶段 1 + 2 | 约 −1,830 | 约 9,500 | | 阶段 3 | 约 −265 | 约 9,230 | | (后续可选:100–150 行档 11 个方法) | 约 −1,320 | 约 7,900 | 收尾后余额从 **49 行**变成约 **50 行**(因为 baseline 重新冻结),但**每个新端点只吃 1 行**而不是 3–4 行,且长逻辑再也进不来。 ## 让步顺序 1. **阶段 1 不可让步**。`__new__` 后门是本单存在的理由;只做行数不做这个等于没做。 2. 阶段 2 可以只搬其中两个模块,剩下的写进进度记录留到下一轮。 3. 阶段 3 可以推迟,但推迟时**阶段 4 也要跟着推迟**——不能在分派还是 `elif` 链的时候就把余量收到 50,那会立刻卡死下一个端点。 4. **不可让步**:纯搬运不改行为、安全套件断言不许改、新模块不得反向 import、不碰冻结的 skill 版本目录。 ## 开工前置命令 ```bash cd /workspace/Jyotisha git status -sb git fetch origin --prune git log --oneline -1 origin/staging # 必须已包含 TASK-qizheng-native-chart-20260915 的合入 git worktree add -b codex/api-server-decomposition-20260916 \ .worktrees/api-server-decomposition-20260916 origin/staging cd .worktrees/api-server-decomposition-20260916 python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45 # 记下搬运前的基线,验收时逐项对照 wc -l scripts/jyotish_api_server.py .venv/bin/python -m pytest tests/test_api_server_security.py -q | tail -3 .venv/bin/python -m pytest tests/test_calculation_p0_regressions.py -q | tail -3 grep -rn "JyotishAPIHandler.__new__" --include=*.py . | grep -v "skills/.*/versions/" ``` 按模块逐次提交,全部完成后 `git push origin HEAD:staging`(会触发 `backend-quality-gate`,`scripts/**`、`tests/**` 都在门禁路径内),推送后核对远端 SHA 与 `/api/health` 的 `deployment.gitCommit`。 ## BUG 编号起点 `origin/staging` 上当前最大号立单时是 **BUG-699**,已被三份并行单预占到 709。本单从 **BUG-710** 起,开工时重新核对。 本单是重构,正常情况下**不需要开 BUG 号**。唯一预期会用到的场景:搬运过程中发现 `__new__` 空壳 handler 已经在某条路径上造成了真实故障——那才开一条,并关联本单。 ## 进度与记录 - 进度记录:`docs/tasks/PROGRESS-api-server-decomposition-20260916.md`。**必须逐模块记录搬运前后的行数、测试结果、以及 MCP / 离线脚本的输出对照。** - 本单不产生用户可感知的行为变化,`CHANGELOG.md` 只写一句结构说明,Skill 版本**不 bump**。 - 环境缺口写 `BLOCKED.md`。 - 索引:`docs/tasks/README.md` 追加本单。