产品 2026-09-15 三项拍板,落成两份新单与两处既有单的修订: - 新增 TASK-freeze-metric-change-20260915(无 BUG 号,后面两单的前置): 两条冻结余量已用完(page.tsx 1951/1951 余 0;api server 11334/11363 余 29), 冻结从「逼新代码往外走」退化成拦路。实证:page.tsx 行数砍 59% 但 Home() 的 useState 从 56 涨到 66;api server 225 个类方法只有 12 处真碰 HTTP。 主门换成耦合指标,行数降为粗护栏;同时推翻 §6「参数式 hook 内部保持 0 个 React hook」——那正是状态搬不走的原因。 - 新增 TASK-home-state-lowering-20260915(无 BUG 号):先搬 rectification* 那 15 个 state 进已经是 dynamic 子树的校正面,Home() useState 66 → ≤53。 零行为变化;串行在 freeze-metric-change + C2 + R3 之后。 - 修订 TASK-consultation-external-evidence-cache-20260915:依赖反转,C1 排在 API server 拆解之前(它动模块级函数,拆解动类方法);补「不得新增类方法、 行数余量仅 29」的硬红线。 - 修订 TASK-api-server-decomposition-20260916:串行依赖加 C1 与 freeze-metric-change;__new__ 计数按 grep 的 4 计(原文 3 是文件数); 阶段 4 收尾口径改写;基线 11,314 → 11,334。 纯文档推送,不触发门禁、不发布镜像、不部署。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
14 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的写权,并行等于自找冲突。 - 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 行。
但真正的问题不是行数,是这个:
# 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 的对话中授权:
- 拆
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 · 收尾重新冻结(必做,收尾)—— 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 不可让步。
__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追加本单。