refactor(api): 抽出 offline compute mixins,关掉 2/4 处 handler 伪造
Independent Staging Quality Gate / validate (push) Canceled after 2m18s
Independent Staging Quality Gate / publish (push) Canceled after 0s

spike 闸门(任务书 §5.2)结果为红,按 §6.3 退回 A 方案。

spike:115 个闭包方法整体移入 ConsultationComputeMixin 后,
tests/test_api_server_security.py 一字不改跑出 1 failed / 128 passed。
test_chart_async_job_executes_in_background 挂在
monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', ...):
调用方法随 mixin 搬走后从新模块 globals 解析,补丁落在旧模块绑定上不生效。
已实证把同一 fake 打到 mixin 模块即恢复原行为,故为落点问题而非搬坏。
循环 import 不是障碍(移动集不引用 JyotishAPIHandler)。

退回 A 的实际交付:新建 scripts/offline_compute_mixins.py,
收 BadRequest、3 个模块级助手,以及 RequestParamMixin /
VedastroEvidenceMixin / SynastryMixin 共 7 个方法(300 行),
JyotishAPIHandler 通过继承保留全部方法,HTTP 侧零变化。
consultation_workflow_service.build_runtime_evidence_helpers 与
local_accuracy_report 改为直接实例化 mixin,不再伪造 handler。

11 个搬走的定义经 SHA-256 逐个比对与搬走前字节级相同;
jyotish_api_server.py 的 diff 为 11 行插入 / 378 行删除,无重排。

scripts 侧 __new__ 4 → 2(剩 2 处都在咨询工作流链上);
类方法 225 → 218;行数 11,291 → 10,924。
合同测试收紧基线并新增 scripts 侧专门断言与反向 import 断言;
两次反向验证(加回 __new__ / 加类方法)均正确变红。
新增 tests/test_offline_compute_mixins.py 覆盖此前零覆盖的 MCP 路径,
并加入 CORE_PYTEST_TARGETS。

