Files
Jyotisha/docs/tasks/TASK-api-server-decomposition-20260916.md
T
Jesse_ChenandClaude Opus 5 e4788dfc00 docs(tasks): 换掉两条增长冻结口径 + page.tsx 状态下沉第一簇 + C1 提前
产品 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
2026-09-15 23:15:37 +00:00

14 KiB
Raw Blame History

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,3142026-09-15 实测已涨到 11,334),契约上限 11,363tests/test_api_server_growth_contract.pybaseline 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 -c4 计,不要按文件数的 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 行
方法长度 100150 行 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:59scripts/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.pyscripts/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 == ... 分支。每加一个端点要吃 34 行预算。

  • 改成 {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 版本目录。

开工前置命令

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-gatescripts/**tests/** 都在门禁路径内),推送后核对远端 SHA 与 /api/healthdeployment.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 追加本单。