docs(tasks): full data parity part 3 brief (agent loop, issue #93)
PyJHora runs in a subprocess worker for the annual method reference only; installing it must leave every other output byte-identical (seven switch points pinned, parent process never imports jhora). Inputs default to local birth time and the requested year (D6, awaiting product confirm). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b662bf59fc
commit
2806bd5b75
@@ -519,3 +519,4 @@
|
||||
| `TASK-full-data-parity-1-20261021.md` | — | #91 完整数据版补齐(一):A 段渲染(中文附录改中文源、71 小节归 6 章、导出星座名)→ B 段引擎(Yogi 点、标准大运子运、Kalachakra 简化子运、年运三年全展开、36 个月星历、解释补长);不设大小上限 | 待领取(等 #89 合入;T3 与 #90 串行) | `codex/full-data-parity-1-20261021` |
|
||||
| `TASK-goel-jaimini-strength-20261022.md` | — | #94 搬上游公开的 Goel Jaimini 强度与 Brahma / Maheshwara / Rudra(4 个上游原样文件,按 #81 决策 A 先例根目录预算 448→452;来源串改书目引用);只做计算、不改报告;冻结身份不变 | 待领取(可与 #89 并行;须在 #91 印章前合入) | `codex/goel-jaimini-strength-20261022` |
|
||||
| `TASK-chart-subject-table-fix-20261022.md` | `PROGRESS-chart-subject-table-20261021.md` | **#86 第 1 次打回**:报告所有表被强制单行(打印跑出纸面);保存他人复制了档案页代码且已分叉;点编辑框内边距会丢修改 | 待领取 | `codex/chart-subject-table-20261021` |
|
||||
| `TASK-full-data-parity-3-20261022.md` | — | #93 完整数据版补齐(三):「独立年度方法参考」正式环境调 PyJHora(子进程隔离;装上后其余输出逐字节不变,7 个换路径开关钉死);输入默认用本地时间与正确年份(D6 待产品确认) | 待领取(等 #91 合入) | `codex/full-data-parity-3-20261022` |
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# 任务书:完整数据版补齐(三)——「独立年度方法参考」照上游调用 PyJHora,其余输出一字不变(agent 循环 issue #93,2026-10-22)
|
||||
|
||||
## 基线
|
||||
|
||||
- 写作基线:`origin/staging` = `f90020efd`(#91 任务书刚推上去,#91 代码未合入)。**开工以 #91 合入后的 `origin/staging` 为准**;#91 未合入不得开工。#91、#92、#93 改同一批渲染文件(`scripts/pl9_full_data_export.py`、`scripts/report_render/`):#92 若已开工,后开工的一方先 `git merge origin/staging`,在 PROGRESS 写明依赖的 staging SHA。
|
||||
- 分支:`codex/full-data-parity-3-20261022`;工作树:`.worktrees/full-data-parity-3-20261022`。执行方 **Grok(Windows + Docker)**,只推这个分支。
|
||||
- 上游参照(只读,放仓库外,同 #91):`https://github.com/732642856/yinduzhanxing.git`,分支 `codex/customer-export-frozen-20261016`(`1d549fe9`):
|
||||
- 渲染:`customer-export/registry/delivery_timing_supplement.py` 的 `_annual_observation_lines`、`_annual_anchor_label`、`annual_method_observations`;
|
||||
- 调用:`scripts/annual_pyjhora_replay.py` 的 `_isolated_annual_worker`、`_call_mudda`、`_call_mudda_with_annual_return_moon`、`_call_patyayini`、`_pyjhora_annual_return_jd`、`_probe_tajika_yogas`、`_probe_pyjhora_per_yoga_callables`、`_annual_return_marker`;
|
||||
- 整理:`scripts/annual_tajika_pack.py` 的 `_normalize_mudda_replay_rows`、`_normalize_patyayini_replay_rows`、`_normalize_pyjhora_tajika_yoga_rows`、`_pyjhora_dasha_lord_label`、`PYJHORA_DASHA_LORDS`、`_PYJHORA_TAJIKA_YOGA_NAMES`。
|
||||
- **不得复制** `subjects.json`、`customer_case.py`、`subject_guard.py`、`export_customer_bilingual.py`、`source_*` 审计文件(含真实个案资料)。
|
||||
- 关联:BUG-1273(本单推翻其「PyJHora 不进生产」约定,见 D1)、BUG-1265、BUG-1275(真恒星口径)、BUG-1272 / #89(英文汉字 0)、ERR-110(冻结身份);`TASK-full-data-parity-1-20261021`(#91,D5 把本节移到本单)、`TASK-full-data-parity-2-20261021`(#92)、`TASK-api-chart-subprocess-gate-20261013`(#64,`_jhora_importable` 短路与 `sitecustomize` 挡导入法)、`TASK-special-lagnas-native-20261008`。
|
||||
|
||||
## 1. 现状实证
|
||||
|
||||
### 1.1 上游这一节怎么来的
|
||||
|
||||
`_annual_observation_lines` 本身不调 PyJHora,只读资料包里 `annual_tajika_pack.external_engine_comparison.pyjhora`(以及 `annual_tajika_series.years` 里另外两年的同名字段)。这些字段由上游 `jyotish_engine._build_pl9_full_annual_section` → `annual_pyjhora_replay.replay_pyjhora_annual` → `_run_isolated_annual_replay` **起子进程**(`sys.executable -c "...from scripts.annual_pyjhora_replay import _isolated_annual_worker..."`,超时 30 s)产出。子进程里:
|
||||
|
||||
| 步骤 | 调用(PyJHora 4.8.7) | 参数 |
|
||||
| --- | --- | --- |
|
||||
| 设岁差 | `drik.set_ayanamsa_mode(ayanamsa.upper())`,再回读 `const._DEFAULT_AYANAMSA_MODE`,不一致整节 blocked | 资料包 `settings.ayanamsa` |
|
||||
| 地点 | `drik.Place(name, lat, lon, tz, elevation)` | 出生地经纬度、出生时区 |
|
||||
| Mudda(出生时月亮) | `mudda.varsha_vimsottari_dasha_start_date(jd, place, age)`、`mudda.mudda_dhasa_bhukthi(jd, place, age)` | `jd` = 出生 JD,`age` = 目标年 − 出生年 |
|
||||
| Mudda(年盘月亮) | `tajaka.annual_chart(jd, place, years=age)` 取回归时刻 → `utils.julian_day_number(...)` → `mudda_dhasa_bhukthi(annual_jd, place, 0)` | 同上 |
|
||||
| Patyayini | `_pyjhora_annual_return_jd`(同一个 `annual_chart`)→ `patyayini.get_dhasa_bhukthi(annual_jd, place)` | 同上;取不到回归时刻退回 `jd + age × 365.256364` |
|
||||
| Tajika 瑜伽 | `tajaka.annual_chart(jd, place, years=age)[0]` 当位置表,逐个调 `tajaka_yoga.get_ithasala / eesarpha / manahoo / kamboola / gairi_kamboola / khallasara / radda _yoga_planet_pairs`、`get_nakta / yamaya _yoga_planet_triples` | 年盘位置 |
|
||||
|
||||
整理后三张表的列:Mudda `order, main_lord, sub_lord, start_display, duration_raw`(天,无结束边界,时区未定);Patyayini `order, main_code, sub_code, start_display, end_display, duration_raw`(年;结束 = 下一段开始,末段未知);Tajika `yoga, observation_status, raw_relationships`(行星编号 JSON)。表头写 `Source: PyJHora`,并印两条实际回归锚点(Patyayini、Tajika)。
|
||||
|
||||
### 1.2 我方现状(`f90020efd`,Linux,本机装有 PyJHora 4.8.7 + pyswisseph 2.10.3.2)
|
||||
|
||||
- 我方已有旧版 `scripts/annual_pyjhora_replay.py`(1,035 行,`336286e42` 起加了 `_jhora_importable()` 短路:找不到 `jhora` 就不起子进程,直接返回与子进程失败时相同的字典,golden 在 `tests/fixtures/api-chart-subprocess-gate/annual-no-jhora.json`、`tajaka-no-jhora.json`)。它比上游旧:`_call_mudda` 没有「年盘月亮」口径,`_call_patyayini` 没有精确回归锚点,`_probe_tajika_yogas` 没有逐个瑜伽的调用。
|
||||
- **它已经接在每次全盘上**:`scripts/jyotish_engine.py::_build_pl9_full_annual_section` 每年调一次 `replay_pyjhora_annual`,`_attach_annual_tajika_pack` 对目标年 + 后两年各调一次。生产镜像没有 PyJHora,所以今天全部短路成 blocked。**一旦镜像装上 PyJHora,这条旧路会真跑,结果写进 `annual_tajika_pack.external_engine_comparison.pyjhora`,被下列现有渲染读出来**(全部按符号定位):
|
||||
- `jyotish_engine._build_pl9_professional_support_section`(`patyayini_replay`);
|
||||
- `jyotish_engine.render_pl9_markdown` 内 `_year_replay_subblocks`(替代 Patyayini 行、Sahams 值)、年运小节的 `replay_patyayini_*` / `replay_sahams*`、Yogi 行的 `pyjhora_behavior_only` 分支、Patyayini 状态行;
|
||||
- `annual_tajika_pack._external_engine_comparison` / `_audit`;
|
||||
- `pl9_reader_export` 读 `normalized_tables.pyjhora_tajika_yoga_rows` 的两处。
|
||||
这就是红线 1 的头号风险:不关掉,装包本身就会改年运章、附录和资料包。
|
||||
- 进程全局状态实测(同一进程内):先按我方 Raman 设好 `swe.set_sid_mode`,调一次 `drik.set_ayanamsa_mode("LAHIRI")` + `mudda.mudda_dhasa_bhukthi` 之后,同一 JD 的岁差从 22.207588° 变成 23.653890°,月亮恒星经度从 23.718016° 变成 22.271715°(差 1.45°)。另外 `import jhora.const` 在导入时就执行 `swe.set_ephe_path(<jhora>/data/ephe)`(该目录只有 txt 星表,没有 `.se1`);`drik.set_ayanamsa_mode` 还会改写 `const._DEFAULT_AYANAMSA_MODE`(PyJHora 默认 `TRUE_PUSHYA`);交点默认真交点(`const._use_true_nodes_for_rahu_ketu = True`,我方默认平交点)。**PyJHora 只能在子进程里跑,不得在 API 进程内导入。**
|
||||
- 子进程实测(虚构盘 1985 年北京,Raman,三年一个子进程,按上游调用次数):导入 0.3~0.6 s,每年 1.9~2.3 s,合计约 **7 s**,峰值 RSS 约 **60 MB**。生产 2 核,预计 10~14 s,由 T6 实测。
|
||||
- 上游两处输入口径与 PyJHora 文档不符(实测,见 D6):
|
||||
1. 上游传入的 `jd` 是 **UT** JD(`swe.julday(..., hour − tz)`),PyJHora 约定是**当地时** JD(`drik` 内部 `jd_utc = jd − place.timezone/24`,`annual_chart` 文档也这样写)。同一虚构盘(东八区):回归时刻 08:34:53 vs 16:35:04,正好差 8 小时;Mudda 首段起点差约 8.5 天。我方 `tajika.py` 的 PyJHora sidecar 已按当地时传(`annual_jd + tz/24`)。
|
||||
2. `years=age` 时 `annual_chart`(Patyayini、Tajika 用它)落在**目标年前一年**(请求 2026 返回 2025-06-15),Mudda 却落在 2026。上游没修,只在表头写「请求年份不等于实际返回年份」。`years=age+1` 才落在目标年。
|
||||
- PyJHora 4.8.7 自身问题:`get_radda_yoga_planet_pairs` 抛 `NameError`;`gairi_kamboola`、`khallasara` 返回 `None`。上游把它们分别印成「未获得结果」「未知」。
|
||||
|
||||
### 1.3 运行时会因「装了 PyJHora」而换路的地方(全部要保持今天的结果)
|
||||
|
||||
| # | 位置(按符号) | 今天(生产无 PyJHora) | 装上后若不改 | 处理 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| S1 | `annual_pyjhora_replay._jhora_importable` → `_run_isolated_annual_replay`、`_run_isolated_tajaka_year_lord`(经 `replay_pyjhora_annual`、`probe_pyjhora_year_lord`,由 `jyotish_engine._build_pl9_full_annual_section` 每年调用) | 短路,返回 golden | 每年起 2 个子进程、旧口径数据进年运章(见 1.2) | 产品路径一律走短路,返回与今天逐字节相同的 golden;只有研究脚本显式传参才起子进程 |
|
||||
| S2 | `unified_consultation_orchestrator._external_engine_cross_validation` → `diagnose_pyjhora_adapter.build_report()` | `find_spec` 为空,`missing_dependency` | **在 API 进程内** `import_module("jhora.utils" / ".horoscope.chart.charts" / ".panchanga.drik")`(导入即改星历路径);`dependency_available`、`missing_dependency` 变;仓库里 `benchmarks/.../run_pyjhora_compare.py` 存在时(测试环境)`status` 从 `missing_dependency` 变 `available` | 产品调用改为不探测、不导入,返回今天的「缺依赖」字典;`build_report()` 默认行为留给命令行诊断 |
|
||||
| S3 | `rectification_three_engine_packet._pyjhora_d1`(`/api/rectification…` 高严谨分支 `build_packet`) | `ModuleNotFoundError`,该引擎行为错误 | **在 API 进程内**导入 `pyjhora_rectification_d1_reference` → `jhora`,三引擎包多出 PyJHora 星座行 | 产品路径固定返回今天的错误行,不导入;不改 `jyotish_api_server.py` |
|
||||
| S4 | `tajika._external_pyjhora_panchavargiya_score`(进程内 `pyjhora_panchavargiya_reference`,失败再走 `_lookup_external_pyjhora_python` 外部解释器) | 只在 `JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1` 时由 `tajika_year_lord._select` 传入 `annual_jd`,否则不触发 | 不变(仍由环境变量控制);但镜像里 `python3` ≠ `sys.executable` 的字面路径,变量一旦被设就会真跑 | 不改代码;加测试:部署文件(`deploy/**`)不得设置该变量;变量未设时装了 PyJHora 年主结果不变 |
|
||||
| S5 | `jyotish_engine` 里 `from pyjhora_canonical_adapter import …`(Chara `pyjhora_blackbox` 口径)、`from pyjhora_yogi_replay import replay_pyjhora_yogi`(Yogi 外部回放) | 两个模块仓库里都**不存在**,走桩函数 / blocked | 不变 | 不得新增这两个模块(红线) |
|
||||
| S6 | `ephemeris_backend_probe._python_module_status("jhora")`、`diagnose_external_engine_adapters`、`scripts/_compute_one_chart.py` | 开发诊断、离线基准工具 | 输出变(显示可用) | 不在产品路径(T1 核实 `ephemeris_backend_probe` 只被 `run_quality_gate.py`、测试引用),不改;PROGRESS 记一句 |
|
||||
| S7 | 测试:`test_ashtakavarga_sodhita_chain.py::test_trikona_step_matches_pyjhora_when_installed`、`test_dasha_api_engine_parity.py` 里经 `_pyjhora_rows`(`pytest.importorskip("jhora")`)的用例 | CI `.venv` 无 PyJHora,跳过 | 门禁 `.venv` 装 `requirements.txt` 后会真跑 | 允许「跳过 → 通过」,必须通过;列进 PROGRESS |
|
||||
|
||||
`tests/test_runtime_jhora_import_scan.py`(BUG-1273,快速门内)按 AST 从全盘、报告、对话卡入口走导入图,**文件名含 `pyjhora` 的模块和 `scripts/research` 不进图**。S2、S3 不在它的入口里,所以今天没被拦。
|
||||
|
||||
### 1.4 依赖
|
||||
|
||||
| 项 | 实测 / 现状 |
|
||||
| --- | --- |
|
||||
| 上游固定版本 | `containers/pyjhora-observation/Dockerfile`:`PyJHora==4.8.7`、`pyswisseph==2.10.3.2`;`requirements-dev.txt`:`PyJHora>=4.8` |
|
||||
| 我方 | `requirements-reference-engines.txt` 已写 `PyJHora==4.8.7`、`geocoder==1.38.1`、`geopy==2.4.1`,注释写「AGPL,不得并入生产 requirements」;`requirements.txt` 里 `pyswisseph` **不带版本号** |
|
||||
| PyJHora 4.8.7 元数据 | `Requires-Dist` 为空(pip 不会因为它动 `pyswisseph`);但 `jhora.utils` 导入 `geocoder`、`geopy`、`timezonefinder`、`numpy`、`pytz`、`dateutil`、`requests`。分类器写 MIT,包内 LICENSE 是 AGPL-3.0 |
|
||||
| 安装体积 | 约 94~97 MB:`data/` 56 MB(`geonames_places_5k_IN.csv` 37 MB、`geonames_places_5k.csv` 7 MB、`data/ephe` 10 MB txt)、`images/` 15 MB、`horoscope/` 9 MB、`tests/` 5 MB、`lang/` 5 MB |
|
||||
| 镜像 | `deploy/railway-api.Dockerfile` 只 `pip install -r requirements.txt`,不需要改 Dockerfile;`requirements*.txt` 在 `deploy/gated-paths.txt` 内,推上去会触发门禁、构建新镜像 |
|
||||
| 门禁 `.venv` | `.gitea/workflows/backend-quality-gate.yml` 安装 `requirements.txt` + `requirements-dev.txt` → 合入后门禁环境也有 PyJHora(S7)。**不改 workflow** |
|
||||
|
||||
### 1.5 渲染出口的现有拦截
|
||||
|
||||
- `pl9_full_data_export._sanitize_pl9_ai_density_markdown_en` 与 `pl9_reader_english.render_pl9_user_markdown_en` 的 `drop_fragments` 含 `"PyJHora"`:**任何含 PyJHora 的行会被整行删掉**。
|
||||
- `tests/test_report_reader_main.py::LEAK_PATTERNS` 含 `PyJHora`、`\bparity\b`、`_unavailable`、`\bschema\b`、`\bsource_path\b`;`test_reader_main_hides_audit_vocabulary` 对完整数据版全文断言零命中。
|
||||
- 所以本节必须在两个清理器和 `_apply_local_report_conventions` **之后**拼接,并且不得印出任何内部失败原因、键名。
|
||||
|
||||
## 2. 根因 / 背景
|
||||
|
||||
1. 上游交付版「独立年度方法参考」是 PyJHora 子进程回放的产物;我方 BUG-1273 起约定 PyJHora 只作离线对照,生产镜像不装,所以这节在我方一直缺。
|
||||
2. 我方代码里早已散落「检测到 PyJHora 就换路」的开关(1.3 的 S1~S5),以前靠「镜像里没有」兜底。装包之后这些兜底全部失效,其中 S1~S3 会在 API 进程内导入 PyJHora 或把旧口径数据印进现有章节。
|
||||
3. PyJHora 改的是 Swiss Ephemeris 的**进程级全局状态**(岁差模式、星历路径),与我方引擎共用一个 `swisseph` 模块,进程内调用必然串味。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
| # | 决定 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| D1 | **推翻** BUG-1273 / `TASK-special-lagnas-native-20261008` 的约定「PyJHora 只作外部对照、不进生产」:正式环境安装 PyJHora,**仅**用于本节。AGPL 许可证风险由产品负责人承担,已知情。`requirements-reference-engines.txt` 与 `diagnose_pyjhora_adapter` 里「不得进生产」的文字随之过时:前者注释改写;后者属于对话输出,按红线 1 不改 | 产品 10-21 |
|
||||
| D2 | 照上游 `_annual_observation_lines` 出三张表:Mudda(各起点口径,即出生时月亮 / 年盘月亮)、Patyayini、Tajika 瑜伽;表头注明「来源:PyJHora」;与前文我方年运分开读,不替换、不合并、不比对 | 产品 10-21 |
|
||||
| D3 | 中英两版都要有;英文汉字为 0(#89 的扫描);中文用中文标签(上游中文版列名是英文,不照搬) | 产品 10-21 + 本单 |
|
||||
| D4 | 某张表调用失败(含 PyJHora 不可用、超时、岁差不支持),该表写「未获得结果 / Result unavailable」,报告照常生成;其他表照常出 | 产品 10-21 |
|
||||
| D5 | 年份范围:与上游 `annual_method_observations` 一致,目标年 + 后两年(与 #91 D8 的三年对应),每年一组三张表 | 照上游 |
|
||||
| D6 | **输入口径(待产品确认,默认如下,Grok 可按默认开工)**:上游两处与 PyJHora 文档不符(1.2)。默认:`jd` 传**当地时** JD(PyJHora 文档口径,与我方 `tajika.py` sidecar 一致);`annual_chart` 用 `years = age + 1`,让 Patyayini、Tajika 的回归锚点落在请求年,Mudda 仍按上游 `years = age`(其起点本来就落在请求年)。实际锚点照上游印出。若产品要求与上游逐字一致,改回 UT JD 与 `years = age`,其余不变。两种做法的差异(回归时刻、首段起点)在 PROGRESS 用 3 张虚构盘列表 | 本单(Claude) |
|
||||
| D7 | 岁差:按资料包 `settings.ayanamsa` 映射到 PyJHora 名称(lahiri→LAHIRI、raman→RAMAN、kp / krishnamurti→KP、fagan_bradley→FAGAN、true_citra→TRUE_CITRA、true_pushya→TRUE_PUSHYA);djwhal_khul、sassanian 等 PyJHora 不支持的写三张表「未获得结果」,**不得**让 PyJHora 静默退回它的默认 TRUE_PUSHYA。设置后回读 `const._DEFAULT_AYANAMSA_MODE` 核对 | 照上游 + 本单 |
|
||||
| D8 | 交点:上游不设(标「适配器不支持」)。本单在子进程里按资料包 `node_mode` 调 `const.set_node_mode(node_mode == "true")`,**在导入 `drik` 之前**设,并在子进程里用一颗交点经度回读核对;核对不过就不设,表头写「交点:PyJHora 默认(真交点)」 | 本单 |
|
||||
| D9 | 我方 BUG-1275 的真恒星口径不套用到 PyJHora:本节数值就是 PyJHora 自己算的,不与我方年运比对、不标一致 / 不一致 | 本单 |
|
||||
| D10 | 位置:完整数据版最后一个二级标题章(在 #91 的六章附录与「结构化资料附录 / Structured Data Appendix」之后),与上游「正文之后追加」一致;只出现在完整数据版,标准版不出 | 本单 |
|
||||
| D11 | 生时校正冻结身份、对话卡、标准版报告、星盘接口不碰本节,也不因装包变化 | 产品 10-21 红线 1 |
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. **装 / 不装逐字节一致(产品红线 1)**:除新增这一节以外,现有所有输出不得变化。验收:同一批盘「装 / 不装」两套全量 pytest 逐条一致(S7 的「跳过 → 通过」除外,必须通过);完整数据版中英 Markdown 切掉本章后逐字节相同;资料包 JSON 去掉本节新键后逐字节相同;校正冻结身份(`scripts/research/sealed_holdout_rerun.py::implementation_identity`)与封存回放(`tests/test_sealed_holdout_contract_freshness.py`、`tests/test_rectification_validation_integrity_gate.py`)逐项相同;对话卡(`read_full_data_domain`)3 张虚构盘逐字节相同。
|
||||
2. **API 进程内不得导入 `jhora`**:PyJHora 只在一个子进程 worker 里导入。生成报告、全盘、对话、校正之后,父进程 `sys.modules` 里不得有 `jhora`;`swisseph` 的岁差(`get_ayanamsa_ut`)与同刻月亮经度在调用本节前后不变。
|
||||
3. **版本固定(产品红线 2)**:`PyJHora==4.8.7`(与上游观测容器、我方 reference 文件一致);`geocoder==1.38.1`、`geopy==2.4.1` 照 `requirements-reference-engines.txt`。**不得改 `requirements.txt` 里 `pyswisseph` 那一行**,不得升级或更换 `pyswisseph`;新旧两个本地镜像里 `pip show pyswisseph` 必须相同,星位一律不变。
|
||||
4. **只有本节显式调用 PyJHora**:S1~S3 的产品路径保持不导入、不起子进程,结果与今天逐字节相同;不得新增 `pyjhora_canonical_adapter.py`、`pyjhora_yogi_replay.py`;不得把 PyJHora 数据写进 `annual_tajika_pack.*`、`normalized_tables`、`external_engine_comparison` 等现有键(现有渲染会读它们)。
|
||||
5. **增长冻结**:不改 `scripts/jyotish_api_server.py`;`tests/test_api_server_growth_contract.py` 不放宽。`scripts/*.py` 根目录已满 448 个(`tests/test_python_hygiene_budget.py::ROOT_PY_BUDGET`),新文件进 `scripts/report_render/`,不得调高预算。
|
||||
6. **不新增静默吞错**:`SILENT_EXCEPT_BUDGET = 1` 不放宽。子进程失败、超时、JSON 不合法都记日志(含原因),表里只写「未获得结果」。
|
||||
7. **报告卫生**:本节不得出现内部键名、`blocked`、`partial_verified`、`_unavailable`、`schema`、`source_path`、Python 异常名;英文汉字 0;中文不出现英文列名(行星编号、瑜伽专名、PyJHora 名称除外)。
|
||||
8. 不动数据库结构与迁移;不改 `.gitea/`、Dockerfile;不推 `main`。
|
||||
9. **冻结文件不碰**:`sealed_holdout_rerun.PRODUCTION_FILES` 所列 11 个文件(`scripts/rectification/*` 10 个 + `scripts/functional_benefics.py`)本单一行不改。
|
||||
10. **隐私**:只用虚构盘与公开名人盘;不复制上游个案文件;PROGRESS 与 Bug 历史实测段只写「虚构盘」。
|
||||
|
||||
## 5. 任务分解(一条任务一个提交,按顺序)
|
||||
|
||||
| # | 内容 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| T0 | **先量风险,再动代码**:在 #91 合入后的 `origin/staging` 上,用两套环境(见第 6 节「装 / 不装」做法)对 3 张固定虚构盘跑完整数据版中英 Markdown 与资料包 JSON,`diff` 两套;全量 pytest 两套逐条比对。预期今天就有差异(S1、S2、S7),逐条归到 1.3 的 S 编号;本地构建基线镜像,记 `docker image inspect --format '{{.Size}}'` 与 `pip show pyswisseph` | PROGRESS 有一张「装 / 不装差异清单」,每条落到 S 编号;出现 S 表以外的差异,**先补进 S 表再继续**(新增条目也要在 T1 关掉);基线镜像大小与 pyswisseph 版本已记 |
|
||||
| T1 | **关掉现有开关(红线 1、4)**:S1 产品路径固定走短路(例如 `replay_pyjhora_annual` / `probe_pyjhora_year_lord` 增加只有研究脚本才传的显式参数,产品调用不传就返回现有 golden,不看 `find_spec`);S2 产品调用改为不探测、不导入,返回今天无 PyJHora 时的字典(`diagnose_pyjhora_adapter` 命令行默认行为不变);S3 产品路径固定返回今天的错误行;S4 加部署文件不设变量的测试。改动只在 `annual_pyjhora_replay.py`、`unified_consultation_orchestrator.py` / `diagnose_pyjhora_adapter.py`、`rectification_three_engine_packet.py`、`tajika_year_lord.py`(如需) | 装了 PyJHora 的环境下:`tests/fixtures/api-chart-subprocess-gate/*.json` 两个 golden 仍逐字节相等、`subprocess.run` 一次都不调;orchestrator 交叉验证段与无 PyJHora 时逐字节相同;三引擎包 PyJHora 行与无 PyJHora 时相同;父进程 `sys.modules` 无 `jhora`。T0 清单里 S1~S3 的差异全部消失。改到的既有断言写三栏(至少下表 A1) |
|
||||
| T2 | **依赖**:`requirements.txt` 加 `PyJHora==4.8.7`、`geocoder==1.38.1`、`geopy==2.4.1`,注释写明「仅供完整数据版『独立年度方法参考』子进程使用,AGPL,产品 10-21 决定」;`pyswisseph` 行不动;`requirements-reference-engines.txt` 注释改成与 D1 一致;不改 Dockerfile、workflow。本地用 `deploy/railway-api.Dockerfile` 构建新镜像 | 新镜像构建成功;`docker run --rm <新镜像> python -c "import jhora, swisseph, geocoder, geopy; ..."` 能导入;新旧镜像 `pip show pyswisseph` 相同;镜像大小增量写 PROGRESS;`pip check` 无新冲突 |
|
||||
| T3 | **子进程 worker**:新增 `scripts/report_render/pyjhora_annual_reference_worker.py`(文件名必须含 `pyjhora`,只在子进程里 `python -m` 运行,**任何产品模块不得 import 它**)与父进程侧 `scripts/report_render/annual_method_reference.py`(不导入 `jhora`、不导入 worker;用 `sys.executable -m scripts.report_render.pyjhora_annual_reference_worker`、`cwd` = 仓库根、stdin 传 JSON、stdout 收 JSON、PyJHora 的打印重定向到 stderr、超时 45 s 起步按 T6 实测调整)。一个子进程算完三年。子进程内按 1.1 表格的调用顺序、D6~D8 的口径;`annual_chart` 每年可只调一次复用结果(须在 3 张虚构盘上证明与逐次调用结果相同)。整理函数(`_normalize_mudda_replay_rows`、`_normalize_patyayini_replay_rows`、`_normalize_pyjhora_tajika_yoga_rows`、锚点、主星编号映射)搬到父进程侧,纯函数。资料包挂在**新键** `worksheets.timing_and_predictive_systems.pyjhora_annual_method_reference`(每年三表 + 实际锚点 + 实际岁差 / 交点 + 每表成功与否;失败原因只进日志)。挂载点放在报告资料包组装处(`professional_report_reference` 或 `jyotish_engine._attach_annual_tajika_pack` 之后,只加一行调用);中英两次渲染共用同一份资料包时不得算两遍,若两种语言各自组装资料包,加按请求哈希的进程内小缓存 | 3 张虚构盘:三年 × 三表都有行;Mudda 两个起点口径各一张;Tajika 9 个瑜伽各一行(`radda` 为「未获得结果」);岁差回读与请求一致;D6 两种口径的差异表进 PROGRESS;父进程 `sys.modules` 无 `jhora`;挡住 `jhora` 时三年 × 三表全部「未获得结果」、报告照常生成;超时与非零退出各有一个单元测试(替身子进程) |
|
||||
| T4 | **渲染**:照 `_annual_observation_lines` 出「独立年度方法参考 / Independent Annual Method Reference」二级标题(D10 位置),章首一句「本节与前面的原生周期分开使用……」;每年一个三级标题「年度方法补充数据(请求年份 2026)/ Supplementary Annual Method Data (requested year 2026)」;紧接「来源:PyJHora。……」/「Source: PyJHora. ……」与实际岁差、交点一行,再印 Patyayini、Tajika 两条实际回归锚点;四个四级标题:Mudda(出生时月亮起算)/ Mudda (Moon at birth)、Mudda(年盘月亮起算)/ Mudda (Moon in annual chart)、Patyayini、Tajika 瑜伽 / Tajika Yoga;每表上方照上游的单位与边界说明(中英);列名中英两套(序号 / No.、大运主星 / MD lord、小运主星 / AD lord、开始 / Start、结束 / End、时长(天)/ Duration (days)、时长(年)/ Duration (years)、瑜伽 / Yoga、结果 / Result、行星关系 / Planet relationships);主星中文用我方中文星名,`L` 写「上升 / Lagna」;Tajika 结果三态照上游(已返回组合,需核对条件 / 检测为空 / 未获得结果);某表失败只印说明行「未获得结果 / Result unavailable」,不印空表。拼接点在 `render_full_data_markdown` 中英两支的 `_apply_local_report_conventions` **之后** | 3 张虚构盘中英:本章是最后一个二级标题;三年各有四个四级标题;「来源:PyJHora / Source: PyJHora」各出现三次;英文全文汉字 0(#89 扫描);中文本章无英文列名;红线 7 卫生扫描对本章零命中;切掉本章后与 T1 完成时的输出逐字节相同 |
|
||||
| T5 | **红线测试**:①新增动态测试:装了 PyJHora 时生成一份完整数据版(中英)、跑一次全盘与一次对话卡,前后比较父进程 `swe.get_ayanamsa_ut(jd)`、同刻月亮经度、`sys.modules` 中无 `jhora`;②`tests/test_runtime_jhora_import_scan.py` **只加不减**:入口加 `scripts/unified_consultation_orchestrator.py`、`scripts/rectification_three_engine_packet.py`、`scripts/report_render/annual_method_reference.py`,并新增断言「产品导入图里没有任何模块 import `pyjhora_annual_reference_worker`」、「`annual_method_reference.py` 以 `-m` 字符串引用该 worker」;③完整数据版 golden / 快照类测试改为切掉本章再比(三栏);④批量生成报告的测试(如 #89 的 200 盘扫描)注入替身 runner,不真起 200 个子进程,本节自己的测试用真 PyJHora 跑 3 张盘 | 变异检查:在 `annual_method_reference.py` 加一行 `import jhora` 后①或②失败;把 S1 的强制短路去掉后 T1 的 golden 测试失败;快速门新增耗时写 PROGRESS |
|
||||
| T6 | **量测与文档**:①生产镜像大小(新旧 `docker image inspect` 与压缩后层大小);②单份报告生成时长增量(中、英各 5 次取中位,Linux 由 Claude 复测,Windows 照报);③内存:子进程峰值 RSS、API 容器在生成报告时的 `docker stats` 峰值;④本章中英字符数;⑤全量 pytest 耗时变化。文档:`docs/BUG_HISTORY.md`(BUG-1273 记录末尾追加「10-22 产品决定推翻『不进生产』,见 #93」,S2 / S3 进程内导入隐患按新编号记)、`CHANGELOG.md`、`deploy/README.md`(镜像含 PyJHora、仅子进程使用、AGPL 由产品决定)、PROGRESS、`docs/testing/full-data-parity-3-20261022.md`(真机:生成完整数据版、翻到最后一章、中英切换、导出 `.md`、旧报告仍能打开、标准版无本章) | 文档齐;中英合计生成仍在 180 s(`ENGINE_TIMEOUT_MS`)以内,超了停下回报 |
|
||||
|
||||
需要改的既有断言(三栏写进 PROGRESS 与测试注释):
|
||||
|
||||
| # | 测试 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | `tests/test_api_chart_subprocess_gate.py::test_jhora_available_still_uses_subprocess` | `_jhora_importable` 为真时两个隔离函数各起一次子进程(`calls == 2`) | 产品调用不传研究参数时,即使可导入也不起子进程、返回 golden;传研究参数时仍起子进程(保留 `calls == 2`) | #93 红线 1:装包后旧回放不得进年运章 |
|
||||
| A2 | `tests/test_report_reader_main.py::test_reader_main_hides_audit_vocabulary` | 完整数据版全文 `LEAK_PATTERNS`(含 `PyJHora`)零命中 | 切掉「独立年度方法参考 / Independent Annual Method Reference」一章后零命中;本章内必须含「来源:PyJHora」或「Source: PyJHora」,且本章对其余 `LEAK_PATTERNS` 零命中 | D1、D2:产品要求表头注明来源 |
|
||||
| A3 | 完整数据版 golden / 快照 / 逐字节比较类测试(T0 列出具体文件) | 全文比较 | 切掉本章后全文比较;本章另有结构测试 | 本章内容依赖是否装 PyJHora |
|
||||
| A4 | `tests/test_runtime_jhora_import_scan.py` | 现有断言 | 现有断言全部保留,只新增入口与断言(T5②) | 加严,不是放宽 |
|
||||
|
||||
## 6. 验收分工
|
||||
|
||||
「装 / 不装」两套环境的做法:
|
||||
|
||||
- **Windows(Grok)**:两个虚拟环境——`.venv`(`requirements.txt` 全装,含 PyJHora)与 `.venv-nojhora`(同一份 requirements 去掉三行新依赖后安装);两者 `pip show pyswisseph` 必须相同。
|
||||
- **Linux(Claude)**:系统 `python3` 已装 PyJHora 4.8.7 + pyswisseph 2.10.3.2;「不装」用 #64 的做法:只含下面 `sitecustomize.py` 的目录放在 `PYTHONPATH` 最前(子进程会继承,worker 里同样导入失败)。
|
||||
|
||||
```python
|
||||
# <临时目录>/nojhora/sitecustomize.py
|
||||
import sys
|
||||
class _Block:
|
||||
def find_spec(self, name, path=None, target=None):
|
||||
if name.split('.')[0] in {'jhora'}:
|
||||
raise ModuleNotFoundError(f"No module named '{name}'")
|
||||
return None
|
||||
sys.meta_path.insert(0, _Block())
|
||||
```
|
||||
|
||||
| 检查 | Grok 自跑 | Claude 验收 |
|
||||
| --- | --- | --- |
|
||||
| T0 差异清单(3 盘中英 Markdown + 资料包 + 全量 pytest,装 / 不装) | 必跑 | 复跑一次(Linux) |
|
||||
| 改到与新增的 Python 测试文件(至少 `test_api_chart_subprocess_gate`、`test_runtime_jhora_import_scan`、`test_report_reader_main`、`test_report_english_edition`、`test_report_full_data_fix`、`test_full_data_domain_reader`、`test_python_hygiene_budget`、`test_api_server_growth_contract`、`test_special_lagnas_birth_instant`、`test_year_lord_*`、`test_tajika_*`、`test_pyjhora_adapter_diagnostics`、本单新增测试)及 #89 的 200 盘扫描,**装、不装各一次** | 必跑 | 复验 |
|
||||
| 本地构建新旧 API 镜像:大小、`pip show pyswisseph`、`pip check`、容器内生成一份完整数据版并量 `docker stats` | 必跑 | 无 Docker,环境缺口,以 Grok 数据为准;部署后看门禁镜像发布日志 |
|
||||
| Python 快速门 `run_quality_gate.py --profile quick` | 不跑 | 以 Claude 为准(装、不装各一次) |
|
||||
| **Python 全量 pytest 与开工基线逐条比对,装 / 不装两套**(红线 1) | 不跑 | 以 Claude 为准 |
|
||||
| 3 盘中英完整数据版切掉本章后逐字节比对(分支装 vs 分支不装 vs 基线不装) | 必跑 | 以 Claude 为准 |
|
||||
| 封存回放与冻结身份(`test_sealed_holdout_contract_freshness`、`test_rectification_validation_integrity_gate`、`implementation_identity()` 输出),装 / 不装 | 必跑 | 复验 |
|
||||
| 生成时长(中、英各 5 次中位) | Windows 数照报 | 以 Claude 的 Linux 数判是否碰 180 s |
|
||||
| 前端 | 本单不改前端,不要求 | — |
|
||||
| `test:db` | 不动数据库,不要求 | — |
|
||||
| 部署后 | — | `/api/health` 的 `deployment.gitCommit` 等于合入 SHA;`swisseph_available = true`;真机清单留给产品 |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
不让步:红线全部(尤其 1~4)、T0、T1、T5、D4 的逐表失败、英文汉字 0。时间不够时依次让出:T3 的 `annual_chart` 复用优化(照上游逐次调用)→ D8 的交点设置(照上游不设,表头写 PyJHora 默认)→ T6 的 `docker stats` 内存峰值(只报子进程 RSS)。让出的写进 PROGRESS。
|
||||
|
||||
## 8. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git log origin/staging --oneline | grep -iE "#91|full-data-parity-1" | head # 确认 #91 已合入,否则不开工
|
||||
git log origin/staging --oneline | grep -iE "#92|full-data-parity-2" | head # #92 若已合入或在途,记进 PROGRESS
|
||||
git worktree add -b codex/full-data-parity-3-20261022 .worktrees/full-data-parity-3-20261022 origin/staging
|
||||
cd .worktrees/full-data-parity-3-20261022
|
||||
# 上游参照放仓库外,只读(#91 已 clone 的可复用)
|
||||
git clone --filter=blob:none https://github.com/732642856/yinduzhanxing.git ../yinduzhanxing-ref
|
||||
git -C ../yinduzhanxing-ref fetch origin codex/customer-export-frozen-20261016
|
||||
git -C ../yinduzhanxing-ref show 1d549fe9:customer-export/registry/delivery_timing_supplement.py | grep -n "_annual_observation_lines"
|
||||
git -C ../yinduzhanxing-ref show 1d549fe9:scripts/annual_pyjhora_replay.py | grep -n "_isolated_annual_worker\|_call_mudda\|_call_patyayini\|_probe_tajika_yogas"
|
||||
# 现有开关(1.3)
|
||||
grep -n "_jhora_importable\|def replay_pyjhora_annual\|def probe_pyjhora_year_lord" scripts/annual_pyjhora_replay.py
|
||||
grep -n "replay_pyjhora_annual\|pyjhora_yogi_replay\|pyjhora_canonical_adapter" scripts/jyotish_engine.py
|
||||
grep -n "build_pyjhora_adapter_report" scripts/unified_consultation_orchestrator.py
|
||||
grep -n "_pyjhora_d1" scripts/rectification_three_engine_packet.py
|
||||
grep -rn "JYOTISH_YEAR_LORD_PYJHORA_REFERENCE" deploy/ scripts/ | head
|
||||
grep -n '"PyJHora"' scripts/pl9_full_data_export.py scripts/pl9_reader_english.py
|
||||
grep -n "PyJHora" tests/test_report_reader_main.py
|
||||
grep -n "^pyswisseph\|PyJHora" requirements.txt requirements-reference-engines.txt
|
||||
ls scripts/*.py | wc -l # 448 = ROOT_PY_BUDGET
|
||||
grep -oE "^## BUG-[0-9]+" docs/BUG_HISTORY.md | sort -u | sort -t- -k2 -n | tail -1
|
||||
```
|
||||
|
||||
## 9. BUG 编号
|
||||
|
||||
写作时 `docs/BUG_HISTORY.md` 最大号 **BUG-1403**。该文件自 `400af57e8` 起有整段重复,必须用上面命令(`sort -u` 后取最大)核对,**开工时最大号 + 1 起**(#89~#92 可能先占号)。预计至少两条新编号:S2 对话交叉验证在 API 进程内导入 PyJHora 的隐患(装包即触发);S3 校正三引擎包在 API 进程内导入 PyJHora 的隐患。BUG-1273 只追加「决策推翻」一段,不改原文、不改状态。
|
||||
Reference in New Issue
Block a user