tests/test_api_server_security.py 一字未改,129 passed。
快速门 pytest 段 798 passed / 1 skipped / 0 failed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-16 03:46:01 +00:00
co-authored by Claude Opus 5
parent 30f9e0943f
commit 5582091851
9 changed files with 792 additions and 468 deletions
@@ -1,105 +1,250 @@
# PROGRESS · 关掉 `JyotishAPIHandler.__new__` 后门
- 日期:2026-09-16
- 分支:`codex/api-server-backdoor-close-20260916`
- 基线:`origin/staging` @ `37e6c519`(任务书写的 `4f643aa0` 已被超车
- 状态**§5.1 完成,§5.2 起停手待决**——闭包重算结果推翻了任务书「8 个方法 / 314 行」的规模前提
- 分支:`codex/api-server-backdoor-close-20260916`rebase 到 `origin/staging` @ `5094fd26`
- 任务书:`docs/tasks/TASK-api-server-backdoor-close-20260916.md`(第二版,B / mixin 方案
- 结论**spike 闸门红 → 按 §6.3 退回 A**。scripts 侧 `__new__` **4 → 2**,未归零。
---
## 1. §5.1 传递闭包重算(强制第一步
## 1. §5.1 闭包复核(在 `dc8cae31` 代码上重算
方法:`ast` 解析 `scripts/jyotish_api_server.py`,从四处 `JyotishAPIHandler.__new__` 实际借用的入口出发,沿 `self._x()` **以及模块级函数的 `handler._x()`** 追传递闭包,并对每个成员扫 `self.headers` / `wfile` / `rfile` / `path` / `client_address` 等 HTTP 上下文属性
`ast` 解析,从四处伪造实际借用的入口出发,沿 `self._x()` **以及模块级函数的 `handler._x()`** 追传递闭包
### 1.1 入口(四处伪造实际借用的方法)
| 位置 | 借用 |
| --- | --- |
| `scripts/consultation_workflow_service.py:20` | 模块级 `execute_consultation_workflow(handler, ...)` |
| `scripts/consultation_workflow_service.py:27` | `_high_rigor_vedastro_official_summary``_compute_vedastro_gateway_archives``_interpretation_source_runtime_coverage` |
| `scripts/capture_report_blocked_repairs_golden.py:59``:234` | `_compute_consultation_workflow` |
| `scripts/local_accuracy_report.py:141` | `_compute_synastry` |
### 1.2 实测结果
| | 任务书 §1 | **实测** |
| --- | ---: | ---: |
| 需要搬的类方法 | 8 | **115** |
| 合计行数 | 314 | **4,104** |
| 其中碰 HTTP 上下文 | 0 | **0** |
| 另外途经的模块级函数(已在类外,不需搬) | — | 65 个 / 2,028 行 |
| 占全类(regex 225 方法 / 11,291 行)比例 | 4 % / 4 % | **51 % / 36 %** |
**前提成立的那一半**:闭包里 **0 个**方法碰 HTTP 上下文。任务书 §5.1 的停手条件(有方法碰 HTTP)未触发。
**前提不成立的那一半**:规模是任务书估算的 **13 倍**
### 1.3 按入口拆分——闭包分裂得很干净
| 入口 | 闭包方法数 | 行数 | 可搬? |
| --- | ---: | ---: | --- |
| `_compute_vedastro_gateway_archives` | 1 | 4 | ✅ |
| `_interpretation_source_runtime_coverage` | 1 | 41 | ✅ |
| `_high_rigor_vedastro_official_summary` | 1 | 220 | ✅ |
| `_compute_synastry` | 4 | 35 | ✅ |
| `_compute_consultation_workflow` | **114** | **4,086** | ❌ |
| 模块级 `execute_consultation_workflow` | 113 | 4,080 | ❌ |
前四项去重合计 **7 个方法 / 300 行**——这正是任务书「8 个方法 / 314 行」的来源。
### 1.4 任务书数字是怎么偏的(已定位)
`execute_consultation_workflow` 是**模块级函数**,不是类方法。一次只跟 `self._x()` 的闭包走到它就停了,于是 `_compute_consultation_workflow`(自身 6 行)被当成叶子。但它签名是 `execute_consultation_workflow(handler, *, body, surface)`,函数体内对传进来的 `handler` 调了 **113 个**类方法。跟进这一层,闭包从 300 行炸到 4,104 行。
`consultation_workflow_service.py:20``capture_report_blocked_repairs_golden.py:59` 这两处伪造都落在这条链上。
### 1.5 闭包里的大方法(与任务书 §4.3 直接冲突)
| 方法 | 行数 |
| | 实测 |
| --- | ---: |
| `_build_chart_prompt_pack` | 289 |
| `_compute_chart_sync` | 280 |
| `_high_rigor_vedastro_official_summary` | 220 |
| `_derived_career_evidence` | 144 |
| `_compute_thematic_report` | 122 |
| `_derive_thematic_evidence` | 111 |
| `_compute_prashna` | 101 |
| 闭包类方法 | **115** |
| 合计行数 | **4,104** |
| 碰 HTTP 上下文(`headers`/`wfile`/`rfile`/`path`/`client_address` | **0** |
| 闭包途经的模块级函数 | 65 个 / 2,028 行 |
| 移动集还需要的模块级类 | 2`BadRequest``JobQueueFull` |
| 移动集还需要的模块级赋值 | 26 |
`_build_chart_prompt_pack`289)与 `_compute_chart_sync`(280)是 ≥150 行的业务方法,属于任务书 §4.3 明令**不得搬**的阶段 2 范围,却落在 §5.2 要求必须搬的闭包里。**§5.2 ∧ §4.3 在真实代码上不可同时满足。**
与第二版任务书 §1 的数字一致,前提「0 个碰 HTTP」成立。
`_high_rigor_vedastro_official_summary` 虽然 220 行,但它是后门入口本身、且含在任务书自己的 314 行估算内,不算 §4.3 的「顺手搬」。)
**循环 import 不是障碍**:移动集的自由名里**不含 `JyotishAPIHandler`**,方向是单向的 `jyotish_api_server → mixin`。任务书 §5.2 点名的这条风险,实测可解。
闭包按入口分裂:
| 入口 | 方法 | 行数 |
| --- | ---: | ---: |
| `_compute_vedastro_gateway_archives` | 1 | 4 |
| `_interpretation_source_runtime_coverage` | 1 | 41 |
| `_high_rigor_vedastro_official_summary` | 1 | 220 |
| `_compute_synastry` | 4 | 35 |
| `_compute_consultation_workflow` | **114** | **4,086** |
---
## 2. 另一处口径偏差:`__new__` 基线不是 4
## 2. §5.2 spike 闸门:**红**
`tests/test_api_server_growth_contract.py` 现行基线 `JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 33`,注释写明「scripts/ production forgeries = 4, tests/ = 29」。
机械切分(源码行切片,方法体逐字搬运):115 方法 → `ConsultationComputeMixin`
`class JyotishAPIHandler(BaseHTTPRequestHandler, ConsultationComputeMixin)`
连同 65 个模块函数 / 2 个类 / 26 个赋值一并移入新模块。两文件均可解析、可 import,
MRO 正常(`_compute_synastry` / `_compute_consultation_workflow` 都能解析到)。
任务书 §4.1 要求 `grep -rn "JyotishAPIHandler.__new__" --include=*.py .`(排除 `skills/*/versions/**`**零命中**。该 grep 覆盖 `tests/`,其中 29 处分布在 20 个测试文件里,**包括 `tests/test_api_server_security.py:34`**——而 §4.2 规定该文件不许改。
`tests/test_api_server_security.py` **一个字不改**直接跑:
因此「零命中」只能理解为 **scripts/ 生产侧零命中**4 → 0),tests/ 的 29 处保持不增长。这与 §4.1 括号里的「基线 4 → 0」一致。
```
1 failed, 128 passed in 43.17s
FAILED tests/test_api_server_security.py::test_chart_async_job_executes_in_background
```
### 2.1 红在哪、为什么
```python
monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', fake_write) # 测试第 3768 行附近
...
assert writes[0][0] == 'api_chart_response' # IndexError: list index out of range
```
`_enqueue_chart_job` 随 mixin 搬到新模块后,它对 `_write_async_job_record` 的查找走
**新模块的 globals**;而测试把补丁打在 `jyotish_api_server` 的 globals 上。两个绑定不再是同一个,
补丁静默失效,`writes` 始终为空。
**这不是搬坏了代码**,已实证:把同一个 fake 改打到 mixin 模块上,行为与搬运前完全一致——
```
endpoint: chart_async mode: async_submitted status: queued
writes captured: 3 first scope: api_chart_response # 正是测试断言的值
```
### 2.2 为什么不能绕过
security 测试用 `jyotish_api_server.X` 打模块级补丁的名字共 6 个,其中 3 个落在移动集里
`_attach_vedastro_main_entry_overview``_load_local_module``_write_async_job_record`)。
任何 B 形态的切分,只要「被打补丁的函数」与「调用它的方法」一起离开 `jyotish_api_server` 的 globals
这条断言就会红。提到第三个共享模块也一样(`from third import X` 在两边各建一个绑定)。
唯一能保住绿的做法是给 `jyotish_api_server` 换一个自定义 module 类、劫持 `__setattr__` 往 mixin 模块转发——
那是新增机制、不是纯搬运,属于任务书 §6.3 明令的「硬做 B」。
按 §6.3**退回 A,不硬做 B。** spike 产物已丢弃,`scripts/` 回滚干净后重做。
---
## 3. 可行性预检(为 §5.2 备
## 3. 实际交付(A 方案
两组小闭包搬出去后对 `jyotish_api_server` 的模块级依赖,均可解析、无反向 import 死结
新建 `scripts/offline_compute_mixins.py`412 行)
| | 方法 | 需一并处理的模块级名字 |
| --- | --- | --- |
| vedastro 证据 | `_high_rigor_vedastro_official_summary``_compute_vedastro_gateway_archives``_interpretation_source_runtime_coverage` | `_build_vedastro_official_full_snapshot_payload_from_chart`(L2942)、`_free_tier_queue_enabled_env`(L2564)、`_preferred_strict_contract`(L3005) |
| 合盘 | `_compute_synastry``_get_float``_check_range``_normalize_degree` | `BadRequest`(L3177) |
| 内容 | 成员 |
| --- | --- |
| `BadRequest` | 从 api server 搬出(api server 改为 import 回来,205 处引用不变) |
| 3 个模块函数 | `_build_vedastro_official_full_snapshot_payload_from_chart``_free_tier_queue_enabled_env``_preferred_strict_contract` |
| `RequestParamMixin` | `_get_float``_check_range``_normalize_degree` |
| `VedastroEvidenceMixin` | `_high_rigor_vedastro_official_summary``_compute_vedastro_gateway_archives``_interpretation_source_runtime_coverage` |
| `SynastryMixin(RequestParamMixin)` | `_compute_synastry` |
这些依赖全在 `jyotish_api_server.py` 内部定义,需随组搬走或提到共享模块,否则新模块就得反向 import(违反 §4.4)。
`class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryMixin)`
HTTP 侧通过继承拿回全部 7 个方法(`self._get_float` 等 74 处调用点一行未改)。
两个调用方改成直接实例化 mixin,不再伪造 handler
- `scripts/consultation_workflow_service.py:27` `build_runtime_evidence_helpers``VedastroEvidenceMixin()`
- `scripts/local_accuracy_report.py:141``SynastryMixin()._compute_synastry(...)`
**仍在用 `__new__` 的 2 处**(都挂在咨询工作流链上,属于 spike 红掉的那一半):
`scripts/consultation_workflow_service.py:20``scripts/capture_report_blocked_repairs_golden.py:59`
### 3.1 改前 / 改后
| 指标 | 改前 | 改后 |
| --- | ---: | ---: |
| `JyotishAPIHandler` 类方法数(`^ (?:async )?def` | 225 | **218** |
| `scripts/jyotish_api_server.py` 行数 | 11,291 | **10,924** |
| 新模块行数 | — | 412 |
| `__new__` scripts 侧 | 4 | **2** |
| `__new__` tests 侧 | 29 | 29(未动) |
| `__new__` 合计 | 33 | **31** |
### 3.2 §4.2「方法体一字不改」的证明
`git show HEAD:scripts/jyotish_api_server.py` 与新模块各自 `ast` 取定义源码切片,逐个 SHA-256 比对,
**11 个搬走的定义全部字节级相同**
```
IDENTICAL M:_get_float cf428670791b cf428670791b (10 lines)
IDENTICAL M:_check_range 5cb38a5550ba 5cb38a5550ba (5 lines)
IDENTICAL M:_normalize_degree b2ac5ec5533a b2ac5ec5533a (2 lines)
IDENTICAL M:_compute_synastry ad868c3b8e1f ad868c3b8e1f (18 lines)
IDENTICAL M:_high_rigor_vedastro_official_summary 42219ca0bef6 42219ca0bef6 (220 lines)
IDENTICAL M:_compute_vedastro_gateway_archives faa9643b7287 faa9643b7287 (4 lines)
IDENTICAL M:_interpretation_source_runtime_coverage 333b5c13515c 333b5c13515c (41 lines)
IDENTICAL T:_build_vedastro_official_full_snapshot_payload_from_chart d1cafe318865 d1cafe318865 (61 lines)
IDENTICAL T:_free_tier_queue_enabled_env 63a3fd275267 63a3fd275267 (7 lines)
IDENTICAL T:_preferred_strict_contract e251697dd292 e251697dd292 (6 lines)
IDENTICAL T:BadRequest 99667903a999 99667903a999 (2 lines)
```
搬运用按行区间删除实现,`jyotish_api_server.py` 的 diff 形态是
**11 行插入(import 块 + 类头一行)/ 378 行删除**,没有重排、没有空行规范化。
---
## 4. 环境
## 4. §5.5 合同测试
`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45``exit=0`,六项检查全绿(python_runtime / fragment_scan / external_engine_adapters / remote_visibility=verified / focused_tests)。
`tests/test_api_server_growth_contract.py`
- 行数 baseline `11291``10924`+300 粗护栏不变)
- 类方法数 baseline `225``218`
- `__new__` 合计 baseline `33``31`
- **新增** `JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE = 2``test_scripts_handler_forgeries_must_not_grow`
单独锁 scripts 生产侧,注释写明目标仍是 0 及剩下 2 处为什么没关
- **新增** `test_offline_mixins_must_not_import_the_http_monolith`(§4.3 的方向断言,`ast` 查 import
- 原有三条断言(`AGENTS.md``must not grow` / `thinly registered`、该测试在 `CORE_PYTEST_TARGETS` 与快速门里)未改,仍绿
`7 passed`(原 5 条 + 新增 2 条)。
### 4.1 两次反向验证
**① 在 `scripts/` 加回一处 `JyotishAPIHandler.__new__`** → 必须红:
```
FAILED tests/test_api_server_growth_contract.py::test_jyotish_api_handler_new_count_must_not_grow
FAILED tests/test_api_server_growth_contract.py::test_scripts_handler_forgeries_must_not_grow
2 failed, 5 passed
```
**② 给 `JyotishAPIHandler` 加一个类方法**(218 → 219)→ 必须红:
```
FAILED tests/test_api_server_growth_contract.py::test_jyotish_api_handler_method_count_must_not_grow
1 failed, 6 passed
```
两次探针均已还原,工作树无残留。
---
## 5. 待决
## 5. 新增端到端守卫
见回报。§5.2 / §5.3 / §5.4 未动,`scripts/``tests/` 零改动
`build_runtime_evidence_helpers` 原本**零测试覆盖**(只有 `mcp_server.py:4751` 在调)
新增 `tests/test_offline_compute_mixins.py`4 条)覆盖 §5.4 验收:
1. `SynastryMixin()._compute_synastry``ashtakoot.calculate_ashtakoot` 逐字段一致(`local_accuracy_report` 路径)
2. `build_runtime_evidence_helpers` 不伪造 handler 也能返回三个键(MCP 路径,端到端)
3. `ast` 断言该函数体内已无 `JyotishAPIHandler`
4. `JyotishAPIHandler` 仍持有全部 7 个方法、继承链正确(HTTP 侧零变化)
同时把该文件加进 `scripts/run_quality_gate.py``CORE_PYTEST_TARGETS`——该列表自带注释
「不在这个清单里的守卫在 CI 里永远不跑」,本单的后门守卫必须进门禁。
---
## 6. §7 验收
```
pytest tests/test_api_server_security.py tests/test_api_server_growth_contract.py \
tests/test_consultation_consumer_context.py tests/test_vedastro_gateway.py \
tests/test_vedastro_snapshot_cache.py
→ 190 passed
```
`tests/test_api_server_security.py` 单独跑:**129 passed,文件一字未改**`git diff --stat tests/` 对该文件为空)。
快速门(退出码直接取,不隔管道):
```
.venv/bin/python scripts/run_quality_gate.py --profile quick → EXIT=127
pytest 段:798 passed, 1 skipped, 0 failed
失败步骤:step: npm test → sh: 1: tsx: not found
```
pytest 段 0 failed,且比 `dc8cae31` 基线(792 passed / 1 skipped**多 6 条**
正好是本轮新增的 4 条(`test_offline_compute_mixins.py`+ 2 条(合同测试)。
`tsx: not found` 是**本工作树没装 `frontend/node_modules`**(主检出 `/workspace/Jyotisha/frontend/node_modules` 有),
不是仓库缺口,也与本单纯 Python 改动无关,故未新开 BLK 条目。
开工预检 `python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45``exit=0`,六项全绿。
### 6.1 门禁外的定向抽查(`BadRequest` 搬家的下游)
`BadRequest``jyotish_api_server` 搬进新模块后由 api server import 回来,
仓内两个直接 `from jyotish_api_server import BadRequest` 的测试照常解析同一个类对象:
- `tests/test_declared_window_chart.py` —— 在快速门的 798 条里,通过
- `tests/test_active_rectification_api.py` —— **不在快速门**,定向跑:`1 failed / 50 passed`
那一条失败 `test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow`
**是基线即红,与本单无关**。在 `5094fd26` 的干净工作树上单独复跑同一条,
失败停在同一行(`tests/test_active_rectification_api.py:666`)、同一断言:
```
{'start_time': '04:16'} != {'start_time': '05:07'}
```
属校正打分的期望漂移,不涉及本轮搬走的任何名字,未修(§4.6 不得顺手修不在本单里的问题)。
---
## 7. 没做的 / 留给下一轮
- **scripts 侧 `__new__` 未归零**,剩 2 处:`consultation_workflow_service.py:20`
`capture_report_blocked_repairs_golden.py:59`。两者都要整条咨询工作流闭包(114 方法 / 4,086 行)搬走,
而搬走就会触发 §2.1 的 monkeypatch 落点问题。**下一轮要先解决的是那 3 个被模块级打补丁的名字怎么安置,不是体量。**
一个可行方向:先把 `_write_async_job_record` / `_load_local_module` /
`_attach_vedastro_main_entry_overview` 连同各自调用方一起留在 api server,
或把异步任务记录层单独抽成一个两边都 import 的模块并同步改测试落点(需产品批准动 security 测试)。
- 阶段 3`do_POST` 路由表)按 §9 不做。
- 本单不产生 Bug 记录,不进 `CHANGELOG.md`
+2 -2
View File
@@ -232,7 +232,7 @@
| `TASK-rectification-title-repair-migration-20260915.md` | `PROGRESS-rectification-title-repair-migration-20260916.md` | BUG-699 / 704 的数据修补写成了 Node 脚本(要 `SCHEMA_DATABASE_URL`),但 `Migrate Staging Database` 只跑 `migrator` 应用 SQL 迁移、不执行任意脚本——产品没有任何按钮能修自己那批错名字的会话。脚本里本来就是纯 SQL,搬进一次性迁移即可复用现成按钮。生产停在 `7b620c7a`(无 `use-rectification-surface.ts`),where 自然匹配 0 行,是 no-op | 待验收(`d6c359b2`,分支 `codex/rectification-title-repair-migration-20260916`):迁移 `20260916020000_rectification_session_title_repair.sql`SQL 由 `sed` 从脚本切出、合同测试比对逐字一致;幂等两层(迁移器记账 + where 改完即不再命中);顺带给 `db-migrate.mjs` 加 NOTICE 转发,否则 node-postgres 丢掉 NOTICE、产品在日志里一个数字都看不到。tsc 0 错、lint 0 error、`npm test` 3334/fail 31 对基线 3329/fail 31+5 全绿)。**无 Docker/无本地 PG**`test:db` 没跑、`db:migrate:check` 缺 `SCHEMA_DATABASE_URL`,记为 BLK-002 | `codex/rectification-title-repair-migration-20260916` |
| `TASK-staging-dispatch-autofill-sha-20260915.md` | `PROGRESS-staging-dispatch-autofill-sha-20260915.md` | `Migrate Staging Database` 每次都要手抄 40 位 SHA,而那个值恰恰是「最新一个过门禁的 staging 提交」——机器能自己算,查询代码那一步里就有。改成留空自动解析、填了仍走原路径(回滚用),三条安全属性一条不丢。**产品 2026-09-15 明确授权修改该 workflow,执行方不得以 AGENTS.md §2.7 拒改**;生产两个按钮保持手填,那是护栏不是麻烦 | 待验收 | `codex/staging-dispatch-autofill-sha-20260915` |
| `TASK-staging-auto-migrate-on-deploy-20260915.md` | `PROGRESS-staging-auto-migrate-on-deploy-20260915.md` | 门禁通过后自动先跑 staging 迁移再部署,不再手点(迁移幂等、无挂起时是 no-op,`db-migrate.mjs --check` 挂起返 3 可用于日志)。今天 `deploy-staging.yml` 完全不提迁移,忘点就让新代码跑在旧 schema 上且无人拦。**产品再次授权改 workflow,范围限 `backend-quality-gate.yml` 的 dispatch 段**;迁移失败必须阻断部署;回滚不自动迁移;生产完全不动。⚠️ 同轮必须把「迁移须对已部署代码向后兼容、破坏性变更拆两轮」写进 AGENTS.md §7.6 | 待验收 | `codex/staging-auto-migrate-on-deploy-20260915` |
| `TASK-api-server-decomposition-20260916.md` | `PROGRESS-api-server-decomposition-20260916.md` | **已取代:阶段 1 由 `TASK-api-server-backdoor-close-20260916` 完成(实测只需 8 方法 / 314 行);阶段 2/3 产品拍板不立单,由新门禁长期推进。以下为原文** · 重构单(串行在 qizheng 单之后;2026-09-15 又加两条前置:C1 外网缓存单先做、freeze-metric-change 先落地)**:把业务逻辑搬出 `JyotishAPIHandler`。核心不是行数,是 3 个文件 **4 处**靠 `JyotishAPIHandler.__new__` 伪造空壳 handler 借方法(`consultation_workflow_service` ×2、`capture_report_blocked_repairs_golden`、`local_accuracy_report`,MCP 也走这条),依赖方向反了、handler 没有 `headers`/`wfile` 随时可炸——实测佐证:**225 个类方法里只有 12 处真的碰 HTTP 上下文**。四阶段:拆 `__new__` 后门 → 抽 ≥150 行业务方法 → `do_POST`/`do_GET` 改路由表 → **阶段 4 已改写**:收尾不再是「行数 baseline + 余量 300→50」(那只是把问题推到三个月后),改成主门 `__new__` 计数必须为 0 + 类方法数不得增长,行数退为粗护栏保持 300 余量。纯搬运不改行为,`test_api_server_security.py` 3841 行断言一条不许改。预计 11,334 → 约 9,230 行。BUG 段 710+ | 已取代 | 见 `TASK-api-server-backdoor-close-20260916.md` |
| `TASK-api-server-decomposition-20260916.md` | `PROGRESS-api-server-decomposition-20260916.md` | **已取代:阶段 1 由 `TASK-api-server-backdoor-close-20260916` 承接(该单第一版估的「8 方法 / 314 行」已被实测证伪,真实闭包 115 方法 / 4,104 行;2026-09-16 收尾只关掉 4 处伪造中的 2 处,另 2 处待后续);阶段 2/3 产品拍板不立单,由新门禁长期推进。以下为原文** · 重构单(串行在 qizheng 单之后;2026-09-15 又加两条前置:C1 外网缓存单先做、freeze-metric-change 先落地)**:把业务逻辑搬出 `JyotishAPIHandler`。核心不是行数,是 3 个文件 **4 处**靠 `JyotishAPIHandler.__new__` 伪造空壳 handler 借方法(`consultation_workflow_service` ×2、`capture_report_blocked_repairs_golden`、`local_accuracy_report`,MCP 也走这条),依赖方向反了、handler 没有 `headers`/`wfile` 随时可炸——实测佐证:**225 个类方法里只有 12 处真的碰 HTTP 上下文**。四阶段:拆 `__new__` 后门 → 抽 ≥150 行业务方法 → `do_POST`/`do_GET` 改路由表 → **阶段 4 已改写**:收尾不再是「行数 baseline + 余量 300→50」(那只是把问题推到三个月后),改成主门 `__new__` 计数必须为 0 + 类方法数不得增长,行数退为粗护栏保持 300 余量。纯搬运不改行为,`test_api_server_security.py` 3841 行断言一条不许改。预计 11,334 → 约 9,230 行。BUG 段 710+ | 已取代 | 见 `TASK-api-server-backdoor-close-20260916.md` |
| `TASK-chart-vedastro-decouple-20260915.md` | `PROGRESS-chart-vedastro-decouple-20260915.md` | **P0**:星盘页首屏那一发 `/api/chart` 没传 `skip_vedastro_main_entry_overview`,实测冷算 0.400.66 秒里约 0.36 秒是 VedAstro 空转(本机连 endpoint 都没配);生产 env 开着 network + fanout,等于首屏同步等 24 个外部请求 + 3 次领域扫描,而 `chart-view-mapper.ts` / `chart-view-contract.ts` 根本不读这份证据。星历页同端点传了标志,两页策略相反。BUG-718,**复发自 BUG-161**(前台请求不得同步串联可选外部证据)。串行在 chart-page-blocking-open 之后 | 待验收 | `codex/chart-vedastro-decouple-20260915` |
| `TASK-vedastro-runtime-ops-20260915.md` | `PROGRESS-vedastro-runtime-ops-20260915.md` | 运行期真相单(与上单并行,文件不重叠;**不得改 `jyotish_api_server.py`**):官方 `vedastro==1.23.25` 其实是 REST 客户端(46 KB,全打 `api.vedastro.org`),且 import 时请求 pypi 并 `pip install --upgrade` 自升级——本机实测 pin 装完一 import 就变 1.23.26`requirements.txt` 的锁在运行期是假的(BUG-719);无 key 时免费层排队是同步 sleep + 全局锁,24 个请求 ≈ 4.8 分钟堵住前台线程(BUG-720,定级依赖生产 key 是否配置)。生产 env 核对清单在 `docs/testing/vedastro-runtime-20260915.md`,**只能由产品负责人执行**。台账 ERR-107 / ERR-108 | 待验收 | `codex/vedastro-runtime-ops-20260915` |
| `TASK-rectification-engine-memoization-20260915.md` | `PROGRESS-rectification-engine-memoization-20260915.md` | **性能单(纯 Python,独占引擎三文件,可并行)**:一次重算 45% 的 CPU 是重复算同一份 Shadbala——`build_candidate_static_context` 每个候选分钟已算过一次却只留哈希、丢掉结果,`_candidate_row` 在「候选 × 事件 × 采样日期」最内层再算 36 遍(实测 2196 次 vs 应 61 次)。过境盘只依赖事件日期却按候选算 2196 次(应 36);鉴别探针一次请求算两遍;`_cached_rows` 是死代码。本机等价实验 3358 → 1604 ms**快 53%**),`candidate_scores` 与 `decision_receipt` 逐字相同(唯一差异是计时字段)。**只做记忆化,不改算法**;year 精度采满 12 个月**产品 2026-09-15 决定不改、研究单也不立**。BUG 段 721 | 已验收(带修复单) | `53a37ce9`(BUG-721)。实现等价性由改前/改后同机对比独立证实;等价 golden 跨机不稳另出修复单 |
@@ -247,7 +247,7 @@
| `TASK-rectification-engine-memoization-fix-20260915.md` | `PROGRESS-rectification-engine-memoization-fix-20260915.md` | **验收修复单(只改测试,一行实现不许动)**:BUG-721 的实现**等价性成立**(我在改前 `6b3248bf` / 改后 `e4788dfc` 同机跑同一 payload`candidate_scores` 逐字相同),9 条计数断言全过;但等价 golden 在本机复现不出来——4 处浮点尾数差(score 1.0e-4 ×2、`margin_percent` 1.1e-3 ×2)。**复发自 BUG-712**(「不得对全精度浮点做整体 `==`」,那一单只落在 ephemeris 一处)。而 `tests/test_rectification_*.py` 在 `CORE_PYTEST_TARGETS` 里,**staging 门禁靠机器舍入碰巧一致才是绿的**。修法:主证据换成**同进程差分**(把 static context 的四个缓存键置 `None` 即可回退旧路径,A/B 严格相等),golden 降为离散字段严格相等 + 浮点带容差(容差按实测 1.1e-3 推);**禁止重建 golden 来「修」**。另含六份 golden 的仓库级排查。BUG-733 | 已验收 | `2b7b4565`(BUG-733)。同进程差分立为主证据,golden 降为离散严格+浮点容差;本机 14 条全绿 |
| `TASK-consultation-residual-hotspots-20260916.md` | `PROGRESS-consultation-residual-hotspots-20260916.md` | **性能单(与拆解单文件不重叠,可并行)**:BUG-727 的快照缓存已验收生效(逐轮 trace 确认每轮 hit、响应体 52 万→40 万字符),但耗时没降——重测 6 次调用 5.30 s 里**网络仍占 70 %**poll 2.42 + ssl read 0.76 + 握手 0.56),本地占星计算只剩 **2.4 %**。剩下那一次外网是 `gateway_status → probe_official_rest_health → probe_calculate_health`:**它不是 ping,是把一份虚构排盘 POST 给 `api.vedastro.org`**,而且 `consume_rate_token()` 会**烧掉对方一个业务限流令牌**,还不复用连接(6 次调用 3 次 TLS 握手,单次 0.186 s),结果零 TTLBUG-734)。另 `yoga_engine._eval_custom` 对静态规则表的 192 条表达式**每请求重新编译 102 次**(多语句的还要先抛一次 SyntaxError 再 parse+改写 AST+compile),占 9.4 %BUG-735)。修法:探测加 TTL(成功 60 s / 失败 10 s,配置变即失效,**诊断端点必须 force_refresh 保持实时**+ 共享 opener;表达式缓存 code object。**硬红线:绝不缓存 yoga 求值结果**(会跨用户串盘)。等价证明用 BUG-733 的同进程差分,不再写跨机 golden。BUG 段 734735 | 待验收 | `codex/consultation-residual-hotspots-20260916`6.3 探测 TTL 与 6.1 编译缓存均完成,**6.2 共享 opener 按让步顺序砍掉**——实测 `urllib.request` 无连接池,模块级共享 opener 在 6 次请求下仍开 6 条 TCP 连接,按任务书写法只能骗过「同一个 opener 实例」的断言而省不掉任何握手,另立单。同口径实测:6 轮前台请求探测 6→1 次、握手 5→1 次、对方限流令牌 6→1 个(**改前第 6 轮探测自己被限流、报出假 `official_blocked`**);yoga 每次检测源码编译 386→第二次起 0,单次 27.4→3.3 ms。等价用 192 条表达式 ×3 盘的同进程差分。`force_refresh` 默认取 `True`(与任务书写的 `False` 相反):诊断端点调的是裸 `gateway_status()` 且该文件不得改,默认实时才不会让运维看到旧结论 |
| `TASK-home-state-lowering-batch2-20260916.md` | — | **状态下沉第二批(纯前端,与另两单可并行)**:第一批 `f8e607c2` 已验收(useState 66→52、useRef 41→39、散装 `rectification*` 归零、全量套件两侧完全相同)。本单照同一套做法搬剩下三簇:`session*` **10** 个(`useSessionManagement` 仍要解构约 40 个参数)、`profile*` **6** 个、`synastry*` **4** 个(无 hook,散在 `Home()`),目标 useState 52 → **≤36**。另修第一批留下的尾巴:`createRectificationShellSetters` 在 render 体里无记忆化 → 每帧新身份 → 多一条 `exhaustive-deps` warning119→**120**),**正解是稳住 setter 身份、不是塞进 deps**(塞进去会每帧重拉入口摘要),本单要把 warning 降回 119。零行为变化;先分类再动手。不占 BUG 号 | 待领取 | — |
| `TASK-api-server-backdoor-close-20260916.md` | — | **取代 decomposition 单的阶段 1** · **第二版(2026-09-16 改写)**:第一版的「8 方法 / 314 行」**错了一个数量级**——闭包只跟了 `self._x()`,漏掉 `_compute_consultation_workflow` 那 6 行委托转给模块级 `execute_consultation_workflow(self, ...)` 的一跳。执行方重算实测 **115 方法 / 4,104 行(占全类 51% 方法),但碰 HTTP 上下文的仍是 0 个**。产品在 A/B/C 中**选定 B(mixin 抽取)**:方法体原样搬进 `ConsultationComputeMixin``JyotishAPIHandler(BaseHTTPRequestHandler, Mixin)` 靠 MRO 解析,离线调用方直接实例化 mixin——四个伪造点全关。**产品同时授权:§4 那条「不得搬 ≥150 行业务方法」按本意解释(本意是防逐行改写的 C 案),mixin 放行。** 必须先过 **spike 闸门**:搬完后 `test_api_server_security.py` 一字不改直接跑,绿则继续、红则退回 A(只搬 7 方法/300 行、关 2 个伪造点)。真风险是循环 import——闭包引用同文件 44 个模块级函数 + 21 个常量。`__new__` 归零只针对 scripts 侧 4→0tests 侧 29 处含不许改的 security 测试,保持不增长)。阶段 3 仍不做。不占 BUG 号 | 待领取 | — |
| `TASK-api-server-backdoor-close-20260916.md` | — | **取代 decomposition 单的阶段 1** · **第二版(2026-09-16 改写)**:第一版的「8 方法 / 314 行」**错了一个数量级**——闭包只跟了 `self._x()`,漏掉 `_compute_consultation_workflow` 那 6 行委托转给模块级 `execute_consultation_workflow(self, ...)` 的一跳。执行方重算实测 **115 方法 / 4,104 行(占全类 51% 方法),但碰 HTTP 上下文的仍是 0 个**。产品在 A/B/C 中**选定 B(mixin 抽取)**:方法体原样搬进 `ConsultationComputeMixin``JyotishAPIHandler(BaseHTTPRequestHandler, Mixin)` 靠 MRO 解析,离线调用方直接实例化 mixin——四个伪造点全关。**产品同时授权:§4 那条「不得搬 ≥150 行业务方法」按本意解释(本意是防逐行改写的 C 案),mixin 放行。** 必须先过 **spike 闸门**:搬完后 `test_api_server_security.py` 一字不改直接跑,绿则继续、红则退回 A(只搬 7 方法/300 行、关 2 个伪造点)。真风险是循环 import——闭包引用同文件 44 个模块级函数 + 21 个常量。`__new__` 归零只针对 scripts 侧 4→0tests 侧 29 处含不许改的 security 测试,保持不增长)。阶段 3 仍不做。不占 BUG 号 | **已执行(spike 红 → 退回 A** | spike 实测:整体 mixin 化后 `test_api_server_security.py` **1 failed / 128 passed**——`test_chart_async_job_executes_in_background` 挂在 `monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', ...)`:调用方法随 mixin 搬走后从新模块 globals 解析,补丁落在旧模块上不生效(已证明把同一 fake 打到 mixin 模块即恢复原行为,非搬坏)。循环 import 不是障碍(移动集不引用 `JyotishAPIHandler`)。按 §6.3 退回 A`scripts/offline_compute_mixins.py` 收 7 方法 / 300 行 + 3 助手 + `BadRequest`,关掉 `consultation_workflow_service:27` 与 `local_accuracy_report:141` 两处伪造,scripts 侧 `__new__` **4 → 2**;类方法 225 → 218、行数 11,291 → 10,924security 测试一字未改 **129 passed**。剩余 2 处伪造都在咨询工作流链上,需另案解决 monkeypatch 落点问题 |
| `RECONCILE-20260916.md` | — | **对账清单(不是任务书)**:状态板与现实脱节(09-15~16 那 10 份早已合入却仍写「待领取/待验收」,本轮已修正),所以另外 7 份 09-10~14 的单**不能拿 README 当证据**——它们没有 `PROGRESS-*.md`,但引用的 BUG 号都是 `resolved`。要么被别的单顺带修了没留记录,要么压根没做。每行回一个「做了/没做」即可。另附三条我已查到确定没做的证据、两条状态未闭环的、以及 BLK-001 仍红(2026-09-16 在 `4f643aa0` 复跑确认) | 待产品负责人 / 执行方回填 | — |
## 命名与归档