Merge annual-narayana-conditional-20261112 into staging (accepted by Claude)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8 # Conflicts: # CHANGELOG.md # docs/BUG_HISTORY.md
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-11-12 — 年运报告将 Narayana 列为条件参考
|
||||
|
||||
- 未决种子的具名分支独立列出,窗口来源数及把握等级依据独立证据。
|
||||
- 本轮沿用当前 Skill 版本(BUG-1855)。
|
||||
|
||||
## 2026-11-12 — 个人报告的 Mudda 表、上升行和技法审计措辞
|
||||
|
||||
- Mudda 覆盖天数保留四位小数,说明引用报告内的完整时间线,中文后续年表沿用首年表头。
|
||||
|
||||
@@ -25071,3 +25071,17 @@
|
||||
- 防复发:合成段落覆盖书名和普通引语、第 7 宫与空宫位、候选与形成、同句与分句,以及其他节继续显示。
|
||||
- 关联记录:#207、#192、BUG-1851、BUG-1852。
|
||||
- 修复版本:`codex/reading-postcheck-gaps-20261112`。
|
||||
|
||||
## BUG-1855 | 年运把 Narayana 计入独立证据且隐藏未决候选
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
- 首次发现 / 最近更新:2026-11-12 / 2026-11-12。
|
||||
- 影响面:年运报告的本命阶段、转折窗口、证据文案及把握上限。
|
||||
- 现象:Narayana 的转折点会增加独立来源数,同向阶段可能抬升把握。未决种子时年包只有阻塞状态及候选星座。
|
||||
- 触发条件:年运包起运未决,或窗口和阶段引用 Narayana。
|
||||
- 根因:年包只消费确定的 Rath 时间轴,窗口聚类和阶段判断把 Narayana 当成一票,前端用同向结果抬级,方法戳沿用旧常量。
|
||||
- 修复:年包使用原生具名分支,范围内原始期间逐支保留。窗口及阶段将 Narayana 标为条件参考,取消同向抬级,方法戳取当前引擎值。
|
||||
- 验证:合成盘和公开盘回归核对候选、方法戳、原始行及改前改后的独立来源数。完整检查见本单进度。
|
||||
- 防复发:后端验证单来源不成窗、条件事件不能桥接窗口,前端验证同向不抬级和候选说明。
|
||||
- 关联记录:#209、#190、BUG-1601、BUG-1650、BUG-1732、BUG-1830、BUG-1842。
|
||||
- 修复版本:`codex/annual-narayana-conditional-20261112`。
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 年运 Narayana 条件参考 · 2026-11-12
|
||||
|
||||
Gitea #209,BUG-1855。按 Claude 评论 42202 认领。任务书在分支首提交 `9e0abcaf5`,基线 `1ba0364a7a8c0f964c3e21c21cb304aaa1899375`。
|
||||
|
||||
## 实施
|
||||
|
||||
- `package.py::_narayana.rath` 在主运未定时,以报告范围起点年龄和范围长度调用引擎的 `narayana_branches`。若引擎默认上限截断,按原始分支总数再取一次。年包逐支保留引擎 ID、原有名称或 ID 同名标签、父分支、原始 MD/AD 行和 `used_for_scoring: false`,仅过滤与报告范围没有交集或长度为零的行。保留 `alternative_seed_signs`。有候选时包层标为 `computed`,参考状态标 `parameter_sensitive`,不提供主时间轴行。
|
||||
- `report_facts.py::_narayana_method` 读取引擎方法常量、`SOURCE` 和 `profile_config()`,未提供的字段为 `null`。`build_cards` 生成方法戳及条件参考区,按原标识保留未决分支。
|
||||
- `report_facts.py::build_cards` 与 `_phases` 将本命 Narayana 的转折点及阶段资料标为条件参考。`_narayana_unresolved` 把具名候选标为未决。`_reading` 保留 Narayana 的主题和各领域倾向,阶段主判及支持、压力来源按 Vimshottari 取值。
|
||||
- `report_facts.py::_windows` 在聚类前排除 Narayana。现有的至少两个独立来源规则处理单来源情形,未满足时不生成窗口。落在已成窗核心时段内且与窗口主题重叠的 Narayana 点单列条件参考,独立计数和窗界由其他来源生成。
|
||||
- `annual-report-sections.ts::confidenceCap` 删除同向抬升,保留 C 上限。`annual-report-generate.ts` 在写作调用中补充条件参考规则和候选信息,在附录说明口径并逐支列出候选。审计表归 #210。
|
||||
|
||||
## 实测和文案
|
||||
|
||||
用同一张仓库公开名人盘跑改前与改后年运窗口。两边各 6 个窗口。改前独立来源数依时间顺序为 `3, 4, 3, 3, 3, 3`,其中 6 个窗口计入 Narayana。改后为 `2, 3, 2, 2, 2, 3`,没有窗口计入 Narayana。12 个 Narayana 转折点仍可供参考,1 个窗口附上同主题条件参考。
|
||||
|
||||
用户可见文案见年运附录。
|
||||
|
||||
- 「阶段以本命 Vimshottari 为主要依据。Narayana 是条件参考,不作为独立印证,采用 Sanjay Rath 版,旧算法供对照。」
|
||||
- 「Narayana 有 N 个候选分支,各自列出,不合并。Narayana 是条件参考,不作为独立印证。」候选逐行写「Narayana 条件参考(不作为独立印证) 」及其名称或 ID。
|
||||
- 「本命大运作背景,小运和细运分领域判断。Narayana 是条件参考,不作为独立印证,可说明主题是否被激活。」
|
||||
- 「宫主只有掌管或落入某领域的宫才进入该领域,吉星凶星同一标准。功能吉凶说明倾向。」
|
||||
- 「Varsha Narayana:移植上游 PVR 30.4 太阳角距版,从 Muntha 起,太阳每走 3° 为一名义年。星座强弱用 Rath 规则,与本命运限分开。条件参考,不作为独立印证。」
|
||||
- 「每节先写盘面依据,再写解读。Narayana 是条件参考,不作为独立印证。把握上限为 C。A 需要你过往事件验证。」
|
||||
- 方法差异一行写「Narayana 两种算法小运不同。Sanjay Rath 版作条件参考,旧算法供对照。」旧快照中的方向差异一行补「Narayana 是条件参考,不作为独立印证。」
|
||||
|
||||
## 既有断言变更
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `test_annual_report_facts.py` 的财富主题激活 | 只匹配 `via == dasha_sign` | 同时要求 `counts_as_support == false` | 验证保留主题、只作条件参考。 |
|
||||
| 同文件的分领域阶段 | Narayana 单独给出压力时总标签 `split` | Vimshottari 判为 `partial/support`,该感情节主判为 `none`,Narayana 压力仍在原轨 | 阶段总评按主证据。 |
|
||||
| 同文件的异向阶段 | 感情主判 `tracks_differ` | Vimshottari 主判 `support`,仍核对两轨为 `support` 和 `pressure` | 保留异向资料供对照。 |
|
||||
|
||||
## 验证
|
||||
|
||||
- Windows 开工预检整体退出码 1,远端核对与外部适配器诊断完成。缺历史 `.workbuddy` 镜像路径的碎片扫描断言在未改动的 staging 复现,错误台账已有同型记录。
|
||||
- 新增 Python 回归及调整后的阶段定向测试共 5 项通过。合成盘使用原引擎计算,核对分支全量、同一报告范围的原始行,以及种子已决时的原时间轴行为。
|
||||
- 前端年运定向套件 36 项通过。`tsc --noEmit -p .` 通过,ESLint 为 0 错误、62 警告。
|
||||
- `pytest tests/ -k annual` 最终复跑,任务分支 161 项通过、3 项失败,staging 157 项通过、3 项失败。失败名称相同,分别是 `test_report_reader_main.py` 的年度日期、两套生产者值,以及 `test_timeline_boundaries_r3.py` 的年度连续性。
|
||||
- `npm run build` 通过,`/` 为静态页面。首页引用 39 个资源,逐文件 gzip -9 后 JS 为 515,860 字节,CSS 为 90,886 字节,两项均与 staging 相同。
|
||||
- Python 快速门为 1,463 项通过、3 项失败、2 项跳过,另有 29 个子测试通过。三项失败均在 staging 复现,位于 `test_upstream_technique_layers.py`,分别涉及上游固定值、Dasha 注册和 Kaala 对拍。
|
||||
- 最终前端全量为 3,655 项,3,628 项通过、27 项失败、0 项取消(退出码 1)。staging 为 3,653 项,3,623 项通过、30 项失败、0 项取消。逐项比对保留全部原有测试并新增 2 项,任务分支的 27 项失败均出现在 staging。仅 staging 失败的 3 项为数据库集成用例。
|
||||
- 浏览器中的真实模型写作留待独立验收。
|
||||
- 仓库隐私检查 62 项通过。
|
||||
@@ -0,0 +1,100 @@
|
||||
# TASK:年运报告的 Narayana 改为条件参考,种子未决时列出具名候选(2026-11-12)
|
||||
|
||||
- Gitea:#209
|
||||
- 分支:`codex/annual-narayana-conditional-20261112`(基于 staging `1ba0364a7a8c0f964c3e21c21cb304aaa1899375`)
|
||||
- Bug 编号:BUG-1855。开工时核对 `docs/BUG_HISTORY.md` 末尾;若已被占用,顺延,并在 PROGRESS 写明。
|
||||
- 负责验收:Claude(jyotisha-de)
|
||||
|
||||
不得自行写“产品决定/裁定”;任务书没写的取舍一律选需拍板。
|
||||
|
||||
不得修改或删除既有断言来让测试通过;确需修改必须写原值/新值/原因,并在 PROGRESS 里逐条列出。
|
||||
|
||||
## 背景
|
||||
|
||||
年运报告(产品里的「年运」功能,和个人报告是两套)在两处违反占星师对 Narayana 的裁定。
|
||||
|
||||
**她的裁定**
|
||||
|
||||
- Narayana 是有条件的参考,不能当作与 Vimshottari 并列的第二套独立证据。
|
||||
- 出处:聊天裁定 2026-11-07 Q1;第五轮裁定 2026-11-08 第 1 条。
|
||||
- 种子或分支未决时,按第七轮 R2 的 D 方案处理:保留所有具名候选和分支 id,各分支独立列出,不拼接,不参与打分。
|
||||
- 出处:第七轮回复 2026-11-09。
|
||||
- 个人报告已经按此改过,见 #190、`docs/tasks/PROGRESS-report-narayana-shadbala-20261111.md`。
|
||||
|
||||
**我们的现状**(行号以 staging 为准,大约位置)
|
||||
|
||||
1. `scripts/annual_service/package.py` 的 `rath()`(约 :1580–1587):
|
||||
- 种子未决时,整层返回 `blocked`,只给出 `alternative_seed_signs`;
|
||||
- 从不调用 `scripts/narayana_rath.py::narayana_branches`(:1748)。
|
||||
2. `scripts/annual_service/report_facts.py`:
|
||||
- Narayana 被当作窗口的独立系统计入:约 :645 的 `add(... "narayana" ...)`,以及 :85、:1245 一带;
|
||||
- 方法戳 `_narayana_method()`(约 :170–188)是写死的 11-06 版本。
|
||||
3. `frontend/src/lib/annual-report-sections.ts` 约 :546–556:Narayana 与 Vimshottari 方向一致时,把等级上限从 C 抬到 B。
|
||||
|
||||
## 要做的
|
||||
|
||||
1. **种子未决时列出具名候选。**
|
||||
- `package.py` 在 `calc_narayana_rath` 没有给出 `mahadasha` 时,调用 `narayana_rath.narayana_branches(lagna, lons, current_age=<报告年起始的年龄>)`。
|
||||
- 把每个分支的 id、名称、MD/AD 行(落在报告年覆盖范围内的)原样放进年包,每个分支标 `used_for_scoring: false`。
|
||||
- 原来的 `alternative_seed_signs` 保留。
|
||||
- 只调用 `narayana_rath.py`,不改它。
|
||||
2. **不再当独立证据。**
|
||||
- `report_facts.py` 里,Narayana 不参与窗口的“两套系统一致”计数,也不算独立来源。
|
||||
- 窗口和阶段里仍可以写“Narayana 激活了哪个主题”,标为条件参考。具体在哪些函数改,自己定位,逐处列进 PROGRESS。
|
||||
3. **去掉等级抬升。**
|
||||
- `annual-report-sections.ts` 里“Vimshottari 与 Narayana 一致就到 B”的分支删掉,上限不因 Narayana 改变。
|
||||
- 其他分级逻辑不动:分级规则等占星师答复后另开单。
|
||||
4. **方法戳改为现算。**
|
||||
- `_narayana_method()` 改为从 `narayana_rath` 模块现有的常量和函数读出 profile、种子规则和分支默认值,不再写死字符串。
|
||||
- 读不到的字段写 `null`,不编。
|
||||
5. **文案。**
|
||||
- 给模型的证据和页面上,凡是写到 Narayana 的地方,标明“条件参考,不作为独立印证”。
|
||||
- 种子未决时写“有 N 个候选分支,各自列出,不合并”。
|
||||
- 措辞放在现有的文案常量里。新增的中文如果是给用户看的,在 PROGRESS 列出原文。
|
||||
|
||||
## 要改的文件
|
||||
|
||||
- `scripts/annual_service/package.py`:只改 `rath()` 一带。
|
||||
- `scripts/annual_service/report_facts.py`
|
||||
- `frontend/src/lib/annual-report-sections.ts`:只删 Narayana 抬级那一段。
|
||||
- 必要时:`frontend/src/lib/annual-report-generate.ts` 里给模型的 Narayana 文案。
|
||||
- 新建 `tests/test_annual_narayana_conditional_20261112.py`;如需要,新建一个前端测试文件。
|
||||
- `docs/BUG_HISTORY.md`:只在末尾追加一条。
|
||||
- `docs/tasks/PROGRESS-annual-narayana-conditional-20261112.md`
|
||||
|
||||
## 不要碰
|
||||
|
||||
- 冻结文件:`scripts/narayana_*.py`(只许 import)、`scripts/jyotish_engine.py`、`scripts/jyotish_api_server.py`、`scripts/yoga_engine.py`、`scripts/consultation_native_layers.py`、`scripts/tajika_mudda.py`、`scripts/shadbala*.py`。
|
||||
- 个人报告:`scripts/report_render/**`、`frontend/src/lib/personal-report*`。
|
||||
- `references/**`。
|
||||
- 年运报告的审计表 `_technique_audit`:另一张单在改,见 #210。
|
||||
|
||||
## 验收标准(写进新测试)
|
||||
|
||||
1. 一张种子未决的盘(自己找或合成)生成年包时,Narayana 层不是整层 `blocked`,而是带 ≥2 个具名分支,每个分支 `used_for_scoring == false`。
|
||||
2. 一张种子已决的盘,原有行为和行不变。
|
||||
3. 窗口打分时,同一窗口 Vimshottari 与 Narayana 方向一致,等级上限不变(不出现 B);系统计数里不含 Narayana。
|
||||
4. `_narayana_method()` 的字段值等于 `narayana_rath` 模块的常量。用测试锁住,模块常量变化时测试会失败。
|
||||
5. 现有年运测试全部通过:`tests/test_annual*`、`frontend/tests/annual-*`。
|
||||
|
||||
## 自测命令
|
||||
|
||||
```
|
||||
python -m pytest -q tests/test_annual_narayana_conditional_20261112.py tests/ -k annual
|
||||
cd frontend && npx tsx --test tests/annual-*.test.ts* && npx tsc --noEmit -p . && npx eslint .
|
||||
python scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
|
||||
```
|
||||
|
||||
## 进度记录
|
||||
|
||||
写在 `docs/tasks/PROGRESS-annual-narayana-conditional-20261112.md`,内容包括:
|
||||
|
||||
- 改动处的清单;
|
||||
- 窗口计数改动前后的对比,用一张公开名人盘;
|
||||
- 新增的用户可见文案;
|
||||
- 测试结果,以及在 staging 上也复现的基线失败。
|
||||
|
||||
## 何时停下来选“需拍板”
|
||||
|
||||
- `narayana_branches` 的输出需要改冻结文件才能接上。
|
||||
- 去掉 Narayana 计数后,某个窗口因为只剩一套系统而规则无法判断,而且现有代码没有给出单系统时的处理。
|
||||
@@ -1391,6 +1391,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
|
||||
|
||||
## 年运报告与开始遮罩(2026-11-01,issue #153 / #154)
|
||||
|
||||
- **年运附录(BUG-1855):** Narayana 标为条件参考。种子未决时在口径说明中列出候选数,每支单独一行。
|
||||
- **入口**:「我的报告」最上面两个横条并排(手机上下排,2026-11-03 方案 A,见 Personal report centre):「年运」横条是 `Sun` 图标方块、标题、一行「这一年每段怎么安排,几个要留意的时间点」,右边描边按钮「打开年运」(手机「打开」),直接打开 `/reports/annual`。没有单独的入口页。个人报告横条上不写份数。
|
||||
- **地址**:`/reports/annual`(`?report=<id>` 打开某一份),放在 `/reports` 下,侧栏仍亮「我的报告」。沿用二级页外壳,标题「年运」,标题旁是人物切换。正文一栏 `min(720px, 100%)` 居中。
|
||||
- **遮罩**:报告的章节骨架在后面虚着(灰标题 + 两条微光条),上面一层 `--color-canvas` 72% 半透明 + 6px 模糊,正中一张卡片(`min(520px, 100%)`、圆角 20px、细边、`--shadow-elevated`,超出时卡内滚动):范围小字「YYYY-MM-DD 到 YYYY-MM-DD」,衬线标题「某人的这一年」;三个胶囊切换「生日年度 / 公历年 / 未来十二个月」(`ToggleGroup`,选中描边 `--color-action-strong`、文字 `--color-action`);三条注意事项(主色勾);出生时间没确认时一行浅底提示,本人和别人都带「先做生时校正」链接(#160 起可以给档案里的其他人做校正);「补充现在的情况(可以不填)」折叠,展开后是关系状态、工作 / 学习、最关心的(最多三项)、已有的计划(一行输入)、不想看的;居中实心按钮「开始写年运」(最小 220×52,手机占满宽),下面一行「本次消耗 N 点,写到一半断掉不扣」。价格读不到时这一行写「计费配置不可用」,按钮不可点。没有别的提示小字。
|
||||
|
||||
@@ -116,6 +116,19 @@ const YEAR_KEY_CN: Readonly<Record<string, string>> = {
|
||||
Lagna: "年盘上升", year_lord: "年主", Muntha: "Muntha", mudda_start: "Mudda 起运星", status: "年盘",
|
||||
};
|
||||
|
||||
const NARAYANA_REFERENCE = "Narayana 是条件参考,不作为独立印证";
|
||||
const NARAYANA_WRITING_RULE = `${NARAYANA_REFERENCE}。提到 Narayana 时,写明这是条件参考。方向一致只表示方向相同,不抬高把握。未决分支分开列,不合并,不作结论。`;
|
||||
|
||||
function narayanaCandidateLines(facts: unknown): string[] {
|
||||
const reference = record(record(record(facts).cards).conditional_narayana);
|
||||
const branches = rows(reference.branches);
|
||||
if (!branches.length) return [];
|
||||
return [
|
||||
`Narayana 有 ${branches.length} 个候选分支,各自列出,不合并。${NARAYANA_REFERENCE}。`,
|
||||
...branches.map((branch) => `Narayana 条件参考(不作为独立印证) ${str(branch.name) || str(branch.branch_id)}`),
|
||||
];
|
||||
}
|
||||
|
||||
function structureName(key: string): string {
|
||||
const year = /^Y(\d{4}):(.+)$/.exec(key);
|
||||
if (year) return `${year[1]} 年${YEAR_KEY_CN[year[2]] ?? year[2]}`;
|
||||
@@ -131,10 +144,10 @@ function conflictLine(row: Row): string {
|
||||
if (row.code === "birth_range_sampled") return `${label}:误差范围内每 ${String(row.resolution_minutes)} 分钟取样、变化处逐分钟定位;没变化的只能说「已检查样本一致」,不是整段都核实过`;
|
||||
if (row.code === "annual_chart_birth_range_not_checked") return `${label}:年盘上升和 Mudda / Patyayini 的日期按所用出生时间算,没有在误差两端重算`;
|
||||
if (row.code === "phase_split") return `${label}:${str(row.phase_id)} 这一段各领域方向不同,分开写,不合成一个总评`;
|
||||
if (row.code === "tracks_differ") return `${label}:${str(row.phase_id)} Vimshottari 和 Narayana 对同一领域方向相反,两边都写`;
|
||||
if (row.code === "tracks_differ") return `${label}:${str(row.phase_id)} Vimshottari 和 Narayana 对同一领域方向相反。${NARAYANA_REFERENCE}。`;
|
||||
if (row.code === "birth_time_not_confirmed") return `${label}:出生时间还没确认,上升、细分盘和大运边界都会跟着动,窗口已放宽`;
|
||||
if (row.code === "phase_counterweight") return `${label}:${str(row.phase_id)} 这一段支持和压力都在,按加权后的主调写,反方向的一面照实写`;
|
||||
if (row.code === "narayana_profiles_differ") return `${label}:${str(row.phase_id)} Narayana 两种算法小运不同,只按 Sanjay Rath 版计入`;
|
||||
if (row.code === "narayana_profiles_differ") return `${label}:${str(row.phase_id)} Narayana 两种算法小运不同。Sanjay Rath 版作条件参考,旧算法供对照。`;
|
||||
if (row.code === "block_blocked") return `${label}:${str(row.path)} 没有算出来(${str(row.reason)})`;
|
||||
return `${label}:${str(row.code)}`;
|
||||
}
|
||||
@@ -189,20 +202,20 @@ export function buildAppendix(facts: unknown, range: AnnualSnapshot["range"], bi
|
||||
id: "method",
|
||||
title: "这份报告的口径",
|
||||
lines: [
|
||||
// BUG-1601 (#166 Q9): Rath is the Narayana skeleton; legacy is comparison only.
|
||||
"阶段骨架看本命大运(Vimshottari)和 Narayana;Narayana 用 Sanjay Rath 版,旧算法只作对照,不算第二份支持",
|
||||
`阶段以本命 Vimshottari 为主要依据。${NARAYANA_REFERENCE},采用 Sanjay Rath 版,旧算法供对照。`,
|
||||
...narayanaCandidateLines(facts),
|
||||
// BUG-1650 (astrologer 2026-11-06): no weighted sum, per domain and per track.
|
||||
"本命大运作背景;小运、细运和 Narayana 小运分领域、分体系判断,再对照两套体系;不加权求和,正负不互相抵消",
|
||||
"宫主只有掌管或落入某领域的宫才进入该领域,吉星凶星同一标准;功能吉凶只是倾向,不是结果;Narayana 运所在星座只表示主题被激活",
|
||||
`本命大运作背景,小运和细运分领域判断。${NARAYANA_REFERENCE},可说明主题是否被激活。`,
|
||||
"宫主只有掌管或落入某领域的宫才进入该领域,吉星凶星同一标准。功能吉凶说明倾向。",
|
||||
"只判断了部分领域时只写这些领域,不概括整段;和前一段相同的写「延续前一阶段的主题」;「平稳」暂不使用",
|
||||
"宫主的功能吉凶、所管宫、所落宫、尊贵、燃烧分别记录;Maraka 单独记录,不作为负分;罗睺计都参考定位星,保留自身位置",
|
||||
"各领域方向一致才有主调,不一致就分领域写,看不出就写看不出;基调只决定建议的方向,正文不写顺紧总评",
|
||||
"年内展开看 Mudda 和 Patyayini,不参与本命运限判断;年盘分四层记:事项关联、Tajika 联系(九种结构判断,来源未核定,只列联系、不作因素)、阻碍、五分力(偏弱不等于压力);只列支持与压力因素,不下成败结论",
|
||||
// 产品 2026-11-07 占星师方案:年盘完整层。
|
||||
"年盘另看:Deeptamsa 内的 Tajika 相位、五分力与 Harsha、年盘 Shadbala(年盘时刻重算,与本命分开)、Saham(PVR 表 74,按回归时刻昼夜取式,参数敏感)",
|
||||
"Varsha Narayana:移植上游 PVR 30.4 太阳角距版,从 Muntha 起,太阳每走 3° 为一名义年,星座强弱用本产品 Narayana(Rath)规则;参数敏感,与本命 Narayana 分开",
|
||||
"Varsha Narayana:移植上游 PVR 30.4 太阳角距版,从 Muntha 起,太阳每走 3° 为一名义年。星座强弱用 Rath 规则,与本命运限分开。条件参考,不作为独立印证。",
|
||||
"行运另看:Sade Sati 阶段(土星在本命月亮第 12、1、2 座)、慢星所在星座的本命 Ashtakavarga(BAV / SAV)、木星土星的 Vedha(只查慢星,参数敏感)",
|
||||
"写法:每节先写盘面依据再写解读;本命两轨(Vimshottari、Narayana)分开判断,不一致两边都写;把握按 A/B/C 标,A 需要你过往事件验证,这份报告最高 B",
|
||||
`每节先写盘面依据,再写解读。${NARAYANA_REFERENCE}。把握上限为 C。A 需要你过往事件验证。`,
|
||||
"行运只算木星、土星、罗睺计都的换宫、停滞和精确经过本命点,只收与本命或报告领域有关的;木星、土星按 Parashari 星座级相照(木星 5、7、9,土星 3、7、10,同座另记)关联各领域的本命星,相照只是关联,不代表好坏;不用统一容许度;天象日期不等于事情发生的日子",
|
||||
`转折窗口:${String(config.window_cluster_days ?? 21)} 天内、指向同一领域、至少 ${String(config.window_min_independent_systems ?? 2)} 个互相独立的来源;罗睺计都算一条轴,Mudda 和 Patyayini 算一层,同一颗慢星的几次经过算一个来源;这些是展示参数,不是统计验证`,
|
||||
"精确相照时刻(木星 120°/180°/240°、土星 60°/180°/270°,及同座)单独列表:只记达到角度的时刻,同一次顺逆往返算一个序列,不设影响范围,也不进转折窗口",
|
||||
@@ -332,11 +345,14 @@ export async function writeAnnualReport(input: Readonly<{
|
||||
input.signal.addEventListener("abort", onAbort, { once: true });
|
||||
try {
|
||||
const written = await input.write({
|
||||
system: systemPrompt(),
|
||||
system: `${systemPrompt()}\n${NARAYANA_WRITING_RULE}`,
|
||||
user: userPrompt({
|
||||
name: input.subject.name,
|
||||
entry,
|
||||
evidence: sectionEvidence(input.facts, entry, input.situation),
|
||||
evidence: {
|
||||
...sectionEvidence(input.facts, entry, input.situation),
|
||||
Narayana参考: [NARAYANA_REFERENCE, ...narayanaCandidateLines(input.facts)],
|
||||
},
|
||||
failures,
|
||||
written: writtenSoFar(),
|
||||
}),
|
||||
|
||||
@@ -539,19 +539,12 @@ function transitEvidence(facts: unknown): Row {
|
||||
|
||||
/**
|
||||
* The highest confidence a section may claim (ai-reading-workflow-prompt [A]/[B]/[C]):
|
||||
* A needs the user's own past events, which this report never has; B needs
|
||||
* independent systems agreeing (a window from two sources, and both natal tracks
|
||||
* pointing the same way in at least one phase); otherwise C.
|
||||
* A needs the user's own past events. The current annual report has no rule
|
||||
* for raising the cap to B using a conditional reference.
|
||||
*/
|
||||
export function confidenceCap(facts: unknown, domain?: AnnualDomain): "B" | "C" {
|
||||
const all = rows(cards(facts).phases);
|
||||
const windows = rows(cards(facts).windows);
|
||||
const domainWindows = domain ? windows.filter((window) => Array.isArray(window.theme_domains) && (window.theme_domains as string[]).includes(domain)) : windows;
|
||||
const agree = all.some((phase) => (domain ? [domain] : [...ANNUAL_DOMAINS]).some((key) => {
|
||||
const tracks = record(record(record(record(phase.lean).domains)[key]).tracks);
|
||||
return str(tracks.vimshottari) !== "none" && str(tracks.vimshottari) === str(tracks.narayana);
|
||||
}));
|
||||
return domainWindows.length > 0 && agree ? "B" : "C";
|
||||
export function confidenceCap(facts: unknown, domain?: AnnualDomain): "B" | "C";
|
||||
export function confidenceCap(): "B" | "C" {
|
||||
return "C";
|
||||
}
|
||||
|
||||
function pointsOf(facts: unknown): Map<string, Row> {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildAppendix, rangeView } from "../src/lib/annual-report-generate";
|
||||
import { confidenceCap } from "../src/lib/annual-report-sections";
|
||||
|
||||
const captured = JSON.parse(readFileSync(new URL("./fixtures/annual-report-facts.json", import.meta.url), "utf8")).facts;
|
||||
const seed = JSON.parse(readFileSync(new URL("./fixtures/narayana-branches-20261109/seed.json", import.meta.url), "utf8"));
|
||||
|
||||
test("the annual window cannot raise confidence for matching Vimshottari and Narayana directions", () => {
|
||||
assert.ok(captured.cards.windows.some((window: { theme_domains: string[] }) => window.theme_domains.includes("relationship")));
|
||||
assert.ok(captured.cards.phases.some((phase: { lean: { domains: { relationship: { tracks: Record<string, string> } } } }) => {
|
||||
const tracks = phase.lean.domains.relationship.tracks;
|
||||
return tracks.vimshottari === "support" && tracks.narayana === "support";
|
||||
}));
|
||||
assert.equal(confidenceCap(captured, "relationship"), "C");
|
||||
});
|
||||
|
||||
test("the annual appendix describes candidate branches separately as conditional references", () => {
|
||||
const facts = structuredClone(captured);
|
||||
const branches = seed.product.branches.slice(0, 2) as Array<{ branch_id: string }>;
|
||||
facts.cards.conditional_narayana = { branches };
|
||||
const appendix = buildAppendix(facts, rangeView(facts, "birthday_year", "annual:birthday_year:2026"), {
|
||||
known: true, beforeMinutes: 0, afterMinutes: 0, source: "family_exact", rectification: "none",
|
||||
});
|
||||
const method = appendix.find((entry) => entry.id === "method");
|
||||
assert.ok(method);
|
||||
assert.ok(method.lines.some((line) => line.includes("Narayana 有 2 个候选分支,各自列出,不合并")));
|
||||
for (const branch of branches) {
|
||||
assert.ok(method.lines.some((line) => line.includes(branch.branch_id) && line.includes("不作为独立印证")));
|
||||
}
|
||||
});
|
||||
@@ -1581,10 +1581,27 @@ def _narayana(ctx: _Context, natal: dict, range_start: datetime, range_end: date
|
||||
nr = _module("narayana_rath")
|
||||
result = nr.calc_narayana_rath(lagna % 12, lons, cycle_count=2)
|
||||
if not result.get("mahadasha"):
|
||||
return block("blocked", "narayana_rath.calc_narayana_rath",
|
||||
start_age = (range_start - ctx.birth_utc).total_seconds() / 86400.0 / NARAYANA_AGE_YEAR_DAYS
|
||||
end_age = (range_end - ctx.birth_utc).total_seconds() / 86400.0 / NARAYANA_AGE_YEAR_DAYS
|
||||
window = {"current_age": start_age, "window_years": end_age - start_age}
|
||||
candidates = nr.narayana_branches(lagna % 12, lons, **window)
|
||||
if candidates["emitted_count"] < candidates["branch_count"]:
|
||||
candidates = nr.narayana_branches(lagna % 12, lons, **window, cap=candidates["branch_count"])
|
||||
branches = []
|
||||
for branch in candidates["branches"]:
|
||||
rows = {key: [row for row in branch.get(key, [])
|
||||
if row["start_age"] < end_age and row["end_age"] > start_age
|
||||
and row["end_age"] > row["start_age"]]
|
||||
for key in ("md_rows", "ad_rows")}
|
||||
branches.append({**branch, **rows, "name": branch.get("name") or branch["branch_id"],
|
||||
"used_for_scoring": False})
|
||||
return block("computed" if branches else "blocked", "narayana_rath.calc_narayana_rath",
|
||||
reason=str(result.get("reason") or result.get("status") or "seed_undecided"),
|
||||
producer_status=result.get("status"),
|
||||
alternative_seed_signs=sorted((result.get("alternatives") or {}).keys()))
|
||||
alternative_seed_signs=sorted((result.get("alternatives") or {}).keys()),
|
||||
branch_schema=candidates["schema"], branches=branches, branch_count=len(branches),
|
||||
reference_status="parameter_sensitive" if branches else "blocked",
|
||||
used_by_product_timeline=False, used_for_scoring=False)
|
||||
md_rows = _age_rows(result["mahadasha"], to_utc, zone, range_start, range_end, level="MD")
|
||||
zero_rows = [{"sign": md.get("sign"), "cycle_index": md.get("cycle_index"), "start_age": md.get("start_age")}
|
||||
for md in result["mahadasha"] if md.get("years") == 0]
|
||||
|
||||
@@ -168,27 +168,32 @@ def _domains_for(houses: list[int]) -> list[str]:
|
||||
|
||||
|
||||
def _narayana_method() -> dict[str, Any]:
|
||||
constants = {
|
||||
"profile": "PROFILE", "duration_profile": "DURATION_PROFILE",
|
||||
"role_profile": "ROLE_PROFILE", "ad_start": "AD_START_PROFILE", "branch_schema": "BRANCH_SCHEMA",
|
||||
"branch_cap": "BRANCH_ENUMERATION_CAP", "branch_window_years": "BRANCH_WINDOW_YEARS",
|
||||
"unresolved": "UNRESOLVED_METHOD_NOTES", "unresolved_effect": "UNRESOLVED_EFFECT",
|
||||
}
|
||||
profiles = {
|
||||
"start_sign": "seed", "dual_lord_rule": "dual_lord", "ad_rule": "ad_direction",
|
||||
"years_profile": "years", "strength_scope": "strength_scope", "saturn_ketu": "saturn_ketu",
|
||||
"start_profile": "start", "clock": "clock", "algorithm_version": "algorithm_version",
|
||||
}
|
||||
try:
|
||||
rath = ap._module("narayana_rath")
|
||||
except Exception: # noqa: BLE001 - the stamp is descriptive; the block reports failures
|
||||
return {"profile": "sanjay_rath", "status": "module_unavailable"}
|
||||
return {**dict.fromkeys((*constants, *profiles, "md_direction")), "status": "module_unavailable"}
|
||||
get_config = getattr(rath, "profile_config", None)
|
||||
config = get_config() if callable(get_config) else getattr(rath, "DEFAULT_STEP_PROFILES", {})
|
||||
return {
|
||||
"profile": getattr(rath, "PROFILE", "sanjay_rath"),
|
||||
"start_sign": "stronger_of_lagna_and_7th_seven_levels",
|
||||
"md_direction": "table_10_saturn_ketu_exceptions",
|
||||
"duration_profile": getattr(rath, "DURATION_PROFILE", None),
|
||||
"dual_lord_rule": getattr(rath, "DUAL_LORD_PROFILE", None),
|
||||
"role_profile": getattr(rath, "ROLE_PROFILE", None),
|
||||
"ad_rule": "arambha_rasi_twelve_equal_parts_table_13",
|
||||
# 第二轮第 7 条:p.66 不是「七层全平后用 AK」的第八条通用规则,原文回到规则 (5)(6) 的比较,待核。
|
||||
"unresolved": ["rath_p66_indira: returns to rules (5)/(6) and the Chara Karaka comparison; pending review, not a general AK rule",
|
||||
"AD direction: J.S. 1.1.34 / 2.4.31-32 with the p.50 Parashara reconciliation, reviewed together; not applied",
|
||||
"same-sign dual lords with equal true longitude: undecided (display-precision ties excluded)"],
|
||||
"unresolved_effect": "a phase whose Narayana period depends on an unresolved branch gives no direction",
|
||||
**{key: getattr(rath, attr, None) for key, attr in constants.items()},
|
||||
**{key: config.get(attr) for key, attr in profiles.items()},
|
||||
"md_direction": getattr(rath, "SOURCE", {}).get("md_order"),
|
||||
}
|
||||
|
||||
|
||||
CONFIG["narayana_method"] = _narayana_method()
|
||||
NARAYANA_CONDITIONAL_NOTE = "条件参考,不作为独立印证"
|
||||
|
||||
|
||||
# ── birth range ────────────────────────────────────────────────────────────
|
||||
@@ -644,7 +649,8 @@ def build_cards(package: dict, natal: dict, samples: list[dict] | None = None) -
|
||||
for row in skeleton_rows:
|
||||
add(_jd(row["start"]), row["start"], "narayana", f"{str(row.get('level')).lower()}_change",
|
||||
[sign_house(row.get("sign"))], sign=row.get("sign"), lord=row.get("lord"), level=row.get("level"),
|
||||
profile=CONFIG["narayana_skeleton_profile"], basis="natal_lagna_house")
|
||||
profile=CONFIG["narayana_skeleton_profile"], basis="natal_lagna_house",
|
||||
counts_as_support=False, note=NARAYANA_CONDITIONAL_NOTE)
|
||||
transits = package.get("slow_transits") or {}
|
||||
context = package.get("transit_context") if isinstance(package.get("transit_context"), dict) else {}
|
||||
av_rows = {(row.get("body"), _jd(row.get("at"))): row for row in _rows(context.get("ashtakavarga"))}
|
||||
@@ -680,6 +686,13 @@ def build_cards(package: dict, natal: dict, samples: list[dict] | None = None) -
|
||||
domains = _domain_cards(natal, charts, phases, windows, events)
|
||||
conflicts = _conflicts(package, phases, natal, events)
|
||||
config = {key: list(value) if isinstance(value, tuple) else value for key, value in CONFIG.items()}
|
||||
config["narayana_method"] = _narayana_method()
|
||||
rath = narayana.get("rath") or {}
|
||||
conditional_narayana = {
|
||||
"status": rath.get("reference_status", rath.get("status")),
|
||||
"note": NARAYANA_CONDITIONAL_NOTE, "counts_as_support": False,
|
||||
"branches": rath.get("branches", []), "branch_count": rath.get("branch_count", 0),
|
||||
}
|
||||
return ap.block(
|
||||
"computed", "annual_service.report_facts.build_cards",
|
||||
config=config,
|
||||
@@ -688,6 +701,7 @@ def build_cards(package: dict, natal: dict, samples: list[dict] | None = None) -
|
||||
sky_events_not_personal=dropped_sky, transit_relations_at_start=in_force,
|
||||
exact_contacts=_exact_contacts(transits, natal),
|
||||
windows=windows, year_overview=year_overview, domains=domains, conflicts=conflicts,
|
||||
conditional_narayana=conditional_narayana,
|
||||
transit_context={
|
||||
"status": context.get("status") or "blocked",
|
||||
"sade_sati": _rows(context.get("sade_sati")),
|
||||
@@ -934,28 +948,27 @@ def _reading(lords: dict[str, Any], natal: dict, functional: dict, narayana_unre
|
||||
entry = {"planet": item["planet"], "level": key, "houses": touched}
|
||||
if item["role"] == "support":
|
||||
sup = True
|
||||
support.append(entry)
|
||||
contributed.add(item["planet"])
|
||||
if track != "narayana":
|
||||
support.append(entry)
|
||||
contributed.add(item["planet"])
|
||||
elif item["role"] == "pressure":
|
||||
pre = True
|
||||
pressure.append(entry)
|
||||
contributed.add(item["planet"])
|
||||
if track != "narayana":
|
||||
pressure.append(entry)
|
||||
contributed.add(item["planet"])
|
||||
tracks[track] = "conflict" if sup and pre else "support" if sup else "pressure" if pre else "none"
|
||||
active = [verdict for verdict in tracks.values() if verdict not in ("none", "unresolved")]
|
||||
if not active:
|
||||
verdict = "none"
|
||||
elif len(set(active)) == 1:
|
||||
verdict = active[0]
|
||||
elif "conflict" in active:
|
||||
verdict = "conflict"
|
||||
else:
|
||||
verdict = "tracks_differ"
|
||||
background = [{"level": key, "planet": item["planet"], "role": item["role"]}
|
||||
verdict = tracks["vimshottari"]
|
||||
background = [{"level": key, "planet": item["planet"], "role": item["role"],
|
||||
**({"counts_as_support": False, "note": NARAYANA_CONDITIONAL_NOTE}
|
||||
if key.startswith("narayana_") else {})}
|
||||
for item in items for key in CONFIG["lean_background"]
|
||||
if key in item["levels"] and set(item["natal_houses"]) & wanted]
|
||||
activated = [{**row, "counts_as_support": False, "note": NARAYANA_CONDITIONAL_NOTE}
|
||||
if row["level"].startswith("narayana_") else row
|
||||
for row in activations if set(row["houses"]) & wanted]
|
||||
domains[domain] = {
|
||||
"verdict": verdict, "direction_basis": "functional_tendency_only", "tracks": tracks,
|
||||
"activated": [row for row in activations if set(row["houses"]) & wanted],
|
||||
"activated": activated,
|
||||
"support": support, "pressure": pressure, "background": background,
|
||||
}
|
||||
judged = {domain: card["verdict"] for domain, card in domains.items() if card["verdict"] != "none"}
|
||||
@@ -1030,6 +1043,7 @@ def _phases(package, charts, vim_rows, skeleton_rows, reference_rows, events, fu
|
||||
"year_chart": chart["target_year"] if chart else None,
|
||||
"vimshottari": {"md": (md or {}).get("lord"), "ad": (ad or {}).get("lord"), "pd": (pd or {}).get("lord")},
|
||||
"narayana": {"profile": CONFIG["narayana_skeleton_profile"],
|
||||
"counts_as_support": False, "note": NARAYANA_CONDITIONAL_NOTE,
|
||||
"md": {"sign": (nmd or {}).get("sign"), "lord": (nmd or {}).get("lord"),
|
||||
"house": sign_house((nmd or {}).get("sign"))},
|
||||
"ad": {"sign": (nad or {}).get("sign"), "lord": (nad or {}).get("lord"),
|
||||
@@ -1087,6 +1101,8 @@ def _sade_sati_at(package: dict, jd: float) -> dict[str, Any]:
|
||||
def _narayana_unresolved(package: dict, md_row: dict | None) -> bool:
|
||||
"""The Rath period rests on a branch the astrologer has not settled (second round, item 7)."""
|
||||
rath = (package["natal_periods"].get("narayana") or {}).get("rath") or {}
|
||||
if rath.get("reference_status") == "parameter_sensitive":
|
||||
return True
|
||||
seed = str(rath.get("seed_basis") or "")
|
||||
if any(flag in seed for flag in ("book_example", "undecided", "rath_p66")):
|
||||
return True
|
||||
@@ -1234,8 +1250,10 @@ def _clusters(events: list[dict]) -> list[list[dict]]:
|
||||
|
||||
|
||||
def _windows(events: list[dict], phases: list[dict], range_lo: float, range_hi: float, obs_jd: float) -> list[dict]:
|
||||
conditional_events = [event for event in events if event["system"] == "narayana"]
|
||||
pool = [event for event in events
|
||||
if not (event["system"] == "tithi_pravesha" and not CONFIG["tithi_pravesha_in_windows"])]
|
||||
if event["system"] != "narayana"
|
||||
and not (event["system"] == "tithi_pravesha" and not CONFIG["tithi_pravesha_in_windows"])]
|
||||
# 原值:21 天内的事件不论主题都聚成一组;新值:只把指向同一领域的事件聚在一起
|
||||
# 原因:占星师 2026-11-06 第 7 条「日期接近但指向不同领域,也不算互相印证」(BUG-1650)
|
||||
by_ids: dict[tuple[str, ...], dict[str, Any]] = {}
|
||||
@@ -1282,6 +1300,11 @@ def _windows(events: list[dict], phases: list[dict], range_lo: float, range_hi:
|
||||
for index, entry in enumerate(picked, start=1):
|
||||
cluster, systems = entry["cluster"], entry["systems"]
|
||||
lo, hi = cluster[0]["jd"], cluster[-1]["jd"]
|
||||
conditional = [{"turning_point_id": event["id"], "system": event["system"],
|
||||
"domains": sorted(set(event["domains"]) & set(entry["themes"])),
|
||||
"counts_as_support": False, "note": NARAYANA_CONDITIONAL_NOTE}
|
||||
for event in conditional_events if lo <= event["jd"] <= hi
|
||||
and set(event["domains"]) & set(entry["themes"])]
|
||||
zone = _zone_from(cluster[0]["instant"])
|
||||
shifts = [event["birth_shift_days"] for event in cluster if event.get("birth_shift_days")]
|
||||
before = -min((s[0] for s in shifts), default=0.0)
|
||||
@@ -1319,6 +1342,7 @@ def _windows(events: list[dict], phases: list[dict], range_lo: float, range_hi:
|
||||
"independent_systems": systems,
|
||||
"independent_count": len(systems),
|
||||
"turning_point_ids": [event["id"] for event in cluster],
|
||||
"conditional_references": conditional,
|
||||
"theme_domains": entry["themes"],
|
||||
"vimshottari_levels": sorted({str(e.get("level")) for e in cluster if e["system"] == "vimshottari"}),
|
||||
"phase_id": phase["id"] if phase else None,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Annual Narayana uses real engine branches but keeps them out of scoring."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import random
|
||||
|
||||
from annual_service import api, package, report_facts
|
||||
import narayana_rath as nr
|
||||
from scripts.research.capture_narayana_branch_fixtures_20261109 import CASES, SEED
|
||||
from tests.test_annual_report_facts import _body
|
||||
|
||||
|
||||
def _unresolved_birth() -> dict:
|
||||
# Replay the repository's pinned synthetic seed fixture with its location.
|
||||
rng = random.Random(SEED)
|
||||
lo, hi = datetime(1940, 1, 1), datetime(2010, 12, 31, 23, 59)
|
||||
minutes = int((hi - lo).total_seconds() // 60)
|
||||
for _ in range(CASES["seed"] + 1):
|
||||
birth = lo + timedelta(minutes=rng.randrange(minutes))
|
||||
lat, lon = rng.uniform(-55, 65), rng.uniform(-180, 180)
|
||||
return _body(year=birth.year, month=birth.month, day=birth.day,
|
||||
hour=birth.hour, minute=birth.minute, lat=lat, lon=lon, tz=round(lon / 15),
|
||||
timezone_id=None, report_range={"kind": "calendar_year", "year": 2026})
|
||||
|
||||
|
||||
def _age(at: datetime, ctx: package._Context) -> float:
|
||||
return (at - ctx.birth_utc).total_seconds() / 86400 / package.NARAYANA_AGE_YEAR_DAYS
|
||||
|
||||
|
||||
def test_unresolved_narayana_package_preserves_each_named_branch():
|
||||
body = _unresolved_birth()
|
||||
ctx = package._Context(api._request(body))
|
||||
natal = package._natal_chart(ctx)
|
||||
lagna, lons = natal["lagna_sign_idx"], natal["narayana_input_longitudes"]
|
||||
assert nr.seed_comparison(lagna, lons)["status"] == "undecided"
|
||||
native = nr.calc_narayana_rath(lagna, lons, cycle_count=2)
|
||||
assert not native.get("mahadasha")
|
||||
annual = api.compute_annual_package(body)["package"]
|
||||
rath = annual["natal_periods"]["narayana"]["rath"]
|
||||
assert rath["status"] == "computed" and rath["reference_status"] == "parameter_sensitive"
|
||||
assert report_facts._narayana_unresolved(annual, None)
|
||||
assert rath["used_by_product_timeline"] is False and rath["used_for_scoring"] is False
|
||||
assert rath["alternative_seed_signs"] == sorted((native.get("alternatives") or {}).keys())
|
||||
start, end = [datetime.fromisoformat(annual["report_range"][key]["utc"])
|
||||
for key in ("start", "end")]
|
||||
lo, hi = _age(start, ctx), _age(end, ctx)
|
||||
original = nr.narayana_branches(lagna, lons, current_age=lo, window_years=hi - lo)
|
||||
original = nr.narayana_branches(lagna, lons, current_age=lo, window_years=hi - lo,
|
||||
cap=original["branch_count"])
|
||||
branches = rath["branches"]
|
||||
assert len(branches) == rath["branch_count"] == original["branch_count"] >= 2
|
||||
assert len({branch["branch_id"] for branch in branches}) == len(branches)
|
||||
assert {b["seed_sign"] for b in branches if b["kind"] == "timeline"} == {
|
||||
nr.SIGNS[lagna], nr.SIGNS[(lagna + 6) % 12],
|
||||
}
|
||||
for branch, expected in zip(branches, original["branches"], strict=True):
|
||||
assert branch["branch_id"] == expected["branch_id"] and branch["name"]
|
||||
assert branch["used_for_scoring"] is False
|
||||
for key in ("md_rows", "ad_rows"):
|
||||
assert branch[key] == [row for row in expected.get(key, [])
|
||||
if row["start_age"] < hi and lo < row["end_age"]
|
||||
and row["start_age"] < row["end_age"]]
|
||||
|
||||
|
||||
def test_resolved_seed_preserves_the_existing_md_and_ad_timeline():
|
||||
ctx = package._Context(api._request(_body()))
|
||||
natal = package._natal_chart(ctx)
|
||||
lagna, lons = natal["lagna_sign_idx"], natal["narayana_input_longitudes"]
|
||||
native = nr.calc_narayana_rath(lagna, lons, cycle_count=2)
|
||||
assert native["seed_sign"] and native["mahadasha"]
|
||||
start, end = datetime(2026, 1, 1, tzinfo=timezone.utc), datetime(2027, 1, 1, tzinfo=timezone.utc)
|
||||
to_utc = package._age_mapper(ctx)
|
||||
expected_md = package._age_rows(native["mahadasha"], to_utc, ctx.zone, start, end, level="MD")
|
||||
expected_ad = []
|
||||
for md in native["mahadasha"]:
|
||||
if md["start_age"] < _age(end, ctx) and md["end_age"] > _age(start, ctx):
|
||||
ad = nr.antardasha(md, lons)
|
||||
if ad["status"] == "selected":
|
||||
expected_ad.extend({**row, "parent_md": md["sign"]} for row in
|
||||
package._age_rows(ad["rows"], to_utc, ctx.zone, start, end, level="AD"))
|
||||
rath = package._narayana(ctx, natal, start, end)["rath"]
|
||||
assert rath["status"] == "computed" and rath["used_by_product_timeline"] is True
|
||||
assert "branches" not in rath and rath["seed_sign"] == native["seed_sign"]
|
||||
assert expected_md and rath["rows"] == expected_md + expected_ad
|
||||
|
||||
|
||||
def _turning_point(system: str, day: int) -> dict:
|
||||
at = package.instant(datetime(2026, 1, day, tzinfo=timezone.utc), timezone.utc)
|
||||
return {"id": system, "system": system, "jd": at["jd_ut"], "instant": at,
|
||||
"domains": ["career"], "kind": "ad_change", "level": "AD"}
|
||||
|
||||
|
||||
def test_windows_count_independent_evidence_without_using_narayana_to_link_dates():
|
||||
vim, nar, transit = [_turning_point(system, day) for system, day in
|
||||
(("vimshottari", 1), ("narayana", 12), ("transit_saturn", 20))]
|
||||
bounds = vim["jd"], transit["jd"] + 15, vim["jd"]
|
||||
assert report_facts._windows([vim, nar], [], *bounds) == []
|
||||
[window] = report_facts._windows([vim, nar, transit], [], *bounds)
|
||||
assert window["independent_systems"] == ["transit_saturn", "vimshottari"]
|
||||
assert window["independent_count"] == 2 and window["turning_point_ids"] == ["vimshottari", "transit_saturn"]
|
||||
assert window["conditional_references"] == [{"turning_point_id": "narayana", "system": "narayana",
|
||||
"domains": ["career"], "counts_as_support": False,
|
||||
"note": report_facts.NARAYANA_CONDITIONAL_NOTE}]
|
||||
march = _turning_point("transit_jupiter", 31)
|
||||
assert report_facts._windows([vim, nar, march], [], *bounds) == []
|
||||
|
||||
|
||||
def test_narayana_method_stamp_reads_the_live_engine_defaults():
|
||||
method = report_facts._narayana_method()
|
||||
assert method["profile"] == nr.PROFILE
|
||||
assert method["start_sign"] == nr.SEED_PROFILE
|
||||
assert method["md_direction"] == nr.SOURCE["md_order"]
|
||||
assert method["duration_profile"] == nr.DURATION_PROFILE
|
||||
assert method["ad_start"] == nr.AD_START_PROFILE
|
||||
assert method["ad_rule"] == nr.AD_DIRECTION_PROFILE
|
||||
assert method["dual_lord_rule"] == nr.DUAL_LORD_PROFILE
|
||||
assert method["algorithm_version"] == nr.ALGORITHM_VERSION
|
||||
assert method["branch_cap"] == nr.BRANCH_ENUMERATION_CAP
|
||||
assert method["branch_window_years"] == nr.BRANCH_WINDOW_YEARS
|
||||
assert method["unresolved"] is None
|
||||
@@ -316,9 +316,11 @@ def test_reading_keeps_domains_apart_and_drops_the_tie_rule() -> None:
|
||||
assert reading["domains"]["career"]["tracks"]["narayana"] == "none"
|
||||
# The Narayana sign's own house (2nd: wealth) is an activation, not a vote for Saturn.
|
||||
wealth = reading["domains"]["wealth"]
|
||||
assert any(row["via"] == "dasha_sign" for row in wealth["activated"])
|
||||
assert any(row["via"] == "dasha_sign" and row["counts_as_support"] is False for row in wealth["activated"])
|
||||
assert wealth["tracks"]["narayana"] == "none"
|
||||
assert reading["label"] == "split", "domains that differ say so; no tie rule"
|
||||
# #209 原值 split;新值仅 Vimshottari 有方向的领域为 partial。Narayana 压力仍单列。
|
||||
assert reading["label"] == "partial" and reading["partial_direction"] == "support"
|
||||
assert reading["domains"]["relationship"]["verdict"] == "none"
|
||||
# The same planet in two tracks is listed once with both levels and never counted twice.
|
||||
once = read({"vimshottari_ad": "Mars", "vimshottari_pd": None, "narayana_ad": {"lord": "Mars", "sign": "Aries", "house": 10}}, natal, FUNCTIONAL)
|
||||
assert [item["levels"] for item in once["lords"]] == [["vimshottari_ad", "narayana_ad"]]
|
||||
@@ -328,7 +330,9 @@ def test_reading_keeps_domains_apart_and_drops_the_tie_rule() -> None:
|
||||
# Opposite tracks on one domain are kept side by side.
|
||||
differ = read({"vimshottari_ad": "Mars", "vimshottari_pd": None, "narayana_ad": {"lord": "Saturn", "sign": "Capricorn", "house": 7}},
|
||||
natal, FUNCTIONAL)
|
||||
assert differ["domains"]["relationship"]["verdict"] == "tracks_differ"
|
||||
# #209 原值 tracks_differ;新值主判沿用 Vimshottari,条件参考仍保留反向倾向。
|
||||
assert differ["domains"]["relationship"]["verdict"] == "support"
|
||||
assert differ["domains"]["relationship"]["tracks"] == {"vimshottari": "support", "narayana": "pressure"}
|
||||
# An unresolved Narayana branch gives no direction.
|
||||
held = read({"vimshottari_ad": None, "vimshottari_pd": None, "narayana_ad": {"lord": "Saturn", "sign": "Capricorn", "house": 7}},
|
||||
natal, FUNCTIONAL, narayana_unresolved=True)
|
||||
|
||||
Reference in New Issue
Block a user