Compare commits

..
Author SHA1 Message Date
jesse-uxandGrok (agent loop) a7fe7e9dd5 修复拆类金样换机器就对不齐的问题,避免测试删掉共用排盘缓存。
指纹不再包含仓库绝对路径;采集时关闭缓存而不是删除目录。基线与本分支 8 个哈希一致,定向 pytest 11 通过。

Co-Authored-By: Grok (agent loop)
2026-10-16 11:48:27 +08:00
jesse-ux 14ad076fe4 Merge remote-tracking branch 'origin/staging' into codex/api-server-service-extract-20261013 2026-10-16 11:30:31 +08:00
Jesse_ChenandClaude Opus 5.5 187fa375f5 docs(tasks): api-server service extract fix brief (issue #70)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
2026-10-16 11:10:12 +08:00
jesse-uxandGrok (agent loop) c7350bcafb 把星盘接口的计算和 HTTP 拆成两个类,离线入口不再伪造请求对象。
方法体未改。定向测试 383 通过;1 条校正回归在基线上同样失败,断言未改。快速门和全量 pytest 未跑。

Co-Authored-By: Grok (agent loop)
2026-10-16 10:24:42 +08:00
28 changed files with 3296 additions and 656 deletions
+14
View File
@@ -36367,4 +36367,18 @@
- 复发自:无
- 修复版本:分支 `codex/dasha-unify-20261013`。开工时本分支最大已用编号是 BUG-1357,1358 未被占用。
## BUG-1359 | 拆类金样把本机路径算进指纹,测试还会删掉共用排盘缓存
- 状态:resolved
- 首次发现:2026-10-16
- 最近更新:2026-10-16
- 影响面:`tests/test_api_service_extract_golden.py` 与采集脚本 `scripts/research/api_service_extract_golden.py`。计算和网页入口的拆分本身没有改。
- 用户现象:没有用户能看见的变化。验收时这条金样在另一台机器上对不齐;全量并行时另一条排盘上下文测试变红,单独跑是绿的。
- 触发条件:在采集金样的那台机器以外运行这条金样测试;或全量 pytest 并行时,别的测试正在用仓库里的 `scratch/local/api_chart_cache`。
- 根因:咨询结果里的清单路径是仓库根目录的绝对路径,规范化只去掉了两个墙钟字段。采集脚本又把这个共用缓存目录整个删掉。缓存有效期为 0 时,现有读函数直接返回空、写函数不落盘。BUG-1060 已经要求采集时把有效期设为 0,因为命中缓存会改变列表顺序;这次新脚本没有沿用,改成了删目录。
- 修复:所有字符串值里的仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 扩展前缀)换成 `<ROOT>`,后面的分隔符统一成 `/`。不按字段名挑选。采集期间设置 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0`,结束恢复原值,不再删除缓存目录。在基线 `fbf04f0b5f31cd8421bc4546840b1c833aaed810` 与本分支各采集一次,8 个哈希相同,用这份结果覆盖金样。方法数常量旁被盖住的 BUG-1341 三栏已恢复,本单说明另起一行。
- 验证:有效期为 0 时读取结果为空,缓存目录文件数采集前后都是 2。基线与本分支 8 个哈希逐个相同。金样、增长合同、以及上次并行变红的那一条一起跑,11 通过、0 失败。没有改那条在改代码之前就已经失败的校正测试的断言。
- 防复发:金样测试仍要求 sha256 与键路径类型全等,没有改成只比键或字节数。采集脚本里不再删除仓库内缓存目录。
- 相关记录:BUG-1060
- 复发自:无
- 修复版本:分支 `codex/api-server-service-extract-20261013`。修复单写明无 BUG 编号;本条是验收打回的测试失败。开工时本分支最大已用编号是 BUG-1358,1351、1352、1353 已有条目,本条用 BUG-1359。
@@ -0,0 +1,88 @@
# PROGRESS · API 计算与 HTTP 拆类(2026-10-16)
- 分支:`codex/api-server-service-extract-20261013`
- 任务书里的基线 `2994588ad4d858dead4835943e5883c5aa2b03f8` 已过期。本分支从当前 `origin/staging` `fbf04f0b5f31cd8421bc4546840b1c833aaed810` 新建,没有上游。该提交已包含 #69 与 #68。
- 没有新 BUG 编号。结构改造,不写 `docs/BUG_HISTORY.md`,不写 `CHANGELOG.md`。
## 开工重测
| 指标 | 任务书写作时 | 开工实测 |
| --- | --- | --- |
| `scripts/jyotish_api_server.py` 换行数 | 10970 | **10906** |
| 四空格 `def`(合同口径) | 218 | **217**(BUG-1341 已下调,本单保持) |
| `JyotishAPIHandler` 类方法 | 215 | **214** |
| `JyotishAPIHandler.__new__` 合计 | 31 | **31**(`scripts/` 2 + `tests/` 29,不含合同文件) |
| 行数基线 / 上限 | 10924 / 11224 | 基线不动。搬后换行数 **10920**,低于基线 |
HTTP 侧按「直接碰 HTTP 成员或覆写 `BaseHTTPRequestHandler`,再向上收到调用闭包」重算,仍是 15 个方法,外加 `server_version`:
- 直接:`_json`、`_html`、`_send_cors_headers`、`_enforce_request_security`、`_job_access_token`、`do_OPTIONS`、`do_GET`、`do_POST`、`_read_json_body`
- 调用者:`_error_json`、`_require_dynamic_rectification_token`、`_get_high_rigor_job`、`_get_chart_job`、`_compute_dynamic_rectification_opportunities`、`_compute_dynamic_rectification_score`
类里没有 `self.xxx =` 赋值。
## 做了什么
- 同一文件里把原类改成 `JyotishComputeService`(199 个计算方法),新增 `JyotishAPIHandler(BaseHTTPRequestHandler, JyotishComputeService)`,放入上面 15 个方法和 `server_version`。`ThreadingHTTPServer` 仍用 `JyotishAPIHandler`。
- 方法体与基线逐个 `ast.dump` 相等:旧类 214 个方法,新文件 214 个方法,不一致 **0**。换类的成员是这 15 个方法加 `server_version`,共 16 个。
- `scripts/consultation_workflow_service.py` 和 `scripts/capture_report_blocked_repairs_golden.py` 改为 `JyotishComputeService()`。`scripts/` 里 `JyotishAPIHandler.__new__` 为 **0**。
- 只用计算方法的 17 个测试文件同样改了构造。`tests/test_active_rectification_api.py` 里 3 处补丁从 HTTP 类改到计算类,否则补丁打不中实例。断言没改。
- 增长合同:总伪造 31 → **5**,生产伪造 2 → **0**,方法数 217 不变,行数基线 10924 不动。新增两条守卫:计算类不是 HTTP 处理器子类(并钉住 MRO 与 `server_version`);计算类方法体不碰 HTTP 成员、也不调用上述 15 个方法。
- 搬前 golden 连跑两次哈希一致,落在 `tests/golden/api_server_service_extract_v1.json`(只存 sha256 和键路径类型)。采集脚本:`scripts/research/api_service_extract_golden.py`。搬后对照由 `tests/test_api_service_extract_golden.py` 重算,通过。
构造语句三栏(17 个测试文件和 2 个生产文件,每处相同):
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| `JyotishAPIHandler.__new__(JyotishAPIHandler)` | `JyotishComputeService()` | 计算类可以正常实例化,不必再绕过 HTTP 构造 |
`tests/test_active_rectification_api.py` 另有三处:
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| 补丁打在 `JyotishAPIHandler` | 补丁打在 `JyotishComputeService` | 实例已是计算类,补丁必须打在实例的类上。断言未改 |
未改的 HTTP 侧伪造(5 处,合同新基线就是这 5 处):`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`。其中安全测试文件 `git diff` 为空。
## 命令和结果
| 检查 | 命令 | 结果数字 |
| --- | --- | --- |
| 搬前 golden 连跑两次 | 基线上采集 MCP / HTTP / 报告裁剪 / 证据助手 | 8 个哈希两次相同 |
| 搬后 golden | `pytest tests/test_api_service_extract_golden.py`(含在下面那条里) | 通过,与搬前哈希一致 |
| 任务书点名测试 + 改过的测试 + 两个调用了采集入口的测试 | `pytest` 上述文件 `-q --tb=line` | **383 通过,1 失败**,约 1046 秒 |
| 方法体对比 | 基线文件与工作树 `ast.dump` | 214 对 214,不一致 0 |
| 反向:计算类临时写 `self.headers` | `test_compute_service_methods_do_not_touch_http` | 失败后已撤回 |
| 反向:`scripts/` 临时加一处伪造标记 | `test_scripts_handler_forgeries_must_not_grow` | 1 > 0,失败后已撤回 |
| 安全测试文件 | `git diff -- tests/test_api_server_security.py` | 空 |
| 生产伪造 | 在 `scripts/**/*.py` 搜伪造标记 | **0** |
| 换行数 | 文件内 `\n` 个数 | 10906 → **10920**(上限 11224) |
| 隐私标记 | `pytest tests/test_repo_privacy_markers.py -q` | 退出码 0 |
搬前哈希(去掉两个墙钟字段后的规范 JSON):
| 案例 | sha256 | 字节 |
| --- | --- | ---: |
| mcp.career | `ad38a539cf9036143333acd538567b7eb94528d537c46ef326905ebc47527a05` | 543868 |
| http.career | `6a6ca421d942e2577e587647324ef9521ea14841a74a6850c11f629df4d9ba96` | 420555 |
| mcp.wealth | `9aaacf64d08f17bca2d3f6b020b1dbb1ec07eaee7727b04bba90430fcbf594d3` | 542629 |
| http.wealth | `f22368bbde38b76dc44e2995c34f7809ca96442405c1f13e39f53c4a2dd90a62` | 419316 |
| mcp.relationship | `be70fd603b1de52026ad40f0d6582a27cb8b148018474fcd7d8c4134509b40ae` | 537785 |
| http.relationship | `4405ae821aef81ec03660994d9f8f4104ae3388f339407bfe2721d05d662059b` | 414472 |
| repairs | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 |
| evidence | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 |
MCP 与 HTTP 彼此不相等:MCP 走 `surface=skill_mcp`,HTTP 方法默认 `api_web`,并且 HTTP 方法还会多挂一层咨询全文。这是原来的两条路径,不是这次拆类造成的。每条路径搬前搬后各自一致。
## 那 1 条失败
`tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` 期望时间窗 `05:07–05:08`,实际得到 `07:00–07:02`。把该测试文件还原到基线后再跑,失败内容相同。不是拆类引入的,也不是「用到了 HTTP」。按任务书,这种红不回退该文件。断言没改。同文件其余测试在本轮通过。
## 偏离
- 任务书交付物写了「删两处死代码」。硬红线写明零引用函数只记进度、本单不删。`_strict_adjudication_bundle_from_contract` 与 `_high_rigor_job_path` 仍只在定义处出现,没有删。
- 后续「把咨询闭包搬出文件、改依赖方向」没做。那一步要改安全测试的补丁落点,任务书明确不做。
- `AGENTS.md` §6 没有写死伪造数字 31 或 2,所以没改措辞。
- `skills/jyotish-vedic-astrology/scripts/` 里的同名副本没改。任务书点名的是 `scripts/`。合同计数也不含技能副本。
- 另有若干 `object.__new__(JyotishAPIHandler)`,不在合同计数里,调用的是计算方法,经继承仍可用。本单没改。
- 没跑:Python 快速门、全量 pytest、前端 `npm test`、`test:db`。没动数据库,也没对生产或 staging 做压测或登录。
@@ -0,0 +1,53 @@
# PROGRESS · API 拆类金样修复(2026-10-16)
- 分支:`codex/api-server-service-extract-20261013`
- 修复单:`docs/tasks/TASK-api-server-service-extract-fix-20261016.md`(来自 `origin/staging` `187fa375`)
- 被打回的提交:`c7350bcafbff64d3c526c2dbf3995c9845b3b66e`
- 基线(金样 `fixtureProvenance.baselineSha`):`fbf04f0b5f31cd8421bc4546840b1c833aaed810`
- 先把只含修复单的 `origin/staging` 合并进本分支(合并提交 `14ad076f`)。没有带进别的代码。
## 做了什么
- 拆类没有重做。没有改 `scripts/jyotish_api_server.py`,也没有改 `tests/test_api_server_security.py`。
- 金样规范化对所有字符串值去掉仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 的 `\\?\` 前缀),换成 `<ROOT>`,其后的分隔符统一成 `/`。不按字段名挑选。
- 采集不再删除 `scratch/local/api_chart_cache`。采集期间把 `JYOTISH_API_CHART_CACHE_TTL_SECONDS` 设为 `0`,结束恢复原值。有效期为 0 时,现有读函数直接返回空,写函数不落盘。
- 在基线提交上用同一套规范化重新采集,再在本分支采集。8 个哈希与键路径类型逐个相同。金样文件按这份结果覆盖。
- `JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE` 上方恢复原来的三行:
- 原值:218(删占位回退之前)
- 新值:217
- 原因:BUG-1341 删除 `JyotishAPIHandler._fallback_dasha_periods`,类方法少 1 个,没有新增方法
- 本单「数值未变」另起一行写在这三行后面,没有盖住历史。
- 没有改断言。`check_golden()` 仍要求 sha256 和键路径类型全等,没有改成只比键或字节数。上一轮在改代码之前就失败的校正测试没有放宽。
- 记了 BUG-1359。修复单写的是无编号;这次是验收打回的测试失败,按 `AGENTS.md` §5 记。开工时本分支最大已用编号是 BUG-1358。BUG-1351、1352、1353 已有条目,没有重用。
## 两次采集的 8 个哈希
基线与本分支打印的是同一组数。六条咨询路径都比上一份金样短 122 字节:上一份还含本机绝对路径,并且排盘缓存有效期字段是默认的 900 秒。这次两边都按「缓存不生效」采集,有效期字段是 0。`repairs` 与 `evidence` 的哈希和字节数与上一份相同。没有再跑「删除缓存目录」那条旧采集。
| 案例 | 基线 sha256 | 基线字节 | 本分支 sha256 | 本分支字节 |
| --- | --- | ---: | --- | ---: |
| evidence | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 |
| http.career | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 |
| http.relationship | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 |
| http.wealth | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 |
| mcp.career | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 |
| mcp.relationship | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 |
| mcp.wealth | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 |
| repairs | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 |
## 命令和结果
| 检查 | 命令 | 结果数字 |
| --- | --- | --- |
| 有效期为 0 时不读不写 | 调用现有的缓存读、写函数 | 读取结果为空;目录里的文件 2 个,调用前后相同;写回的有效期字段是 0,未命中 |
| 两次采集时缓存目录 | 基线采集、本分支采集各看一次目录 | 两次都是采集前 2 个文件、采集后 2 个文件,名单相同 |
| 金样、增长合同、上次并行变红的那一条 | `.venv\Scripts\python.exe -m pytest tests/test_api_service_extract_golden.py tests/test_api_server_growth_contract.py tests/test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo -q --tb=line` | **11 通过,0 失败**,退出码 0,约 132 秒。其中金样 1、增长合同 9、上下文那一条 1 |
| 历史三栏是否还在删除行里 | `git diff origin/staging -- tests/test_api_server_growth_contract.py` | 原来的三行没有出现在删除行里 |
| 全量 pytest、Python 快速门、前端 `npm test` | 没跑 | 修复单的全量对照由验收方复跑 |
| `npm run test:db` | 没跑 | 没改数据库 |
## 偏离
- 修复单写「无 BUG 编号」。本轮按仓库规则补了 BUG-1359,原因见上。
- 没有把「删除共用缓存」和「有效期为 0」再各采一次来拆开那 122 字节。删除共用目录会再次打到别的测试,修复单不允许。金样以「缓存不生效」为准。
- 没跑全量 `pytest -n 4`,所以不能在这里声称失败清单已与基线逐名一致。
@@ -0,0 +1,72 @@
# TASK · API 单体拆类 · 修复单(issue #70,第 1 次打回)
- 日期:2026-10-16
- 被验提交:`c7350bcafbff64d3c526c2dbf3995c9845b3b66e`(分支 `codex/api-server-service-extract-20261013`)
- 原任务书:`docs/tasks/TASK-api-server-service-extract-20261013.md`
- 执行方:Grok;在**同一分支**上追加提交(基于 `c7350bcaf`,不改写已推历史);若 `origin/staging` 已前进,用 `git merge origin/staging`,不要 rebase
- BUG 编号:无(仍属结构改造)
## 验收结论(Claude,Linux)
拆类本身**通过**;新增的金样测试**未通过**,因此整单打回。
| 项 | 结论 | 证据 |
| --- | --- | --- |
| T2 纯移动 | 通过 | AST 对比:基线与本分支 215 个方法名集合相同、方法体逐个 `ast.dump` 相等;模块级函数与其他顶层语句全等;`JyotishComputeService` 200 个方法,`JyotishAPIHandler` 15 个 HTTP 方法 + `server_version` + docstring |
| T3 生产伪造归零 | 通过 | `grep -rn "JyotishAPIHandler.__new__" scripts` 0 命中 |
| T3 结果等价 | 通过(Claude 自证) | 基线工作树与本分支各跑 `capture_mcp('career')`(清缓存),逐字段比对只差 3 个 `manifest_path` 绝对路径(两个工作树目录名不同);基线连跑两次 0 差异;`repairs`、`evidence` 两路哈希两边相同 |
| T4 测试伪造下调 | 通过 | 17 个文件只改构造语句与类型注解;伪造合计 31 → 5 |
| T5 合同与守卫 | 通过(附一处文字要求,见 F3) | 两条新守卫在本机绿 |
| `test_api_server_security.py` 零改动 | 通过 | `git diff --stat` 空 |
| 本机 API 冒烟 | 通过 | 基线与本分支并排:`/api/health` 200、`/api/chart` 虚构盘 200(只差时间戳字段)、非 JSON 400、`text/plain` 415、3 MB 请求体 400、未知路径 404、OPTIONS 200、`Server: JyotishAPI/6.9.14` 头,响应体逐字相同 |
| **T1 金样测试** | **未通过** | 见 F1、F2 |
| 全量 pytest(`-n 4`)与基线逐名比对 | **未通过** | 本分支比基线多 2 条失败:`test_api_service_extract_golden.py::test_api_service_extract_golden_matches_baseline`(单独跑也红)、`test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo`(单独跑绿,并行红,见 F2) |
## F1 · 金样哈希含本机绝对路径,换机器必红(必做)
**现象**:本机单独跑 `pytest tests/test_api_service_extract_golden.py`,7 条哈希全部不等,如 `http.career sha256 6a6ca4… != 0c8ef6… bytes 420555 != 441296`。
**根因**:咨询工作流响应里有 `external_parity_gate.manifest_path`、`real_case_calibration.holdout_replay_manifest.manifest_path`、`real_case_calibration.outcome_replay_manifest.manifest_path` 等字段,值是运行机器上的绝对路径。`canonical_text` 只去掉了两个墙钟字段,没处理路径,所以金样只在采集它的那台 Windows 机器、那个目录下成立。CI 和任何其他检出都会红。
**修法**:
1. 在 `scripts/research/api_service_extract_golden.py` 的规范化里,把所有字符串值中的仓库根目录绝对路径(`str(ROOT)`,以及 Windows 下的正斜杠 / 反斜杠两种写法)替换成固定占位符 `<ROOT>`,并把其后的路径分隔符统一成 `/`。不要按字段名白名单去删,避免以后新增路径字段又漏。
2. 在**基线**(`origin/staging` 上本单开工时的 SHA,写进 `fixtureProvenance`)重新采集金样,覆盖 `tests/golden/api_server_service_extract_v1.json`。
3. 在本分支上用 `check_golden()` 对照,全部相等。
**验收标准**:Claude 在 Linux 上、任意检出目录下单独跑这条测试为绿;Grok 在 Windows 上同样为绿。PROGRESS 里贴两次采集(基线、本分支)的 8 个哈希。
## F2 · 测试过程中删除共用排盘缓存目录,并行时打红别的测试(必做)
**现象**:全量 `pytest -n 4` 时 `test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo` 变红;单独跑为绿,基线全量也为绿。
**根因**:`clear_chart_cache()` 对仓库内的 `scratch/local/api_chart_cache` 做 `shutil.rmtree`。全量测试多进程并行,其他测试正在读写同一个目录,被中途删掉。研究脚本手动跑时清缓存没问题,但放进 pytest 就成了共享状态破坏。
**修法**(二选一,推荐 a):
- a. 不删目录,改为在采集期间设置环境变量 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0` 让排盘缓存不生效(现有测试 `tests/test_commercial_domain_calculation_contract.py` 已用这个办法),采集结束恢复原值。先确认 TTL 为 0 时确实既不读也不写缓存;若仍会写入,则同时用 `monkeypatch` / 临时目录把 `_api_chart_cache_dir` 指向 `tmp_path`。
- b. 测试里用 `monkeypatch.setattr(jyotish_api_server, "_api_chart_cache_dir", lambda: tmp_path / "cache")`,研究脚本的 `clear_chart_cache` 只清这个临时目录。
**验收标准**:
- 金样测试里不再出现对仓库内 `scratch/local/api_chart_cache` 的删除(`grep -n rmtree` 只允许作用于临时目录)。
- 本分支全量 `pytest -n 4` 的失败清单与基线逐名一致(Claude 复跑)。
- 金样结论不变(缓存不生效时与清空缓存时的输出相同;如有差异,在 PROGRESS 写明并以「缓存不生效」为准重采)。
## F3 · 方法数常量旁的旧三栏被改写(必做,小)
`tests/test_api_server_growth_contract.py` 里 `JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE` 上方原有的三栏(`原值:218(删占位回退之前)/ 新值:217 / 原因:BUG-1341 …`)被改成了 `原值:217 / 新值:217 / 原因:…`,丢了 BUG-1341 那次下调的记录。
**修法**:恢复原来三行原样;本单的说明(数值未变)如需保留,另起一行注释写在其后,不覆盖历史。
**验收标准**:`git diff origin/staging -- tests/test_api_server_growth_contract.py` 中,原有三栏三行不再出现在删除行里。
## 不要做
- 不改拆类本身(T2/T3/T4 已通过),不动 `scripts/jyotish_api_server.py`。
- 不改 `tests/test_api_server_security.py`。
- 不放宽金样比对(不许改成只比键路径或字节数)。
## 回报
在 issue #70 回报新 SHA,表格里写:金样单独跑结果(Windows)、全量 pytest 失败清单相对基线的差(若 Windows 能跑)、F3 的 diff 片段。
@@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from consultation_plan_contract import PLAN_VERSION, _ROUTE_CONTRACTS # noqa: E402
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_api_server import JyotishComputeService # noqa: E402
BIRTH = {
"year": 1993,
@@ -55,8 +55,8 @@ CONSUMER_KEEP = (
CHART_KEEP = ("success", "ascendant", "planets", "houses", "dasha")
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def _body(theme: str) -> dict:
+2 -2
View File
@@ -15,9 +15,9 @@ for path in (ROOT, SCRIPTS_DIR):
def execute_consultation_workflow(body: dict[str, Any], *, surface: str = "api") -> dict[str, Any]:
from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow as _execute
from jyotish_api_server import JyotishComputeService, execute_consultation_workflow as _execute
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
return _execute(handler, body=body, surface=surface)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,325 @@
"""Canonical hashes for the compute/HTTP class split.
Fictional smoke chart only. Clock fields are removed before hashing.
Absolute repository paths in string values are replaced with <ROOT>.
Payload values are not written; the golden file stores sha256 and key-path types.
"""
from __future__ import annotations
import hashlib
import json
import os
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
GOLDEN_PATH = ROOT / "tests" / "golden" / "api_server_service_extract_v1.json"
CLOCK_KEYS = {"cache_created_at", "cache_expires_at"}
THEMES = ("career", "wealth", "relationship")
BASELINE_SHA = "fbf04f0b5f31cd8421bc4546840b1c833aaed810"
CACHE_TTL_ENV = "JYOTISH_API_CHART_CACHE_TTL_SECONDS"
ROOT_PLACEHOLDER = "<ROOT>"
# Synthetic positions reused from tests/test_offline_compute_mixins.py.
# Not a birth record.
EVIDENCE_CHART = {
"success": True,
"ascendant": {"sign": "Aries", "sign_idx": 0, "degree": 12.0, "lon": 12.0},
"planets": {
"Sun": {"sign": "Leo", "lon": 130.0},
"Moon": {"sign": "Cancer", "lon": 100.0},
},
}
def prepare_env() -> None:
os.environ["VEDASTRO_ENABLE_NETWORK"] = "0"
os.environ.setdefault("JYOTISH_SKIP_LOCAL_ENV", "1")
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
for path in (str(ROOT), str(SCRIPTS)):
if path not in sys.path:
sys.path.insert(0, path)
@contextmanager
def chart_cache_disabled() -> Iterator[None]:
"""Turn the shared chart cache off. Do not delete the repository directory."""
previous = os.environ.get(CACHE_TTL_ENV)
os.environ[CACHE_TTL_ENV] = "0"
try:
yield
finally:
if previous is None:
os.environ.pop(CACHE_TTL_ENV, None)
else:
os.environ[CACHE_TTL_ENV] = previous
def root_spellings(root: Path = ROOT) -> tuple[str, ...]:
native = os.path.normpath(str(root))
forward = native.replace("\\", "/")
spellings = {native, forward}
if os.name == "nt":
spellings.add("\\\\?\\" + native)
return tuple(sorted(spellings, key=len, reverse=True))
def neutralize_path_string(text: str, root: Path = ROOT) -> str:
for spelling in root_spellings(root):
text = text.replace(spelling, ROOT_PLACEHOLDER)
if ROOT_PLACEHOLDER not in text:
return text
pieces = text.split(ROOT_PLACEHOLDER)
rebuilt = [pieces[0]]
for piece in pieces[1:]:
if piece.startswith("\\") or piece.startswith("/"):
end = 0
while end < len(piece) and piece[end] not in " \t\r\n\"'":
end += 1
piece = piece[:end].replace("\\", "/") + piece[end:]
rebuilt.append(piece)
return ROOT_PLACEHOLDER.join(rebuilt)
def neutralize_paths(value: Any, root: Path = ROOT) -> Any:
if isinstance(value, str):
return neutralize_path_string(value, root)
if isinstance(value, dict):
return {key: neutralize_paths(item, root) for key, item in value.items()}
if isinstance(value, list):
return [neutralize_paths(item, root) for item in value]
return value
def theme_body(theme: str) -> dict[str, Any]:
"""Fictional smoke chart from the task brief. Coordinates only, no person."""
return {
"year": 1990,
"month": 1,
"day": 1,
"hour": 12,
"minute": 0,
"second": 0,
"lat": 28.6139,
"lon": 77.2090,
"tz": 5.5,
"theme": [theme],
"defer_optional_external_evidence": True,
"today": "2026-10-13",
}
def strip_clock(value: Any) -> Any:
if isinstance(value, dict):
return {
key: strip_clock(item)
for key, item in value.items()
if key not in CLOCK_KEYS
}
if isinstance(value, list):
return [strip_clock(item) for item in value]
return value
def json_type(value: Any) -> str:
if value is None:
return "null"
if isinstance(value, bool):
return "boolean"
if isinstance(value, (int, float)):
return "number"
if isinstance(value, str):
return "string"
if isinstance(value, list):
return "array"
if isinstance(value, dict):
return "object"
return type(value).__name__
def flatten_keypaths(value: Any, prefix: str = "", *, depth: int = 0, max_depth: int = 2) -> dict[str, str]:
out: dict[str, str] = {}
label = prefix or "$"
out[label] = json_type(value)
if depth >= max_depth:
return out
if isinstance(value, dict):
for key in sorted(item for item in value if isinstance(item, str)):
child = f"{prefix}.{key}" if prefix else key
out.update(flatten_keypaths(value[key], child, depth=depth + 1, max_depth=max_depth))
elif isinstance(value, list) and value:
child = f"{prefix}[]" if prefix else "[]"
out.update(flatten_keypaths(value[0], child, depth=depth + 1, max_depth=max_depth))
return out
def canonical_text(value: Any) -> str:
return json.dumps(neutralize_paths(strip_clock(value)), sort_keys=True, ensure_ascii=False)
def digest(value: Any) -> dict[str, Any]:
text = canonical_text(value)
return {
"sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
"canonicalBytes": len(text.encode("utf-8")),
"keypaths": flatten_keypaths(strip_clock(value)),
}
def capture_mcp(theme: str) -> Any:
prepare_env()
with chart_cache_disabled():
from consultation_workflow_service import execute_consultation_workflow
return execute_consultation_workflow(theme_body(theme), surface="skill_mcp")
def capture_http(theme: str) -> Any:
"""HTTP handler method, on the compute class after the split."""
prepare_env()
with chart_cache_disabled():
from jyotish_api_server import JyotishComputeService
return JyotishComputeService()._compute_consultation_workflow(theme_body(theme))
def capture_repairs() -> dict[str, Any]:
prepare_env()
with chart_cache_disabled():
from capture_report_blocked_repairs_golden import (
BIRTH,
MODULE_KEEP,
THEMES as REPAIR_THEMES,
VARGA_KEEP,
_body,
_handler,
_trim_workflow,
)
payload: dict[str, Any] = {
"source": "scripts/jyotish_api_server.py _compute_consultation_workflow",
"smoke_birth": {**BIRTH, "note": "fictional capture identity; not a real person"},
"trim": {
"keep_varga_keys": list(VARGA_KEEP),
"keep_modules": list(MODULE_KEEP),
"drop": [
"ai_prompt_pack",
"vedastro",
"varga_spectrum",
"guided_topics",
"western_spectrum",
"technique_audit_table",
],
},
"themes": {},
}
for theme in REPAIR_THEMES:
raw = _handler()._compute_consultation_workflow(_body(theme))
payload["themes"][theme] = _trim_workflow(raw)
return payload
def capture_evidence() -> Any:
prepare_env()
from consultation_workflow_service import build_runtime_evidence_helpers
return build_runtime_evidence_helpers(EVIDENCE_CHART)
def capture_cases(*, include_http: bool = True, include_repairs: bool = True) -> dict[str, Any]:
cases: dict[str, Any] = {}
for theme in THEMES:
cases[f"mcp.{theme}"] = capture_mcp(theme)
if include_http:
cases[f"http.{theme}"] = capture_http(theme)
if include_repairs:
cases["repairs"] = capture_repairs()
cases["evidence"] = capture_evidence()
return cases
def summarize(cases: dict[str, Any]) -> dict[str, Any]:
return {name: digest(value) for name, value in cases.items()}
def provenance() -> dict[str, Any]:
return {
"fictional": True,
"command": "python scripts/research/api_service_extract_golden.py --check",
"baselineSha": BASELINE_SHA,
"env": {
"VEDASTRO_ENABLE_NETWORK": "0",
"JYOTISH_SKIP_LOCAL_ENV": "1",
"JYOTISH_API_CHART_CACHE_TTL_SECONDS": "0",
},
"clockFieldsRemoved": sorted(CLOCK_KEYS),
"rootPlaceholder": ROOT_PLACEHOLDER,
"mcpSurface": "skill_mcp",
"note": (
"fictional smoke chart from the task brief; payload values are not stored; "
"repository absolute paths in string values are replaced with <ROOT>"
),
}
def document_from_summary(summary: dict[str, Any]) -> dict[str, Any]:
return {
"kind": "sha256_and_keypath_types",
"fixtureProvenance": provenance(),
"cases": summary,
}
def write_golden(summary: dict[str, Any], path: Path = GOLDEN_PATH) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
text = json.dumps(document_from_summary(summary), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
path.write_text(text, encoding="utf-8")
def load_golden(path: Path = GOLDEN_PATH) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def check_golden(path: Path = GOLDEN_PATH) -> list[str]:
expected = load_golden(path)["cases"]
actual = summarize(capture_cases())
mismatches: list[str] = []
if set(expected) != set(actual):
mismatches.append(f"case names {sorted(expected)} != {sorted(actual)}")
for name in sorted(set(expected) & set(actual)):
if expected[name]["sha256"] != actual[name]["sha256"]:
mismatches.append(
f"{name} sha256 {expected[name]['sha256']} != {actual[name]['sha256']} "
f"bytes {expected[name]['canonicalBytes']} != {actual[name]['canonicalBytes']}"
)
elif expected[name]["keypaths"] != actual[name]["keypaths"]:
mismatches.append(f"{name} keypaths differ")
return mismatches
def main(argv: list[str]) -> int:
prepare_env()
if "--check" in argv:
mismatches = check_golden()
if mismatches:
print("MISMATCH")
for item in mismatches:
print(item)
return 1
print("MATCH", len(load_golden()["cases"]))
return 0
summary = summarize(capture_cases())
for name, row in summary.items():
print(f"{name} {row['sha256']} {row['canonicalBytes']}")
if "--write" in argv:
write_golden(summary)
print("WROTE", GOLDEN_PATH)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -14,11 +14,11 @@ if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
import jyotish_api_server as api_server # noqa: E402
from jyotish_api_server import BadRequest, JyotishAPIHandler # noqa: E402
from jyotish_api_server import BadRequest, JyotishComputeService # noqa: E402
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def test_active_rectification_questions_api_builds_choice_workflow() -> None:
@@ -515,7 +515,7 @@ def test_high_rigor_event_rectification_requires_real_vedastro_candidate_discrim
},
)
monkeypatch.setattr(
JyotishAPIHandler,
JyotishComputeService,
"_compute_vedastro_gateway_run",
lambda *_args, **_kwargs: {
"status": "ok",
@@ -633,7 +633,7 @@ def test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow(mon
},
)
monkeypatch.setattr(
JyotishAPIHandler,
JyotishComputeService,
"_compute_vedastro_gateway_run",
lambda *_args, **_kwargs: {
"status": "ok",
@@ -825,7 +825,7 @@ def test_search_events_difference_cannot_override_identical_minute_snapshots(mon
},
)
monkeypatch.setattr(
JyotishAPIHandler,
JyotishComputeService,
"_compute_vedastro_gateway_run",
lambda *_args, **_kwargs: {
"status": "ok",
+2 -2
View File
@@ -77,14 +77,14 @@ def test_v4_wrapper_uses_raman_spec_and_never_confirms_a_minute(monkeypatch) ->
def test_v4_api_validates_event_kind_and_date_boundaries(monkeypatch) -> None:
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
from scripts.jyotish_api_server import BadRequest, JyotishComputeService
captured = {}
monkeypatch.setattr(
"scripts.rectification.api_service.score_candidates",
lambda request: captured.setdefault("request", request) or {},
)
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
body = {
"birth_date": "1997-08-08",
"start_time": "05:00",
+103 -14
View File
@@ -2,11 +2,12 @@
New endpoints and features must live in new modules and be thinly registered
from the main file. Line count is only a coarse guardrail; the live gates are
JyotishAPIHandler method count and JyotishAPIHandler.__new__ forgery sites.
handler method count and JyotishAPIHandler.__new__ forgery sites.
Baselines re-measured 2026-09-16 after TASK-api-server-backdoor-close-20260916
moved seven compute-only methods into scripts/offline_compute_mixins.py and
closed two of the four production forgeries.
Baselines re-measured 2026-10-16 after TASK-api-server-service-extract-20261013
moved HTTP methods onto JyotishAPIHandler and left calculation on
JyotishComputeService. Production forgeries went from 2 to 0. The two classes
stay in this file so existing module-attribute patches still land.
"""
from __future__ import annotations
@@ -23,6 +24,9 @@ AGENTS = ROOT / "AGENTS.md"
# Live `wc -l scripts/jyotish_api_server.py` equivalent:
# Path.read_bytes().count(b"\n"), measured 2026-09-16 on origin/staging @ 51a65d92.
# New features must not consume this budget; open a module instead.
# 原值:10924
# 新值:10924
# 原因:行数基线按任务书保持不动。拆类只加了类声明,实测行数仍不超过这条基线。
JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 10924
JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300
@@ -33,19 +37,19 @@ JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300
# 原值:218(删占位回退之前)
# 新值:217
# 原因:BUG-1341 删除 JyotishAPIHandler._fallback_dasha_periods,类方法少 1 个,没有新增方法
# 本单数值未变,仍是 217:只把 HTTP 方法换到子类,不增删方法。
JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 217
# `JyotishAPIHandler.__new__` in scripts/ and tests/ `*.py`, excluding this file.
# Re-measured 2026-09-16 after TASK-api-server-backdoor-close-20260916:
# scripts/ production forgeries 4 -> 2, tests/ unchanged at 29.
JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 31
# 原值:31
# 新值:5
# 原因:生产 2 处和只用计算方法的测试改为 JyotishComputeService()。留下的 5 处要碰 HTTP,本单不改。
JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 5
# Production forgeries only. The two survivors both sit on the consultation
# workflow chain (`consultation_workflow_service.execute_consultation_workflow`
# and `capture_report_blocked_repairs_golden`); closing them needs the 4,086-line
# mixin extraction that the 2026-09-16 spike showed breaks cross-module
# monkeypatching in tests/test_api_server_security.py. Target stays 0.
JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE = 2
# Production forgeries only. Offline callers instantiate JyotishComputeService.
# 原值:2
# 新值:0
# 原因:consultation_workflow_service 与 capture_report_blocked_repairs_golden 改为正常实例化计算类。
JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE = 0
# Compute-only mixins lifted out of the handler must never depend back on the
# HTTP monolith, or the backdoor simply moves house.
@@ -141,3 +145,88 @@ def test_quality_gate_runs_api_server_growth_contract() -> None:
assert "tests/test_api_server_growth_contract.py" in CORE_PYTEST_TARGETS
quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8")
assert '"tests/test_api_server_growth_contract.py"' in quality_gate
_HTTP_ATTRS = frozenset({
"headers",
"wfile",
"rfile",
"client_address",
"send_response",
"send_header",
"end_headers",
"path",
"command",
})
_HTTP_METHOD_NAMES = frozenset({
"_json",
"_html",
"_send_cors_headers",
"_read_json_body",
"_enforce_request_security",
"_job_access_token",
"do_GET",
"do_POST",
"do_OPTIONS",
"_error_json",
"_get_chart_job",
"_get_high_rigor_job",
"_require_dynamic_rectification_token",
"_compute_dynamic_rectification_score",
"_compute_dynamic_rectification_opportunities",
})
def test_compute_service_is_not_an_http_handler() -> None:
"""JyotishComputeService can be constructed. HTTP stays on the subclass."""
from http.server import BaseHTTPRequestHandler
from scripts.jyotish_api_server import JyotishAPIHandler, JyotishComputeService
assert not issubclass(JyotishComputeService, BaseHTTPRequestHandler)
assert issubclass(JyotishAPIHandler, JyotishComputeService)
assert issubclass(JyotishAPIHandler, BaseHTTPRequestHandler)
mro_names = [item.__name__ for item in JyotishAPIHandler.__mro__]
assert mro_names.index("BaseHTTPRequestHandler") < mro_names.index("JyotishComputeService")
assert JyotishAPIHandler.server_version == "JyotishAPI/6.9.14"
assert "server_version" not in JyotishComputeService.__dict__
for name in _HTTP_METHOD_NAMES:
assert name in JyotishAPIHandler.__dict__, name
assert name not in JyotishComputeService.__dict__, name
def _http_touches(fn: ast.AST) -> list[str]:
hits: list[str] = []
for node in ast.walk(fn):
if (
isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id == "self"
and node.attr in _HTTP_ATTRS | _HTTP_METHOD_NAMES
):
hits.append(f"{node.lineno}:{node.attr}")
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "getattr"
and len(node.args) >= 2
and isinstance(node.args[0], ast.Name)
and node.args[0].id == "self"
and isinstance(node.args[1], ast.Constant)
and node.args[1].value in _HTTP_ATTRS | _HTTP_METHOD_NAMES
):
hits.append(f"{node.lineno}:getattr:{node.args[1].value}")
return hits
def test_compute_service_methods_do_not_touch_http() -> None:
tree = ast.parse(API_SERVER.read_text(encoding="utf-8"))
compute = next(
node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "JyotishComputeService"
)
offenders: list[str] = []
for node in compute.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
offenders.extend(f"{node.name}:{hit}" for hit in _http_touches(node))
assert offenders == []
+17
View File
@@ -0,0 +1,17 @@
"""Recompute the fictional smoke chart and compare canonical hashes.
The stored golden keeps sha256 and key-path types only.
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
def test_api_service_extract_golden_matches_baseline() -> None:
path = Path(__file__).resolve().parents[1] / "scripts" / "research" / "api_service_extract_golden.py"
spec = importlib.util.spec_from_file_location("api_service_extract_golden", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.check_golden() == []
+2 -2
View File
@@ -13,7 +13,7 @@ if str(SCRIPTS) not in sys.path:
import domain_calculation_service as calculation_service # noqa: E402
import jyotish_api_server # noqa: E402
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_api_server import JyotishComputeService # noqa: E402
from jyotish_engine import _compute_chart_from_args # noqa: E402
BIRTH = {
@@ -121,7 +121,7 @@ def test_chart_hash_matches_domain_cli_and_rest(
cli, _asc_idx, _jd, _ayanamsa = _compute_chart_from_args(
SimpleNamespace(**BIRTH, node_mode="true")
)
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(
rest = JyotishComputeService()._compute_chart_sync(
{**BIRTH, "node_mode": "true", "transit_date": "2026-07-11"}
)
@@ -11,7 +11,7 @@ if str(SCRIPTS) not in sys.path:
import domain_calculation_service as calculation_service # noqa: E402
import jyotish_api_server # noqa: E402
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_api_server import JyotishComputeService # noqa: E402
BIRTH = {
"year": 1990,
@@ -100,7 +100,7 @@ def test_api_chart_uses_same_domain_contract_and_preserves_shape(
) -> None:
import domain_calculation_service
import jyotish_api_server
from jyotish_api_server import JyotishAPIHandler
from jyotish_api_server import JyotishComputeService
monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0")
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0")
@@ -111,7 +111,7 @@ def test_api_chart_uses_same_domain_contract_and_preserves_shape(
)
expected = domain_calculation_service.compute_chart(BIRTH)
result = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(
result = JyotishComputeService()._compute_chart_sync(
{**BIRTH, "transit_date": "2026-07-11"}
)
@@ -141,7 +141,7 @@ def test_api_chart_response_uses_domain_contract_hash(monkeypatch: pytest.Monkey
)
expected = calculation_service.compute_chart({**BIRTH, "node_mode": "true"})
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(
rest = JyotishComputeService()._compute_chart_sync(
{**BIRTH, "node_mode": "true", "transit_date": "2026-07-11"}
)
@@ -160,7 +160,7 @@ def test_api_visible_chart_values_come_from_domain_service(monkeypatch: pytest.M
request = {**BIRTH, "node_mode": "true", "transit_date": "2026-07-11"}
expected = calculation_service.compute_chart(request)
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(request)
rest = JyotishComputeService()._compute_chart_sync(request)
assert rest["ascendant"]["lon"] == pytest.approx(expected["ascendant"]["lon"], abs=1e-8)
for planet in ("Sun", "Moon", "Rahu", "Ketu"):
@@ -187,7 +187,7 @@ def test_api_sade_sati_uses_domain_true_saturn_transit(monkeypatch: pytest.Monke
ayanamsa=BIRTH["ayanamsa"],
)
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(request)
rest = JyotishComputeService()._compute_chart_sync(request)
assert rest["sade_sati"]["transit_saturn_lon"] == pytest.approx(
expected["transit_saturn_lon"], abs=1e-8
@@ -210,7 +210,7 @@ def test_api_dasha_boundary_comes_from_domain_service(monkeypatch: pytest.Monkey
moon_lon=chart["planets"]["Moon"]["lon"],
)
rest = JyotishAPIHandler.__new__(JyotishAPIHandler)._compute_chart_sync(request)
rest = JyotishComputeService()._compute_chart_sync(request)
assert rest["dasha"]["current_md"] == expected["birth_balance"]["lord"]
assert rest["dasha"]["remaining_years"] == pytest.approx(
+3 -3
View File
@@ -12,7 +12,7 @@ if SCRIPTS not in sys.path:
sys.path.insert(0, SCRIPTS)
from birth_time_rectifier import get_effective_accuracy, get_enabled_vargas # noqa: E402
from jyotish_api_server import JyotishAPIHandler, _build_consumer_context # noqa: E402
from jyotish_api_server import JyotishComputeService, _build_consumer_context # noqa: E402
CANDIDATE_WINDOW_COPY = re.compile(
r'candidate windows|not_auto_rectified|按时间范围|建议先做事件反验',
@@ -21,8 +21,8 @@ CANDIDATE_WINDOW_COPY = re.compile(
PRECISION_ANNOTATION_COPY = re.compile(r'填报时间|标注精度|precision', re.I)
def _handler() -> JyotishAPIHandler:
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
handler = JyotishComputeService()
handler._compute_vedastro_gateway_run = lambda body: { # type: ignore[method-assign]
'status': 'official_blocked',
'official_closure_reason': 'accuracy_contract_probe',
+3 -3
View File
@@ -17,7 +17,7 @@ from jyotish_api_server import ( # noqa: E402
_PRECISE_BIRTH_TIME_ACCURACY,
_ROUTE_DOMAIN_CONTEXT,
_ROUTE_REQUIRED_LAYERS,
JyotishAPIHandler,
JyotishComputeService,
_attach_local_consultation_layers,
_build_consumer_context,
_compact_vedastro_cross_check,
@@ -26,8 +26,8 @@ from jyotish_api_server import ( # noqa: E402
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator # noqa: E402
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
_BIRTH = {'year': 1995, 'month': 8, 'day': 18, 'hour': 12, 'minute': 0}
+6 -6
View File
@@ -267,9 +267,9 @@ def _consultation_body(**overrides):
def test_declared_route_executes_even_when_question_text_names_another_domain():
"""One question, several domains: text routing can only ever agree with one of them."""
from scripts.jyotish_api_server import JyotishAPIHandler
from scripts.jyotish_api_server import JyotishComputeService
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
question = "我的事业和财运接下来会怎么走,两者之间该怎么取舍"
wealth = handler._compute_consultation_workflow(_consultation_body(
@@ -287,9 +287,9 @@ def test_declared_route_executes_even_when_question_text_names_another_domain():
def test_declared_route_cannot_smuggle_a_route_off_the_server_allowlist():
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
from scripts.jyotish_api_server import BadRequest, JyotishComputeService
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
with pytest.raises(BadRequest, match="unsupported consultation workflow route"):
handler._compute_consultation_workflow(_consultation_body(
@@ -308,9 +308,9 @@ def test_declared_route_cannot_smuggle_a_route_off_the_server_allowlist():
def test_api_dry_run_exposes_validated_plan_contract_and_rejects_tampering():
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
from scripts.jyotish_api_server import BadRequest, JyotishComputeService
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
body = {
**timing_body(),
"dry_run": True,
@@ -16,7 +16,7 @@ if SCRIPTS not in sys.path:
import flexible_birth_time_profile as profile_module # noqa: E402
from jyotish_engine import _birth_datetime_from_args # noqa: E402
from scripts.consultation_domain_registry import CANONICAL_DOMAINS # noqa: E402
from scripts.jyotish_api_server import JyotishAPIHandler, _load_local_module # noqa: E402
from scripts.jyotish_api_server import JyotishComputeService, _load_local_module # noqa: E402
_SMOKE_BIRTH = {
@@ -31,11 +31,11 @@ _SMOKE_BIRTH = {
}
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler) -> None:
def _stub_consultation_runtime(monkeypatch, handler: JyotishComputeService) -> None:
chart = {
"success": True,
"birth_info": {"date": "1993-06-15", "time": "10:30", "tz": 8},
+4 -4
View File
@@ -7,7 +7,7 @@ import pytest
from scripts.consultation_domain_registry import CANONICAL_DOMAINS
from scripts.consultation_plan_contract import plan_route_contract
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
from scripts.jyotish_api_server import BadRequest, JyotishComputeService
from scripts.report_orchestrator import (
INDEPENDENT_REPORT_THEMES,
ThemeName,
@@ -18,11 +18,11 @@ from scripts.report_orchestrator import (
_NATIVE_THEMATIC_DOMAINS = {"career", "marriage", "wealth", "health"}
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler, *, include_strict_contracts: bool = True) -> None:
def _stub_consultation_runtime(monkeypatch, handler: JyotishComputeService, *, include_strict_contracts: bool = True) -> None:
chart = {
"success": True,
"birth_info": {"date": "1997-08-08", "time": "05:00", "tz": 8},
+2 -2
View File
@@ -79,9 +79,9 @@ def test_window_packet_probes_always_pass_hour_and_minute(monkeypatch: pytest.Mo
def test_api_handler_rejects_hour_on_declared_window() -> None:
from jyotish_api_server import BadRequest, JyotishAPIHandler
from jyotish_api_server import BadRequest, JyotishComputeService
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
with pytest.raises(BadRequest, match="single birth minute"):
handler._compute_declared_window_chart({
**BIRTH_PLACE,
+2 -2
View File
@@ -192,7 +192,7 @@ def test_api_turns_invalid_birth_time_accuracy_into_bad_request(monkeypatch) ->
return {"modules": {}}
monkeypatch.setattr(api, "_load_local_module", lambda _name: FakeEngine)
handler = api.JyotishAPIHandler.__new__(api.JyotishAPIHandler)
handler = api.JyotishComputeService()
with pytest.raises(
api.BadRequest,
@@ -218,7 +218,7 @@ def test_api_turns_invalid_report_window_into_bad_request(monkeypatch) -> None:
raise ValueError("birth_time_window_must_not_cross_midnight")
monkeypatch.setattr(api, "_load_local_module", lambda _name: FakeEngine)
handler = api.JyotishAPIHandler.__new__(api.JyotishAPIHandler)
handler = api.JyotishComputeService()
with pytest.raises(api.BadRequest, match="birth_time_window_must_not_cross_midnight"):
handler._compute_full_reading_for_thematic({
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from scripts.jyotish_api_server import JyotishAPIHandler
from scripts.jyotish_api_server import JyotishComputeService
from scripts.jyotish_engine import _build_ai_prompt_pack, _build_health_narrative_payload
@@ -44,7 +44,7 @@ def test_health_narrative_uses_only_health_vargas_lords_dasha_and_related_shadba
def test_api_prompt_pack_keeps_compact_health_narrative():
report = _report()
chart = {**report["chart"], "modules": report["modules"], "birth": {}, "dasha": {}}
prompt = JyotishAPIHandler.__new__(JyotishAPIHandler)._build_chart_prompt_pack(chart)
prompt = JyotishComputeService()._build_chart_prompt_pack(chart)
seed = prompt["evidence_snapshot"]["health_narrative"]
assert seed["status"] == "parameter_sensitive"
assert "D6/D8/D30" in seed["headline"]
+3 -3
View File
@@ -6,11 +6,11 @@ SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
if SCRIPTS not in sys.path:
sys.path.insert(0, SCRIPTS)
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_api_server import JyotishComputeService # noqa: E402
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def _planets():
+2 -2
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from scripts.jyotish_api_server import BadRequest, JyotishAPIHandler
from scripts.jyotish_api_server import BadRequest, JyotishComputeService
ROOT = Path(__file__).resolve().parents[1]
@@ -39,7 +39,7 @@ def test_cli_prashna_blocks_legacy_approximation_modes():
def test_api_prashna_rejects_client_planets_and_computes_context():
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
body = {
"question_text": "Test question", "question_timestamp": "2026-07-12T12:00:00+08:00",
"lat": 39.9042, "lon": 116.4074, "timezone": 8,
@@ -9,7 +9,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from jyotish_api_server import JyotishAPIHandler # noqa: E402
from jyotish_api_server import JyotishComputeService # noqa: E402
from relationship_analysis import analyze_relationship # noqa: E402
from relationship_event_class_evidence import build_event_class_evidence # noqa: E402
@@ -74,7 +74,7 @@ def test_consultation_shaped_1990_chart_feeds_nonempty_event_class_hits() -> Non
)
)
chart = golden["chart"]
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
handler = JyotishComputeService()
dasha = (chart.get("modules") or {}).get("dasha") or {}
dasha_info = handler._thematic_dasha_info(chart, dasha)
assert dasha_info["maha_dasha"] == "Saturn"
+3 -3
View File
@@ -2,11 +2,11 @@
from __future__ import annotations
from scripts.jyotish_api_server import JyotishAPIHandler
from scripts.jyotish_api_server import JyotishComputeService
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def test_prefers_dasha_sub_periods_current_over_birth_md() -> None:
+4 -4
View File
@@ -11,7 +11,7 @@ import pytest
from scripts import jyotish_api_server
from scripts import vedastro_foreground as foreground
from scripts.jyotish_api_server import JyotishAPIHandler
from scripts.jyotish_api_server import JyotishComputeService
from scripts.vedastro_snapshot_cache import (
lookup_snapshot,
snapshot_cache_dir,
@@ -21,8 +21,8 @@ from scripts.vedastro_snapshot_cache import (
)
def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
def _handler() -> JyotishComputeService:
return JyotishComputeService()
def _verified_gateway(*, request_id: str, reference_date: str) -> dict:
@@ -56,7 +56,7 @@ def _fake_chart() -> dict:
}
def _stub_local(monkeypatch, handler: JyotishAPIHandler, gateway) -> None:
def _stub_local(monkeypatch, handler: JyotishComputeService, gateway) -> None:
monkeypatch.setattr(handler, "_compute_chart", lambda body: _fake_chart())
monkeypatch.setattr(
handler,