fix(dasha): astrologer second ruling — Drig uses Narayana years, Ashtottari decided by the Rahu gate, Sthira variants kept apart (BUG-1676, BUG-1677, BUG-1678)
Independent Staging Quality Gate / validate-frontend (push) Successful in 5m9s
Independent Staging Quality Gate / validate-python (push) Successful in 5m49s
Independent Staging Quality Gate / publish (push) Successful in 2m50s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
This commit is contained in:
2026-11-07 14:05:06 +08:00
co-authored by Claude Opus 5.5
parent 16457906f9
commit 9cfc823e2e
18 changed files with 227 additions and 155 deletions
+7
View File
@@ -1,5 +1,12 @@
# 印度占星 Skill 更新日志
## 2026-11-07 — 占星师第二次裁定:Drig 用 Narayana 年数、Ashtottari 只看罗睺条件、Sthira 两种方法分开保存
- Drig 每个星座的年数改为与本站 Narayana 同一个函数(双主星择强、入旺落陷调整都算),依据 PVR 21.2(5);Sri Aurobindo 的 Scorpio Drig 1906–1913 作为回归案例(BUG-1678)。
- Ashtottari 是否适用只看 BPHS 罗睺条件(不在上升,且在上升主星起算第 4、5、7、9、10 位,与上升主同宫不算);昼夜与月相条件照算但只作记录(BUG-1676)。
- Sthira 主表仍用 Goel 完整方法;K.N. Rao 短法单独保存整条时间线作对照,不拼接(BUG-1677)。
- Shadbala 不改:太阳 Cheshta = Ayana、月亮 Cheshta = Paksha(BPHS 27.18),各计一次、不乘 2;计算说明补一句来源。
## 2026-11-07 — 占星师裁定:Ashtottari 严格适用口径、Sthira 用 Goel 完整方法、Drig 年数标待核
- Ashtottari 是否适用改为 BPHS 第 26 章两条同时满足:罗睺不在上升、且在上升主星起算第 4、5、7、9、10 位;白半月夜生或黑半月日生。月亮星宿只定起运星和余额。不满足时报告写「不启用、只作对照」并保留对照表;满足时写「适用、仍对参数敏感」。完整解读、星宿大运模块与大运接口用同一口径(BUG-1676)。
+10 -9
View File
@@ -22977,12 +22977,13 @@
## BUG-1676 | Ashtottari 适用性用了未闭合来源的「月亮星宿 + 月相」白名单;报告并列两条规则、说「本仓尚未裁定」
- 状态:fixed-pending-verify(代码 + 回归测试;staging 待产品复核导出文件)
- 首次发现 / 最近更新:2026-11-07(同一档案对比上游 skill 导出后提问占星师;占星师当日裁定)
- 首次发现 / 最近更新:2026-11-07(同一档案对比上游 skill 导出后提问占星师;占星师当日两次裁定,第二次修订第一次)
- 影响面:`scripts/ashtottari_rule_profiles.py`(新增严格口径)、`scripts/ashtottari_dasha.py`、`scripts/jyotish_engine.py`(Ashtottari 调用、条件大运对照副本、星宿大运模块同步、大运总表带 `applicability`)、`scripts/pl9_reader_export.py`(标准 Dasha 表格页 Ashtottari 说明)、`scripts/report_render/parity_chapters.py`(星宿大运上下文的适用条件表)。
- 现象:引擎按「月亮星宿 + 月相」白名单判断 Ashtottari 是否适用;星宿大运模块另用「罗睺不在 1/4/7/10 宫」;报告两条并列,结论不同时写「本仓尚未裁定以哪条为准」。
- 根因:白名单的来源链没有闭合,却当作生产门槛;BPHS 第 26 章两条条件(第 17 偈罗睺相对上升主星、第 23 偈昼夜与月相)只作为候选,且候选版漏了「罗睺不在上升」。
- 修复:按占星师 2026-11-07 裁定,生产口径 `strict_rahu_from_lagna_lord_and_paksha_daynight_v1` = 两道条件同时满足:① 罗睺不在上升星座,且在上升主星起算第 4、5、7、9、10 位(第 1 位不计);② 白半月夜生或黑半月日生。任一条不满足 → `not_applicable`,报告保留对照表并写占星师给的「不启用、只作对照」原句;缺昼夜等输入且另一条未失败 → `blocked`;满足 → 写「适用、仍对参数敏感」原句。月亮星宿只定起运星与余额。星宿大运模块的 `applicable` 与引擎结果同步,不适用时不再给当前 Ashtottari。适用条件表改为两道条件各自是否满足和本盘情况。
- 验证:`tests/test_astrologer_rulings_20261107.py`(两道条件组合、罗睺在上升、与上升主星同宫、缺昼夜输入;计算器只在适用时出主表、不适用时出对照副本;虚构盘中英两版原句与条件表)。
- 修复:生产口径 `bphs_rahu_from_lagna_lord_excluding_lagna_v1`。第一次裁定是两条同时满足;第二次裁定(同日,以此为准):只有 BPHS 的几何条件决定是否适用——罗睺不在上升星座,且在上升主星起算第 4、5、7、9、10 位;罗睺与上升主同宫(第 1 位)不算。白半月夜生 / 黑半月日生属于后世或其他传承的附加条件,照算并在报告里写「只作记录,不参与判定」。不适用 → `not_applicable`,报告保留对照表并写「不启用、只作对照」原句;缺罗睺或上升主星位置 → `blocked`;适用 → 写「适用、仍对参数敏感」原句。月亮星宿只定起运星与余额。完整解读、星宿大运模块与 `/api/dasha` 用同一口径。
- 来源调研(2026-11-07):BPHS 原句 *lagnesat kendrakonasthe rahau lagnam vina sthite*(罗睺在上升主星的角宫 / 三方宫,不在上升);昼夜与月相规则在论坛和 Rudrayamala 引文中配对不一致,未见带真实盘的验证反馈。
- 验证:`tests/test_astrologer_rulings_20261107.py`(罗睺条件决定、昼夜月相只记录、罗睺在上升、与上升主星同宫、缺输入;计算器只在适用时出主表、不适用时出对照副本;虚构盘中英两版原句与条件表);`/api/dasha` 与完整解读 Ashtottari 行一致(`tests/test_dasha_api_engine_parity.py`)。。
- 防复发:适用性规则必须写明出处偈号与 profile 名;来源链未闭合的规则只能做对照,不能做生产门槛。
- 关联记录:BUG-1677、BUG-1678(同一轮裁定)
- 修复版本:分支 `codex/report-list-ui-20261106`
@@ -22994,22 +22995,22 @@
- 影响面:`scripts/rashi_dashas_native.py`(`select_brahma_full`、`_sthira`)、`scripts/rashi_dasha_report.py`(Sthira 说明行)。
- 现象:Sthira 从短法选出的 Brahma 所在星座起算;完整方法(`scripts/goel_jaimini_strength.py`,已在仓内)没有接入。
- 根因:Sthira 最早只实现了短法;完整强度后来单独发布,没有接到 Sthira。
- 修复:按占星师裁定(Goel《Predicting through Jaimini Astrology》第 5 章,PDF 页 106–107),默认 profile `goel_full_brahma_ch1_ch5_v1`:Brahma 用第 1 章完整强度,Sthira 从 Brahma 所在星座起、恒顺行、动 / 固 / 双星座 7 / 8 / 9 年;短法结果作为 `brahma_comparison` 在报告里写一句对照。完整法选不出唯一 Brahma 时才退回短法,并标 `_fallback`。
- 修复:按占星师裁定(Goel《Predicting through Jaimini Astrology》第 5 章,PDF 页 106–107),默认 profile `goel_full_brahma_ch1_ch5_v1`:Brahma 用第 1 章完整强度,Sthira 从 Brahma 所在星座起、恒顺行、动 / 固 / 双星座 7 / 8 / 9 年;短法作为显式 alternative(`sthira_variant = kn_rao_short`,含自己的整条时间线)存进 `brahma_comparison`,报告写一句对照,不与主表拼接;主表 `sthira_variant = goel_full`。占星师第二次裁定(同日)维持 Goel 完整方法为默认:K.N. Rao 一派的短法有长期使用反馈(Jyotisha Bharati 引 K.N. Rao 为 Akhila Kumar 书所写序),但不据此覆盖 Goel。完整法选不出唯一 Brahma 时才退回短法,并标 `_fallback`。
- 验证:`tests/test_astrologer_rulings_20261107.py::test_sthira_uses_the_full_method_and_shows_the_short_one`(Goel 印刷页 15 的例题位置:短法得火星 / 天蝎,与书一致)。
- 待核(已写进报告说明):同一例题按第 1 章强度表,太阳 240 高于金星 172.5,完整法得太阳 / 白羊;书中第 5 章正文却写金星更强、从双鱼起。OCR 丢了对比表的第二列,原因无法从文本确定。没有为对上书例改规则,已作为问题交占星师。
- 防复发:同一技法有两种口径时,报告写明主口径与对照口径,不悄悄只取一种。
- 关联记录:BUG-1676、BUG-1678
- 修复版本:分支 `codex/report-list-ui-20261106`
## BUG-1678 | Drig 大运年数借用 Narayana 的奇偶足年数公式,报告没有说明
## BUG-1678 | Drig 大运年数只借了 Narayana 的计数,没算入旺落陷与双主择强;报告没有说明
- 状态:fixed-pending-verify(只改标注;年数规则待占星师给出出处)
- 状态:fixed-pending-verify(代码 + 回归测试;staging 待产品复核导出文件)
- 首次发现 / 最近更新:2026-11-07(同一档案对比:本站、上游、PyJHora 三方年数都不同;占星师当日裁定)
- 影响面:`scripts/rashi_dashas_native.py`(`_drig` 加 `year_rule_status`、`confidence_status`)、`scripts/rashi_dasha_report.py`(Drig 说明行)。
- 影响面:`scripts/narayana_rath.py`(抽出 `rath_sign_period`,Narayana 行为不变)、`scripts/rashi_dashas_native.py`(`_drig`)、`scripts/rashi_dasha_report.py`(Drig 说明行)。Drig 不进多系统收敛、应期判断或生时纠正评分(已核对 `scripts/active_rectification_*`、`scripts/rectification/`)。
- 现象:Drig 每段年数用 `calculate_narayana_period_years(profile="jaimini_odd_footed_v1")`,报告只写方法名,看不出年数是借来的。
- 根因:实现时没有 Drig 自己的年数出处,直接复用 Narayana 的奇偶足计数。
- 修复:星座顺序保留;年数标 `pending_source_adjudication`、`parameter_sensitive`,报告写明「按奇偶足数到星座主星,没有计入 Narayana 的入旺、落陷等调整;年数规则待核;本表只作对照,不用于应期判断」。Drig 本来就不进多系统收敛与应期判断,这次核对了一遍。来源冲突:占星师 2026-11-07 裁定 Drig 不得复用 Narayana 年数;PVR《Vedic Astrology: An Integrated Approach》第 21.2 节第 (5) 条写明 Drig 年数「与 Narayana 相同」,并有完整例题(Chart 36)和公开案例(Sri Aurobindo,Scorpio 1906–1913)。本站对 Aurobindo 算出 Scorpio 1905–1913(8 年),差的一年正是火星落陷的 Narayana 调整。已交占星师裁定,裁定前不改年数。
- 验证:`tests/test_astrologer_rulings_20261107.py::test_drig_years_are_labelled_pending_and_sthira_note_is_shown`。
- 修复:占星师第二次裁定(2026-11-07,采用 PVR《Vedic Astrology: An Integrated Approach》第 21.2 节第 (5) 条):Drig 年数与本站 Narayana(Sanjay Rath 版)共用同一个星座年数函数 `rath_sign_period`——双主星择强、入旺落陷调整、罗睺计都规则、方向都一样。旧的计数年数留在每行 `legacy_years`,只作历史对照,不进主报告。双主择强分不出结果时整节不上表并写原因。
- 验证:`tests/test_astrologer_rulings_20261107.py::test_drig_uses_narayana_years_and_matches_pvr_aurobindo`(公开人物 Sri Aurobindo,PVR 第 21 章例 83:Scorpio Drig 1906–1913,7 年;本站算出同一结果,旧计数为 8 年,差的是火星落陷调整;整条星座顺序也一并断言)。
- 防复发:借用另一技法的公式时必须在结果里写明,并标参数敏感。
- 关联记录:BUG-1676、BUG-1677
- 修复版本:分支 `codex/report-list-ui-20261106`
+1 -1
View File
@@ -457,7 +457,7 @@ def calculate_ashtottari_dasha(birth_info: dict) -> dict:
strict = None
if rule_family == RULE_FAMILY_STRICT:
# BUG-1676: both BPHS 26 gates; the Moon's nakshatra only sets the start.
# BUG-1676: the BPHS Rahu gate decides; the Moon's nakshatra only sets the start.
strict = evaluate_strict_ashtottari(birth_info)
applicable = strict["status"] == "applicable"
applicability_reason = strict_reason_en(strict)
+22 -26
View File
@@ -90,26 +90,29 @@ def evaluate_rule_family(rule_family: str, payload: dict[str, Any]) -> tuple[boo
return False, f"Unknown Ashtottari rule family: {rule_family}"
# Astrologer ruling 2026-11-07 (BUG-1676): the production gate is the
# conservative intersection of two BPHS ch. 26 conditions, not a choice between
# them. Gate 1 (v.17): Rahu is not in the Lagna sign and sits 4/5/7/9/10 from
# the Lagna lord (the 1st is excluded). Gate 2 (v.23): Shukla Paksha with a
# night birth, or Krishna Paksha with a day birth. The Moon-nakshatra whitelist
# is not a production gate (its source chain is not closed); the Moon's
# nakshatra still sets the starting lord and the balance.
RULE_FAMILY_STRICT = "strict_rahu_from_lagna_lord_and_paksha_daynight_v1"
STRICT_RULE_SOURCE = "BPHS ch. 26, Ashtottari conditions v.17 and v.23"
# Astrologer rulings 2026-11-07 (BUG-1676). Second reply: the production gate
# is BPHS's geometric condition only: Rahu is not in the natal Lagna and sits
# 4/5/7/9/10 from the Lagna lord (Rahu with the Lagna lord, the 1st, does not
# count). The Shukla-night / Krishna-day rule belongs to later or other
# lineages; it is computed and shown as a recorded variant and never decides.
# The Moon-nakshatra whitelist is not a gate either; the Moon's nakshatra still
# sets the starting lord and the balance.
RULE_FAMILY_STRICT = "bphs_rahu_from_lagna_lord_excluding_lagna_v1"
STRICT_RULE_SOURCE = "BPHS, Ashtottari condition: Rahu not in the Lagna, in a kendra / trikona from the Lagna lord"
def evaluate_strict_ashtottari(payload: dict[str, Any]) -> dict[str, Any]:
"""Both gates, each reported. status: applicable / not_applicable / blocked."""
"""The Rahu gate decides; the Paksha / day-night gate is recorded only.
status: applicable / not_applicable / blocked (Rahu inputs missing).
"""
rahu = payload.get("rahu_sign_index")
lagna = payload.get("ascendant_sign_index", payload.get("lagna_rashi_index"))
lagna_lord = payload.get("lagna_lord_sign_index")
daytime = payload.get("birth_is_daytime")
shukla = payload.get("is_shukla_paksha")
rahu_gate: dict[str, Any] = {"rule": "rahu_from_lagna_lord", "source": "BPHS 26 v.17"}
rahu_gate: dict[str, Any] = {"rule": "rahu_from_lagna_lord", "role": "decides", "source": "BPHS"}
if rahu is None or lagna is None or lagna_lord is None:
rahu_gate.update(status="missing", missing=[
name for name, value in (("rahu_sign_index", rahu), ("ascendant_sign_index", lagna),
@@ -123,7 +126,7 @@ def evaluate_strict_ashtottari(payload: dict[str, Any]) -> dict[str, Any]:
status="pass" if not in_lagna and position in ALLOWED_RAHU_HOUSES_FROM_LAGNA_LORD else "fail",
)
paksha_gate: dict[str, Any] = {"rule": "paksha_day_night", "source": "BPHS 26 v.23"}
paksha_gate: dict[str, Any] = {"rule": "paksha_day_night", "role": "recorded_only", "source": "later / other lineages"}
if daytime is None or shukla is None:
paksha_gate.update(status="missing", missing=[
name for name, value in (("birth_is_daytime", daytime), ("is_shukla_paksha", shukla)) if value is None])
@@ -134,13 +137,7 @@ def evaluate_strict_ashtottari(payload: dict[str, Any]) -> dict[str, Any]:
status="pass" if bool(shukla) != bool(daytime) else "fail",
)
gates = [rahu_gate, paksha_gate]
if any(gate["status"] == "fail" for gate in gates):
status = "not_applicable"
elif any(gate["status"] == "missing" for gate in gates):
status = "blocked"
else:
status = "applicable"
status = {"pass": "applicable", "fail": "not_applicable"}.get(rahu_gate["status"], "blocked")
return {
"rule_family": RULE_FAMILY_STRICT,
"source": STRICT_RULE_SOURCE,
@@ -154,16 +151,15 @@ def strict_reason_en(result: dict[str, Any]) -> str:
rahu = gates.get("rahu_from_lagna_lord") or {}
paksha = gates.get("paksha_day_night") or {}
parts = []
if rahu.get("status") == "missing" or paksha.get("status") == "missing":
missing = (rahu.get("missing") or []) + (paksha.get("missing") or [])
parts.append("missing inputs: " + ", ".join(missing))
if rahu.get("status") == "missing":
parts.append("missing inputs: " + ", ".join(rahu.get("missing") or []))
if rahu.get("status") in {"pass", "fail"}:
where = "in the Lagna" if rahu.get("rahu_in_lagna") else f"{rahu.get('position_from_lagna_lord')} from the Lagna lord"
parts.append(f"Rahu {where}, {'meets' if rahu['status'] == 'pass' else 'does not meet'} the 4/5/7/9/10 condition")
if paksha.get("status") in {"pass", "fail"}:
side = "Shukla" if paksha.get("is_shukla_paksha") else "Krishna"
when = "day" if paksha.get("birth_is_daytime") else "night"
parts.append(f"{side} Paksha and a {when} birth, {'meets' if paksha['status'] == 'pass' else 'does not meet'} the day-night condition")
parts.append(f"{side} Paksha and a {when} birth (day-night variant, recorded only: {'met' if paksha['status'] == 'pass' else 'not met'})")
return "; ".join(parts)
@@ -172,13 +168,13 @@ def strict_reason_zh(result: dict[str, Any]) -> str:
rahu = gates.get("rahu_from_lagna_lord") or {}
paksha = gates.get("paksha_day_night") or {}
parts = []
if rahu.get("status") == "missing" or paksha.get("status") == "missing":
parts.append("缺少输入,无法判断")
if rahu.get("status") == "missing":
parts.append("缺少罗睺或上升主星位置,无法判断")
if rahu.get("status") in {"pass", "fail"}:
where = "罗睺在上升星座" if rahu.get("rahu_in_lagna") else f"罗睺在上升主星起算第 {rahu.get('position_from_lagna_lord')} 位"
parts.append(f"{where},{'符合' if rahu['status'] == 'pass' else '不符合'}第 4、5、7、9、10 位的条件")
if paksha.get("status") in {"pass", "fail"}:
side = "白半月" if paksha.get("is_shukla_paksha") else "黑半月"
when = "白天" if paksha.get("birth_is_daytime") else "夜间"
parts.append(f"{side}{when}出生,{'符合' if paksha['status'] == 'pass' else '不符合'}昼夜与月相条件")
parts.append(f"{side}{when}出生(昼夜与月相条件只作来源差异记录,{'符合' if paksha['status'] == 'pass' else '不符合'},不参与判定)")
return ";".join(parts)
+1 -1
View File
@@ -308,7 +308,7 @@ def compute_dasha(
# BUG-1676: same strict gate as the full reading (astrologer 2026-11-07).
payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({
**conditional,
"applicability_rule_family": "strict_rahu_from_lagna_lord_and_paksha_daynight_v1",
"applicability_rule_family": "bphs_rahu_from_lagna_lord_excluding_lagna_v1",
})
return {"periods": _major_rows(payload)}
if key == "kalachakra":
+6 -6
View File
@@ -4197,12 +4197,12 @@ def render_pl9_markdown(packet: dict) -> str:
reason_text = '本地引擎当前把这套大运标记为不适用。'
if execution_status == 'not_applicable':
if key == 'ashtottari' and isinstance(family.get('applicability'), dict):
# BUG-1676: the two strict gates, in the table's language.
# BUG-1676: the deciding Rahu gate (and the recorded variant), in the table's language.
try:
from ashtottari_rule_profiles import strict_reason_zh
except ImportError: # pragma: no cover
from scripts.ashtottari_rule_profiles import strict_reason_zh
reason_text = '严格口径两条条件未同时满足:' + strict_reason_zh(family['applicability']) + '。'
reason_text = '不满足 BPHS 条件:' + strict_reason_zh(family['applicability']) + '。'
rows.append((label, 'not_applicable', '—', str(reason_text)))
return
rows.append((label, 'blocked', 'blocked', str(reason_text)))
@@ -4210,7 +4210,7 @@ def render_pl9_markdown(packet: dict) -> str:
_family_status_row('Vimshottari', 'vimshottari', 'parameter_sensitive', '主时间线已接入正文;当前已能给出 Dasha at Birth / Balance of Dasha / Current Dasha,但仍不升级为确定事件断言。')
_family_status_row('Narayana', 'narayana', 'parameter_sensitive', '双大运正文入口已接入,但当前仍主要输出本地年龄轴;与原始 PL9 的日期边界 / 起运序列 parity 尚未闭环。')
_family_status_row('Yogini', 'yogini', 'parameter_sensitive', 'Yogini 周期已返回,但外部起始规则与边界 parity 尚未闭环。')
_family_status_row('Ashtottari', 'ashtottari', 'parameter_sensitive / unverified', 'Ashtottari 按严格口径(BPHS 26.17 与 26.23 两条同时满足)适用,仍对参数敏感。')
_family_status_row('Ashtottari', 'ashtottari', 'parameter_sensitive / unverified', 'Ashtottari 按 BPHS 条件适用(罗睺不在上升,且在上升主星起算第 4、5、7、9、10 位),仍对参数敏感。')
_family_status_row('Kala Chakra', 'kala_chakra', 'parameter_sensitive', 'Kala Chakra 本地周期已返回,但算法冲突标签仍需保留。')
_family_status_row('Tribhagi', 'tribhagi', 'parameter_sensitive', '条件大运:子周期为 PyJHora 等分法,未做多引擎 parity。')
_family_status_row('Tribhagi 40', 'tribhagi_40', 'parameter_sensitive', '条件大运:40 年单份;子周期为 PyJHora 等分法,未做多引擎 parity。')
@@ -16911,10 +16911,10 @@ def cmd_full_reading(args):
try:
from ashtottari_dasha import calculate_ashtottari_dasha
# BUG-1676: astrologer ruling 2026-11-07, both BPHS 26 gates.
# BUG-1676: astrologer ruling 2026-11-07: Rahu from the Lagna lord decides.
ashtottari_data = calculate_ashtottari_dasha({
**conditional_birth_info,
'applicability_rule_family': 'strict_rahu_from_lagna_lord_and_paksha_daynight_v1',
'applicability_rule_family': 'bphs_rahu_from_lagna_lord_excluding_lagna_v1',
})
report['modules']['ashtottari_dasha'] = ashtottari_data
nak_ashtottari = (report['modules'].get('nakshatra_dasha') or {}).get('ashtottari')
@@ -17000,7 +17000,7 @@ def cmd_full_reading(args):
conditional_reference = {}
for output_id, module_id, function_name, extra in (
('ashtottari_dasha', 'ashtottari_dasha', 'calculate_ashtottari_dasha',
{'applicability_rule_family': 'strict_rahu_from_lagna_lord_and_paksha_daynight_v1'}),
{'applicability_rule_family': 'bphs_rahu_from_lagna_lord_excluding_lagna_v1'}),
('shodashottari_dasha', 'shodashottari_dasha', 'calculate_shodashottari_dasha', {}),
('dwadashottari_dasha', 'dwadashottari_dasha', 'calculate_dwadashottari_dasha', {'sukramsaka_profile': 'navamsa_lagna_taurus_or_libra'}),
('dwisaptatisama_dasha', 'dwisaptatisama_dasha', 'calculate_dwisaptatisama_dasha', {}),
+28 -14
View File
@@ -602,6 +602,31 @@ def ad_direction(md_sign_idx: int, start_sign_idx: int, profile: str, planet_lon
}
def rath_sign_period(sign_idx: int, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""Years of one sign under the production Narayana profile.
Dual-lord choice (p.43), dignity adjustment and node rules included.
Shared by Narayana and Drig (BUG-1678: astrologer ruling 2026-11-07,
PVR Integrated Approach 21.2(5): Drig years are found as in Narayana).
"""
decision = resolve_dual_lord(sign_idx, planet_lons)
period = decision.get("period")
lord = decision.get("lord")
if period is None and lord:
lord_sign_idx = nd._get_planet_sign(lord, planet_lons)
if lord_sign_idx is None:
lord_sign_idx = sign_idx
period = rath_period_years(sign_idx, lord, lord_sign_idx)
elif period is None:
lord_sign_idx = sign_idx
else:
lord_sign_idx = nd._get_planet_sign(lord, planet_lons) if lord else None
if lord_sign_idx is None:
lord_sign_idx = sign_idx
return {"decision": decision, "period": period, "lord": lord, "lord_sign_idx": lord_sign_idx,
"years": None if period is None else period["years"]}
def mahadasha_sequence(seed_sign_idx: int, planet_lons: Dict[str, float],
*, start_age: float = 0.0, cycle_count: int = 2) -> Dict[str, Any]:
"""Rath MD rows from one start sign (cycle 2 = twelve minus cycle 1)."""
@@ -609,20 +634,9 @@ def mahadasha_sequence(seed_sign_idx: int, planet_lons: Dict[str, float],
rows: List[Dict[str, Any]] = []
cursor = float(start_age)
for position, sign_idx in enumerate(order["order"], start=1):
decision = resolve_dual_lord(sign_idx, planet_lons)
period = decision.get("period")
lord = decision.get("lord")
if period is None and lord:
lord_sign_idx = nd._get_planet_sign(lord, planet_lons)
if lord_sign_idx is None:
lord_sign_idx = sign_idx
period = rath_period_years(sign_idx, lord, lord_sign_idx)
elif period is None:
lord_sign_idx = sign_idx
else:
lord_sign_idx = nd._get_planet_sign(lord, planet_lons) if lord else None
if lord_sign_idx is None:
lord_sign_idx = sign_idx
sign_period = rath_sign_period(sign_idx, planet_lons)
decision, period, lord = sign_period["decision"], sign_period["period"], sign_period["lord"]
lord_sign_idx = sign_period["lord_sign_idx"]
years = None if period is None else period["years"]
row = {
"sign": SIGNS[sign_idx], "sign_idx": sign_idx, "lord": lord,
+4 -4
View File
@@ -658,12 +658,12 @@ _CONDITIONAL_REASON_ZH = {
# BUG-1676: astrologer's wording, 2026-11-07.
_ASHTOTTARI_NOTE = {
'applicable': {
'en': 'Ashtottari Dasha is applicable under the selected conditional rule profile: Rahu is placed in a qualifying position from the Lagna lord, and the birth satisfies the day–night/Paksha condition. This remains a parameter-sensitive traditional timing layer.',
'zh': '按本站选定的条件规则,Ashtottari 大运适用:罗睺相对上升主星的位置符合条件,出生的昼夜与月相也符合条件。这仍是对参数敏感的传统时间层。',
'en': 'Ashtottari Dasha is applicable under the selected rule profile: Rahu is not in the Lagna and is placed in a kendra or trikona from the Lagna lord. This remains a parameter-sensitive traditional timing layer.',
'zh': '按本站选定的规则,Ashtottari 大运适用:罗睺不在上升,且在上升主星起算的角宫或三方宫。这仍是对参数敏感的传统时间层。',
},
'not_applicable': {
'en': 'Ashtottari Dasha is not activated under the selected strict rule profile. The calculation is retained as a reference-only comparison and is not used as an independent timing basis.',
'zh': '按本站选定的严格规则,本盘不启用 Ashtottari 大运。下表保留计算结果,只作对照,不作为独立的时间依据。',
'en': 'Ashtottari Dasha is not activated under the selected rule profile. The calculation is retained as a reference-only comparison and is not used as an independent timing basis.',
'zh': '按本站选定的规则,本盘不启用 Ashtottari 大运。下表保留计算结果,只作对照,不作为独立的时间依据。',
},
}
+13 -14
View File
@@ -121,25 +121,25 @@ def _sthira_note(section: Mapping, language: str) -> str:
comparison = section.get("brahma_comparison") if isinstance(section.get("brahma_comparison"), Mapping) else None
fallback = str(section.get("profile_id") or "").endswith("_fallback")
if language == "en":
text = (f"Brahma by the short method: {planet}; the full method gave no unique Brahma. " if fallback
text = (f"Brahma by the K.N. Rao short method: {planet}; Goel's full method gave no unique Brahma. " if fallback
else f"Brahma by Goel's full method: {planet}; Sthira starts from {sign}. ")
if comparison:
same = comparison.get("brahma_planet") == planet
text += ("The short method gives the same Brahma." if same else
f"Comparison: the short method gives {comparison.get('brahma_planet')}, starting from {comparison.get('start_sign')}; "
"the two Brahma profiles differ, and the short method is not used for this table.")
text += ("The K.N. Rao short method gives the same Brahma." if same else
f"Alternative: the K.N. Rao short method gives {comparison.get('brahma_planet')}, starting from {comparison.get('start_sign')}; "
"it is kept as a separate comparison and is not used for this table.")
if not fallback:
text += (" Open point: in the book's chapter 5 first example the text picks Venus over the Sun, while the chapter 1 "
"totals give the Sun the higher score; how the full method compares the lords is pending verification.")
return text.strip()
cn = lambda name: _PLANET_CN.get(str(name), str(name))
text = (f"完整方法没有选出唯一的 Brahma,本表按短法:Brahma 为{cn(planet)}。" if fallback
text = (f"Goel 完整方法没有选出唯一的 Brahma,本表按 K.N. Rao 短法:Brahma 为{cn(planet)}。" if fallback
else f"Brahma 按 Goel 完整方法定为{cn(planet)},Sthira 从{_SIGN_CN.get(sign, sign)}起。")
if comparison:
same = comparison.get("brahma_planet") == planet
text += ("短法结果相同。" if same else
f"对照:短法定为{cn(comparison.get('brahma_planet'))},从{_SIGN_CN.get(str(comparison.get('start_sign')), comparison.get('start_sign'))}起;"
"两种 Brahma 判定口径不同,本表不用短法。")
text += ("K.N. Rao 短法结果相同。" if same else
f"对照:K.N. Rao 短法定为{cn(comparison.get('brahma_planet'))},从{_SIGN_CN.get(str(comparison.get('start_sign')), comparison.get('start_sign'))}起;"
"短法单独保留作对照,本表不用。")
if not fallback:
text += "待核:书中第 5 章例 1 的正文选金星,而按第 1 章强度表太阳分数更高;完整方法如何比较三颗宫主仍待核对。"
return text
@@ -176,14 +176,13 @@ def render_rashi_dasha_chapter(packet: Mapping) -> str:
lines.extend([f"### {title}", "", _method_line(section, language), ""])
if key == "sthira" and section.get("status") == "listed":
lines.extend([_sthira_note(section, language), ""])
if key == "drig" and section.get("year_rule_status") == "pending_source_adjudication":
if key == "drig" and section.get("year_rule") == "narayana_rath_sign_period":
lines.extend([
"The sign order is checked. The year lengths count from the sign to its lord (odd-footed forward, "
"even-footed backward) without Narayana's exaltation / debilitation adjustments; the year rule is pending "
"verification. This table is for comparison and is not used for timing conclusions."
"Sign order by aspects from the 9th, 10th and 11th houses; each sign's years follow this site's Narayana "
"rules (PVR 21.2(5)), including the dual-lord choice and exaltation / debilitation adjustments."
if language == "en"
else "星座顺序已核对。每段年数按奇偶足从星座数到星座主星,没有计入 Narayana 的入旺、落陷等调整;"
"年数规则待核。本表只作对照,不用于应期判断。",
else "星座顺序按第 9、10、11 宫及其相照星座排列;每个星座的年数与本站 Narayana 大运同一套规则(PVR 21.2(5)),"
"包括双主星择强与入旺、落陷调整。",
"",
])
if key == "niryaana_shoola" and section.get("status") == "listed":
+28 -12
View File
@@ -305,10 +305,14 @@ def _sthira(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Ma
short = select_brahma(lagna, planet_lons, graha, rashi)
comparison = None
if short.get("status") == "selected":
# Astrologer 2026-11-07: an explicit alternative with its own timeline;
# never spliced into the main table.
comparison = {
"sthira_variant": "kn_rao_short",
"profile_id": GOEL_SHORT_BRAHMA_PROFILE,
"brahma_planet": short["planet"],
"start_sign": SIGNS[int(short["sign_index"]) % 12],
"periods": _sthira_periods(birth, int(short["sign_index"])),
}
if full.get("status") == "selected":
profile, picked = GOEL_FULL_BRAHMA_PROFILE, full
@@ -318,7 +322,8 @@ def _sthira(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Ma
else:
return _omitted(GOEL_FULL_BRAHMA_PROFILE, source_zh, source_en, "本轮不上表:Brahma 星座没有选出。", "This report does not include Sthira: the Brahma sign was not selected.")
start_sign = int(picked["sign_index"])
extra = {"brahma_planet": picked["planet"], "direction": "direct", "brahma_comparison": comparison}
extra = {"brahma_planet": picked["planet"], "direction": "direct", "brahma_comparison": comparison,
"sthira_variant": "kn_rao_short" if profile.endswith("_fallback") else "goel_full"}
if profile.endswith("_fallback"):
extra["full_method_reason"] = full.get("reason")
return _listed(profile, source_zh, source_en, _sthira_periods(birth, start_sign), **extra)
@@ -337,24 +342,35 @@ def _narayana_years(sign_idx: int, planet_lons: Mapping[str, float]) -> int:
def _drig(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Mapping) -> dict:
source_zh = "Crux of Vedic Astrology,印页 288、395"
source_en = "Crux of Vedic Astrology, printed pages 288 and 395"
# BUG-1678: the sign order is checked; the year rule is pending. The years
# below count to the sign lord without Narayana's dignity adjustments.
# PVR, Integrated Approach 21.2(5), says Drig years are found as in Narayana
# dasa (Sri Aurobindo: Scorpio 1906-1913); the astrologer ruled 2026-11-07
# that they must not reuse Narayana's. Open question; comparison only.
source_zh = "Crux of Vedic Astrology,印页 288、395;年数按 PVR《Vedic Astrology: An Integrated Approach》21.2(5)"
source_en = "Crux of Vedic Astrology, printed pages 288 and 395; years per PVR, Vedic Astrology: An Integrated Approach 21.2(5)"
# BUG-1678, astrologer ruling 2026-11-07 (second reply): Drig years are the
# production Narayana years (same sign-period function: dual lords, dignity
# and node adjustments, direction). PVR 21.2(5). The older count-to-lord
# years stay on each row as `legacy_years`, comparison only.
try:
from narayana_rath import rath_sign_period
except ImportError: # pragma: no cover - package import
from scripts.narayana_rath import rath_sign_period
profile = "drig_rath_aspect_sequence_v1"
periods = []
cursor = birth
for sign_idx in drig_aspect_sequence(lagna):
years = _narayana_years(sign_idx, planet_lons)
end = add_years(cursor, years)
sign_period = rath_sign_period(sign_idx, dict(planet_lons))
years = sign_period["years"]
if years is None:
return _omitted(profile, source_zh, source_en,
"本轮不上表:Narayana 的双主星择强没有分出结果,这一宫的年数无法确定。",
"This report does not include Drig: the Narayana dual-lord choice is undecided, so a sign's years are unknown.")
end = add_years(cursor, int(years))
children = _drig_children(sign_idx, cursor, end, graha, planet_lons)
periods.append(_period(sign_idx, years, cursor, end, children))
row = _period(sign_idx, int(years), cursor, end, children)
row["lord"] = sign_period["lord"]
row["legacy_years"] = _narayana_years(sign_idx, planet_lons)
periods.append(row)
cursor = end
return _listed(profile, source_zh, source_en, periods, direction="aspect_groups",
year_rule_status="pending_source_adjudication", confidence_status="parameter_sensitive")
year_rule="narayana_rath_sign_period", confidence_status="parameter_sensitive")
def _drig_children(md_sign: int, start: date, end: date, graha: Mapping, planet_lons: Mapping[str, float]) -> list[dict]:
+2 -2
View File
@@ -292,7 +292,7 @@ def calculation_notes(language: str) -> str:
"- D5、D6、D8、D11 按 PVR《Vedic Astrology: An Integrated Approach》6.2.5–6.2.11 的分盘规则。D8 动宫从白羊、固定宫从射手、双宫从狮子起算;选 Raman 岁差不会改用另一套 Raman D8 规则。",
"- Yogini 大运按本命月亮星宿与剩余弧度起算,一年取 365.25 天。表中第一段是出生余额,不是一段新的完整大运。",
"- Vimshottari 第一段小运和细运接着原来完整周期的钟点走,只是不列出生前那一部分,不在出生时刻重新起算。",
"- Shadbala 按本仓写明的各分量方法计算。最低门槛按 BPHS:太阳 6.5、月亮 6、火星 5、水星 7、木星 6.5、金星 5.5、土星 5 Rupa;门槛比值与总分是两回事。各分量按占星师在 #166 选定的口径;外部软件用的口径不同,个别行星是否达标可能相反,所以达标与否对参数敏感。达标只说明力量,不等于吉;未达标也不等于凶,吉凶要看功能吉凶、宫主、落宫、大运与行运。",
"- Shadbala 按本仓写明的各分量方法计算。最低门槛按 BPHS:太阳 6.5、月亮 6、火星 5、水星 7、木星 6.5、金星 5.5、土星 5 Rupa;门槛比值与总分是两回事。太阳的 Cheshta 等于他的 Ayana Bala,月亮的 Cheshta 等于她的 Paksha Bala(BPHS 27.18),所以在 Kala 细项和 Cheshta 两处各出现一次,这是原文规定,不是重复计算,也不再乘 2。各分量按占星师在 #166 选定的口径;外部软件用的口径不同,个别行星是否达标可能相反,所以达标与否对参数敏感。达标只说明力量,不等于吉;未达标也不等于凶,吉凶要看功能吉凶、宫主、落宫、大运与行运。",
"- 落陷取消(Neecha Bhanga)记录的是取消条件,行星仍是落陷;它本身不等于王者格,也不保证结果好。",
"- 计算用的是用户提供的出生资料和上面写明的计算口径。大运交界是算出的时间边界,不是到了那天就会发生的事。检出的瑜伽是规则筛出的候选,还要看成立与取消条件。",
"- 显示精度:时间到整秒,角度最多四位小数。",
@@ -303,7 +303,7 @@ def calculation_notes(language: str) -> str:
"- D5, D6, D8 and D11 use the PVR divisional rules (Vedic Astrology: An Integrated Approach, 6.2.5-6.2.11). D8 starts from Aries for movable signs, Sagittarius for fixed signs and Leo for dual signs. Raman ayanamsa does not select the separate Raman D8 convention.",
"- Yogini uses the natal Moon nakshatra and its remaining arc, with a year of 365.25 days. Its first visible period is the birth balance, not a new full period.",
"- The first Vimshottari AD and PD continue the original full-period clock; only their pre-birth portions are omitted. They do not restart at birth.",
"- Shadbala uses the stated local component methods. Minimum strengths follow BPHS: Sun 6.5, Moon 6, Mars 5, Mercury 7, Jupiter 6.5, Venus 5.5, Saturn 5 Rupas; the threshold ratio is distinct from total strength. Components follow the profile the astrologer chose in #166; external software uses other profiles, and a planet can meet the minimum in one and miss it in the other, so the verdict is parameter-sensitive. Meeting the minimum describes strength only and is not a favourable verdict; missing it is not an unfavourable one. That depends on functional roles, lordship, placement, dashas and transits.",
"- Shadbala uses the stated local component methods. Minimum strengths follow BPHS: Sun 6.5, Moon 6, Mars 5, Mercury 7, Jupiter 6.5, Venus 5.5, Saturn 5 Rupas; the threshold ratio is distinct from total strength. The Sun's Cheshta is his Ayana Bala and the Moon's Cheshta is her Paksha Bala (BPHS 27.18), so each value appears once in the Kala detail and once as Cheshta; this is the text's rule, not double counting, and nothing is doubled. Components follow the profile the astrologer chose in #166; external software uses other profiles, and a planet can meet the minimum in one and miss it in the other, so the verdict is parameter-sensitive. Meeting the minimum describes strength only and is not a favourable verdict; missing it is not an unfavourable one. That depends on functional roles, lordship, placement, dashas and transits.",
"- Neecha Bhanga records a cancellation condition while retaining the debilitated placement. It does not by itself establish a Raja Yoga or promise a favourable outcome.",
"- Calculations use the birth details supplied and the stated calculation profile. Dasha boundaries are calculated period boundaries, not promises of events on those dates. Detected yogas are rule-screened candidates requiring condition and cancellation review.",
"- Display precision: timestamps to whole seconds, angles to at most four decimals.",
+9 -8
View File
@@ -791,8 +791,9 @@ def _nakshatra_context(packet: dict, language: str) -> str:
if windows:
head = ["小运", "主星星宿", "四分", "年数", "对本命月亮的 Tara"] if zh else ["Antardasha", "Lord's nakshatra", "Pada", "Years", "Tara from the Moon"]
parts.append(("本大运各小运主星所在星宿:" if zh else "Nakshatra of each AD lord in this MD:") + "\n\n" + _md_table(head, windows))
# BUG-1676: astrologer ruling 2026-11-07. Production gate = both BPHS 26
# conditions; the Moon-nakshatra whitelist and "Rahu not in a kendra" are not gates.
# BUG-1676: astrologer rulings 2026-11-07. The BPHS Rahu condition decides;
# the day-night / Paksha rule is a recorded variant; the Moon-nakshatra
# whitelist and "Rahu not in a kendra" are not gates.
applicability = ashtottari.get("applicability") if isinstance(ashtottari, dict) and isinstance(ashtottari.get("applicability"), dict) else {}
gates = applicability.get("gates") if isinstance(applicability.get("gates"), dict) else {}
if gates:
@@ -815,18 +816,18 @@ def _nakshatra_context(packet: dict, language: str) -> str:
else:
paksha_basis = "—"
rows = [
["罗睺相对上升主星(BPHS 26.17):不在上升,且在第 4、5、7、9、10 位" if zh else "Rahu from the Lagna lord (BPHS 26.17): not in the Lagna, 4/5/7/9/10",
["罗睺相对上升主星(BPHS,决定是否适用):不在上升,且在第 4、5、7、9、10 位" if zh else "Rahu from the Lagna lord (BPHS, decides): not in the Lagna, 4/5/7/9/10",
_gate_cell(rahu), rahu_basis],
["昼夜与月相(BPHS 26.23):白半月夜生或黑半月日生" if zh else "Day-night and Paksha (BPHS 26.23): Shukla night or Krishna day",
["昼夜与月相(其他传承,只作记录,不参与判定):白半月夜生或黑半月日生" if zh else "Day-night and Paksha (other lineages, recorded only): Shukla night or Krishna day",
_gate_cell(paksha), paksha_basis],
]
status = applicability.get("status")
verdict = {
"applicable": ("两道条件都满足,Ashtottari 适用(对参数敏感)。", "Both conditions are met; Ashtottari applies (parameter-sensitive)."),
"not_applicable": ("至少一道条件不满足,本盘不启用 Ashtottari;大运表只作对照。", "At least one condition fails; Ashtottari is not activated for this chart and its table is for comparison only."),
"blocked": ("缺少判断所需的输入,暂不判定。", "Inputs needed for the decision are missing; not decided."),
"applicable": ("罗睺条件满足,Ashtottari 适用(对参数敏感)。", "The Rahu condition is met; Ashtottari applies (parameter-sensitive)."),
"not_applicable": ("罗睺条件不满足,本盘不启用 Ashtottari;大运表只作对照。", "The Rahu condition is not met; Ashtottari is not activated for this chart and its table is for comparison only."),
"blocked": ("缺少罗睺或上升主星的位置,暂不判定。", "Rahu or Lagna-lord position is missing; not decided."),
}.get(status)
head = ["Ashtottari 适用条件(严格口径:两条都要满足)", "是否满足", "本盘情况"] if zh else ["Ashtottari condition (strict profile: both required)", "Met", "This chart"]
head = ["Ashtottari 适用条件", "是否满足", "本盘情况"] if zh else ["Ashtottari condition", "Met", "This chart"]
text = _md_table(head, rows)
if verdict:
text += "\n\n" + verdict[0 if zh else 1]
+14 -14
View File
@@ -490,10 +490,10 @@
"years": [
12,
6,
5,
4,
6,
1,
7,
3,
4,
2,
4,
7,
@@ -1148,14 +1148,14 @@
"years": [
2,
1,
2,
1,
0,
8,
1,
1,
1,
9,
2,
3,
4,
12
]
@@ -1830,16 +1830,16 @@
],
"years": [
11,
9,
8,
5,
10,
7,
6,
1,
2,
6,
9,
9,
5,
2,
8,
10,
5,
3,
11
]
},
@@ -2488,15 +2488,15 @@
],
"years": [
12,
7,
8,
5,
11,
1,
2,
7,
6,
9,
8,
7,
5,
10
]
+61 -32
View File
@@ -1,10 +1,12 @@
"""Astrologer rulings 2026-11-07 (BUG-1676, BUG-1677, BUG-1678).
"""Astrologer rulings 2026-11-07, two replies (BUG-1676, BUG-1677, BUG-1678).
- Ashtottari: production gate is both BPHS 26 conditions (v.17 Rahu from the
Lagna lord, not in the Lagna, 4/5/7/9/10; v.23 Shukla night or Krishna day).
Not met: the table stays as a comparison copy only.
- Sthira: Brahma by Goel's full method; the short method is a comparison.
- Drig: sign order kept, year lengths a named candidate pending a source.
- Ashtottari: the BPHS Rahu condition decides (not in the Lagna; 4/5/7/9/10
from the Lagna lord; with the Lagna lord does not count). The Shukla-night /
Krishna-day rule is recorded only. Not met: comparison copy only.
- Sthira: Brahma by Goel's full method; the K.N. Rao short method is an explicit
alternative with its own timeline.
- Drig: years are the production Narayana years (PVR 21.2(5)); Sri Aurobindo's
Scorpio Drig dasha 1906-1913 (7 years) is the regression case.
- Shadbala: profile-sensitive threshold verdict, not a favourable / unfavourable one.
No private birth data: synthetic positions, Goel's printed example, fictional charts.
"""
@@ -27,25 +29,28 @@ _BASE = {"ascendant_sign_index": 0, "lagna_lord_sign_index": 9, "is_shukla_paksh
@pytest.mark.parametrize("rahu, daytime, shukla, status", [
(1, False, True, "applicable"), # Taurus: 5th from Capricorn; Shukla night
(1, True, False, "applicable"), # Krishna day
(1, True, True, "not_applicable"), # Shukla day fails v.23
(9, False, True, "not_applicable"), # with the Lagna lord (1st) is excluded
(1, False, True, "applicable"), # Taurus: 5th from Capricorn
(1, True, True, "applicable"), # Shukla day: the day-night rule does not decide
(3, True, False, "applicable"), # Cancer: 7th from the lord
(9, False, True, "not_applicable"), # with the Lagna lord (1st) does not count
(0, False, True, "not_applicable"), # 4th from the lord but in the Lagna
(2, False, True, "not_applicable"), # 6th from the lord
])
def test_strict_gate_needs_both_conditions(rahu, daytime, shukla, status) -> None:
def test_rahu_gate_decides_and_paksha_is_recorded(rahu, daytime, shukla, status) -> None:
result = evaluate_strict_ashtottari({**_BASE, "rahu_sign_index": rahu, "birth_is_daytime": daytime, "is_shukla_paksha": shukla})
assert result["status"] == status
assert set(result["gates"]) == {"rahu_from_lagna_lord", "paksha_day_night"}
gates = result["gates"]
assert gates["rahu_from_lagna_lord"]["role"] == "decides"
assert gates["paksha_day_night"]["role"] == "recorded_only"
assert gates["paksha_day_night"]["status"] == ("pass" if shukla != daytime else "fail")
def test_missing_day_night_blocks_instead_of_guessing() -> None:
result = evaluate_strict_ashtottari({**_BASE, "rahu_sign_index": 1, "birth_is_daytime": None})
assert result["status"] == "blocked"
assert result["gates"]["paksha_day_night"]["missing"] == ["birth_is_daytime"]
# A failed gate decides even when the other one is missing.
assert evaluate_strict_ashtottari({**_BASE, "rahu_sign_index": 2, "birth_is_daytime": None})["status"] == "not_applicable"
def test_missing_inputs() -> None:
# Day-night missing does not block: it does not decide.
assert evaluate_strict_ashtottari({**_BASE, "rahu_sign_index": 1, "birth_is_daytime": None})["status"] == "applicable"
blocked = evaluate_strict_ashtottari({**_BASE, "rahu_sign_index": None})
assert blocked["status"] == "blocked"
assert blocked["gates"]["rahu_from_lagna_lord"]["missing"] == ["rahu_sign_index"]
def _info(**overrides) -> dict:
@@ -59,9 +64,9 @@ def _info(**overrides) -> dict:
def test_calculator_runs_only_when_applicable_and_keeps_a_reference_copy() -> None:
applied = calculate_ashtottari_dasha(_info())
assert applied["applicable"] and applied["execution_status"] == "executed" and applied["major"]
off = calculate_ashtottari_dasha(_info(birth_is_daytime=True))
off = calculate_ashtottari_dasha(_info(rahu_sign_index=2))
assert off["applicable"] is False and off["execution_status"] == "not_applicable" and off["major"] == []
reference = calculate_ashtottari_dasha(_info(birth_is_daytime=True, standard_table_profile="pl9_reports_all_dasha_tables_v1"))
reference = calculate_ashtottari_dasha(_info(rahu_sign_index=2, standard_table_profile="pl9_reports_all_dasha_tables_v1"))
assert reference["execution_status"] == "reference_only" and reference["applicable"] is False
assert [row["planet"] for row in reference["major"]] == [row["planet"] for row in applied["major"]]
@@ -80,15 +85,17 @@ def test_report_carries_the_gates_and_the_comparison_wording(fictional) -> None:
timing = packet["timing_and_predictive_systems"]
module = timing["ashtottari_dasha"]
assert module["applicability_rule_family"] == RULE_FAMILY_STRICT
assert module["execution_status"] == "not_applicable" # fictional chart 0: Krishna Paksha, night birth
assert timing["conditional_dasha_reference"]["ashtottari_dasha"]["periods"]
nak = packet["advanced_systems"]["nakshatra_dasha"]
assert nak["ashtottari"]["applicable"] is False and "ashtottari_current" not in nak
assert "按本站选定的严格规则,本盘不启用 Ashtottari 大运。下表保留计算结果,只作对照,不作为独立的时间依据。" in chinese
assert ("Ashtottari Dasha is not activated under the selected strict rule profile. The calculation is retained "
"as a reference-only comparison and is not used as an independent timing basis.") in english
assert "Ashtottari 适用条件(严格口径:两条都要满足)" in chinese
assert "Ashtottari condition (strict profile: both required)" in english
# Fictional chart 0: Rahu 9th from the Lagna lord, so the BPHS gate passes.
assert module["applicability"]["gates"]["rahu_from_lagna_lord"]["position_from_lagna_lord"] == 9
assert module["execution_status"] == "executed" and module["applicable"] is True and module["major"]
assert nak["ashtottari"]["applicable"] is True
assert "ashtottari_dasha" not in timing["conditional_dasha_reference"]
assert "按本站选定的规则,Ashtottari 大运适用:罗睺不在上升,且在上升主星起算的角宫或三方宫。这仍是对参数敏感的传统时间层。" in chinese
assert ("Ashtottari Dasha is applicable under the selected rule profile: Rahu is not in the Lagna and is placed in "
"a kendra or trikona from the Lagna lord. This remains a parameter-sensitive traditional timing layer.") in english
assert "| 昼夜与月相(其他传承,只作记录,不参与判定):白半月夜生或黑半月日生 | 否 | 黑半月夜间出生 |" in chinese
assert "罗睺条件满足,Ashtottari 适用(对参数敏感)。" in chinese
assert "月亮星宿与月相(Moon nakshatra and paksha)" not in chinese
@@ -100,21 +107,43 @@ def test_sthira_uses_the_full_method_and_shows_the_short_one() -> None:
sthira = calculate_rashi_dashas(date(2000, 1, 1), 165.0, positions)["sthira"]
assert sthira["profile_id"] == "goel_full_brahma_ch1_ch5_v1"
# The short method matches the book (Mars in Scorpio).
assert sthira["brahma_comparison"] == {"profile_id": "goel_rao_short_brahma_method_v1", "brahma_planet": "Mars", "start_sign": "Scorpio"}
comparison = sthira["brahma_comparison"]
assert sthira["sthira_variant"] == "goel_full" and comparison["sthira_variant"] == "kn_rao_short"
assert (comparison["brahma_planet"], comparison["start_sign"]) == ("Mars", "Scorpio")
assert comparison["periods"][0]["sign"] == "Scorpio" and len(comparison["periods"]) == 12
# By the ch. 1 totals Sun (240) outscores Venus (172.5); the ch. 5 text says Venus.
# Recorded as an open question, not tuned to the book's answer.
assert sthira["brahma_planet"] == "Sun" and sthira["periods"][0]["sign"] == "Aries"
assert [row["years"] for row in sthira["periods"][:3]] == [7, 8, 9]
def test_drig_years_are_labelled_pending_and_sthira_note_is_shown(fictional) -> None:
def test_drig_uses_narayana_years_and_matches_pvr_aurobindo() -> None:
# Sri Aurobindo, public data: 15 Aug 1872, about 05:00 local mean time, Calcutta.
# PVR, Integrated Approach ch. 21 Example 83: Scorpio Drig dasha 1906-1913.
from scripts.domain_calculation_service import compute_chart
chart = compute_chart(dict(year=1872, month=8, day=15, hour=5, minute=0, second=0, lat=22.5667, lon=88.3667,
tz=5.8911, ayanamsa="lahiri", node_mode="mean"))
lons = {p: float(chart["planets"][p]["degree_raw"]) for p in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")}
drig = calculate_rashi_dashas(date(1872, 8, 15), float(chart["ascendant"]["degree_raw"]), lons)["drig"]
assert drig["year_rule"] == "narayana_rath_sign_period"
assert [row["sign"][:3] for row in drig["periods"]] == ["Pis", "Sag", "Vir", "Gem", "Ari", "Leo", "Sco", "Aqu", "Tau", "Cap", "Lib", "Can"]
scorpio = next(row for row in drig["periods"] if row["sign"] == "Scorpio")
assert (scorpio["start"][:4], scorpio["end"][:4], scorpio["years"]) == ("1906", "1913", 7)
# The older count-to-lord years stay on the row for comparison (Mars debilitated: 8 without the adjustment).
assert scorpio["legacy_years"] == 8
def test_drig_and_sthira_notes_are_shown(fictional) -> None:
_packet, chinese, english = fictional
assert "没有计入 Narayana 的入旺、落陷等调整;年数规则待核。本表只作对照,不用于应期判断。" in chinese
assert "the year rule is pending verification. This table is for comparison and is not used for timing conclusions." in english
assert "每个星座的年数与本站 Narayana 大运同一套规则(PVR 21.2(5)),包括双主星择强与入旺、落陷调整。" in chinese
assert "each sign's years follow this site's Narayana rules (PVR 21.2(5))" in english
assert "Brahma 按 Goel 完整方法定为" in chinese and "Brahma by Goel's full method:" in english
def test_shadbala_threshold_is_profile_sensitive_not_a_verdict(fictional) -> None:
_packet, chinese, english = fictional
assert "太阳的 Cheshta 等于他的 Ayana Bala,月亮的 Cheshta 等于她的 Paksha Bala(BPHS 27.18)" in chinese
assert "this is the text's rule, not double counting, and nothing is doubled" in english
assert "达标只说明力量,不等于吉;未达标也不等于凶" in chinese
assert "Meeting the minimum describes strength only and is not a favourable verdict" in english
@@ -101,12 +101,12 @@ def test_einstein_full_reading_exposes_ten_conditional_dasha_modules() -> None:
and "| Year Lord |" not in line
]
# 原值: rows[:5] == original(Ashtottari 行为 executed / 本地候选规则)
# 新值: 其余四行不变;Ashtottari 行按严格口径判为 not_applicable 并写两道条件
# 新值: 其余四行不变;Ashtottari 行按 BPHS 罗睺条件判为 not_applicable 并写本盘情况
# 原因: BUG-1676,占星师 2026-11-07 裁定 Ashtottari 生产口径(这张公开盘罗睺在上升主星起算第 11 位)
assert [row for row in rows[:5] if not row.startswith("| Ashtottari |")] == [
row for row in original[:5] if not row.startswith("| Ashtottari |")]
ashtottari_row = next(row for row in rows[:5] if row.startswith("| Ashtottari |"))
assert ashtottari_row.startswith("| Ashtottari | not_applicable | — | 严格口径两条条件未同时满足:")
assert ashtottari_row.startswith("| Ashtottari | not_applicable | — | 不满足 BPHS 条件:")
for label in NEW_TABLE_LABELS:
assert any(f"| {label} |" in line for line in rows)
assert any("| Year Lord |" in line for line in rows)
+7 -2
View File
@@ -256,11 +256,16 @@ def test_rath_profile_is_the_product_default_and_stays_out_of_rectification_scor
新值:Rath 是产品默认:narayana_legacy_label(数据卡 / 报告 / 星盘页大运,product_narayana)与
annual_service/package.py(年度时间轴)导入它;冻结的打分文件与 rectification/ 仍都不导入展示层或 Rath 模块,
生时校正继续用旧算法
原因:#166 Q9 产品决定 2026-11-04(BUG-1601);本测试的前提被这条裁定反转"""
原因:#166 Q9 产品决定 2026-11-04(BUG-1601);本测试的前提被这条裁定反转
原值:users == [annual_service/package.py, narayana_legacy_label.py]
新值:加 rashi_dashas_native.py(Drig 星座年数调用 rath_sign_period)和 annual_service/report_facts.py
(BUG-1650 年度报告读 Rath 骨架,早已导入,本测试当时未同步)
原因:BUG-1678,占星师 2026-11-07 裁定 Drig 年数与 Narayana 相同;该文件不在生时校正打分文件内(下面的断言照旧)"""
pattern = re.compile(r"\bnarayana_rath\b")
users = sorted(str(path.relative_to(ROOT)) for path in (ROOT / "scripts").rglob("*.py")
if path.name != "narayana_rath.py" and pattern.search(path.read_text(encoding="utf-8")))
assert users == ["scripts/annual_service/package.py", "scripts/narayana_legacy_label.py"]
assert users == ["scripts/annual_service/package.py", "scripts/annual_service/report_facts.py",
"scripts/narayana_legacy_label.py", "scripts/rashi_dashas_native.py"]
from scripts.annual_service.report_facts import CONFIG
from scripts.narayana_legacy_label import RATH_LABEL_ZH
assert CONFIG["narayana_skeleton_profile"] == "rath" and "默认" in RATH_LABEL_ZH
+4
View File
@@ -301,6 +301,10 @@ def test_pyjhora_fixture_diffs_stay_on_the_recorded_steps() -> None:
# 原值:乔布斯 / 奥巴马 / 爱因斯坦三盘 native.chara 年数各有一段(水瓶或天蝎)与 PyJHora 不同,diff_steps 含 years。
# 新值:三盘 Chara 年数与 PyJHora 4.8.7 对照行逐段相同,diff_steps 只剩 subperiod。
# 原因:BUG-1566 天蝎 / 水瓶双主星按盘面判定(一主在本宫取另一主)。PyJHora 对照行未重跑。
# 原值:native.drig.years 按奇偶足数到星座主星(不计入旺落陷与双主择强)。
# 新值:native.drig.years 重冻为本站 Narayana(Rath)同一星座年数函数的结果;星座顺序与 diff_steps 不变。
# 原因:BUG-1678,占星师 2026-11-07 第二次裁定 Drig 年数与 Narayana 相同(PVR 21.2(5))。PyJHora 对照行未重跑;
# 新年数与 PyJHora 更接近(乔布斯前 12 段逐段相同),剩下的差别仍在已记录的 direction 一步。
payload = json.loads(FIXTURE.read_text(encoding="utf-8"))
assert payload["pyjhora_version"] == "4.8.7"
assert payload["generator_python_has_pyjhora"] is True
+8 -8
View File
@@ -158,13 +158,13 @@ def test_avastha_names_follow_the_calculator_bug_1661() -> None:
def test_nakshatra_context_lists_the_two_strict_ashtottari_gates() -> None:
# 原值: 并列「罗睺不在角宫」与「月亮星宿与月相」两条规则,结论不同时写「两条规则结论不同」
# 新值: 严格口径两道条件(BPHS 26.17 罗睺相对上升主星、26.23 昼夜与月相)各自是否满足,加总结论
# 原因: BUG-1676,占星师 2026-11-07 裁定生产口径为两条同时满足;月亮星宿白名单不作门槛
# 新值: BPHS 罗睺条件决定是否适用;昼夜与月相只作记录;加结论
# 原因: BUG-1676,占星师 2026-11-07 两次裁定(第二次:昼夜月相属其他传承,不参与判定);月亮星宿白名单不作门槛
from scripts.report_render.parity_chapters import _nakshatra_context
gates = {
"rahu_from_lagna_lord": {"status": "pass", "position_from_lagna_lord": 5, "rahu_in_lagna": False},
"paksha_day_night": {"status": "fail", "is_shukla_paksha": True, "birth_is_daytime": True},
"paksha_day_night": {"status": "fail", "is_shukla_paksha": True, "birth_is_daytime": True, "role": "recorded_only"},
}
packet = {"worksheets": {
"advanced_systems": {"nakshatra_dasha": {
@@ -179,18 +179,18 @@ def test_nakshatra_context_lists_the_two_strict_ashtottari_gates() -> None:
}},
"timing_and_predictive_systems": {"ashtottari_dasha": {
"applicable": False,
"applicability": {"status": "not_applicable", "gates": gates},
"applicability": {"status": "applicable", "gates": gates},
}},
}}
text = _nakshatra_context(packet, "zh")
assert "| 当前大运 | 北交点/罗睺,15.23-33.23 岁,已走 76% |" in text
assert "| 小运 北交点/罗睺 | Uttara Phalguni | 1 | 2.70 | 至友Tara(吉) |" in text
assert "| 罗睺相对上升主星(BPHS 26.17):不在上升,且在第 4、5、7、9、10 位 | 是 | 罗睺在上升主星起算第 5 位 |" in text
assert "| 昼夜与月相(BPHS 26.23):白半月夜生或黑半月日生 | 否 | 白半月白天出生 |" in text
assert "至少一道条件不满足,本盘不启用 Ashtottari" in text
assert "| 罗睺相对上升主星(BPHS,决定是否适用):不在上升,且在第 4、5、7、9、10 位 | 是 | 罗睺在上升主星起算第 5 位 |" in text
assert "| 昼夜与月相(其他传承,只作记录,不参与判定):白半月夜生或黑半月日生 | 否 | 白半月白天出生 |" in text
assert "罗睺条件满足,Ashtottari 适用(对参数敏感)。" in text
assert "两条规则结论不同" not in text
english = _nakshatra_context(packet, "en")
assert "| Day-night and Paksha (BPHS 26.23): Shukla night or Krishna day | No | Shukla Paksha, day birth |" in english
assert "| Day-night and Paksha (other lineages, recorded only): Shukla night or Krishna day | No | Shukla Paksha, day birth |" in english
assert not any("一" <= ch <= "鿿" for ch in english)