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
34 KiB
任务书:完整数据版补齐(三)——「独立年度方法参考」照上游调用 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):
- 上游传入的
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)。 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. 根因 / 背景
- 上游交付版「独立年度方法参考」是 PyJHora 子进程回放的产物;我方 BUG-1273 起约定 PyJHora 只作离线对照,生产镜像不装,所以这节在我方一直缺。
- 我方代码里早已散落「检测到 PyJHora 就换路」的开关(1.3 的 S1~S5),以前靠「镜像里没有」兜底。装包之后这些兜底全部失效,其中 S1~S3 会在 API 进程内导入 PyJHora 或把旧口径数据印进现有章节。
- 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):除新增这一节以外,现有所有输出不得变化。验收:同一批盘「装 / 不装」两套全量 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 张虚构盘逐字节相同。 - API 进程内不得导入
jhora:PyJHora 只在一个子进程 worker 里导入。生成报告、全盘、对话、校正之后,父进程sys.modules里不得有jhora;swisseph的岁差(get_ayanamsa_ut)与同刻月亮经度在调用本节前后不变。 - 版本固定(产品红线 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必须相同,星位一律不变。 - 只有本节显式调用 PyJHora:S1~S3 的产品路径保持不导入、不起子进程,结果与今天逐字节相同;不得新增
pyjhora_canonical_adapter.py、pyjhora_yogi_replay.py;不得把 PyJHora 数据写进annual_tajika_pack.*、normalized_tables、external_engine_comparison等现有键(现有渲染会读它们)。 - 增长冻结:不改
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/,不得调高预算。 - 不新增静默吞错:
SILENT_EXCEPT_BUDGET = 1不放宽。子进程失败、超时、JSON 不合法都记日志(含原因),表里只写「未获得结果」。 - 报告卫生:本节不得出现内部键名、
blocked、partial_verified、_unavailable、schema、source_path、Python 异常名;英文汉字 0;中文不出现英文列名(行星编号、瑜伽专名、PyJHora 名称除外)。 - 不动数据库结构与迁移;不改
.gitea/、Dockerfile;不推main。 - 冻结文件不碰:
sealed_holdout_rerun.PRODUCTION_FILES所列 11 个文件(scripts/rectification/*10 个 +scripts/functional_benefics.py)本单一行不改。 - 隐私:只用虚构盘与公开名人盘;不复制上游个案文件;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 里同样导入失败)。
# <临时目录>/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. 开工前置命令
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 只追加「决策推翻」一段,不改原文、不改状态。