Merge codex/goel-jaimini-strength-20261022 (#94) into staging
# Conflicts: # CHANGELOG.md # docs/tasks/README.md
This commit is contained in:
@@ -26,6 +26,11 @@
|
||||
- 报告「行星原始位置」表按内容定列宽。星宿的英文名用浅色小字,跟在中文后面同一行。速度写到小数点后 4 位。放不下时表在自己的框里横向滑动。
|
||||
- Skill 版本不变(BUG-1410)。
|
||||
|
||||
## 2026-10-22 — 引擎算出 Goel Jaimini 强度和三个特殊点
|
||||
|
||||
- full-reading 与 jaimini 输出增加 Goel 第 1 章完整强度,以及 Brahma、Maheshwara、Rudra。报告正文暂不显示这一章。
|
||||
- Skill 版本不 bump。
|
||||
|
||||
## 2026-10-21 — 合盘时间线写出起止,删掉的模型改用默认模型
|
||||
|
||||
- 关系时间线每段标题写出起止年月。第一段从本月开始时,起点写「现在」或 Now。
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
上游 `5fb8379c` 原文;我方 Niryaana 选择器在 `scripts/rashi_dashas_native.py`;书稿未入库
|
||||
|
||||
# Goel Jaimini public calculation module
|
||||
|
||||
## Scope and entry points
|
||||
|
||||
`scripts/goel_jaimini_strength.py` publishes the existing frozen runtime's
|
||||
position-only arithmetic, with input validation and explicit blocked points.
|
||||
It contains no customer identity, birth record, report, private fixture, or
|
||||
runtime dependency on a frozen directory. The historical frozen-engine
|
||||
publication receipt remains true: its complete payload is not published.
|
||||
|
||||
Public functions:
|
||||
|
||||
- `calculate_goel_strength(planet_longitudes, node_occupancy_policy=...)`
|
||||
- `build_goel_chart_evidence(planet_longitudes, ascendant_longitude)`
|
||||
|
||||
Both accept already calculated sidereal longitudes in degrees, in `[0, 360)`.
|
||||
Supply exactly Sun, Moon, Mars, Mercury, Jupiter, Venus, Saturn, Rahu and Ketu;
|
||||
nodes must be opposite. The builder also requires a finite numeric Ascendant.
|
||||
No ayanamsa, node mode, birth time or ephemeris is inferred. The caller owns
|
||||
those upstream settings. Invalid inputs raise `ValueError`; equal seven-karaka
|
||||
degrees block the calculation instead of inventing a ranking.
|
||||
|
||||
The native `jaimini --mode special` / `--mode all` commands and `full-reading`
|
||||
now expose `goel_full_strength` and `goel_special_points`. The builder's
|
||||
`strength` and `special_points` keys map to those two report keys. Existing
|
||||
eight-karaka and other Jaimini profiles remain independent.
|
||||
|
||||
## Sources and page numbering
|
||||
|
||||
V. P. Goel, *Predicting through Jaimini Astrology*, Chapter 1, printed pp10–17
|
||||
(PDF file pages14–21). Local source:
|
||||
`V. P. Goel, Predicting through Jaimini Astrology`.
|
||||
Transcription SHA256:
|
||||
`0a8eb5e6de409d5d8ae6684be927b1273acdc5fa72a7f1c36c31de33907341a6`.
|
||||
Line numbers below refer to that transcription, not printed line numbers.
|
||||
|
||||
| Calculation | Printed pages | PDF pages | Transcription locator |
|
||||
|---|---|---|---|
|
||||
| Dignity values and natural friendship | 10–11 | 14–15 | L461 onward |
|
||||
| Seven-karaka rank; Kendriya, Kartari, Adhipati | 11–12 | 15–16 | L510–553 |
|
||||
| Sign permanent and karaka components | 13 | 17 | L558–593 |
|
||||
| Sthana, aspect roles, sign-lord strength | 14 | 18 | L598–632 |
|
||||
| Worked planetary/sign totals | 15 | 19 | L636–697 |
|
||||
| Brahma and Maheshwara; first Rudra rule | 16 | 20 | L701–727 |
|
||||
| Rudra malefic-contact exception | 17 | 21 | L731–736 |
|
||||
| Mercury/Jupiter as sign lord: double role credit | 103 | 107 | L4359–4389 |
|
||||
|
||||
The p103 clarification occurs in Goel's **short method** chapter. Only its
|
||||
explicit double-role explanation is used to clarify the three-role reading
|
||||
of p14. The short method's different weights are NOT mixed into this full
|
||||
method. Numeric reproduction of p15 also checks this role convention.
|
||||
|
||||
Degree-specific Moolatrikona ranges reuse the existing named
|
||||
`raman_manual_degree_mt_exaltation_first_v1` profile from
|
||||
`scripts/trikona_raman.py`: B. V. Raman, *A Manual of Hindu Astrology*,
|
||||
section29, printed p12. Goel provides the score categories; this auxiliary
|
||||
profile supplies degree ranges. Exaltation is checked first. This is an
|
||||
explicit method choice, not a claim that all Goel/Raman conventions coincide.
|
||||
|
||||
## Formula contract
|
||||
|
||||
Planet total is dignity + karaka + kendriya + kartari + adhipati.
|
||||
|
||||
- Dignity: exalted60, Moolatrikona45, own30, friend22.5, neutral15,
|
||||
enemy7.5, debilitated3.75; natural friendship, not compound friendship.
|
||||
- Seven classical planets ranked by degree within sign: 60,45,30,22.5,15,7.5,3.75.
|
||||
Nodes never receive these karaka points.
|
||||
- Kendra / Panaphara / Apoklima from AK: 60 / 30 / 15.
|
||||
- Kartari: 60 when the planet is in an odd sign and both adjacent signs have
|
||||
occupants; nodes count in this component under both occupancy policies.
|
||||
- Adhipati: 60 for a qualifying odd-sign lord with a malefic conjunction or
|
||||
Jaimini rasi aspect. All classical planets except Moon own an odd sign.
|
||||
This implementation declares Sun/Mars/Saturn/Rahu/Ketu malefics and excludes
|
||||
self-contact; phase-dependent Moon and conditioned Mercury are not added.
|
||||
|
||||
Sign total is permanent + occupying karaka points + Sthana + aspect roles
|
||||
+ sign-lord total. Movable/fixed/dual permanent values are15/30/60.
|
||||
Sthana is zero if empty, otherwise45+15*n. Jupiter, Mercury and sign lord are
|
||||
three roles worth60 each; if one planet fulfills two roles it can earn120.
|
||||
Conjunction and aspect qualify a role once, not twice. Aspects are Jaimini
|
||||
sign aspects, not Parashari planetary aspects.
|
||||
|
||||
Special points:
|
||||
|
||||
- Brahma first checks AK's eighth lord occupying AK's eighth. Otherwise it
|
||||
compares Lagna/seventh Goel totals and selects the strongest non-Saturn
|
||||
6/8/12 lord from the stronger anchor. The Saturn exclusion belongs to the
|
||||
fallback branch; it is not silently applied to the first override.
|
||||
- Maheshwara is AK's eighth lord; if exalted, compare AK's twelfth lord.
|
||||
Saturn is allowed.
|
||||
- Rudra compares Lagna's second/eighth lords. The weaker candidate wins when
|
||||
it alone has malefic contact; when both have contact the stronger wins.
|
||||
The frozen convention treats conjunction and rasi aspect as contact;
|
||||
the p17 exception itself says “aspect” and its discussion adds association.
|
||||
Retain this declared convention when porting rather than hiding it.
|
||||
- Equal final scores remain `blocked`; no silent degree or list-order tie-break.
|
||||
|
||||
## Node discrepancy and default
|
||||
|
||||
Printed p14 explicitly includes Rahu and Ketu in Sthana. The p15 worked
|
||||
totals reproduce when they are excluded from **that component only**.
|
||||
`include_nodes_text` is the default because it follows the explicit rule.
|
||||
`exclude_nodes_worked_example` is the reproducibility alternative, retained
|
||||
under `strength.worked_example_node_exclusion` by the builder.
|
||||
|
||||
This switch does NOT remove nodes from Kartari or malefic contacts and does
|
||||
not give them karaka points. For the published example, excluding nodes gives
|
||||
Virgo281.25 / Pisces363.75; including them gives296.25 /378.75. The other ten
|
||||
sign totals and all seven planet totals agree between policies. This is an
|
||||
observed text/table discrepancy, not proof of the author's intended erratum.
|
||||
|
||||
## Relation to 9f08b57b and 258e08e3
|
||||
|
||||
These are **different algorithms**:
|
||||
|
||||
| Field / producer | Comparison | Method / use |
|
||||
|---|---|---|
|
||||
| `lagna_seventh_strength` | First/seventh Goel numeric totals | Goel Brahma/Sthira-related explicit selector evidence |
|
||||
| Historical `niryaana_shoola_strength` | Same first/seventh totals | Deprecated frozen-export alias only |
|
||||
| `select_niryaana_shoola_start_sign` | Second/eighth | PVR Ayur ordered comparison: occupancy, all-planet rasi aspects, exalted-occupant presence |
|
||||
|
||||
`9f08b57b` implements the last row and `258e08e3` explicitly includes both
|
||||
nodes in its occupancy/aspect counts. Those changes are already on main.
|
||||
This publication does not replace them. The legacy alias now carries
|
||||
`deprecated: true`, `comparison: lagna_vs_seventh`, and `not_for` metadata.
|
||||
Use the canonical new key for Goel totals; use the existing PVR selector for
|
||||
Niryaana. Sthira's existing explicit-input short selector is a separate
|
||||
contract: do not label Goel full weights as the Chapter5 short weights, and
|
||||
do not infer that publishing these points automatically executes Sthira Dasha.
|
||||
|
||||
## Portable synthetic example
|
||||
|
||||
```python
|
||||
from scripts.goel_jaimini_strength import build_goel_chart_evidence
|
||||
|
||||
positions = dict(Sun=29., Moon=58., Mars=87., Mercury=116.,
|
||||
Jupiter=145., Venus=174., Saturn=203., Rahu=245., Ketu=65.)
|
||||
result = build_goel_chart_evidence(positions, 0.)
|
||||
```
|
||||
|
||||
This is deliberately invented positional data, not a real birth chart.
|
||||
|
||||
| Planet | Dignity | Karaka | Kendriya | Kartari | Adhipati | Total |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Sun | 60 | 60 | 60 | 0 | 0 | 180 |
|
||||
| Moon | 60 | 45 | 30 | 0 | 0 | 135 |
|
||||
| Mars | 7.5 | 30 | 15 | 60 | 60 | 172.5 |
|
||||
| Mercury | 7.5 | 22.5 | 60 | 0 | 0 | 90 |
|
||||
| Jupiter | 22.5 | 15 | 30 | 60 | 60 | 187.5 |
|
||||
| Venus | 3.75 | 7.5 | 15 | 0 | 60 | 86.25 |
|
||||
| Saturn | 60 | 3.75 | 60 | 0 | 0 | 123.75 |
|
||||
|
||||
AK=Sun. Lagna=367.5; seventh=225. Brahma=Jupiter; Maheshwara=Mars;
|
||||
Rudra=Mars. Gemini Sthana=75 (alternative60); Sagittarius Sthana=60
|
||||
(alternative0). The existing PVR Niryaana selector instead selects Taurus.
|
||||
|
||||
`tests/test_goel_jaimini_strength.py` contains these literal expectations,
|
||||
the printed p15 totals without birth identity, branch checks, input rejection,
|
||||
blocked points and native entry wiring. Run:
|
||||
|
||||
```bash
|
||||
python3 -m pytest -q tests/test_goel_jaimini_strength.py tests/test_niryaana_shoola_dasha.py
|
||||
```
|
||||
|
||||
Status: source-bounded wired, with one published numerical table replay and
|
||||
synthetic arithmetic regression. No predictive-case validation, global
|
||||
closure, PL9 parity or automatic change to a frozen customer pin is claimed.
|
||||
|
||||
Verification:166 tests passed across `test_goel_jaimini_strength`,
|
||||
`test_niryaana_shoola_dasha`, `test_sthira_brahma_goel_short_method`,
|
||||
`test_trikona_raman_native`, `test_jaimini`, `test_jaimini_source_identity`
|
||||
and `test_bphs_validation_runtime`. This includes the real full-reading CLI
|
||||
and its Goel output, as well as the existing PVR Niryaana behavior.
|
||||
|
||||
## Current-window exploration and publication boundary
|
||||
|
||||
Started from main258e08e3 in an isolated `codex/goel-publication` worktree;
|
||||
the original workspace was on customer-export-r32-release with unrelated
|
||||
dirty files, all preserved. Checked all six existing linked worktree paths,
|
||||
branch/recent-commit status, tracked file inventory, producer/CLI/report/test
|
||||
searches and the two whole-machine sweep records. WorkBuddy skill is a
|
||||
symlink to the primary repository; its registry remains a separate authority.
|
||||
The configured r32 runtime supplies the reviewed position-only Goel module;
|
||||
its hash was235fc8f90718b6da45af0ee1edc2a7c195be92664ff55bbb7b8e0398b605b33d.
|
||||
The archived 10-16 publication receipt confirms the full payload was excluded.
|
||||
The separately inspected local 10-16 r2 engine has neither this named module
|
||||
nor the two Goel output fields; therefore the archive date must not be used
|
||||
as proof of that report's actual generating pin. This extraction is from
|
||||
the inspected r32 source, not a claim that the omitted r2 payload contained it.
|
||||
Repository APIs/renderers remain consumers of the native engine, not a second
|
||||
implementation. No external engine code or book pages are republished here.
|
||||
|
||||
Pre-work check passed, including external adapter diagnostics and remote
|
||||
visibility; direct `git ls-remote origin refs/heads/main` returned258e08e3.
|
||||
The broader governance audit separately flags `main_worktree_dirty` and
|
||||
`remote_head_unverified`; the former is real and preserved, and the latter
|
||||
does not certify publication despite the successful direct visibility check.
|
||||
Capability authority snapshot and governance audit were invoked;
|
||||
the former's historical customer examples are not authority for this module.
|
||||
The reused chains are Goel position arithmetic, Raman dignity helpers,
|
||||
Jaimini rasi aspects, and the existing PVR Niryaana selector. Unclosed items
|
||||
are lineage differences, equal-strength selections without a source rule and
|
||||
independent predictive verification. This is code publication, not a chart
|
||||
interpretation, personal report export, or MEVG predictive validation.
|
||||
@@ -484,3 +484,9 @@ Prevention: 打分路径新增或改动输入模块时,先查它在不在两
|
||||
`TASK-report-chart-blank-columns-20261003` T2 写的是「`_map_to_houses_values` 增字段」,`scripts/ashtakavarga.py` 在 `sealed_holdout_rerun.PRODUCTION_FILES` 里,只加字段、分数不变也让 `test_rectification_validation_integrity_gate` 4 条与 `test_sealed_holdout_contract_freshness` 3 条失败(ERR-110 同型)。改为在 `pl9_reader_export.py` 用引擎已有字段拆分,冻结文件零改动,门禁恢复全绿,无需重新冻结。
|
||||
|
||||
Prevention: 写任务书时(不只是执行时)就对照 `PRODUCTION_FILES` 与 dataset `frozen_scoring.files`;只为展示的字段优先在消费层(reader / mapper)从已有数据推出,不进冻结文件。关联 ERR-110、BUG-1200。
|
||||
|
||||
## ERR-116 | Goel 首/七强度不是 PVR Niryaana 的二/八,完整法也不是简法 | observed 2026-10-22
|
||||
|
||||
Goel《Predicting through Jaimini Astrology》第 1 章完整强度的 `lagna_seventh_strength` 比的是命宫和第七宫的 Goel 总分。弃用别名 `niryaana_shoola_strength` 是同一组首/七数字,带 `deprecated`、`replacement`、`not_for`。我方 Niryaana Shoola(#84)的 `select_niryaana_shoola_start_sign` 在 `scripts/rashi_dashas_native.py`,比的是第二宫和第八宫的 PVR Ayur 口径。两套不能互相接入。`scripts/goel_short_strength.py` 是第 5 章简法,给 Sthira / Niryaana 等用,也不是这一套完整法。
|
||||
|
||||
Prevention: 不要把 `lagna_seventh_strength` 或 `niryaana_shoola_strength` 接到 Niryaana 选择或大运计算。不要用 `goel_jaimini_strength` 替换 `goel_short_strength`。报告里印这一章交给 #91。关联 issue #94、#84。
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 进度:Goel Jaimini 强度(issue #94,2026-10-22)
|
||||
|
||||
执行分支 `codex/goel-jaimini-strength-20261022`。工作树 `.worktrees/goel-jaimini-strength-20261022`。未 push。Python 用 `G:\Ferti\Jyotisha\.worktrees\agent-loop\.venv\Scripts\python.exe`(本工作树没有 `.venv`)。`core.autocrlf=false`。上游只读仓 `G:\Ferti\yinduzhanxing-ref`,提交 `5fb8379c`。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- T1:四个上游模块放到 `scripts/` 根目录。`ROOT_PY_BUDGET` 448 → 452,常量旁写了三栏。`scripts/*.py` 计数 452。
|
||||
- T2:测试原样搬入,只把 Niryaana 选择器的 import 改到 `scripts.rashi_dashas_native`。`test_bphs_validation_runtime.py` 新增 `test_full_reading_exposes_goel_jaimini_strength`,用文件里已有的虚构 `BASE_BIRTH_ARGS`。
|
||||
- T3:`_goel_jaimini_evidence` 接到 `cmd_jaimini`(`mode` 为 `all` 或 `special`)和 full-reading 写入 `modules.jaimini` 之前。
|
||||
- T4:接线后的口径测试(两种交点政策、七星同度 blocked 且不出 AK / 排序、full-reading 喂给 Goel 的经度)。
|
||||
- T5:三张固定虚构盘与 200 张固定种子虚构盘。没有让步缩盘:三张盘 full-reading 均值 5.212 秒,200 张 × 交点两种口径约 34.74 分钟,低于 40 分钟,mean 与 true 都跑了。
|
||||
- T6:研究笔记、ERR-116、CHANGELOG。Skill 版本不 bump。
|
||||
|
||||
## 四个 blob
|
||||
|
||||
| 文件 | 上游 `5fb8379c` | 落地后 `git hash-object` |
|
||||
| --- | --- | --- |
|
||||
| `scripts/goel_jaimini_strength.py` | `2170f781740affbad7b8ff4e048b88d87f997bcc` | `d13b3f785a86a17970e450f111d60fc8d52108ac` |
|
||||
| `scripts/trikona_raman.py` | `1fc9283396acba98b9113bde7af0e27a80841a30` | 相同 |
|
||||
| `scripts/sthira_brahma_goel_short_method.py` | `6e010b54f5ba480db3e6d8d79e711a1cc2d59fbf` | 相同 |
|
||||
| `scripts/trikona_dasha.py` | `e6c8a27cf5e23e5c686154406ba5183226d12d79` | 相同 |
|
||||
|
||||
`goel_jaimini_strength.py` 相对上游的 diff 只有 `SOURCE` 一行,LF。旧值是上游本地书稿路径,没有入库。新值是 `SOURCE = 'V. P. Goel, Predicting through Jaimini Astrology'`。
|
||||
|
||||
测试文件上游 blob `e29c47182c03c2277cff46e271a7f664c3349601`。相对上游先只改一行:
|
||||
|
||||
```
|
||||
-from scripts.niryaana_shoola_dasha import select_niryaana_shoola_start_sign
|
||||
+from scripts.rashi_dashas_native import select_niryaana_shoola_start_sign
|
||||
```
|
||||
|
||||
T4 在这个文件末尾另加了三条接线测试,不再是单行 diff。
|
||||
|
||||
## 接线与上游的两处差异
|
||||
|
||||
1. 捕获 `(ImportError, KeyError, TypeError, ValueError)` 时 `logger.warning` 记下异常类和信息,`reason` 写成 `f'{exc.__class__.__name__}: {exc}'`。这几类失败在 helper 内返回 blocked,不会抛进 full-reading 外层的 `except Exception`,因此不会把整个 `modules.jaimini` 带掉。
|
||||
2. `cmd_jaimini` 传给 Goel 的经度用 `pd.get('degree_raw', pd['degree'])`。模拟盘只有 `degree` 时回退,上游那条特殊点测试仍过。full-reading 本来的 `planet_lons` 已是 `degree_raw`,原样传给 Goel。上升点仍用原来的 `asc_deg`(`lon`)。
|
||||
|
||||
没有把 `lagna_seventh_strength` / `niryaana_shoola_strength` 接到 Niryaana 选择或大运。没有改冻结文件、报告渲染、API 主文件、数据库。
|
||||
|
||||
## T5 数据(全是虚构盘)
|
||||
|
||||
三张盘是 `tests/test_report_english_edition.py` 的 day / night / south。`PYTHONHASHSEED=0`。
|
||||
|
||||
单独计时的 full-reading:day 6.343 秒、night 4.692 秒、south 4.600 秒,均值 5.212 秒。三张都是 `executed`,Goel AK 与 `chara_karaka_7` 的 AK 都是同一颗星(Venus / Mercury / Jupiter)。四个新键都在。
|
||||
|
||||
改前引擎是接线提交的父本 `eb67962d7`。改前改后:
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| 中文完整数据版 Markdown | 三张 SHA-256 相同(day `1de47056e69f…`,night `0415e1a20dfe…`,south `51eb80c838cf…`) |
|
||||
| 英文完整数据版 Markdown | 三张 SHA-256 相同(day `3e6eaadae15e…`,night `a36abde746d5…`,south `adef6214ebbb…`) |
|
||||
| full-reading 与 `cmd_jaimini` 的 jaimini,去掉四个新键 | SHA-256 相同 |
|
||||
| 改后四个新键 | full-reading 与 `cmd_jaimini`(mode=all)都有,状态 `executed` |
|
||||
| `build_domain_facts` 选出的对话卡 | 12 个域 × 3 张盘,规范化 JSON 相同 |
|
||||
|
||||
`read_full_data_domain` 的原始副本不是逐字节相同。它整份拷贝 `worksheets`,其中 `advanced_systems.jaimini` 多了四个新键;这是任务书已经写明的资料包 JSON 变大,正文不印。另外两边还有运行时噪声:审计耗时、缓存时间、PyJHora 年盘快照、一张盘的 Yogini `reference_date`。去掉四个新键和这些噪声字段后,day / night 还剩 PyJHora 年盘快照差异,south 只剩 `reference_date`。没有为了抹平这些差异去改渲染或对话卡。
|
||||
|
||||
200 张固定种子(`Random(20261022)`,年 1940–2015、日 1–28、纬度 −45~60、经度 −120~150、时区按经度取半小时、岁差在 lahiri / raman / kp 间)。mean 与 true 各 200 次,4 个进程,墙钟 556.6 秒。
|
||||
|
||||
| 项 | 数 |
|
||||
| --- | --- |
|
||||
| 次数 | 400 |
|
||||
| `goel_full_strength.status == executed` | 400 |
|
||||
| blocked | 0 |
|
||||
| AK 与 `chara_karaka_7` 不一致 | 0 |
|
||||
| 单次 full-reading | 均值 5.537 秒,最小 4.839,最大 7.848 |
|
||||
| mean / true 均值 | 5.530 / 5.544 秒 |
|
||||
|
||||
三张盘改前改后的 full-reading 耗时互相重叠(改前均值 5.672 秒,同一次改后 6.069 秒,另一次改后计时 5.212 秒),没有拆出稳定的增量。
|
||||
|
||||
冻结身份交集(`PRODUCTION_FILES` + `RESEARCH_FILES` + `frozen_scoring.files` 对上 `origin/staging...HEAD`)输出是 `[]`。
|
||||
|
||||
## 测试
|
||||
|
||||
工作树内 `pytest -q`。Windows 默认 GBK 标准输出打不出 full-reading 里已有的 emoji(`U+1F4D0`、`U+1F534`),`test_bphs_validation_runtime.py` 两条因此在子进程退出码 1。下面的数字是 `PYTHONUTF8=1` 再跑的结果。屏蔽 PyJHora 用临时目录的 `sitecustomize.py` 放在 `PYTHONPATH` 最前(`import jhora` 变成 `ModuleNotFoundError`),没有卸载包。
|
||||
|
||||
两次都是 238 条收集、237 通过、1 跳过、0 失败。跳过的是 `test_consultation_contract_golden.py` 里未设置 `JYOTISH_LIVE_CONTRACT_GOLDEN=1` 的线上重抓。
|
||||
|
||||
| 文件 | 结果 |
|
||||
| --- | --- |
|
||||
| `tests/test_goel_jaimini_strength.py` | 22 通过 |
|
||||
| `tests/test_bphs_validation_runtime.py` | 3 通过 |
|
||||
| `tests/test_python_hygiene_budget.py` | 2 通过 |
|
||||
| `tests/test_rashi_dashas_native.py` | 18 通过 |
|
||||
| `tests/test_niryaana_shoola_dasha.py` | 3 通过 |
|
||||
| `tests/test_jaimini.py` | 42 通过 |
|
||||
| `tests/test_runtime_jhora_import_scan.py` | 4 通过 |
|
||||
| `tests/test_api_server_growth_contract.py` | 9 通过 |
|
||||
| `tests/test_consultation_contract_golden.py` | 2 通过,1 跳过 |
|
||||
| `tests/test_sealed_holdout_contract_freshness.py` | 9 通过 |
|
||||
| `tests/test_rectification_validation_integrity_gate.py` | 22 通过 |
|
||||
| `tests/test_reported_offset_research.py` | 14 通过 |
|
||||
| `tests/test_rectification_engine_memoization.py` | 25 通过 |
|
||||
| `tests/test_repo_privacy_markers.py` | 62 通过(文档提交前再复跑一次) |
|
||||
|
||||
没有弱化既有断言。没有重冻,没有改冻结记录或 golden。
|
||||
|
||||
## 移交 #91
|
||||
|
||||
本单不印报告。交给 #91:
|
||||
|
||||
- 报告印「Goel Jaimini 强度与特殊点」章,中英标签齐全。`blocked` 的中文写「无法判定」、英文写「Not determinable」,并注明原因(七星度数相同 / 强度相同无唯一选择)。
|
||||
- `jyotish_engine.py` 受限资料 `jaimini_special_points.reason` 里「Brahma/Maheshwara/Rudra still lack a same-profile structured producer」随印章一并改。
|
||||
- 交点两种口径并列印出,标明默认遵循正文(`include_nodes_text`)。
|
||||
@@ -518,7 +518,7 @@
|
||||
| `TASK-report-english-han-leak-20261021.md` | `PROGRESS-report-english-han-leak-20261021.md` | #89 个人报告英文版约 4 成整份丢弃:漏译汉字源头补英文孪生字段、修吃空格;200 张固定种子回归 + 快速门 smoke(复发自 BUG-1272);中文逐字节不变 | 待领取(最先做;#91/#92 在后) | `codex/report-english-han-leak-20261021` |
|
||||
| `TASK-chart-dasha-yogini-20261021.md` | — | #88 星盘页大运右栏切换 Chara / Yogini,Yogini 补小运(按比例,内部核对,页面注明未经外部软件核对);Ashtottari 另立单(起运表与 PyJHora 8/12 宿不一致) | 待领取(等 #86、#87 合入) | `codex/chart-dasha-yogini-20261021` |
|
||||
| `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-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` |
|
||||
| `TASK-full-data-parity-3-20261022.md` | — | #93 完整数据版补齐(三):「独立年度方法参考」正式环境调 PyJHora(子进程隔离;装上后其余输出逐字节不变,7 个换路径开关钉死);输入默认用本地时间与正确年份(D6 待产品确认) | 待领取(等 #91 合入) | `codex/full-data-parity-3-20261022` |
|
||||
| `TASK-synastry-staging-fix-fix-20261022.md` | `PROGRESS-synastry-staging-fix-20261021.md` | **#85 第 1 次打回**(T7~T11 主体通过):新地址没登记进路由清单致快速门红;CHANGELOG 与 T11 矛盾;侧栏点亮测试是空断言 | 待领取 | `codex/synastry-staging-fix-20261021` |
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Goel's full Jaimini strength and special-point profile, not Bhava Bala.
|
||||
|
||||
Source: Predicting through Jaimini Astrology, printed pp10-17 (PDF pp14-21).
|
||||
The published p15 example is the numeric regression. Dignity degree ranges
|
||||
reuse the explicitly named Raman manual profile; no universal lineage claim.
|
||||
"""
|
||||
import math
|
||||
from scripts.trikona_raman import CLASSICAL, DIGNITY_PROFILE, _dignity, _validate_positions
|
||||
from scripts.narayana_dasha import _rasi_aspects
|
||||
from scripts.sthira_brahma_goel_short_method import SIGNS, SIGN_LORDS
|
||||
|
||||
PROFILE = 'goel_full_jaimini_strength_ch1_v1'
|
||||
SOURCE = 'V. P. Goel, Predicting through Jaimini Astrology'
|
||||
KARAKA_POINTS = (60, 45, 30, 22.5, 15, 7.5, 3.75)
|
||||
MALEFICS = ('Sun', 'Mars', 'Saturn', 'Rahu', 'Ketu')
|
||||
|
||||
|
||||
def calculate_goel_strength(planet_longitudes, *, node_occupancy_policy='include_nodes_text'):
|
||||
if node_occupancy_policy not in {'include_nodes_text', 'exclude_nodes_worked_example'}:
|
||||
raise ValueError('explicit Goel node occupancy policy required')
|
||||
_validate_positions(planet_longitudes)
|
||||
signs = {p: int(v // 30) for p, v in planet_longitudes.items()}
|
||||
degrees = {p: planet_longitudes[p] % 30 for p in CLASSICAL}
|
||||
ranked = sorted(CLASSICAL, key=degrees.get, reverse=True)
|
||||
if any(math.isclose(degrees[a], degrees[b], rel_tol=0, abs_tol=1e-10)
|
||||
for i, a in enumerate(ranked) for b in ranked[i + 1:]):
|
||||
return dict(status='blocked', reason='equal_degree_seven_karaka_rank_requires_finer_positions', profile_id=PROFILE)
|
||||
ak = ranked[0]
|
||||
karakas = dict(zip(ranked, KARAKA_POINTS))
|
||||
|
||||
def contacts(sign, candidates):
|
||||
return [p for p in candidates if signs[p] == sign or _rasi_aspects(signs[p], sign)]
|
||||
|
||||
planets = {}
|
||||
for p in CLASSICAL:
|
||||
sign = signs[p]
|
||||
dignity, moola = _dignity(p, planet_longitudes[p])
|
||||
kendra = (60, 30, 15)[(sign - signs[ak]) % 3]
|
||||
before = [q for q, s in signs.items() if s == (sign - 1) % 12]
|
||||
after = [q for q, s in signs.items() if s == (sign + 1) % 12]
|
||||
kartari = 60 if sign % 2 == 0 and before and after else 0
|
||||
malefic_contacts = contacts(sign, [q for q in MALEFICS if q != p])
|
||||
adhipati = 60 if p != 'Moon' and malefic_contacts else 0
|
||||
planets[p] = dict(sign_idx=sign, degree_in_sign=degrees[p],
|
||||
dignity=dignity, mooltrikona=moola, karaka=karakas[p], kendriya=kendra,
|
||||
kartari=kartari, adhipati=adhipati, malefic_contacts=malefic_contacts,
|
||||
kartari_before=before, kartari_after=after,
|
||||
total=moola + karakas[p] + kendra + kartari + adhipati)
|
||||
rows = []
|
||||
for sign in range(12):
|
||||
lord = SIGN_LORDS[sign]
|
||||
occupants = [p for p, s in signs.items() if s == sign]
|
||||
permanent = (15, 30, 60)[sign % 3]
|
||||
karaka = sum(karakas.get(p, 0) for p in occupants)
|
||||
counted = occupants if node_occupancy_policy == 'include_nodes_text' else [p for p in occupants if p in CLASSICAL]
|
||||
sthana = 45 + 15 * len(counted) if counted else 0
|
||||
# Three roles, not distinct planets: Goel p103 explicitly explains
|
||||
# double credit when Mercury/Jupiter is also the sign lord.
|
||||
aspect_roles = [role for role, p in [('Jupiter', 'Jupiter'), ('Mercury', 'Mercury'), ('lord', lord)]
|
||||
if contacts(sign, [p])]
|
||||
aspect = 60 * len(aspect_roles)
|
||||
rows.append(dict(sign_idx=sign, sign=SIGNS[sign], lord=lord,
|
||||
permanent=permanent, karaka=karaka, sthana=sthana, occupants=occupants, counted_occupants=counted,
|
||||
aspect=aspect, aspect_roles=aspect_roles, lord_strength=planets[lord]['total'],
|
||||
total=permanent + karaka + sthana + aspect + planets[lord]['total']))
|
||||
return dict(status='executed', confidence_status='parameter_sensitive', profile_id=PROFILE,
|
||||
source=SOURCE, source_locator='PDF pp14-21; printed pp10-17; lines461-736',
|
||||
dignity_profile=DIGNITY_PROFILE, malefic_profile='Sun_Mars_Saturn_Rahu_Ketu_excluding_self',
|
||||
node_occupancy_policy=node_occupancy_policy,
|
||||
aspect_profile='jaimini_rasi_drishti', aspect_role_policy='distinct_roles',
|
||||
atmakaraka=ak, planets=planets, rashi_strengths=rows,
|
||||
claim_boundary='Named Goel full method with declared dignity ranges; not PL9 or all-lineage parity.')
|
||||
|
||||
|
||||
def build_goel_chart_evidence(planet_longitudes, ascendant_longitude):
|
||||
if (type(ascendant_longitude) not in (int, float)
|
||||
or not math.isfinite(ascendant_longitude)
|
||||
or not 0 <= ascendant_longitude < 360):
|
||||
raise ValueError('ascendant longitude must be a finite number in [0,360)')
|
||||
strength = calculate_goel_strength(planet_longitudes)
|
||||
result = dict(strength=strength, special_points={})
|
||||
if strength['status'] != 'executed':
|
||||
result['special_points'] = {
|
||||
name: dict(status='blocked', reason=strength['reason'], profile_id=PROFILE)
|
||||
for name in ('Brahma', 'Maheshwara', 'Rudra')
|
||||
}
|
||||
return result
|
||||
strength['worked_example_node_exclusion'] = calculate_goel_strength(
|
||||
planet_longitudes, node_occupancy_policy='exclude_nodes_worked_example')
|
||||
strength['source_discrepancy'] = (
|
||||
'Printed p14 includes nodes in Sthana; printed p15 worked totals omit them. '
|
||||
'Both numeric alternatives are preserved; the explicit text is the active profile.')
|
||||
lagna = int(float(ascendant_longitude) % 360 // 30)
|
||||
seventh = (lagna + 6) % 12
|
||||
planets = strength['planets']
|
||||
rashis = strength['rashi_strengths']
|
||||
ak_sign = planets[strength['atmakaraka']]['sign_idx']
|
||||
|
||||
def choose(candidates, rule):
|
||||
candidates = list(dict.fromkeys(candidates))
|
||||
scores = {p: planets[p]['total'] for p in candidates}
|
||||
winners = [p for p in candidates if scores[p] == max(scores.values())]
|
||||
return dict(status='executed' if len(winners) == 1 else 'blocked',
|
||||
value=winners[0] if len(winners) == 1 else None,
|
||||
candidates=scores, rule=rule,
|
||||
reason=None if len(winners) == 1 else 'equal_Goel_strength_no_unique_selector',
|
||||
profile_id=PROFILE, confidence_status='parameter_sensitive',
|
||||
source=SOURCE, source_locator='PDF pp20-21; printed pp16-17; lines701-736')
|
||||
|
||||
eighth_ak = SIGN_LORDS[(ak_sign + 7) % 12]
|
||||
if planets[eighth_ak]['sign_idx'] == (ak_sign + 7) % 12:
|
||||
brahma = choose([eighth_ak], 'eighth_lord_from_AK_occupies_eighth_from_AK')
|
||||
elif rashis[lagna]['total'] == rashis[seventh]['total']:
|
||||
brahma = dict(status='blocked', reason='equal_Lagna_seventh_Goel_strength', profile_id=PROFILE)
|
||||
else:
|
||||
anchor = lagna if rashis[lagna]['total'] > rashis[seventh]['total'] else seventh
|
||||
brahma = choose([SIGN_LORDS[(anchor + h - 1) % 12] for h in (6, 8, 12)
|
||||
if SIGN_LORDS[(anchor + h - 1) % 12] != 'Saturn'],
|
||||
'strongest_6_8_12_lord_from_stronger_Lagna_seventh_excluding_Saturn')
|
||||
brahma['anchor_sign_idx'] = anchor
|
||||
maheswara = choose([eighth_ak] + ([SIGN_LORDS[(ak_sign + 11) % 12]]
|
||||
if planets[eighth_ak]['dignity'] == 'exalted' else []),
|
||||
'eighth_lord_from_AK_compare_twelfth_if_exalted')
|
||||
rudra_candidates = list(dict.fromkeys([SIGN_LORDS[(lagna + 7) % 12], SIGN_LORDS[(lagna + 1) % 12]]))
|
||||
afflicted = [p for p in rudra_candidates if planets[p]['malefic_contacts']]
|
||||
rudra = choose(afflicted if len(afflicted) == 1 else rudra_candidates,
|
||||
'stronger_8th_2nd_lord_unless_only_weaker_has_malefic_contact')
|
||||
result['special_points'] = dict(Brahma=brahma, Maheshwara=maheswara, Rudra=rudra)
|
||||
result['lagna_seventh_strength'] = dict(lagna=rashis[lagna]['total'],
|
||||
seventh=rashis[seventh]['total'], profile_id=PROFILE, source=SOURCE,
|
||||
comparison='lagna_vs_seventh')
|
||||
# Frozen-export compatibility only. PVR Niryaana is second/eighth Ayur
|
||||
# comparison; callers must not route these Goel totals into that selector.
|
||||
result['niryaana_shoola_strength'] = dict(result['lagna_seventh_strength'],
|
||||
deprecated=True, replacement='lagna_seventh_strength',
|
||||
not_for='pvr_niryaana_shoola_ayur_rasi_strength_v1')
|
||||
return result
|
||||
@@ -14339,6 +14339,39 @@ def cmd_aspects(args):
|
||||
# ============================================================================
|
||||
# 17. Jaimini系统(v3.7新增)
|
||||
# ============================================================================
|
||||
def _goel_jaimini_evidence(planet_longitudes, ascendant_longitude):
|
||||
"""Shared CLI/full-reading view of the source-bounded Goel producer.
|
||||
|
||||
Failures stay inside this helper. A Goel error must not escape into the
|
||||
full-reading ``except Exception`` that drops the whole jaimini module.
|
||||
"""
|
||||
try:
|
||||
from scripts.goel_jaimini_strength import build_goel_chart_evidence
|
||||
goel = build_goel_chart_evidence(planet_longitudes, ascendant_longitude)
|
||||
fields = {
|
||||
'goel_full_strength': goel['strength'],
|
||||
'goel_special_points': goel['special_points'],
|
||||
}
|
||||
for key in ('lagna_seventh_strength', 'niryaana_shoola_strength'):
|
||||
if key in goel:
|
||||
fields[key] = goel[key]
|
||||
return fields
|
||||
except (ImportError, KeyError, TypeError, ValueError) as exc:
|
||||
logger.warning(
|
||||
"Goel Jaimini strength failed: %s: %s",
|
||||
exc.__class__.__name__,
|
||||
exc,
|
||||
)
|
||||
reason = f'{exc.__class__.__name__}: {exc}'
|
||||
return {
|
||||
'goel_full_strength': {'status': 'blocked', 'reason': reason},
|
||||
'goel_special_points': {
|
||||
name: {'status': 'blocked', 'reason': reason}
|
||||
for name in ('Brahma', 'Maheshwara', 'Rudra')
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def cmd_jaimini(args):
|
||||
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
|
||||
if chart is None:
|
||||
@@ -14411,6 +14444,11 @@ def cmd_jaimini(args):
|
||||
result['graha_padas'] = calc_graha_padas(planet_lons)
|
||||
if mode in ('all', 'special'):
|
||||
result['special_lagnas'] = calc_special_lagnas(asc_idx, args.hour, args.minute + _arg_second(args) / 60.0)
|
||||
goel_lons = {}
|
||||
for pn, pd in planets.items():
|
||||
if isinstance(pd, dict) and 'degree' in pd:
|
||||
goel_lons[pn] = pd.get('degree_raw', pd['degree'])
|
||||
result.update(_goel_jaimini_evidence(goel_lons, asc_deg))
|
||||
return _build_response_envelope('jaimini', result, args=args, execution_status='executed')
|
||||
|
||||
|
||||
@@ -16283,6 +16321,7 @@ def cmd_full_reading(args):
|
||||
except Exception as dk_e:
|
||||
jaimini_result['darakaraka'] = {'error': str(dk_e)}
|
||||
|
||||
jaimini_result.update(_goel_jaimini_evidence(planet_lons, asc_deg))
|
||||
report['modules']['jaimini'] = jaimini_result
|
||||
except Exception as e:
|
||||
report['errors'].append(f"jaimini: {e}")
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Source-bounded Brahma selector for the Goel/Rao short-method profile.
|
||||
|
||||
This is intentionally explicit-input only. It implements the short method
|
||||
described in the local Jaimini transcriptions: choose the stronger of Lagna
|
||||
and the seventh sign, inspect the 6th/8th/12th lords from that anchor, exclude
|
||||
Saturn, then rank the remaining candidates by declared strength and degree.
|
||||
It does not implement the separate odd/back-rising or BPHS override clauses.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
SIGNS = (
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
)
|
||||
SIGN_LORDS = {
|
||||
0: "Mars", 1: "Venus", 2: "Mercury", 3: "Moon",
|
||||
4: "Sun", 5: "Mercury", 6: "Venus", 7: "Mars",
|
||||
8: "Jupiter", 9: "Saturn", 10: "Saturn", 11: "Jupiter",
|
||||
}
|
||||
PROFILE_ID = "goel_rao_short_brahma_method_v1"
|
||||
|
||||
|
||||
def _candidate(
|
||||
*,
|
||||
house: int,
|
||||
anchor_sign_index: int,
|
||||
planets: dict[str, dict[str, Any]],
|
||||
strengths: dict[str, float],
|
||||
) -> dict[str, Any]:
|
||||
sign_index = (anchor_sign_index + house - 1) % 12
|
||||
lord = SIGN_LORDS[sign_index]
|
||||
planet = planets.get(lord) if isinstance(planets.get(lord), dict) else {}
|
||||
degree = planet.get("degree_in_sign", planet.get("degree"))
|
||||
return {
|
||||
"source_house": house,
|
||||
"source_sign_index": sign_index,
|
||||
"source_sign": SIGNS[sign_index],
|
||||
"planet": lord,
|
||||
"planet_sign_index": planet.get("sign_index", planet.get("sign_idx")),
|
||||
"planet_sign": planet.get("sign"),
|
||||
"degree_in_sign": float(degree) if isinstance(degree, (int, float)) else None,
|
||||
"declared_strength": strengths.get(lord),
|
||||
"saturn_excluded": lord == "Saturn",
|
||||
}
|
||||
|
||||
|
||||
def select_brahma_goel_short_method(
|
||||
*,
|
||||
lagna_sign_index: int,
|
||||
planets: dict[str, dict[str, Any]],
|
||||
planet_strengths: dict[str, float],
|
||||
lagna_strength: float,
|
||||
seventh_strength: float,
|
||||
) -> dict[str, Any]:
|
||||
"""Select Brahma under the explicit Goel/Rao short-method profile."""
|
||||
if not 0 <= int(lagna_sign_index) < 12:
|
||||
return {"status": "blocked", "reason": "lagna_sign_index must be 0..11", "candidates": []}
|
||||
if not isinstance(planets, dict) or not isinstance(planet_strengths, dict):
|
||||
return {"status": "blocked", "reason": "planets and planet_strengths are required", "candidates": []}
|
||||
if not isinstance(lagna_strength, (int, float)) or not isinstance(seventh_strength, (int, float)):
|
||||
return {"status": "blocked", "reason": "lagna_strength and seventh_strength are required", "candidates": []}
|
||||
|
||||
anchor = int(lagna_sign_index) if float(lagna_strength) >= float(seventh_strength) else (int(lagna_sign_index) + 6) % 12
|
||||
candidates = [
|
||||
_candidate(house=house, anchor_sign_index=anchor, planets=planets, strengths=planet_strengths)
|
||||
for house in (6, 8, 12)
|
||||
]
|
||||
eligible = [row for row in candidates if not row["saturn_excluded"] and row["declared_strength"] is not None]
|
||||
if not eligible:
|
||||
return {
|
||||
"profile_id": PROFILE_ID,
|
||||
"status": "blocked",
|
||||
"confidence_status": "parameter_sensitive",
|
||||
"reason": "No non-Saturn candidate has a declared strength.",
|
||||
"anchor": {"sign_index": anchor, "sign": SIGNS[anchor], "selected_by": "lagna_vs_seventh_strength"},
|
||||
"candidates": candidates,
|
||||
}
|
||||
eligible.sort(
|
||||
key=lambda row: (
|
||||
float(row["declared_strength"]),
|
||||
float(row["degree_in_sign"]) if row["degree_in_sign"] is not None else -1.0,
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
selected = eligible[0]
|
||||
return {
|
||||
"profile_id": PROFILE_ID,
|
||||
"status": "executed",
|
||||
"confidence_status": "parameter_sensitive",
|
||||
"verification_status": "source_bounded_explicit_input",
|
||||
"anchor": {
|
||||
"sign_index": anchor,
|
||||
"sign": SIGNS[anchor],
|
||||
"selected_by": "stronger_of_lagna_and_seventh",
|
||||
"lagna_strength": float(lagna_strength),
|
||||
"seventh_strength": float(seventh_strength),
|
||||
},
|
||||
"candidates": candidates,
|
||||
"selected": selected,
|
||||
"source_rule": (
|
||||
"Goel/Rao short method: strongest of 6th, 8th, and 12th lords from "
|
||||
"the stronger Lagna/7th anchor, excluding Saturn."
|
||||
),
|
||||
"unresolved": [
|
||||
"odd_sign_and_back_rising_filter_not_applied",
|
||||
"BPHS_verse_172_karaka_and_saturn_node_override_not_applied",
|
||||
"strength_source_is_declared_input_not_recomputed_here",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Separate Trikona profiles: Raghava Bhatta MD and Raman confirmed-parent AD."""
|
||||
import argparse
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
if __package__ in (None, ''):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from scripts.narayana_dasha import SIGNS, SIGN_LORDS, _NARAYANA_DIGNITY
|
||||
|
||||
PROFILE = 'trikona_raghava_bhatta_explicit_seed_md_v1'
|
||||
SOURCE = 'https://saptarishisastrology.com/trikona-dasa-a-critical-review-by-iranganti-rangacharya/'
|
||||
RAMAN_AD_PROFILE = 'trikona_raman_94_confirmed_full_md_ad_v1'
|
||||
RAMAN_SOURCE = 'https://www.scribd.com/doc/127930441/Studies-in-Jaimini-Astrology-by-b-v-Raman'
|
||||
|
||||
|
||||
def build_trikona_raman_antardasa_report(*, md_sign_idx, full_md_years,
|
||||
full_md_start, parent_source, parent_profile, year_days, profile):
|
||||
"""Raman section94 AD only; a confirmed, untruncated parent is input."""
|
||||
if profile != RAMAN_AD_PROFILE or parent_profile != 'raman_studies_jaimini_92_93':
|
||||
raise ValueError('explicit Raman AD and Raman sections92/93 parent profiles required')
|
||||
if type(md_sign_idx) is not int or not 0 <= md_sign_idx < 12:
|
||||
raise ValueError('md_sign_idx must be integer 0..11')
|
||||
if not isinstance(parent_source, str) or not parent_source.strip():
|
||||
raise ValueError('parent_source must identify the confirmed full MD, not birth balance')
|
||||
for name, value, low, high in [('full_md_years', full_md_years, 0, 12),
|
||||
('year_days', year_days, 300, 400)]:
|
||||
if (isinstance(value, bool) or not isinstance(value, (int, float)) or
|
||||
not math.isfinite(value) or not low <= value <= high or value == 0):
|
||||
raise ValueError(f'{name} outside supported finite range')
|
||||
if not isinstance(full_md_start, str):
|
||||
raise ValueError('full_md_start must be aware ISO datetime')
|
||||
anchor = datetime.fromisoformat(full_md_start.replace('Z', '+00:00'))
|
||||
if anchor.tzinfo is None or anchor.utcoffset() is None:
|
||||
raise ValueError('full_md_start must include UTC offset')
|
||||
anchor = anchor.astimezone(timezone.utc)
|
||||
# Raman section155 specifies these groups; neither parity nor ninth-from-parent.
|
||||
direction = 1 if md_sign_idx in (0, 1, 2, 6, 7, 8) else -1
|
||||
offsets = [group + trine for group in range(4) for trine in (0, 4, 8)]
|
||||
try:
|
||||
edges = [(anchor + timedelta(days=full_md_years * year_days * i / 12)).isoformat()
|
||||
for i in range(13)]
|
||||
except OverflowError as exc:
|
||||
raise ValueError('calendar boundary outside supported datetime range') from exc
|
||||
periods = [dict(sign_idx=(md_sign_idx + direction * offset) % 12,
|
||||
sign=SIGNS[(md_sign_idx + direction * offset) % 12],
|
||||
years=full_md_years / 12, start_utc=edges[i], end_utc=edges[i + 1],
|
||||
start_age=full_md_years * i / 12, end_age=full_md_years * (i + 1) / 12)
|
||||
for i, offset in enumerate(offsets)]
|
||||
result = dict(status='parameter_sensitive', variant_profile_id=profile,
|
||||
tradition_profile='bv_raman_studies_in_jaimini_astrology', source=RAMAN_SOURCE,
|
||||
source_locator='section94 pp61-62; Savya groups section155 p127',
|
||||
parent=dict(sign_idx=md_sign_idx, sign=SIGNS[md_sign_idx], years=full_md_years,
|
||||
start_utc=edges[0], end_utc=edges[-1], source=parent_source,
|
||||
profile=parent_profile, status='supplied_confirmed_full_parent'),
|
||||
year_days=year_days, direction=direction, periods=periods,
|
||||
child_periods=dict(level='AD', status='executed', count=12),
|
||||
pratyantardasa=dict(status='blocked', reason='section94_does_not_establish_PD'),
|
||||
limits=['Parent seed, duration and full start supplied, not calculated or independently verified',
|
||||
'No birth-balance clipping; do not supply a residual MD as a full parent',
|
||||
'Fixed year_days is explicit caller convention, not a source date oracle',
|
||||
'Raman AD only; never attach to Raghava Bhatta or PyJHora parents automatically',
|
||||
'No PD, event inference or default customer export'])
|
||||
lines = ['# Trikona — Raman confirmed full MD / AD', '', f'Profile: {profile}',
|
||||
f'Source: {RAMAN_SOURCE} — {result["source_locator"]}', '',
|
||||
'| AD | Years | Start UTC | End UTC |', '| --- | --- | --- | --- |']
|
||||
lines += [f"| {p['sign']} | {p['years']} | {p['start_utc']} | {p['end_utc']} |" for p in periods]
|
||||
lines += ['', *result['limits'], '', '```json', json.dumps(result, ensure_ascii=False, indent=2), '```']
|
||||
return dict(status=result['status'], customer_export_admitted=False,
|
||||
evidence_appendix=[result], exports=dict(markdown='\n'.join(lines),
|
||||
ai_evidence_bundle=deepcopy(result)))
|
||||
|
||||
|
||||
def build_trikona_native_report(*, lagna_sign_idx, seed_sign_idx, seed_source,
|
||||
planet_longitudes, epoch, year_days, profile):
|
||||
if profile != PROFILE:
|
||||
raise ValueError('explicit Raghava Bhatta profile required')
|
||||
if any(type(v) is not int or not 0 <= v < 12 for v in (lagna_sign_idx, seed_sign_idx)):
|
||||
raise ValueError('sign indices must be integers 0..11')
|
||||
if (seed_sign_idx - lagna_sign_idx) % 12 not in (0, 4, 8):
|
||||
raise ValueError('seed must be Lagna, fifth or ninth')
|
||||
if not isinstance(seed_source, str) or not seed_source.strip():
|
||||
raise ValueError('seed_source must explain the supplied strength decision')
|
||||
required = set(SIGN_LORDS.values())
|
||||
if not isinstance(planet_longitudes, dict) or set(planet_longitudes) != required:
|
||||
raise ValueError('exactly seven classical planetary longitudes required; no node co-lords')
|
||||
if any(isinstance(v, bool) or not isinstance(v, (int, float)) or
|
||||
not math.isfinite(v) or not 0 <= v < 360 for v in planet_longitudes.values()):
|
||||
raise ValueError('planetary longitudes must be finite in [0,360)')
|
||||
if isinstance(year_days, bool) or not isinstance(year_days, (int, float)) or not math.isfinite(year_days) or not 300 <= year_days <= 400:
|
||||
raise ValueError('explicit year_days 300..400 required')
|
||||
if not isinstance(epoch, str):
|
||||
raise ValueError('aware ISO epoch required')
|
||||
anchor = datetime.fromisoformat(epoch.replace('Z', '+00:00'))
|
||||
if anchor.tzinfo is None or anchor.utcoffset() is None:
|
||||
raise ValueError('epoch must include UTC offset')
|
||||
anchor = anchor.astimezone(timezone.utc)
|
||||
direction = 1 if seed_sign_idx % 2 == 0 else -1
|
||||
if seed_sign_idx % 3 == 0:
|
||||
offsets = list(range(12))
|
||||
elif seed_sign_idx % 3 == 1:
|
||||
offsets = [5 * i for i in range(12)]
|
||||
else:
|
||||
offsets = [group + trine for group in range(4) for trine in (0, 4, 8)]
|
||||
periods = []
|
||||
age = 0
|
||||
for offset in offsets:
|
||||
sign = (seed_sign_idx + direction * offset) % 12
|
||||
lord = SIGN_LORDS[SIGNS[sign]]
|
||||
lord_sign = int(planet_longitudes[lord] // 30)
|
||||
count_direction = 1 if sign % 2 == 0 else -1
|
||||
count = ((lord_sign - sign) * count_direction) % 12 + 1
|
||||
dignity = _NARAYANA_DIGNITY[lord]
|
||||
adjustment = int(lord_sign in dignity['exalted']) - int(lord_sign in dignity['debilitated'])
|
||||
years = 12 if lord_sign == sign else min(12, count + adjustment)
|
||||
end = age + years
|
||||
try:
|
||||
start_utc = (anchor + timedelta(days=age * year_days)).isoformat()
|
||||
end_utc = (anchor + timedelta(days=end * year_days)).isoformat()
|
||||
except OverflowError as exc:
|
||||
raise ValueError('calendar boundary outside supported datetime range') from exc
|
||||
periods.append(dict(sign_idx=sign, sign=SIGNS[sign], lord=lord,
|
||||
lord_sign_idx=lord_sign, inclusive_count=count, dignity_adjustment=adjustment,
|
||||
years=years, start_age=age, end_age=end, start_utc=start_utc, end_utc=end_utc))
|
||||
age = end
|
||||
result = dict(status='parameter_sensitive', variant_profile_id=PROFILE,
|
||||
tradition_profile='raghava_bhatta_as_presented_by_rangacharya', source=SOURCE,
|
||||
lagna_sign_idx=lagna_sign_idx, seed_sign_idx=seed_sign_idx,
|
||||
seed_selection=dict(status='supplied_not_calculated', source=seed_source),
|
||||
epoch_utc=anchor.isoformat(), year_days=year_days, periods=periods,
|
||||
child_periods=dict(status='blocked', reason='method_specific_AD_PD_rules_not_established'),
|
||||
limits=['Supplied strongest trine; strength selection is not calculated',
|
||||
'Full first MD begins at explicit epoch; no fractional birth balance inferred',
|
||||
'Fixed-day calendar is caller convention, not verified source dates',
|
||||
'First cycle only; no automatic customer export or event certainty'])
|
||||
lines = ['# Trikona — Raghava Bhatta / Rangacharya method 2', '',
|
||||
'Status: parameter_sensitive', f'Source: {SOURCE}', f'Profile: {PROFILE}', '',
|
||||
'| Sign | Years | Start UTC | End UTC |', '| --- | --- | --- | --- |']
|
||||
lines += [f"| {p['sign']} | {p['years']} | {p['start_utc']} | {p['end_utc']} |" for p in periods]
|
||||
lines += ['', *result['limits'], 'AD/PD: blocked — method-specific rules not established',
|
||||
'', '```json', json.dumps(result, ensure_ascii=False, indent=2), '```']
|
||||
return dict(status=result['status'], customer_export_admitted=False,
|
||||
evidence_appendix=[result], exports=dict(markdown='\n'.join(lines), ai_evidence_bundle=deepcopy(result)))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--request', type=Path, required=True)
|
||||
parser.add_argument('--mode', choices=('method2-md', 'raman-confirmed-md-ad', 'raman-native'), default='method2-md')
|
||||
parser.add_argument('--format', choices=('json', 'markdown'), default='json')
|
||||
args = parser.parse_args()
|
||||
builder = (build_trikona_raman_antardasa_report if args.mode == 'raman-confirmed-md-ad'
|
||||
else build_trikona_native_report)
|
||||
if args.mode == 'raman-native':
|
||||
from scripts.trikona_raman import build_trikona_raman_report
|
||||
builder = build_trikona_raman_report
|
||||
report = builder(**json.loads(args.request.read_text()))
|
||||
print(report['exports']['markdown'] if args.format == 'markdown' else json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Raman's numerical Rasi strength and Trikona MD/AD, explicit conventions.
|
||||
|
||||
Studies in Jaimini Astrology sections62-72,79-86,92-94. This is Raman's
|
||||
adaptation, not a universal Jaimini comparator or the Raghava Bhatta method.
|
||||
"""
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import math
|
||||
|
||||
from scripts.jaimini import FRIENDSHIP
|
||||
from scripts.narayana_dasha import SIGNS, SIGN_LORDS, _NARAYANA_DIGNITY, _rasi_aspects
|
||||
from scripts.trikona_dasha import RAMAN_AD_PROFILE, RAMAN_SOURCE, build_trikona_raman_antardasa_report
|
||||
|
||||
PROFILE = 'trikona_raman_numeric_seed_md_ad_candidate_v1'
|
||||
STRENGTH_PROFILE = 'raman_studies_62_72_numeric_v1'
|
||||
DIGNITY_PROFILE = 'raman_manual_degree_mt_exaltation_first_v1'
|
||||
CLASSICAL = ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn')
|
||||
SAVYA = {0, 1, 2, 6, 7, 8}
|
||||
AMSA = (60, 45, 30, 22.5, 15, 7.5, 3.75)
|
||||
# Raman, A Manual of Hindu Astrology, section29 printed p12. These differ
|
||||
# from the repository Shadbala table; never silently substitute that table.
|
||||
MT = {'Sun': (4, 0, 20), 'Moon': (1, 4, 30), 'Mars': (0, 0, 18),
|
||||
'Mercury': (5, 16, 20), 'Jupiter': (8, 0, 13), 'Venus': (6, 0, 10),
|
||||
'Saturn': (10, 0, 20)}
|
||||
|
||||
|
||||
def _validate_positions(positions):
|
||||
if not isinstance(positions, dict) or set(positions) != set(CLASSICAL) | {'Rahu', 'Ketu'}:
|
||||
raise ValueError('exactly seven classical planets plus Rahu and Ketu required')
|
||||
if any(type(v) not in (int, float) or not math.isfinite(v) or not 0 <= v < 360
|
||||
for v in positions.values()):
|
||||
raise ValueError('longitudes must be finite numbers in [0,360)')
|
||||
if not math.isclose((positions['Rahu'] - positions['Ketu']) % 360, 180, abs_tol=1e-7, rel_tol=0):
|
||||
raise ValueError('Rahu and Ketu must be opposite in the same position profile')
|
||||
|
||||
|
||||
def _dignity(planet, longitude):
|
||||
sign, degree = int(longitude // 30), longitude % 30
|
||||
if sign in _NARAYANA_DIGNITY[planet]['exalted']:
|
||||
return 'exalted', 60
|
||||
if sign in _NARAYANA_DIGNITY[planet]['debilitated']:
|
||||
return 'debilitated', 3.75
|
||||
mt_sign, lo, hi = MT[planet]
|
||||
if sign == mt_sign and lo <= degree < hi:
|
||||
return 'moolatrikona', 45
|
||||
owner = SIGN_LORDS[SIGNS[sign]]
|
||||
if owner == planet:
|
||||
return 'own', 30
|
||||
if owner in FRIENDSHIP[planet]['friend']:
|
||||
return 'friend', 22.5
|
||||
if owner in FRIENDSHIP[planet]['enemy']:
|
||||
return 'enemy', 7.5
|
||||
return 'neutral', 15
|
||||
|
||||
|
||||
def calculate_raman_rasi_strength(*, planet_longitudes, strength_profile,
|
||||
dignity_profile, druk_role_policy):
|
||||
if strength_profile != STRENGTH_PROFILE or dignity_profile != DIGNITY_PROFILE:
|
||||
raise ValueError('explicit Raman strength and dignity profiles required')
|
||||
if druk_role_policy not in ('distinct_planets', 'distinct_roles'):
|
||||
raise ValueError('druk_role_policy must be distinct_planets or distinct_roles')
|
||||
_validate_positions(planet_longitudes)
|
||||
signs = {p: int(v // 30) for p, v in planet_longitudes.items()}
|
||||
degrees = {p: planet_longitudes[p] % 30 for p in CLASSICAL}
|
||||
ranked = sorted(CLASSICAL, key=degrees.get, reverse=True)
|
||||
ties = [[p for p in CLASSICAL if degrees[p] == value] for value in set(degrees.values())]
|
||||
ties = sorted((group for group in ties if len(group) > 1), key=lambda group: degrees[group[0]], reverse=True)
|
||||
result = dict(profile=strength_profile, dignity_profile=dignity_profile,
|
||||
druk_role_policy=druk_role_policy, source=RAMAN_SOURCE, locator='sections62-72',
|
||||
status='blocked' if ties else 'executed', rank_ties=ties, planet_strengths={}, rasi_strengths=[])
|
||||
# Section65 does not specify equal-rank scores. Preserve own-sign strengths
|
||||
# below, but never fabricate a total/rank/Atmakaraka when a degree tie exists.
|
||||
ak = None if ties else ranked[0]
|
||||
result['atmakaraka'] = ak
|
||||
if not ties:
|
||||
for rank, planet in enumerate(ranked):
|
||||
label, dignity = _dignity(planet, planet_longitudes[planet])
|
||||
kendra = (60, 30, 15)[(signs[planet] - signs[ak]) % 3]
|
||||
result['planet_strengths'][planet] = dict(dignity_label=label,
|
||||
moolatrikona_bala=dignity, amsa_bala=AMSA[rank], kendra_bala=kendra,
|
||||
total=dignity + AMSA[rank] + kendra)
|
||||
for sign in range(12):
|
||||
lord = SIGN_LORDS[SIGNS[sign]]
|
||||
role_planets = ['Jupiter', 'Mercury', lord]
|
||||
if druk_role_policy == 'distinct_planets':
|
||||
role_planets = list(dict.fromkeys(role_planets))
|
||||
hits = [p for p in role_planets if signs[p] == sign or _rasi_aspects(signs[p], sign)]
|
||||
occupants = [p for p, position in signs.items() if position == sign]
|
||||
chara = (15, 30, 60)[sign % 3]
|
||||
sthira = 45 + 15 * len(occupants) if occupants else 0
|
||||
own = chara + 60 * len(hits) + sthira
|
||||
special = 60 if ak and signs[ak] == sign else (None if ties else 0)
|
||||
lord_strength = None if ties else result['planet_strengths'][lord]['total']
|
||||
result['rasi_strengths'].append(dict(sign_idx=sign, sign=SIGNS[sign], lord=lord,
|
||||
chara_bala=chara, druk_bala=60 * len(hits), druk_contributors=hits,
|
||||
sthira_bala=sthira, occupants=occupants, own_strength=own,
|
||||
lord_strength=lord_strength, special_strength=special,
|
||||
total=None if ties else own + lord_strength + special))
|
||||
if ties:
|
||||
result['reason'] = 'equal_degree_AMSA_rank_rule_not_established; provide finer positions or a sourced tie profile'
|
||||
return result
|
||||
|
||||
|
||||
def _duration(sign, signs, *, node_dignity_profile):
|
||||
"""Sections79-86, retaining the selected lord and decision trace."""
|
||||
direction = 1 if sign in SAVYA else -1
|
||||
classical = SIGN_LORDS[SIGNS[sign]]
|
||||
lords = [classical] + ({7: ['Ketu'], 10: ['Rahu']}.get(sign, []))
|
||||
base = {p: ((signs[p] - sign) * direction) % 12 or 12 for p in lords}
|
||||
adjustment = {p: (int(signs[p] in _NARAYANA_DIGNITY[p]['exalted']) -
|
||||
int(signs[p] in _NARAYANA_DIGNITY[p]['debilitated']))
|
||||
if p in CLASSICAL else 0 for p in lords}
|
||||
trace = ['section79_savya_count_minus_one']
|
||||
selected = lords[:]
|
||||
if len(lords) == 2:
|
||||
if signs[lords[0]] == signs[lords[1]]:
|
||||
trace.append('section82_same_own' if signs[lords[0]] == sign else 'sections83_84_conjunction')
|
||||
# Conjunction outside own sign explicitly refers to section79 only.
|
||||
years = base[lords[0]]
|
||||
return dict(status='executed', years=years, lords=selected, trace=trace,
|
||||
count_direction=direction, candidates=base, node_dignity_profile=node_dignity_profile)
|
||||
away = [p for p in lords if signs[p] != sign]
|
||||
if len(away) == 1:
|
||||
selected = away
|
||||
trace.append('section85_prefer_lord_away_from_own')
|
||||
else:
|
||||
associates = {p: sum(s == signs[p] for name, s in signs.items() if name != p) for p in lords}
|
||||
selected = [p for p in selected if associates[p] == max(associates.values())]
|
||||
trace.append('section86_associate_count')
|
||||
if len(selected) > 1:
|
||||
modes = {p: signs[p] % 3 for p in selected}
|
||||
selected = [p for p in selected if modes[p] == max(modes.values())]
|
||||
trace.append('section86_dual_fixed_movable')
|
||||
if len(selected) > 1:
|
||||
selected = [p for p in selected if base[p] == max(base[q] for q in selected)]
|
||||
trace.append('section86_larger_span')
|
||||
if classical in selected and adjustment[classical] == -1:
|
||||
return dict(status='blocked', reason='section86_over_debilitated_and_node_exaltation_exception_unresolved',
|
||||
lords=selected, candidates=base, trace=trace)
|
||||
years_by_lord = {p: (12 if signs[p] == sign else base[p] + adjustment[p]) for p in selected}
|
||||
if len(set(years_by_lord.values())) != 1:
|
||||
return dict(status='blocked', reason='equal_colord_rank_with_different_adjusted_years',
|
||||
lords=selected, candidates=years_by_lord, trace=trace)
|
||||
years = next(iter(years_by_lord.values()))
|
||||
return dict(status='executed' if years > 0 else 'blocked', years=years,
|
||||
reason=None if years > 0 else 'nonpositive_adjusted_span_no_clipping_rule',
|
||||
lords=selected, trace=trace, candidates=base, dignity_adjustments=adjustment,
|
||||
count_direction=direction, node_dignity_profile=node_dignity_profile)
|
||||
|
||||
|
||||
def build_trikona_raman_report(*, lagna_sign_idx, planet_longitudes, epoch, year_days,
|
||||
profile, strength_profile, dignity_profile, druk_role_policy, node_dignity_profile):
|
||||
if profile != PROFILE:
|
||||
raise ValueError('explicit Raman native profile required')
|
||||
if type(lagna_sign_idx) is not int or not 0 <= lagna_sign_idx < 12:
|
||||
raise ValueError('lagna_sign_idx must be integer 0..11')
|
||||
if node_dignity_profile != 'unassigned_no_adjustment':
|
||||
raise ValueError('node_dignity_profile must explicitly select unassigned_no_adjustment')
|
||||
if type(year_days) not in (int, float) or not math.isfinite(year_days) or not 300 <= year_days <= 400:
|
||||
raise ValueError('year_days must be finite in 300..400')
|
||||
if not isinstance(epoch, str):
|
||||
raise ValueError('aware ISO epoch required')
|
||||
anchor = datetime.fromisoformat(epoch.replace('Z', '+00:00'))
|
||||
if anchor.tzinfo is None or anchor.utcoffset() is None:
|
||||
raise ValueError('epoch must include UTC offset')
|
||||
anchor = anchor.astimezone(timezone.utc)
|
||||
strength = calculate_raman_rasi_strength(planet_longitudes=planet_longitudes,
|
||||
strength_profile=strength_profile, dignity_profile=dignity_profile, druk_role_policy=druk_role_policy)
|
||||
signs = {p: int(v // 30) for p, v in planet_longitudes.items()}
|
||||
trines = [(lagna_sign_idx + offset) % 12 for offset in (0, 4, 8)]
|
||||
candidates = [strength['rasi_strengths'][s] for s in trines]
|
||||
seeds = [] if strength['status'] == 'blocked' else [r['sign_idx'] for r in candidates
|
||||
if r['total'] == max(c['total'] for c in candidates)]
|
||||
durations = [dict(sign_idx=s, sign=SIGNS[s], **_duration(s, signs,
|
||||
node_dignity_profile=node_dignity_profile)) for s in range(12)]
|
||||
branches = []
|
||||
for seed in seeds:
|
||||
age = 0
|
||||
dated = True
|
||||
periods = []
|
||||
direction = 1 if seed in SAVYA else -1
|
||||
for offset in [group + trine for group in range(4) for trine in (0, 4, 8)]:
|
||||
sign = (seed + direction * offset) % 12
|
||||
row = deepcopy(durations[sign])
|
||||
if row['status'] != 'executed':
|
||||
dated = False
|
||||
if dated:
|
||||
try:
|
||||
start = (anchor + timedelta(days=age * year_days)).isoformat()
|
||||
end = (anchor + timedelta(days=(age + row['years']) * year_days)).isoformat()
|
||||
except OverflowError as exc:
|
||||
raise ValueError('calendar boundary outside supported datetime range') from exc
|
||||
row.update(start_age=age, end_age=age + row['years'], start_utc=start, end_utc=end,
|
||||
calendar_status='calculated_with_caller_fixed_year')
|
||||
child = build_trikona_raman_antardasa_report(md_sign_idx=sign,
|
||||
full_md_years=row['years'], full_md_start=start,
|
||||
parent_source='Native Raman sections79-86/92-93; see duration trace',
|
||||
parent_profile='raman_studies_jaimini_92_93', year_days=year_days, profile=RAMAN_AD_PROFILE)
|
||||
row['antardasas'] = child['evidence_appendix'][0]['periods']
|
||||
# Parent and child formulas can round sub-microsecond floating
|
||||
# days differently; share the exact already-calculated edge.
|
||||
row['antardasas'][-1]['end_utc'] = end
|
||||
age += row['years']
|
||||
else:
|
||||
row.update(calendar_status='blocked_by_unresolved_prior_or_current_duration', antardasas=[])
|
||||
periods.append(row)
|
||||
branches.append(dict(seed_sign_idx=seed, direction=direction,
|
||||
status='parameter_sensitive' if dated else 'partial', periods=periods))
|
||||
result = dict(status='blocked' if not seeds else 'parameter_sensitive', variant_profile_id=profile,
|
||||
tradition_profile='bv_raman_studies_in_jaimini_astrology', source=RAMAN_SOURCE,
|
||||
strength=strength, seed_selection=dict(status='blocked' if not seeds else 'parallel_tie' if len(seeds)>1 else 'calculated',
|
||||
candidates=candidates, selected_signs=seeds), duration_evidence=durations, branches=branches,
|
||||
epoch_utc=anchor.isoformat(), year_days=year_days, node_dignity_profile=node_dignity_profile,
|
||||
pratyantardasa=dict(status='blocked', reason='Raman_section94_does_not_establish_PD'),
|
||||
limits=['Raman numerical adaptation, not universal Jaimini strength',
|
||||
'Druk role deduplication is explicit; source does not resolve duplicate roles',
|
||||
'Degree MT follows Raman Manual section29; exaltation checked first by explicit profile',
|
||||
'Node dignity left unassigned by caller convention, not a universal no-exaltation doctrine',
|
||||
'Full first MD starts at caller epoch; no fractional birth-balance inferred',
|
||||
'Fixed year_days not source-date verified; first cycle only; no customer admission'])
|
||||
lines = ['# Trikona — Raman numerical seed / MD / AD', '', f'Profile: {profile}',
|
||||
f'Source: {RAMAN_SOURCE}', '', '## Strength and seed',
|
||||
'| Trine sign | Strength |', '| --- | --- |']
|
||||
lines += [f"| {r['sign']} | {r['total']} |" for r in candidates]
|
||||
for branch in branches:
|
||||
lines += ['', f"## Seed {SIGNS[branch['seed_sign_idx']]}",
|
||||
'| MD | Years | Start UTC | End UTC |', '| --- | --- | --- | --- |']
|
||||
lines += [f"| {r['sign']} | {r.get('years', 'blocked')} | {r.get('start_utc', 'blocked')} | {r.get('end_utc', 'blocked')} |"
|
||||
for r in branch['periods']]
|
||||
lines += ['', *result['limits'], '', '```json', json.dumps(result, ensure_ascii=False, indent=2), '```']
|
||||
return dict(status=result['status'], customer_export_admitted=False, evidence_appendix=[result],
|
||||
exports=dict(markdown='\n'.join(lines), ai_evidence_bundle=deepcopy(result)))
|
||||
@@ -63,3 +63,12 @@ def test_varga_cli_d9_uses_same_navamsa_mapping_as_varga_full() -> None:
|
||||
assert short["ascendant"] == full["Ascendant"]["sign"]
|
||||
for planet in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]:
|
||||
assert short[planet]["sign"] == full[planet]["sign"]
|
||||
|
||||
|
||||
def test_full_reading_exposes_goel_jaimini_strength() -> None:
|
||||
report = _run_engine("full-reading", *BASE_BIRTH_ARGS)
|
||||
goel = report["modules"]["jaimini"]
|
||||
assert goel["goel_full_strength"]["status"] == "executed"
|
||||
assert len(goel["goel_full_strength"]["rashi_strengths"]) == 12
|
||||
assert set(goel["goel_special_points"]) == {"Brahma", "Maheshwara", "Rudra"}
|
||||
assert goel["niryaana_shoola_strength"]["deprecated"] is True
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Hand-checked synthetic positions, plus Goel's printed p15 worked table.
|
||||
|
||||
No private birth data; these fixtures exercise supplied-position arithmetic.
|
||||
"""
|
||||
from copy import deepcopy
|
||||
import importlib
|
||||
import importlib.util
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SYNTHETIC = dict(Sun=29., Moon=58., Mars=87., Mercury=116.,
|
||||
Jupiter=145., Venus=174., Saturn=203., Rahu=245., Ketu=65.)
|
||||
|
||||
|
||||
def module():
|
||||
assert importlib.util.find_spec('scripts.goel_jaimini_strength'), 'Goel producer must be published'
|
||||
return importlib.import_module('scripts.goel_jaimini_strength')
|
||||
|
||||
|
||||
def test_synthetic_five_component_totals_and_special_points():
|
||||
original = deepcopy(SYNTHETIC)
|
||||
result = module().build_goel_chart_evidence(SYNTHETIC, 0.)
|
||||
strength = result['strength']
|
||||
assert strength['atmakaraka'] == 'Sun'
|
||||
assert {p: row['total'] for p, row in strength['planets'].items()} == dict(
|
||||
Sun=180, Moon=135, Mars=172.5, Mercury=90, Jupiter=187.5,
|
||||
Venus=86.25, Saturn=123.75)
|
||||
assert {p: row['value'] for p, row in result['special_points'].items()} == dict(
|
||||
Brahma='Jupiter', Maheshwara='Mars', Rudra='Mars')
|
||||
assert strength['rashi_strengths'][0]['total'] == 367.5
|
||||
assert strength['rashi_strengths'][6]['total'] == 225
|
||||
assert SYNTHETIC == original
|
||||
|
||||
|
||||
def test_node_policy_only_changes_sthana_and_sign_total():
|
||||
m = module()
|
||||
text = m.calculate_goel_strength(SYNTHETIC)
|
||||
example = m.calculate_goel_strength(SYNTHETIC, node_occupancy_policy='exclude_nodes_worked_example')
|
||||
assert text['planets'] == example['planets']
|
||||
# Gemini has Mars+Ketu; Sagittarius has only Rahu.
|
||||
assert (text['rashi_strengths'][2]['sthana'], example['rashi_strengths'][2]['sthana']) == (75, 60)
|
||||
assert (text['rashi_strengths'][8]['sthana'], example['rashi_strengths'][8]['sthana']) == (60, 0)
|
||||
assert text['rashi_strengths'][2]['total'] - example['rashi_strengths'][2]['total'] == 15
|
||||
assert text['rashi_strengths'][8]['total'] - example['rashi_strengths'][8]['total'] == 60
|
||||
|
||||
|
||||
def test_goel_printed_p15_numeric_example_without_birth_identity():
|
||||
positions = dict(Sun=20+18/60, Moon=210+20+15/60, Mars=210+23+2/60,
|
||||
Mercury=30+11+12/60, Jupiter=150+3+13/60,
|
||||
Venus=330+17+28/60, Saturn=7+7/60,
|
||||
Rahu=330+5+58/60, Ketu=150+5+58/60)
|
||||
result = module().calculate_goel_strength(positions, node_occupancy_policy='exclude_nodes_worked_example')
|
||||
assert {p: r['total'] for p, r in result['planets'].items()} == dict(
|
||||
Sun=240, Moon=93.75, Mars=210, Mercury=97.5, Jupiter=101.25,
|
||||
Venus=172.5, Saturn=146.25)
|
||||
assert [r['total'] for r in result['rashi_strengths']] == [
|
||||
412.5, 337.5, 217.5, 228.75, 330, 281.25, 247.5, 465,
|
||||
281.25, 221.25, 236.25, 363.75]
|
||||
|
||||
|
||||
def test_equal_karaka_degrees_block_all_special_point_choices():
|
||||
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Moon=59.), 0.)
|
||||
assert result['strength']['status'] == 'blocked'
|
||||
assert all(result['special_points'][p]['status'] == 'blocked'
|
||||
for p in ('Brahma', 'Maheshwara', 'Rudra'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad', [math.nan, math.inf, -1., 360., True, '0'])
|
||||
def test_invalid_ascendant_rejected(bad):
|
||||
with pytest.raises(ValueError):
|
||||
module().build_goel_chart_evidence(SYNTHETIC, bad)
|
||||
|
||||
|
||||
def test_legacy_lagna_seventh_alias_does_not_replace_pvr_second_eighth():
|
||||
result = module().build_goel_chart_evidence(SYNTHETIC, 0.)
|
||||
legacy = result['niryaana_shoola_strength']
|
||||
assert (legacy['lagna'], legacy['seventh']) == (367.5, 225)
|
||||
assert legacy['deprecated'] is True
|
||||
assert legacy['comparison'] == 'lagna_vs_seventh'
|
||||
from scripts.rashi_dashas_native import select_niryaana_shoola_start_sign
|
||||
pvr = select_niryaana_shoola_start_sign(ascendant_longitude=0., planet_longitudes=SYNTHETIC)
|
||||
assert pvr['selected_sign_idx'] == 1
|
||||
assert pvr['profile'] == 'pvr_niryaana_shoola_ayur_rasi_strength_v1'
|
||||
|
||||
|
||||
def test_brahma_ak_eighth_occupancy_overrides_lagna_selector():
|
||||
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Sun=239., Mercury=86.), 0.)
|
||||
assert result['special_points']['Brahma']['value'] == 'Mercury'
|
||||
assert result['special_points']['Brahma']['rule'] == 'eighth_lord_from_AK_occupies_eighth_from_AK'
|
||||
|
||||
|
||||
def test_exalted_ak_eighth_lord_compares_twelfth_for_maheshwara():
|
||||
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Mars=297.), 0.)
|
||||
assert result['special_points']['Maheshwara']['value'] == 'Jupiter'
|
||||
assert result['special_points']['Maheshwara']['candidates'] == {'Mars': 150, 'Jupiter': 187.5}
|
||||
|
||||
|
||||
def test_rudra_weaker_malefic_contact_exception():
|
||||
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Sun=329., Rahu=125., Ketu=305.), 0.)
|
||||
planets = result['strength']['planets']
|
||||
assert planets['Mars']['total'] == 127.5
|
||||
assert planets['Mars']['malefic_contacts'] == []
|
||||
assert planets['Venus']['total'] == 101.25
|
||||
assert planets['Venus']['malefic_contacts'] == ['Mars']
|
||||
assert result['special_points']['Rudra']['value'] == 'Venus'
|
||||
|
||||
|
||||
def test_incomplete_chart_entry_preserves_blocked_points():
|
||||
from scripts.jyotish_engine import _goel_jaimini_evidence
|
||||
result = _goel_jaimini_evidence({}, 0.)
|
||||
assert result['goel_full_strength']['status'] == 'blocked'
|
||||
assert all(r['status'] == 'blocked' for r in result['goel_special_points'].values())
|
||||
assert 'niryaana_shoola_strength' not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad', [dict(SYNTHETIC, Sun=math.nan),
|
||||
dict(SYNTHETIC, Ketu=66.), {p: v for p, v in SYNTHETIC.items() if p != 'Saturn'}])
|
||||
def test_incomplete_or_inconsistent_positions_rejected(bad):
|
||||
with pytest.raises(ValueError):
|
||||
module().calculate_goel_strength(bad)
|
||||
|
||||
|
||||
def test_jaimini_special_entry_exposes_real_goel_results(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
from scripts import jyotish_engine as engine
|
||||
chart = {'planets': {p: {'degree': v} for p, v in SYNTHETIC.items()},
|
||||
'ascendant': {'degree': 0.}}
|
||||
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
|
||||
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
|
||||
assert result.get('goel_full_strength', {}).get('atmakaraka') == 'Sun'
|
||||
assert result['goel_special_points']['Brahma']['value'] == 'Jupiter'
|
||||
assert result['niryaana_shoola_strength']['deprecated'] is True
|
||||
|
||||
|
||||
def test_wired_special_entry_keeps_both_node_policies(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
from scripts import jyotish_engine as engine
|
||||
chart = {'planets': {p: {'degree': round(v, 4), 'degree_raw': v} for p, v in SYNTHETIC.items()},
|
||||
'ascendant': {'degree': 0., 'lon': 0.}}
|
||||
chart['planets']['Sun']['degree'] = 1.0
|
||||
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
|
||||
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
|
||||
strength = result['goel_full_strength']
|
||||
assert strength['status'] == 'executed'
|
||||
assert strength['node_occupancy_policy'] == 'include_nodes_text'
|
||||
exclusion = strength['worked_example_node_exclusion']
|
||||
assert exclusion['node_occupancy_policy'] == 'exclude_nodes_worked_example'
|
||||
assert strength['atmakaraka'] == 'Sun'
|
||||
assert abs(strength['planets']['Sun']['degree_in_sign'] - (SYNTHETIC['Sun'] % 30)) <= 1e-6
|
||||
|
||||
|
||||
def test_wired_equal_degrees_block_without_atmakaraka_or_rank(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
from scripts import jyotish_engine as engine
|
||||
positions = dict(SYNTHETIC, Moon=59.)
|
||||
chart = {'planets': {p: {'degree': v} for p, v in positions.items()},
|
||||
'ascendant': {'degree': 0.}}
|
||||
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
|
||||
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
|
||||
strength = result['goel_full_strength']
|
||||
assert strength['status'] == 'blocked'
|
||||
assert strength['reason'] == 'equal_degree_seven_karaka_rank_requires_finer_positions'
|
||||
assert 'atmakaraka' not in strength
|
||||
assert 'planets' not in strength
|
||||
for name in ('Brahma', 'Maheshwara', 'Rudra'):
|
||||
point = result['goel_special_points'][name]
|
||||
assert point['status'] == 'blocked'
|
||||
assert 'value' not in point
|
||||
assert 'candidates' not in point
|
||||
|
||||
|
||||
def test_full_reading_feeds_goel_degree_raw(monkeypatch):
|
||||
import scripts.goel_jaimini_strength as goel
|
||||
from scripts.jyotish_engine import cmd_full_reading
|
||||
captured = []
|
||||
real = goel.build_goel_chart_evidence
|
||||
|
||||
def wrapped(longitudes, ascendant):
|
||||
captured.append(dict(longitudes))
|
||||
return real(longitudes, ascendant)
|
||||
|
||||
monkeypatch.setattr(goel, 'build_goel_chart_evidence', wrapped)
|
||||
report = cmd_full_reading(type('Args', (), {
|
||||
'name': 'fictional',
|
||||
'year': 1990, 'month': 1, 'day': 1, 'hour': 12, 'minute': 0, 'second': 0,
|
||||
'lat': 39.9, 'lon': 116.4, 'tz': 8.0,
|
||||
'node_mode': 'mean', 'ayanamsa': 'lahiri',
|
||||
'today': '2026-10-22', 'transit_date': '2026-10-22',
|
||||
'target_year': 2026, 'age': 36,
|
||||
'birth_time_accuracy': 'confirmed',
|
||||
})())
|
||||
assert captured, 'full-reading did not call Goel'
|
||||
planets = report['modules']['chart']['planets']
|
||||
expected = {
|
||||
name: pdata['degree_raw']
|
||||
for name, pdata in planets.items()
|
||||
if isinstance(pdata, dict) and 'degree_raw' in pdata
|
||||
}
|
||||
assert expected
|
||||
for longitudes in captured:
|
||||
assert set(longitudes) == set(expected)
|
||||
for name, raw in expected.items():
|
||||
assert abs(float(longitudes[name]) - float(raw)) <= 1e-6
|
||||
strength = report['modules']['jaimini']['goel_full_strength']
|
||||
assert strength['status'] == 'executed'
|
||||
assert strength['node_occupancy_policy'] == 'include_nodes_text'
|
||||
assert strength['worked_example_node_exclusion']['node_occupancy_policy'] == 'exclude_nodes_worked_example'
|
||||
@@ -18,11 +18,12 @@ SCRIPTS = ROOT / "scripts"
|
||||
# 本地新写的静默处理仍不得出现;允许的命中只有这一处。
|
||||
SILENT_EXCEPT_BUDGET = 1
|
||||
UPSTREAM_SILENT_EXCEPT = "scripts/classical_astrology.py:12558"
|
||||
# 原值:446
|
||||
# 新值:448
|
||||
# 原因:产品 2026-10-18(issue #81 选 A):上游原样文件按上游路径放在 scripts/ 根目录,
|
||||
# classical_astrology.py、unified_western_reading.py 两个;本地新写的文件仍不得增加。
|
||||
ROOT_PY_BUDGET = 448
|
||||
# 原值:448
|
||||
# 新值:452
|
||||
# 原因:产品 2026-10-18 #81 决策 A 先例,issue #94:上游原样文件 goel_jaimini_strength、
|
||||
# trikona_raman、sthira_brahma_goel_short_method、trikona_dasha 按上游路径放根目录;
|
||||
# 本地新写的文件仍不得增加。
|
||||
ROOT_PY_BUDGET = 452
|
||||
|
||||
|
||||
def _silent_handlers() -> list[str]:
|
||||
|
||||
Reference in New Issue
Block a user