diff --git a/CHANGELOG.md b/CHANGELOG.md index bea9be7d..cf35c376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-11-12 — 年运报告将 Narayana 列为条件参考 + +- 未决种子的具名分支独立列出,窗口来源数及把握等级依据独立证据。 +- 本轮沿用当前 Skill 版本(BUG-1855)。 + ## 2026-11-12 — 个人报告的 Mudda 表、上升行和技法审计措辞 - Mudda 覆盖天数保留四位小数,说明引用报告内的完整时间线,中文后续年表沿用首年表头。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index eabffced..cf05f4aa 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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`。 diff --git a/docs/tasks/PROGRESS-annual-narayana-conditional-20261112.md b/docs/tasks/PROGRESS-annual-narayana-conditional-20261112.md new file mode 100644 index 00000000..89cae57d --- /dev/null +++ b/docs/tasks/PROGRESS-annual-narayana-conditional-20261112.md @@ -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 项通过。 diff --git a/docs/tasks/TASK-annual-narayana-conditional-20261112.md b/docs/tasks/TASK-annual-narayana-conditional-20261112.md new file mode 100644 index 00000000..07779fa0 --- /dev/null +++ b/docs/tasks/TASK-annual-narayana-conditional-20261112.md @@ -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 计数后,某个窗口因为只剩一套系统而规则无法判断,而且现有代码没有给出单系统时的处理。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 051338d9..d3dd9b23 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -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=` 打开某一份),放在 `/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 点,写到一半断掉不扣」。价格读不到时这一行写「计费配置不可用」,按钮不可点。没有别的提示小字。 diff --git a/frontend/src/lib/annual-report-generate.ts b/frontend/src/lib/annual-report-generate.ts index 2b3da98e..9f6fa8ec 100644 --- a/frontend/src/lib/annual-report-generate.ts +++ b/frontend/src/lib/annual-report-generate.ts @@ -116,6 +116,19 @@ const YEAR_KEY_CN: Readonly> = { 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(), }), diff --git a/frontend/src/lib/annual-report-sections.ts b/frontend/src/lib/annual-report-sections.ts index 9b69ada6..9b5de8f8 100644 --- a/frontend/src/lib/annual-report-sections.ts +++ b/frontend/src/lib/annual-report-sections.ts @@ -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 { diff --git a/frontend/tests/annual-narayana-conditional-20261112.test.ts b/frontend/tests/annual-narayana-conditional-20261112.test.ts new file mode 100644 index 00000000..8f19263e --- /dev/null +++ b/frontend/tests/annual-narayana-conditional-20261112.test.ts @@ -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 } } } }) => { + 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("不作为独立印证"))); + } +}); diff --git a/scripts/annual_service/package.py b/scripts/annual_service/package.py index 40cd6e05..4d5a5d88 100644 --- a/scripts/annual_service/package.py +++ b/scripts/annual_service/package.py @@ -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] diff --git a/scripts/annual_service/report_facts.py b/scripts/annual_service/report_facts.py index 047f8570..1911f105 100644 --- a/scripts/annual_service/report_facts.py +++ b/scripts/annual_service/report_facts.py @@ -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, diff --git a/tests/test_annual_narayana_conditional_20261112.py b/tests/test_annual_narayana_conditional_20261112.py new file mode 100644 index 00000000..98f6d51e --- /dev/null +++ b/tests/test_annual_narayana_conditional_20261112.py @@ -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 diff --git a/tests/test_annual_report_facts.py b/tests/test_annual_report_facts.py index 1a5f4d81..3dc20074 100644 --- a/tests/test_annual_report_facts.py +++ b/tests/test_annual_report_facts.py @@ -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)