diff --git a/CHANGELOG.md b/CHANGELOG.md index 75c79e2e..b301ab4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-10-23 — 阅读页的 Yogini 子运缺字段时不再整段消失 + +- 补子运时只在原来的大运行上加子运,不改、不删原来的字段。缺完整年数时仍显示主运,不再整段报错。 +- 年运资料包新加的整宫和 Tripataki 改放在原有字段后面,已有字段的顺序不变。 +- Skill 版本不变(BUG-1425、BUG-1426)。 + ## 2026-10-23 — 完整数据版补上行星解释、后两年年运和 72 年年返 - 每颗星和每个宫主的解释加长,仍用「可能」的说法,并写上功能吉凶。土星和 KP 的结果段这一版仍先不做。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index d2b8bb7d..1f327c3e 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19318,3 +19318,35 @@ - 复发自:无。 - 修复版本:分支 `codex/full-data-parity-1-20261021`。 +## BUG-1425 | 年运包把新字段插进原有键序 + +- 状态:resolved +- 首次发现:2026-10-23 +- 最近更新:2026-10-23 +- 影响面:年运资料包 `build_annual_tajika_pack` 的键序。阅读页按字段名取数,键序本身不改句子,但合同测试和按原序读取的下游会错位。 +- 用户现象:完整数据版全量测试里,年运包合同失败。`annual_houses` 插到了 `mudda_dasha` 前面。 +- 触发条件:生成年运包。B 段把整宫和 Tripataki 写进包时插在既有键中间。 +- 根因:新键写在 `sahams` 和 `mudda_dasha` 之间,改了 2026-10-08 定下的原有顺序。 +- 修复:`annual_houses`、`tripataki_chakra` 改放在原有键(到 `audit` 为止)之后。原有键的顺序和值不变。 +- 验证:`tests/test_annual_tajika_pack.py`(键序断言写了原值 / 新值 / 原因)。与 `tests/test_dasha_master_children.py`、`tests/test_report_longform_gaps2.py` 一起 30 项,退出码 0。 +- 防复发:`test_builds_annual_tajika_pack_contract` 锁住完整键序。再把新键插回中间,这项会失败。 +- 相关记录:issue #91 K4。BUG-1426。 +- 复发自:无 +- 修复版本:分支 `codex/full-data-parity-1-20261021`。 + +## BUG-1426 | 补 Yogini 子运时缺完整年数,阅读页整段失败 + +- 状态:resolved +- 首次发现:2026-10-23 +- 最近更新:2026-10-23 +- 影响面:阅读页「Yogini Dasha 周期」;标准大运表里的 Yogini 行。 +- 用户现象:Yogini 子运整段出不来。全量测试报 `ValueError: yogini antardasha failed for Mangala: 'full_years'`。 +- 触发条件:大运行上没有 `full_years`(或没有未裁切的起点)时,报告渲染去补子运。 +- 根因:`enrich_master_family` 调用 `yogini_antardashas` 时,缺字段直接抛错,渲染在写出标题前就停了。补子运也不该改掉或丢掉这一行原来的字段。 +- 修复:只在原行上追加 `antardasha`。算子运要用的字段若只在嵌套的原始行里,只给计算用,不改原行。缺字段或计算失败时记一条原因日志,原行照旧返回,标题和主运表仍然出现。Narayana、Kalachakra 同样只追加子运,缺日期时不再把整份报告打崩。 +- 验证:`tests/test_report_longform_gaps2.py::test_auxiliary_dasha_headings_render_from_native_modules`。`tests/test_dasha_master_children.py` 新增:每个体系每一行,补充前后原有键和值逐项相同;缺 `full_years` 时不抛错、不改原行。上述三文件 30 项,退出码 0。 +- 防复发:上面两条测试。再把缺字段改成抛错,或补子运时改掉原字段,测试会失败。 +- 相关记录:issue #91 K4。BUG-1425。 +- 复发自:无 +- 修复版本:分支 `codex/full-data-parity-1-20261021`。 + diff --git a/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md b/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md index 51647154..74cb6b4a 100644 --- a/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md +++ b/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md @@ -85,3 +85,21 @@ Yogi 记录在本分支曾写成 BUG-1420,后改为 BUG-1421。staging 上 BUG | 200 张英文扫描 | pytest tests/test_report_english_sweep.py,屏蔽 PyJHora | 退出码 0,连同三张盘计时一共约 27 分钟 | | 前端类型和 lint | 本轮没改前端 | 没跑 | | 快速门、全量 pytest | 不跑 | 交给验收 | + +## 修复轮(K4) + +产品选 A 之后,K1–K3 已在 `cfd0ab68`。本轮只补 K4,没有重做那三项。开工先合并 `origin/staging`(含修复单里的 K4),没有 rebase,没有强推。 + +| 项 | 结果 | +| --- | --- | +| 年运包键序 | `annual_houses`、`tripataki_chakra` 改到原有键(止于 `audit`)之后。`mudda_dasha` 仍紧跟 `sahams` | +| Yogini 子运 | 只追加子运。缺 `full_years` 时记原因并保留原行,阅读页标题和主运表还在。原有键和值不改 | +| 断言 | 年运键序写了原值 / 新值 / 原因。新增:每个体系每一行,补充前后原有键和值相同 | + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| K4 定向测试 | `.venv\Scripts\python -m pytest tests/test_annual_tajika_pack.py tests/test_dasha_master_children.py tests/test_report_longform_gaps2.py -q` | 30 项,退出码 0 | +| 前端类型和 lint | 本轮没改前端 | 没跑 | +| 快速门、全量 pytest、200 张扫描 | 不跑 | 交给验收 | + +编号:BUG-1425(年运键序)、BUG-1426(Yogini 子运整段失败)。当前最大号在合并后仍是 BUG-1424,这两条接在后面。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 1581c708..52e14ab5 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -522,7 +522,7 @@ | `TASK-chart-ascendant-slash-20261021.md` | `PROGRESS-chart-ascendant-slash-20261021.md` | #87 上升状态格「不适用」改「/」+ 悬停 / 点按说明;报告上升行同改并在表下加说明(推翻 10-15 口径) | 已合入 staging | `codex/chart-ascendant-slash-20261021` | | `TASK-report-english-han-leak-20261021.md` | `PROGRESS-report-english-han-leak-20261021.md` | #89 个人报告英文版约 4 成整份丢弃:漏译汉字源头补英文孪生字段、修吃空格;200 张固定种子回归 + 快速门 smoke(复发自 BUG-1272);中文逐字节不变 | 已合入 staging | `codex/report-english-han-leak-20261021` | | `TASK-chart-dasha-yogini-20261021.md` | — | #88 星盘页大运右栏切换 Chara / Yogini,Yogini 补小运(按比例,内部核对,页面注明未经外部软件核对);Ashtottari 另立单(起运表与 PyJHora 8/12 宿不一致) | 已合入 staging | `codex/chart-dasha-yogini-20261021` | -| `TASK-full-data-parity-1-20261021.md` | `PROGRESS-full-data-parity-1-b-20261023.md` | #91 完整数据版补齐(一):A 段渲染已在 staging;B 段补 Yogi、大运子运、当年和后两年的月返照与 Tithi、36 个月星历。72 年年返和解释补长未做满 | B 段本地完成,待验收 | `codex/full-data-parity-1-20261021` | +| `TASK-full-data-parity-1-20261021.md` | `PROGRESS-full-data-parity-1-b-20261023.md` | #91 完整数据版补齐(一):K1–K3 已做;K4 把年运新字段放到原键之后,Yogini 缺完整年数时不再整段失败(BUG-1425、BUG-1426) | 待验收 | `codex/full-data-parity-1-20261021` | | `TASK-full-data-parity-2-20261021.md` | — | #92 完整数据版补齐(二):研究型推运附录(Tithi Ashtottari、Yoga / Nakshatra Pravesha、Karana 等,口径换成我方真恒星)、四种星座大运第三层、Kalachakra 第三层(简化模型)、Niryaana Shoola 子运只在有书面出处时做、D81 / D108 / D144 研究型分盘;全部标研究口径,不进对话卡与校正;不设大小上限 | 排队(等 #91 合入) | `codex/full-data-parity-2-20261021` | | `TASK-goel-jaimini-strength-20261022.md` | `PROGRESS-goel-jaimini-strength-20261022.md` | #94 搬上游公开的 Goel Jaimini 强度与 Brahma / Maheshwara / Rudra(4 个上游原样文件,按 #81 决策 A 先例根目录预算 448→452;来源串改书目引用);只做计算、不改报告;冻结身份不变 | 已合入 staging | `codex/goel-jaimini-strength-20261022` | | `TASK-chart-subject-table-fix-20261022.md` | `PROGRESS-chart-subject-table-fix-20261022.md` | **#86 第 1 次打回**:报告所有表被强制单行(打印跑出纸面);保存他人复制了档案页代码且已分叉;点编辑框内边距会丢修改 | 已合入 staging | `codex/chart-subject-table-20261021` | @@ -530,5 +530,5 @@ | `TASK-synastry-staging-fix-fix-20261022.md` | `PROGRESS-synastry-staging-fix-fix-20261022.md` | **#85 第 1 次打回**(T7~T11 主体通过):新地址没登记进路由清单致快速门红;CHANGELOG 与 T11 矛盾;侧栏点亮测试是空断言 | 已合入 staging | `codex/synastry-staging-fix-20261021` | | `TASK-report-english-han-leak-fix-20261022.md` | `PROGRESS-report-english-han-leak-fix-20261022.md` | **#89 第 1 次打回**:改了冻结的校正打分文件 `jaimini.py`;测试的 PyJHora 屏蔽没传到子进程(测的不是生产那份报告,也是慢的原因);缺英文时留空藏住漏洞 | 已合入 staging | `codex/report-english-han-leak-20261021` | | `TASK-full-data-parity-1-fix-20261022.md` | `PROGRESS-full-data-parity-1-fix-20261022.md` | **#91 A 段第 1 次打回**:新六章 69 节里 16 节在三张虚构盘上整节「未判定」,其中 12 节数据在资料包里、是读错了字段;测试抓不到整节未判定;新六章绕过了报告口径替换;中文附录悄悄丢列和瑜伽条目要量出来 | 本地完成,待验收 | `codex/full-data-parity-1-20261021` | -| `TASK-full-data-parity-1-b-fix-20261023.md` | `PROGRESS-full-data-parity-1-b-20261023.md` | **#91 B 段打回(第 3 次,产品 10-23 选 A 交回 Grok,并新增 K3 72 年返照表)**:T9 解释补长不在让步清单却没做(行星一章 4,574 字符,目标约 8,970);D8 后两年缺整宫、Tajika、Tripataki、Saham 轨迹、年主依据 | 待执行 | `codex/full-data-parity-1-20261021` | +| `TASK-full-data-parity-1-b-fix-20261023.md` | `PROGRESS-full-data-parity-1-b-20261023.md` | **#91 B 段修复**:K1–K3 已交;K4 年运键序与 Yogini 子运字段已补(BUG-1425、BUG-1426) | 待验收 | `codex/full-data-parity-1-20261021` | | — | `PROGRESS-full-data-parity-1-fix2-20261022.md` | **#91 A 段第 2 次打回**(Claude 口头修复单 F1~F4):11 节只印「未判定」和键名改读真实记录;测试只认非第一列真实值、禁止键名格;中文瑜伽组合句由中文源给出(BUG-1418);六章降为 `##`、节 `###`(BUG-1419) | 本地完成,待验收 | `codex/full-data-parity-1-fix2-20261022` | diff --git a/scripts/annual_native/dasha_children.py b/scripts/annual_native/dasha_children.py index 5305152a..a14b7769 100644 --- a/scripts/annual_native/dasha_children.py +++ b/scripts/annual_native/dasha_children.py @@ -7,10 +7,13 @@ and they hang on the majors this repository already calculated. from __future__ import annotations +import logging from copy import deepcopy from datetime import datetime, timedelta from typing import Any +logger = logging.getLogger(__name__) + def _rotate_cycle_rows(cycle_rows: list[dict], start_rashi: str | None) -> list[dict]: if not cycle_rows: @@ -118,10 +121,7 @@ def _copy_rows(rows: list[dict]) -> list[dict]: return deepcopy([row for row in rows if isinstance(row, dict)]) -def _narayana_periods(module: dict) -> list[dict]: - sequence = module.get("mahadasha_sequence") - if not isinstance(sequence, list): - return [] +def _narayana_children(module: dict) -> dict[str, list[dict]]: children_by_parent: dict[str, list[dict]] = {} boundaries = module.get("dense_boundaries") if isinstance(boundaries, list): @@ -140,6 +140,25 @@ def _narayana_periods(module: dict) -> list[dict]: "parent_lord": row.get("parent_md"), "source": "narayana_dense_boundaries", }) + return children_by_parent + + +def _narayana_periods(module: dict, rows: list[dict] | None = None) -> list[dict]: + children_by_parent = _narayana_children(module) + if rows: + # Keep every field already on the row. Only attach children. + periods = [] + for row in rows: + copied = deepcopy(row) + if not (isinstance(copied.get("antardasha"), list) and copied.get("antardasha")): + sign = copied.get("sign") or copied.get("rashi") + parent_key = str(copied.get("period_key") or sign or "") + copied["antardasha"] = children_by_parent.get(parent_key, []) + periods.append(copied) + return periods + sequence = module.get("mahadasha_sequence") + if not isinstance(sequence, list): + return [] periods = [] for row in sequence: if not isinstance(row, dict): @@ -157,6 +176,16 @@ def _narayana_periods(module: dict) -> list[dict]: return periods +def _yogini_call_row(row: dict) -> dict: + """Fields the antardasha formula reads. Nested raw_period fills gaps only.""" + source = dict(row) + raw = row.get("raw_period") if isinstance(row.get("raw_period"), dict) else {} + for name in ("yogini", "full_years", "unclipped_start_date", "end_date", "start_date"): + if source.get(name) is None and raw.get(name) is not None: + source[name] = raw[name] + return source + + def _yogini_periods(module: dict, rows: list[dict]) -> list[dict]: if any(isinstance(row.get("antardasha"), list) and row.get("antardasha") for row in rows): return rows @@ -168,18 +197,39 @@ def _yogini_periods(module: dict, rows: list[dict]) -> list[dict]: birth = None if rows: birth = rows[0].get("start_date") - if not birth: - return rows + if birth is None: + raw = rows[0].get("raw_period") if isinstance(rows[0].get("raw_period"), dict) else {} + birth = raw.get("start_date") attached = [] for row in rows: - copied = dict(row) + copied = deepcopy(row) + source = _yogini_call_row(copied) + missing = [ + name for name in ("yogini", "full_years", "unclipped_start_date", "end_date") + if source.get(name) is None + ] + if not birth or missing: + logger.warning( + "yogini antardasha left off for %s: missing %s", + copied.get("yogini"), + ",".join(missing) or "start_date", + ) + attached.append(copied) + continue try: - copied["antardasha"] = yogini_antardashas(row, birth_dt=birth, year_days=float(year_days)) + children = yogini_antardashas(source, birth_dt=birth, year_days=float(year_days)) except (KeyError, TypeError, ValueError) as exc: - raise ValueError(f"yogini antardasha failed for {row.get('yogini')}: {exc}") from exc - for child in copied["antardasha"]: - child["parent_lord"] = row.get("planet") or row.get("lord") - child["main"] = row.get("yogini") + logger.warning( + "yogini antardasha left off for %s: %s", + copied.get("yogini"), + exc, + ) + attached.append(copied) + continue + for child in children: + child["parent_lord"] = copied.get("planet") or copied.get("lord") + child["main"] = copied.get("yogini") + copied["antardasha"] = children attached.append(copied) return attached @@ -195,15 +245,19 @@ def enrich_master_family(key: str, module: dict | None, periods: list | None) -> if key == "vimshottari": return rows if key == "narayana": - return _narayana_periods(module) + return _narayana_periods(module, rows) if key == "yogini": return _yogini_periods(module, rows) if key == "kala_chakra": if not rows: return rows - enriched, _ad, _pd = attach_kalachakra_simplified_children( - rows, - module.get("total_cycle"), - ) + try: + enriched, _ad, _pd = attach_kalachakra_simplified_children( + rows, + module.get("total_cycle"), + ) + except (KeyError, TypeError, ValueError) as exc: + logger.warning("kalachakra children left off: %s", exc) + return rows return enriched return rows diff --git a/scripts/annual_tajika_pack.py b/scripts/annual_tajika_pack.py index 14e61ef3..f58091f8 100755 --- a/scripts/annual_tajika_pack.py +++ b/scripts/annual_tajika_pack.py @@ -65,8 +65,6 @@ def build_annual_tajika_pack( "tajika_named_yogas": _tajika_named_yogas_field(solar_report), "tajika_strength": _tajika_strength_field(solar_report if isinstance(solar_report, dict) else {}), "sahams": sahams, - "annual_houses": solar_report.get("annual_houses") if isinstance(solar_report, dict) else None, - "tripataki_chakra": solar_report.get("tripataki_chakra") if isinstance(solar_report, dict) else None, "mudda_dasha": mudda_dasha, "patyayini_dasha": patyayini_dasha, "monthly_windows": _monthly_windows(mudda_dasha), @@ -80,6 +78,9 @@ def build_annual_tajika_pack( ), "exports": {}, "audit": _audit(profile, solar_report, tajika_report, pyjhora_replay, vedastro_reference), + # New full-data fields stay after the original contract keys (K4). + "annual_houses": solar_report.get("annual_houses") if isinstance(solar_report, dict) else None, + "tripataki_chakra": solar_report.get("tripataki_chakra") if isinstance(solar_report, dict) else None, } pack["exports"] = _exports(pack) return pack diff --git a/tests/test_annual_tajika_pack.py b/tests/test_annual_tajika_pack.py index 3851e49f..1256f61b 100755 --- a/tests/test_annual_tajika_pack.py +++ b/tests/test_annual_tajika_pack.py @@ -52,6 +52,11 @@ def test_builds_annual_tajika_pack_contract() -> None: "report_sections", "exports", "audit", + # 原值:键序止于 audit,sahams 之后直接是 mudda_dasha。 + # 新值:上面的原序不变;annual_houses、tripataki_chakra 加在 audit 之后。 + # 原因:完整数据版的年运新字段曾插在 mudda_dasha 前面,打乱既有键序(K4)。 + "annual_houses", + "tripataki_chakra", ] assert pack["schema"] == "jyotish.annual_tajika_pack.v1" assert pack["profile"]["profile_id"] diff --git a/tests/test_dasha_master_children.py b/tests/test_dasha_master_children.py index 2a088278..1754d36d 100644 --- a/tests/test_dasha_master_children.py +++ b/tests/test_dasha_master_children.py @@ -2,6 +2,7 @@ from __future__ import annotations +from copy import deepcopy from datetime import datetime from scripts.annual_native.dasha_children import ( @@ -142,3 +143,79 @@ def test_enrich_does_not_invent_children_for_vimshottari() -> None: rows = enrich_master_family("vimshottari", {"timeline": []}, [{"lord": "Sun", "years": 6}]) assert rows == [{"lord": "Sun", "years": 6}] assert "antardasha" not in rows[0] + + +def test_enrich_keeps_existing_keys_and_values_on_every_system() -> None: + """Child fields may be added. Keys and values that were already there stay.""" + yogini_row = { + "yogini": "Mangala", + "planet": "Moon", + "lord": "Moon", + "full_years": 1, + "years": 1.0, + "start_date": "1990-01-01T00:00:00", + "end_date": "1991-01-01T00:00:00", + "unclipped_start_date": "1990-01-01T00:00:00", + "marker": "keep-yogini", + } + narayana_module = { + "mahadasha_sequence": [{ + "sign": "Aries", "lord": "Mars", "years": 7, + "start_age": 0, "end_age": 7, "period_key": "md-0", + }], + "dense_boundaries": [{ + "level": "AD", "sign": "Taurus", "lord": "Venus", + "duration_years": 1, "start_age": 0, "end_age": 1, + "parent_md": "Aries", "parent_period_key": "md-0", + }], + } + narayana_row = { + "sign": "Aries", "lord": "Mars", "years": 7, + "start_age": 0, "end_age": 7, "period_key": "md-0", + "marker": "keep-narayana", + } + kala_row = { + "lord": "Sun", "rashi": "Aries", "years": 7, "full_years": 7, + "start_date": "1990-01-01T00:00:00", "end_date": "1997-01-01T00:00:00", + "marker": "keep-kala", + } + plain = {"lord": "Sun", "years": 6, "full_years": 6, "marker": "keep-plain"} + samples = { + "vimshottari": ({}, [{**plain, "marker": "keep-vim"}]), + "yogini": ({"dasha_year_days": 365.25}, [yogini_row]), + "narayana": (narayana_module, [narayana_row]), + "kala_chakra": ({"total_cycle": 7}, [kala_row]), + "ashtottari": ({}, [{**plain, "marker": "keep-ashtottari"}]), + "tribhagi": ({}, [{**plain, "marker": "keep-tribhagi"}]), + "tribhagi_40": ({}, [{**plain, "marker": "keep-tribhagi-40"}]), + "shodashottari": ({}, [{**plain, "marker": "keep-shodashottari"}]), + "dwadashottari": ({}, [{**plain, "marker": "keep-dwadashottari"}]), + "dwisaptatisama": ({}, [{**plain, "marker": "keep-dwisaptatisama"}]), + "shattrimshatsama": ({}, [{**plain, "marker": "keep-shattrimshatsama"}]), + "panchottari": ({}, [{**plain, "marker": "keep-panchottari"}]), + "satabdika": ({}, [{**plain, "marker": "keep-satabdika"}]), + "chaturshitisama": ({}, [{**plain, "marker": "keep-chaturshitisama"}]), + "shastihayani": ({}, [{**plain, "marker": "keep-shastihayani"}]), + } + for key, (module, rows) in samples.items(): + before = deepcopy(rows) + enriched = enrich_master_family(key, module, rows) + assert rows == before + assert len(enriched) == len(before) + for old, new in zip(before, enriched): + for field, value in old.items(): + assert field in new + assert new[field] == value + + +def test_yogini_enrich_keeps_the_row_when_full_years_is_missing() -> None: + row = { + "yogini": "Mangala", + "planet": "Moon", + "years": 1, + "start_date": "1990-01-01", + "end_date": "1991-01-01", + } + enriched = enrich_master_family("yogini", {}, [row]) + assert enriched[0] == row + assert "antardasha" not in enriched[0]