The line-count cap is nearly spent (11314 of 11363), but counting lines misreads the problem. Three call sites outside the HTTP layer reach the business logic by doing JyotishAPIHandler.__new__ on an uninitialised handler — consultation_workflow_service twice, which is what MCP calls, plus the golden capture and the offline accuracy report. The declared "shared boundary" module imports back into the monolith, so the dependency arrow points the wrong way and new logic has nowhere to land except that class. Four phases: remove the __new__ backdoor, extract the remaining methods over 150 lines, replace the 72-branch elif dispatch with a table, then re-freeze the baseline with a 50-line allowance instead of 300. Pure relocation; the 3841-line security suite is the safety net and none of its assertions may change. Serial after the qizheng brief, which owns the file until it merges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
12 KiB
TASK-api-server-decomposition-20260916 · 把业务逻辑搬出 HTTP handler
基线
- 代码基线:
origin/staging=2d7698ea(文档树6f74aa67)。开工时以最新origin/staging为准。 - 串行依赖:本单必须等
TASK-qizheng-native-chart-20260915合入 staging 之后才开工。那一单独占scripts/jyotish_api_server.py的写权,并行等于自找冲突。 - 本单不改任何行为,不新增功能,不动前端。
为什么要做:不是因为文件长,是因为它成了事实上的服务层
scripts/jyotish_api_server.py 现在 11,314 行,契约上限 11,363(tests/test_api_server_growth_contract.py:baseline 11,063 + 300 bugfix 余量)——只剩 49 行。
但真正的问题不是行数,是这个:
# 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 处这样做:
| 位置 | 用途 |
|---|---|
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 的对话中授权:
- 拆
scripts/jyotish_api_server.py,按体积与依赖方向抽取,不按业务横切。 - 本单只做阶段 1–3;把 222 个方法拆成 mixin 类的「第三刀」先不做,等前三阶段落地后复盘再定。
- 收尾时重新冻结行数 baseline,并把 bugfix 余量从 300 收到 50——否则文件会慢慢涨回去。
- 全程纯搬运:不改行为、不改签名、不顺手优化。
硬红线
- 纯搬运。 不得修改任何被搬运代码的逻辑、参数、返回结构、异常类型。允许的改动只有:函数从方法变成模块级函数(
self参数消失或显式传入)、import 调整、调用点改成薄转发。 tests/test_api_server_security.py(3,841 行)是本单唯一的安全网,必须逐条通过,一个断言都不许改。 若某条断言因搬运而失败,说明搬错了,改代码不改断言。- 一轮搬一个模块。 每搬完一个模块提交一次,并跑
.venv/bin/python scripts/run_quality_gate.py --profile quick。不得把多个模块攒成一个大提交。 - 不得改
skills/jyotish-vedic-astrology/versions/**下的任何文件——那是冻结的历史 skill 版本副本,里面有同名脚本,改了等于篡改历史版本。 - 不得改
.gitea/workflows/**、不得改deploy/gated-paths.txt的既有条目、不得提升main。 - 不得新增任何端点、不得改任何端点的请求/响应合同。
- 不改前端、不改数据库结构。
- 每个新模块必须不 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 |
— | 同上 |
做法:
- 先读清楚这四个函数实际用到了 handler 的哪些成员。如果只用到别的纯函数/模块,那
self就是纯装饰,直接去掉。 - 搬到新模块后,
JyotishAPIHandler上留薄转发方法(每个 ≤3 行),保证 HTTP 侧行为零变化。 - 改写
scripts/consultation_workflow_service.py:直接 import 新模块,删掉两处JyotishAPIHandler.__new__,并去掉对jyotish_api_server的 import。 - 同样修
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不再 importjyotish_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:断言新模块不 importjyotish_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.mdERR-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 · 重新冻结行数契约(必做,收尾)
- 把
tests/test_api_server_growth_contract.py的JYOTISH_API_SERVER_LINE_COUNT_BASELINE更新为本单收尾时的实际行数。 - 余量从 300 改为 50。
- 同步更新该文件顶部 docstring 里的日期与说明,以及
AGENTS.md§6 里引用的口径。 - 不得在本单之外的任何轮次里调高 baseline。
验收标准
tests/test_api_server_growth_contract.py通过,且新 cap = 新实际行数 + 50。AGENTS.md§6 的措辞与新口径一致。
预期收益(按 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 不可让步。
__new__后门是本单存在的理由;只做行数不做这个等于没做。 - 阶段 2 可以只搬其中两个模块,剩下的写进进度记录留到下一轮。
- 阶段 3 可以推迟,但推迟时阶段 4 也要跟着推迟——不能在分派还是
elif链的时候就把余量收到 50,那会立刻卡死下一个端点。 - 不可让步:纯搬运不改行为、安全套件断言不许改、新模块不得反向 import、不碰冻结的 skill 版本目录。
开工前置命令
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追加本单。