- 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
18 KiB
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/staging77057a819(写作时);开工以前置两单合入后的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仍 importjyotish_api_server(依赖方向的根治属于后续阶段,需要拍板改补丁落点,见末尾)。
5. 硬红线
- 纯移动:方法体逐字不改;只允许把 15 个 HTTP 侧方法与
server_version从原类剪到新 HTTP 子类、改类声明、调整两个调用方的构造语句。用git diff --color-moved=dimmed-zebra与 AST 方法体对比(第 6 节 T2)证明。 tests/test_api_server_security.py一个字节不改且全绿。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,不在本单删。- 类方法数(合同口径)不得增长;
JyotishAPIHandler.__new__计数只许降。 - 不新增端点、不改任何端点请求 / 响应合同;HTTP 路由、安全检查、限流 429、异常映射行为不变。
- 新计算类不得继承
BaseHTTPRequestHandler,其方法不得引用 HTTP 成员(加 AST 测试钉住)。 - 不改
.gitea/workflows/**、不动表结构、不升级依赖、不碰skills/*/versions/**。 - golden 只能来自真实引擎输出;改既有断言写三栏(本单预期只有增长合同常量)。
6. 任务分解
T1 · 搬前 golden(必做,先做)
- 写一个采集脚本(放
scripts/research/或测试 helper,不放进scripts/根目录),在基线工作树上用第 2.5 节方法采集:- MCP 入口
consultation_workflow_service.execute_consultation_workflow:career、wealth、relationship 三题各一次; - HTTP 侧 handler 的
_compute_consultation_workflow(构造方式与现有测试相同)同三题; scripts/capture_report_blocked_repairs_golden.py现有产物;mcp_server.py里经由build_runtime_evidence_helpers的路径一次(9 月已有端到端测试,复用)。
- MCP 入口
- 去掉
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)仍成立(经由新基类)。
- AST 对比脚本:基线与本分支两个文件里,所有方法按名取
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零命中。
- T1 的 MCP 与 HTTP 两路 golden 逐字节一致(第 2.5 节方法);
T4 · 测试伪造下调(可砍)
- 对第 2.4 节「只用计算方法」的 17 个文件,把构造语句换成
JyotishComputeService();逐文件跑,红了就回退该文件(说明它其实用到 HTTP 侧),回退名单写进进度记录。 - 每处改动按三栏记录(原值
JyotishAPIHandler.__new__(JyotishAPIHandler)/ 新值JyotishComputeService()/ 原因:计算类可正常实例化,不再需要绕过 HTTP 构造)。
T5 · 合同与守卫(必做)
tests/test_api_server_growth_contract.py:JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE2 → 0;JYOTISH_API_HANDLER_NEW_COUNT_BASELINE31 → 收尾实测;方法数基线若因删死代码下降同步下调;行数基线不动(只要不超)。三栏写在常量旁注释与进度记录。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. 开工前置命令
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
后续阶段(本单不做)
- 把咨询闭包搬出文件(107 方法 / 3,805 行 + 62 函数 / 1,958 行):前提是先解决 5 个被按模块属性打补丁的函数。可行做法是把
_load_local_module与异步任务记录层抽成独立模块、两边都按「模块属性」调用,并把测试补丁落点改到新模块——会改test_api_server_security.py的补丁行,需产品拍板(9 月红线)。之后consultation_workflow_service.py才能不再 import 单体。 - 按入口继续切计算类:图表计算、报告、校正、外部证据各成模块(每次一个,增长合同只许降)。
do_POST/do_GET改路由表(9 月 decomposition 阶段 3,仍未做)。- 测试里剩下的 5 处 HTTP 侧伪造:给 HTTP 子类一个测试用构造工厂(放
tests/辅助模块),伪造计数归零。 - 附带发现:排盘缓存命中时 Argala 列表顺序与未命中不同(第 2.5 节),另开单。