fix(report,chart): blank columns — drop Kranti, split Ashtakavarga house SAV, ascendant nakshatra, engine varga dignity, no duplicate charts (BUG-1199..1203)
- Report p33 table: no Kranti column (no producer; materials.kranti stays blocked). - Report p53 table: the "SAV" column was the with-Lagna total (386). Split in the reader into SAV / Lagna BAV / SAV + Lagna with a totals row; ashtakavarga.py is untouched (frozen rectification identity, ERR-115). - /api/chart ascendant carries nakshatra / pada / lord (_nakshatra_fields shared with the nine bodies); chart cache schema 4 -> 5. - /api/varga_full bodies carry dignity_level from varga_dignity_level, the function the Vimsopaka path now shares (A/B identical); the chart page shows the D1 status wording and no longer derives a three-level dignity itself. - Report page draws each chart once: the trailing block skips charts the body already fenced (same rule as the export); the birth-time caveat moves under the body's chart heading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0e856f9cb6
commit
ae18952e51
@@ -1,5 +1,14 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-03 — 报告和星盘页不再出现「一整列横杠」;八分法宫位表的 SAV 数字改正
|
||||
|
||||
- 个人报告「赤纬与速度」表删掉一直为空的 Kranti 列(本站没有它的独立算法,赤纬列已是真实值)(BUG-1199)。
|
||||
- 个人报告「Ashtakavarga 完整宫位分数」表:以前标着 SAV 的那一列其实是「含上升合计」(12 宫合计 386),已改正为三列——SAV(合计 337)、上升贡献(合计 49)、含上升合计(386),末行加合计。生时校正使用的分数不变(BUG-1200)。
|
||||
- 星盘页本命表上升那一行补上星宿、宿主、pada(例:狮子 14°32′ = Purva Phalguni 第 1 足、宿主金星)(BUG-1201)。
|
||||
- 星盘页分盘表的「庙旺」列改为与本命「状态」列同一套十档说法(入旺、本垣、入庙、极友、入友、中性、入敌、极敌、落陷、落陷取消),以前只写入旺 / 自宫 / 落陷、其余一律「—」(BUG-1202)。
|
||||
- 报告末尾不再重复画一遍正文已有的分盘;「分盘对出生时间敏感」这句挪到正文分盘标题下(BUG-1203)。旧报告正文里没有分盘图的,末尾照旧补上。
|
||||
- Skill 版本不变;不改数据库结构。新生成的报告才有新表格,旧报告内容不变。
|
||||
|
||||
## 2026-10-02 — 生时校正至少要 4 件有年月的经历、覆盖 3 个方面才给时间范围;上游同步第五轮(燃烧、诅咒格措辞、Badhaka 等)
|
||||
|
||||
- 生时校正:以前 3 件有年月的经历、2 个方面(例如上学和工作)就会给出生时间范围;现在至少要 4 件、覆盖 3 个方面(例如再加一件搬家或感情)才开始给范围和选择题。还不够时,提示会写清楚还差几件、还缺哪类(例:「再来两件记得大概年月的事,其中至少一件不是上学或工作的,就能开始筛」),不再一律写「再来一件」(BUG-1193)。打分方法不变,算法版本不变;以前的校正记录照常打开。
|
||||
|
||||
@@ -16134,3 +16134,75 @@
|
||||
- 防复发:同上。
|
||||
- 相关记录:BUG-1195。
|
||||
- 修复版本:`codex/upstream-sync5-20261002`(未推送)。
|
||||
|
||||
## BUG-1199 | 报告「Declination / Kranti / Speed」表的 Kranti 列整列为「-」
|
||||
|
||||
- 状态:resolved(分支验证;待部署 staging)
|
||||
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
|
||||
- 影响面:`scripts/pl9_reader_export.py` p33 表(中英两版报告正文与目录)。
|
||||
- 现象:产品真机截图,个人报告「Declination / Kranti / Speed」表七行 Kranti 全是 `-`,像没算出来。
|
||||
- 触发条件:任何一份报告。
|
||||
- 根因:reader 读 `strengths.kranti_local.planets[*].kranti_degrees`,全仓没有任何 producer;引擎 materials `kranti` 自标 `missing_in_local / blocked`(no independent Kranti producer)。表照样留了这一列。
|
||||
- 修复:产品选删列:表改为 `Planet | Degree | Declination | Speed`,标题 `Declination / Speed`。不按古典公式补算(无 PL9 原值可对照,Declination 已是真实赤纬)。materials `kranti: blocked` 审计口径与 `full_report_quality_gate.RESTRICTED_MATERIAL_IDS` 不变。
|
||||
- 验证:`tests/test_report_chart_blank_columns.py::test_declination_table_has_no_kranti_column`(中英两版:无 Kranti、四列、无 `-`)。
|
||||
- 防复发:没有 producer 的字段不进读者表格;要加列先有来源。
|
||||
- 相关记录:BUG-1200~1203(同一张截图)。
|
||||
- 复发自:无
|
||||
- 修复版本:`codex/report-chart-blank-columns-20261003`
|
||||
|
||||
## BUG-1200 | 报告「Ashtakavarga 完整宫位分数」把含上升合计标成 SAV,另两列为「-」
|
||||
|
||||
- 状态:resolved(分支验证;待部署 staging)
|
||||
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
|
||||
- 影响面:`scripts/pl9_reader_export.py` p53 表(数据源 `scripts/ashtakavarga.py::_map_to_houses_values`,未改)。
|
||||
- 现象:表头「House | Sign | SAV | BAV/承接 | Full with 上升」,后两列全 `-`;「SAV」列 12 宫合计 386,与同页「Sign | SAV score」表(合计 337)对不上。
|
||||
- 触发条件:任何一份报告。
|
||||
- 根因:`_map_to_houses_values(full_sav, …)` 只写 `sign` + `sav_score`,传入的是 `full_sav = sav + lagna_sav`(337 + 49);reader 用 `row.get('sav') or row.get('sav_score')` 取到含上升合计标成 SAV,`bav` / `full_with_lagna` 字段不存在。**数字标错,不只是缺列**。
|
||||
- 修复:在 reader 拆分,**`ashtakavarga.py` 不改**——它在生时校正冻结身份(`sealed_holdout_rerun.PRODUCTION_FILES`)里,最初按任务书改它新增字段,快速门 `test_rectification_validation_integrity_gate` 4 条转红(ERR-110);改为 reader 用引擎已有的按星座 SAV(`sav.scores`)与上升 BAV(`bav.Lagna.bindus`)拆出,且要求 `SAV + 上升 BAV == house_scores_full.sav_score` 逐宫成立才填,否则 SAV / 上升两列写 `-`、不出合计行,绝不把合计填进 SAV 列。`sav_score` 语义与数值不变(生时校正在读)。表改为 `House | Sign | SAV | Lagna BAV | SAV + Lagna`(中文版「上升 BAV」「SAV + 上升」),末行合计。
|
||||
- 验证:`tests/test_report_chart_blank_columns.py`:每行 SAV + 上升 BAV = 最后一列、合计 337 / 49 / 386(中英两版);SAV 列 = 按星座 SAV、上升列 = Lagna bindus、末列 = `house_scores_full.sav_score`;删掉 Lagna BAV 时 SAV / 上升两列为 `-` 且无合计行。验证完整性门禁与封存评测新鲜度测试保持全绿(冻结身份未变)。
|
||||
- 防复发:报告表格列只读语义明确的字段,不用 `or` 链兜到别的口径。
|
||||
- 相关记录:BUG-1199。
|
||||
- 复发自:无
|
||||
- 修复版本:`codex/report-chart-blank-columns-20261003`
|
||||
|
||||
## BUG-1201 | 星盘页本命表上升行的星宿 / 宿主 / pada 一直是「—」
|
||||
|
||||
- 状态:resolved(分支验证;待部署 staging)
|
||||
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
|
||||
- 影响面:`scripts/jyotish_engine.py::compute_chart_data`(`/api/chart` 的 `ascendant`)、`frontend` 星盘页 D1 / 月亮盘表上升行。
|
||||
- 现象:上升行星宿、宿主、pada 三格为「—」,九曜都有。
|
||||
- 根因:`compute_chart_data` 给九曜按 `nak_span` 算了 `nakshatra / nakshatra_pada / nakshatra_lord`,`result["ascendant"]` 漏了;前端按「引擎不给就不推」留空。
|
||||
- 修复:抽 `_nakshatra_fields(sidereal_lon)`,九曜、计都、上升共用;`/api/chart` 缓存 `cache_schema_version` 4 → 5(缓存 TTL 900 s,改版本只影响查找键;指纹 / 绑定只用出生资料,不受影响)。前端只改注释。
|
||||
- 验证:`tests/test_report_chart_blank_columns.py`(边界 0°、13°20′ 两侧、360°;golden 盘上升 Uttara Phalguni 1 足、九曜与 golden 逐星一致);`frontend/tests/chart-varga-table.test.tsx` 上升行断言;旧缓存无字段仍渲染「—」。golden `chart-view-golden.json` 用 `_nakshatra_fields` 重录(命令写在 `sourceCommand`)。
|
||||
- 防复发:同一个量(星宿)只有一个计算函数,新增盘点直接复用。
|
||||
- 相关记录:BUG-1076(D1 表逐字节基线:本次放开上升三格,见进度记录三栏)。
|
||||
- 复发自:无
|
||||
- 修复版本:`codex/report-chart-blank-columns-20261003`
|
||||
|
||||
## BUG-1202 | 星盘页分盘表「庙旺」只做三档,敌宫 / 中性等一律「—」
|
||||
|
||||
- 状态:resolved(分支验证;待部署 staging)
|
||||
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
|
||||
- 影响面:`/api/varga_full`(新模块 `scripts/varga_dignity.py`)、`jyotish_engine.varga_dignity_level`、`frontend/src/lib/chart-view-{varga,mapper,contract}.ts`、`vedic-chart-tables.json`。
|
||||
- 现象:D9 表只有入旺 / 自宫 / 落陷有字(太阳在金牛=敌宫、月亮在摩羯=中性都是「—」),产品以为没算。
|
||||
- 根因:BUG-1076 / 1099 有意只做三档,前端自带一份 `vargaDignity` 表;引擎 Vimsopaka 路径早有完整判定没接过来。
|
||||
- 修复(产品 10-03 拍板,**有限推翻 BUG-1076「三档庙旺」与 BUG-1099「三档庙旺保留」**;BUG-1099 能力声明规则不变):抽 `varga_dignity_level(planet, varga_planets)`(暂时关系取该分盘自身位置,与 Vimsopaka 一致),Vimsopaka 与 `/api/varga_full` 共用;`/api/varga_full` 每星加 `dignity_level`;前端只校验并用 D1 状态列同一措辞(`DIGNITY_LABELS`)显示;删除前端 `vargaDignity` 及只为它服务的四张表。旧缓存无等级时整列隐藏,不自算。
|
||||
- 验证:Vimsopaka 抽函数前后同机 A/B(`PYTHONHASHSEED=0`,3 张虚构盘 `cmd_full_reading` 的 vimsopaka / varga_full / jaimini / 行星 / 上升)逐字节相同;`tests/test_api_server_security.py::test_varga_full_standard_divisions_carry_the_vimsopaka_dignity_level`(与 `varga.calc_all_vargas` 的 Vimsopaka 输入逐星同座同级);`tests/test_chart_page_vedic_tables_contract.py`(真实 TS mapper 投影 = 引擎等级,golden 覆盖入庙 / 落陷 / 中性 / 入敌 / 入友;措辞表 = `DIGNITY_LABELS`);`frontend/tests/chart-varga-table.test.tsx`、`chart-view-varga-dignity.test.ts`。
|
||||
- 防复发:分盘庙旺只有引擎一套;页面不得再写第二套判定。
|
||||
- 相关记录:BUG-1076、BUG-1099。
|
||||
- 复发自:无(范围扩展,不是回归)。
|
||||
- 修复版本:`codex/report-chart-blank-columns-20261003`
|
||||
|
||||
## BUG-1203 | 报告末尾「分盘核对」把正文已画过的分盘再画一遍
|
||||
|
||||
- 状态:resolved(分支验证;待部署 staging)
|
||||
- 首次发现 / 最近更新:2026-10-03 / 2026-10-03
|
||||
- 影响面:`frontend/src/components/personal-report/report-calculation-charts.tsx`、`personal-report-page.tsx`、`src/lib/report-chart-block.ts`、`report-export-blocks.ts`、`scripts/pl9_reader_export.py`(提示句)。
|
||||
- 现象:正文「本命与分盘北印度图盘」已画全部分盘,正文后又出现「分盘核对」+ 提示句 + D1、D2… 第二遍,在目录外、样式散。
|
||||
- 根因:页面在 Markdown 正文后无条件挂 `ReportCalculationCharts`;导出早已按正文围栏去重,页面没用同一规则。
|
||||
- 修复:抽 `fencedReportChartIds`,页面与导出共用;页面只画正文没有围栏的结构化分盘(新报告整段消失,老报告缺哪张补哪张)。提示句由引擎写到正文分盘标题下(英文版走术语表),组件里的提示删除。
|
||||
- 验证:`frontend/tests/report-calculation-charts.test.tsx`(真实 golden 正文 → 整段不渲染;无围栏正文 → 照旧且不带提示;页面与导出跳过同一批盘);导出原有测试(含冻结哈希)全绿;`tests/test_report_chart_blank_columns.py::test_chart_section_carries_the_divisional_caveat`(中英)。
|
||||
- 防复发:同一张盘页面只画一次;页面与导出共用去重函数。
|
||||
- 相关记录:BUG-607 / 616(报告图盘渲染)。
|
||||
- 复发自:无
|
||||
- 修复版本:`codex/report-chart-blank-columns-20261003`
|
||||
|
||||
@@ -472,3 +472,9 @@ Prevention: `frontend/tests/runtime-sources-no-test-imports.test.ts`;研究 ha
|
||||
`57782aea` 改 `scripts/functional_benefics.py`(功能吉凶 v2)让原生打分同输入出不同分(reported-offset 900 例里 230 例变化),但该文件不在 `sealed_holdout_rerun.py` `PRODUCTION_FILES` 和 dataset `frozen_scoring.files` 里,`tests/test_rectification_validation_integrity_gate.py` 全绿;随后只把 `scoring_service.py` 的 `ALGORITHM_VERSION` 从 scoring-9 改成 scoring-10,同一门禁 8 条失败。冻结身份是显式审过的文件清单,不是传递依赖,清单外的打分输入会静默漂移。
|
||||
|
||||
Prevention: 打分路径新增或改动输入模块时,先查它在不在两份清单里;`functional_benefics.py` 已补进 `PRODUCTION_FILES` 并由 `test_functional_roles_are_part_of_the_production_identity` 锁住;改动后按 ERR-110 重新冻结(本次标签 `functional_v2_2026_10_02`)。关联 BUG-1181、ERR-110。
|
||||
|
||||
## ERR-115 | 任务书指定给冻结文件 `ashtakavarga.py` 加展示字段,快速门验证完整性 4 条转红 | observed 2026-10-03
|
||||
|
||||
`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。
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# PROGRESS · 报告与星盘页的「空列」(2026-10-03)
|
||||
|
||||
- 任务书:`docs/tasks/TASK-report-chart-blank-columns-20261003.md`(`3bbc2daa`)
|
||||
- 基线:`origin/staging` `3bbc2daa`(任务书写的 `1af34880` + 任务书本身,代码相同)
|
||||
- 分支 / worktree:`codex/report-chart-blank-columns-20261003` / `.worktrees/report-chart-blank-columns-20261003`
|
||||
- 执行方式:产品 10-03 说「请你执行」,Claude 直接实现并独立验收。
|
||||
- BUG:1199~1203(开工核对最大号 1198)
|
||||
|
||||
## 完成情况
|
||||
|
||||
| 任务 | 结果 | 要点 |
|
||||
| --- | --- | --- |
|
||||
| T1 Kranti 删列(BUG-1199) | 完成 | `pl9_reader_export.py` p33 表四列;materials `kranti: blocked` 与质量门 `RESTRICTED_MATERIAL_IDS` 未动 |
|
||||
| T2 八分法三列(BUG-1200) | 完成,**实现方式偏离任务书** | 见下「偏离」 |
|
||||
| T3 上升星宿(BUG-1201) | 完成 | `_nakshatra_fields` 九曜 / 计都 / 上升共用;`cache_schema_version` 4→5;指纹 / 绑定核查:只用出生资料(`report-birth-binding`、`secondary-page-data` 的 `profileFingerprint`),报告封面哈希是报告正文;均不受影响 |
|
||||
| T4 分盘十档庙旺(BUG-1202) | 完成 | `jyotish_engine.varga_dignity_level`(Vimsopaka 与 `/api/varga_full` 共用);新模块 `scripts/varga_dignity.py`,API 主文件只改一行(行数 10923 不变);前端删 `vargaDignity` 与四张表,旧缓存无等级整列隐藏 |
|
||||
| T5 末尾分盘去重 + 提示前移(BUG-1203) | 完成 | `fencedReportChartIds` 页面与导出共用;提示句由引擎写在 p6-p17 标题下(中英) |
|
||||
|
||||
未用让步顺序。
|
||||
|
||||
## 偏离任务书:T2 不改 `ashtakavarga.py`
|
||||
|
||||
任务书写「`_map_to_houses_values` 增字段」。照做后快速门红 4 条(`test_rectification_validation_integrity_gate`)、`test_sealed_holdout_contract_freshness` 红 3 条:该文件在生时校正冻结身份里(ERR-110 同型;台账新增 ERR-115,是任务书本身的疏漏)。改为 reader 用 `sav.scores` 与 `bav.Lagna.bindus` 拆分,并要求逐宫 `SAV + 上升 BAV == house_scores_full.sav_score` 才填;`ashtakavarga.py` 已回到基线字节,门禁恢复全绿,无需重新冻结。红线 1(`sav_score` 不变)照样满足。
|
||||
|
||||
## 既有断言改动(原值 / 新值 / 原因)
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `frontend/tests/chart-varga-table.test.tsx` D1 逐字节基线 | 表 == `chart-d1-table-baseline.html` | 表 == 基线把上升行唯一一组「— — — —」换成「Uttara Phalguni / ☉ / 1 / —」;并断言基线里这样的全横杠行只有一处 | BUG-1201:上升三格现在有引擎值;其余逐字节不变 |
|
||||
| 同文件 D9 行 | `dignity == "own"` / `"debilitated"`;markup `>自宫<`、`>落陷<` | `"OWN_SIGN"` / `"DEBILITATED"`;markup `>入庙(Own Sign)<`、`>落陷(Debilitated)<`,另加入友 / 中性 / 入敌 / 极敌四行 | BUG-1202:十档引擎等级、D1 状态列措辞 |
|
||||
| 同文件「null 不关列」 | tbody 不含 `同位/入旺/自宫/落陷` | tbody 不含 `同位` 与 `(Exalted|Own Sign|Debilitated|Neutral)` | 同上,标签换了;断言强度不变 |
|
||||
| `frontend/tests/chart-view-varga-dignity.test.ts` | `vargaDignity` 三档优先级 8 条断言 | `vargaDignityLevel` 接受十档、拒绝旧三档值与非字符串;标签与 D1 措辞一致、键集合 = 十档 | 前端自算函数已删除(不留第二套判定) |
|
||||
| `tests/test_chart_page_vedic_tables_contract.py` | `test_tables_equal_engine_constants`、`test_every_planet_sign_pair_matches_engine_dignity_level`(钉三档表) | `test_dignity_labels_equal_engine_status_wording`(措辞表 = `DIGNITY_LABELS`,旧四表已不存在)+ `test_d9_projected_dignity_is_the_engine_level`(真实 TS mapper 投影 = 引擎等级,golden 覆盖入庙 / 落陷 / 中性 / 入敌 / 入友) | 同上 |
|
||||
|
||||
golden:`frontend/tests/fixtures/chart-view-golden.json` 只新增字段(上升三项、每颗分盘星 `dignity_level`),原值零改动;重录方式写在 `sourceCommand`(`_nakshatra_fields(ascendant.lon)`、`annotate_varga_dignity` 作用于已存位置,即 `/api/chart`、`/api/varga_full` 现在的调用)。
|
||||
|
||||
报告大 fixture(`report-reader-main-fictional*.json`、`report-density-fictional-reader.json`)是 9 月真实输出,里面仍是旧表;前端没有断言碰这两张表,未重录(重录会混入其后大量无关引擎变化)。新表由 Python 测试现场渲染断言。
|
||||
|
||||
## 验收
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | 0 error(126 warning 均为既有;改动文件单独跑 eslint 0 问题) |
|
||||
| `npm test`(Node 22) | 4927 项 / 失败 24;基线 `3bbc2daa` 4921 项 / 失败 24;失败清单逐名一致,全为无 Docker 的数据库 / 部署套件;新增 6 项 |
|
||||
| `next build` | 通过,`/` 为 `○ Static` |
|
||||
| 首屏 gzip(`index.html` 引用的 29 个脚本 / 样式 gzip-9 求和) | 基线 625,123 → 本分支 626,061,**+0.15%**。基线构建在最后 standalone 复制步因整机磁盘满(ENOSPC)退出,编译与页面生成已完成,`index.html` 与引用文件齐全,数字有效 |
|
||||
| Python 定向 | `test_report_chart_blank_columns`(新 12 项)、`test_chart_page_vedic_tables_contract`、`test_api_server_security`(新增 1 项)、`test_report_english_edition`、`test_report_reader_main`、`test_report_chart_block`、`test_ashtakavarga_invariants`、`test_api_server_growth_contract`、`test_rectification_validation_integrity_gate`、`test_sealed_holdout_contract_freshness` 全绿 |
|
||||
| 快速门 `run_quality_gate.py --profile quick` | Python 段 1047 passed / 1 skipped;最后一步 `npm test` 因上述 24 项无 Docker 失败而整体判红,与基线同 |
|
||||
| 既有红 | `test_dynamic_rectification_fact_priority::test_historical_event_priority_preserves_vimshottari_actor_difference` 在基线 `3bbc2daa` 同样失败(`selection_priority 0.0`),与本轮无关,未处理 |
|
||||
| Vimsopaka 抽函数 A/B | `PYTHONHASHSEED=0`,3 张虚构盘 `cmd_full_reading` 的 vimsopaka / varga_full / jaimini / 行星 / 上升(除新增星宿字段)基线与本分支逐字节相同 |
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 无登录态与 Chrome:真机走查留给产品,清单 `docs/testing/report-chart-blank-columns-20261003.md`。
|
||||
- 无 Docker:数据库 / 部署套件 24 项按基线逐名比对。
|
||||
- 整机磁盘曾满(他会话占用):一次快速门因 `/tmp` 建不了目录全部 error(未计入结论),腾出自己的构建产物后重跑得到上表结果。
|
||||
@@ -412,4 +412,4 @@
|
||||
| `TASK-yoga-lunar-node-consistency-20261002.md` | `PROGRESS-yoga-lunar-node-consistency-20261002.md` | 月亮前后有星格(Sunaphaa/Anaphaa/Duradhara)计入罗睺计都、孤立格(Kemadruma)不计,泰勒/齐达内卡上同时出现互相矛盾的格局;统一为只计五颗真行星 | **已并入** `TASK-upstream-sync4-20261002.md` T5(交付见 `PROGRESS-upstream-sync4-20261002.md`,`f3ed9509` + `0a4e6d0a`,BUG-1187) | 原分支未使用;在 `codex/upstream-sync4-20261002` 交付 |
|
||||
| `TASK-upstream-sync4-20261002.md` | `PROGRESS-upstream-sync4-20261002.md` | 上游同步第四轮(对照 yinduzhanxing `0a6696c5`):婚姻计数删关系次数断语、双重过运相位错一宫、Shadbala 四处计算错、格局列表截断与空回退、月亮/太阳类格局只计五星(吸收月亮单)、Arudha 只留 jaimini 一个来源、定位星循环与未标定概率;BUG 从 1183 起 | **待验收**(子代理直接执行完毕,未推送;追加 F3 太阳最低要求;BUG-1183~1191) | 分支 `codex/upstream-sync4-20261002` |
|
||||
| `TASK-upstream-sync5-20261002.md` | `PROGRESS-upstream-sync5-20261002.md` | 上游同步第五轮:Narayana 默认 K.N. Rao、Chara Karaka 罗睺、燃烧统一、八分法净化、Yogini、诅咒格措辞、Budha-Aditya、Badhaka;生时校正至少 4 件事 3 个领域 | **待验收**(PM 移植 U2–U8 + 子代理 U8 后续 / R1 / 冻结 / 评测 / 回测第七轮,未推送;U1 延后;分数不变不升 scoring-11,policy v3 → v4;BUG-1192~1198) | 分支 `codex/upstream-sync5-20261002` |
|
||||
| `TASK-report-chart-blank-columns-20261003.md` | `PROGRESS-report-chart-blank-columns-20261003.md` | 报告/星盘页空列:Kranti 删列、八分法宫位表 SAV 标错(实为含上升 386)补三列、上升补星宿、分盘庙旺接引擎十档(有限推翻 BUG-1076/1099 三档)、报告末尾重复分盘去重(BUG-1199~1203);待领取 |
|
||||
| `TASK-report-chart-blank-columns-20261003.md` | `PROGRESS-report-chart-blank-columns-20261003.md` | 报告/星盘页空列:Kranti 删列、八分法宫位表 SAV 标错(实为含上升 386)补三列、上升补星宿、分盘庙旺接引擎十档(有限推翻 BUG-1076/1099 三档)、报告末尾重复分盘去重(BUG-1199~1203);Claude 直接执行,已合入 staging,待部署核对与真机 |
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# 真机清单 · 报告与星盘页空列(2026-10-03,BUG-1199~1203)
|
||||
|
||||
前提:staging 已部署本轮提交(`/api/health` 的 `deployment.gitCommit` 等于合入 SHA)。用自己的账号,不用别人的资料。
|
||||
|
||||
## 报告(要**新生成**一份;旧报告内容不会变)
|
||||
|
||||
1. 星盘档案 → 本人 → 生成个人报告,等它完成后打开。
|
||||
2. 目录里「强度、关系与 Ashtakavarga 技术页」下应写「Declination / Speed」,不再有 Kranti。点进去:表只有 Planet / Degree / Declination / Speed 四列,没有整列「-」。
|
||||
3. 找「Ashtakavarga 完整宫位分数」:五列「House · Sign · SAV · 上升 BAV · SAV + 上升」,每行 SAV + 上升 BAV = 最后一列;最后一行「合计」应是 337 / 49 / 386。SAV 列和上方「Sign | SAV score」表按星座对得上。
|
||||
4. 目录「出生资料与图盘 → 本命与分盘北印度图盘」:标题下第一句是「分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。」
|
||||
5. 滚到报告最底部:不应再出现「分盘核对」和第二遍 D1 / D2 图。
|
||||
6. 右上切「English」:第 2–5 步对应英文版同样成立(表头「Lagna BAV」「SAV + Lagna」,合计行「Total」)。
|
||||
7. 导出 → 「图盘」:导出的图盘数量与修复前一样(每张一次)。
|
||||
8. 打开一份**以前生成的**报告:能正常打开;若末尾原本有「分盘核对」,它只会在正文缺图时出现。
|
||||
|
||||
## 星盘页
|
||||
|
||||
9. 打开「星盘」→ 本命表第一行「上升」:星宿、宿主、pada 三格有值(例:狮子 14°32′ 应为 Purva Phalguni、宿主金星符号、pada 1);状态格仍为「—」。
|
||||
10. 切到 D9:「庙旺」列每颗星都有字(入旺 / 本垣 / 入庙 / 极友 / 入友 / 中性 / 入敌 / 极敌 / 落陷 / 落陷取消之一),写法与 D1「状态」列一样;上升行写「不适用」。同位列不变(只有同位的写「同位」)。
|
||||
11. 切 D10、D2 各看一眼:庙旺列同样每颗星有字。
|
||||
12. 手机宽度(或把窗口拉窄)看 D9 表:庙旺列文字变长后表格可横向滚动,不撑破页面。
|
||||
+3
-3
@@ -653,7 +653,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
- **Surface:** page floor `--color-canvas-soft`; the Markdown article is a `--color-canvas` sheet with a hairline and `--radius-lg`. Print flattens the sheet, hides chrome and the TOC, and pins the light palette.
|
||||
- **Print inside the shell:** printing takes the whole document, so the reader ships a second `media="print"` stylesheet that hides the sidebar and the 46px header **and unlocks them**: `.chat-app` / `.chat-panel` are `height: 100%; overflow: hidden`, which would otherwise clip a nine-section report to one page. It lives on the component rather than in `globals.css` so it exists only while the reader is mounted and no other surface has to know about it. The `@page` rule is unchanged.
|
||||
- **Typography:** Markdown `h2` uses `--type-display-sm` serif at weight 400. `h3` uses `--type-title-md` sans at weight 500. Body is `--type-body-md` at 1.65. Honesty labels (`blocked` / `conflict` / `parameter_sensitive`) stay visible; the reading-nav and quality-matrix sections are not collapsed.
|
||||
- **Charts:** a rehype pass groups each `####` chart heading (optional note paragraph, then the `jyotish-chart` fence) into `.personal-report-chart-card` and consecutive cards into the existing `.personal-report-chart-grid`. A lone card uses `is-single`. The grid is two columns; print and the 760px breakpoint stay one column. Do not float chart headings or figures, and do not use negative margins to fake columns. Occupants are one-character Chinese names plus integer degree (`日 12°`), with `逆` after the name when retrograde (`土逆 3°`). House corners show rasi numbers 1–12. Chart JSON fences are stripped from the `.md` download. Markdown still uses `react-markdown` + `remark-gfm`, `skipHtml`, and no `rehype-raw`. The article tree is memoized; TOC highlight state lives in `ReportToc`, not in `PersonalReportMarkdownView`.
|
||||
- **Charts:** a rehype pass groups each `####` chart heading (optional note paragraph, then the `jyotish-chart` fence) into `.personal-report-chart-card` and consecutive cards into the existing `.personal-report-chart-grid`. A lone card uses `is-single`. The grid is two columns; print and the 760px breakpoint stay one column. Do not float chart headings or figures, and do not use negative margins to fake columns. Occupants are one-character Chinese names plus integer degree (`日 12°`), with `逆` after the name when retrograde (`土逆 3°`). House corners show rasi numbers 1–12. Chart JSON fences are stripped from the `.md` download. The chart section opens with one caveat line under its heading ("分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。", engine-written, English in the English edition). Each chart is drawn once: the trailing 「分盘核对」 block only draws structured charts the body has no fence for — the export's rule, shared via `fencedReportChartIds` — so a current report has no trailing block at all and an older report keeps only the charts its body lacks (BUG-1203). Markdown still uses `react-markdown` + `remark-gfm`, `skipHtml`, and no `rehype-raw`. The article tree is memoized; TOC highlight state lives in `ReportToc`, not in `PersonalReportMarkdownView`.
|
||||
- **Accent:** headings stay ink. Dark ink is never a page-scale rule or card edge.
|
||||
- **Tables:** wide Markdown tables sit in `overflow-x: auto` wrappers; print unwraps them. Report tables (Markdown and fact tables, never chat bubbles) carry a header band of `--color-ink` at 6% and zebra rows at 3%, both mixed into transparency so they work on paper and on the dark sheet; print lightens them to 5% / 2.5%.
|
||||
- **Width:** 1120px for the reader chrome; the article takes `minmax(0, 1fr)` and the TOC rail `minmax(11rem, 15rem)` to its right. The TOC collapses into a drawer below 860px.
|
||||
@@ -1152,7 +1152,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句
|
||||
| pada | 1–4,居中 |
|
||||
| 状态 | 尊贵 / 庙旺等,原样保留,只是挪到最后一列 |
|
||||
|
||||
第一行是上升。上升是盘点不是星体,引擎不返回它的星宿 / pada / 宿主,这三格写「—」而不是填一个看起来合理的值。数字列 `tabular-nums`,符号列与宿主列居中。
|
||||
第一行是上升。星宿 / 宿主 / pada 用引擎按上升经度算出的值,和九曜同一个算法(2026-10-03,BUG-1201;之前引擎漏算,这三格一直是「—」);旧缓存里没有这三项时仍写「—」,页面不自己推算。上升没有尊贵,状态格写「—」。数字列 `tabular-nums`,符号列与宿主列居中。
|
||||
|
||||
本轮不新增第二层页签:参照里「宫位 / 大运 / 星力 / 瑜伽」等页签的内容后续另立单。
|
||||
|
||||
@@ -1161,7 +1161,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句
|
||||
| 盘型 | 列 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 本命 | 上面八列 | 不变(与基线渲染逐字节相同) |
|
||||
| D2–D60 | 行星 · 分盘星座(度°分′)· 宫位 · 本命星座 · 庙旺;仅 D9 再有同位 | 宫位从该分盘上升数;庙旺只写「入旺 / 自宫 / 落陷」,其余「—」,上升此格「不适用」。2026-09-29 收口:仅 D9 的 D1↔D9 同座比较显示为同位,其他分盘不显示此列。两列各由服务端能力声明控制,缺声明保守隐藏,不以是否命中推断支持性。**没有星宿 / 宿主 / 足**:那是本命经度的属性。逆行在行星格后标「逆」(分盘不改顺逆)。首行是分盘上升 |
|
||||
| D2–D60 | 行星 · 分盘星座(度°分′)· 宫位 · 本命星座 · 庙旺;仅 D9 再有同位 | 宫位从该分盘上升数;庙旺是引擎给的十档(入旺 / 本垣 / 入庙 / 极友 / 入友 / 中性 / 入敌 / 极敌 / 落陷 / 落陷取消),措辞与本命「状态」列一字不差(2026-10-03,BUG-1202,取代原先「入旺 / 自宫 / 落陷,其余—」的三档);旧缓存没有引擎等级时整列不显示,页面不自算;上升此格「不适用」。2026-09-29 收口:仅 D9 的 D1↔D9 同座比较显示为同位,其他分盘不显示此列。两列各由服务端能力声明控制,缺声明保守隐藏,不以是否命中推断支持性。**没有星宿 / 宿主 / 足**:那是本命经度的属性。逆行在行星格后标「逆」(分盘不改顺逆)。首行是分盘上升 |
|
||||
| 月亮盘 | 本命八列 | 宫位从月亮所在星座数;表头「月亮盘 · 以月亮所在星座(X)为第 1 宫」;盘上「As」画在本命上升落的那一宫(`VedicChartSvg` 的 `ascendantHouse`,其它盘默认 1 宫不变) |
|
||||
| Bhava | 行星 · 黄道经度 · 星座宫 · Bhava 宫 · 换宫;另一张「宫 · 宫头 · 起点(sandhi)」12 行 | Sripati。换宫的行用 `--color-selected` 浅底。拿不到真实 MC 时这一栏走失败句,不画等宫(BUG-1077) |
|
||||
| 行运 | 行星 · 今天的位置 · 速度 · 落本命 · 本命同星 | 表头写计算时刻「行运 · YYYY-MM-DD 12:00(时区)」,上方一句「以本命上升为第 1 宫,放的是这一天中午的行星位置。岁差和交点与本命相同。」 |
|
||||
|
||||
@@ -78,7 +78,8 @@ function PlanetRow({ row }: { row: ChartViewPlanetRow }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The ascendant is a chart point: the engine returns no nakshatra for it.
|
||||
* The ascendant is a chart point: nakshatra, lord and pada come from the
|
||||
* engine (BUG-1201); it has no dignity, so the status cell stays empty.
|
||||
* `house` is 1 except on the Moon chart, which counts from the Moon.
|
||||
*/
|
||||
function AscendantRow({ row, house = row.house }: { row: ChartViewAscendantRow; house?: number }) {
|
||||
|
||||
@@ -566,7 +566,7 @@ export function PersonalReportPage({ reportId, initialLanguage = "zh" }: { repor
|
||||
<div className="personal-report-reader-body" lang={shown === "en" ? "en" : "zh-CN"}>
|
||||
{/* Siblings remount per edition under distinct keys; a shared key left the old edition on screen (BUG-1126). */}
|
||||
<PersonalReportMarkdownView key={`markdown-${shown}`} language={shown} markdown={markdown} drawnThrough={drawnThrough} />
|
||||
{state.calculationCharts && <ReportCalculationCharts charts={state.calculationCharts} language={shown} />}
|
||||
{state.calculationCharts && <ReportCalculationCharts charts={state.calculationCharts} markdown={markdown} language={shown} />}
|
||||
{factTables && <ReportFactTables key={`facts-${shown}`} language={shown} tables={factTables} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useMemo } from "react";
|
||||
import type { ChartV1, ReportDocumentV2 } from "@/lib/personal-report-contract";
|
||||
import { fencedReportChartIds } from "@/lib/report-chart-block";
|
||||
import { hasRealChartData, VedicChartSvg } from "./vedic-chart-svg";
|
||||
import type { ReportLanguage } from "@/lib/report-language";
|
||||
|
||||
const COPY = {
|
||||
zh: { heading: "分盘核对", note: "原始计算供核对,分盘对出生时间敏感,不单独增加结论的确定性。" },
|
||||
en: { heading: "Divisional charts for checking", note: "Raw calculations for checking. Divisional charts are sensitive to birth time and do not by themselves make a conclusion more certain." },
|
||||
zh: { heading: "分盘核对" },
|
||||
en: { heading: "Divisional charts for checking" },
|
||||
} as const;
|
||||
|
||||
export const ReportCalculationCharts = memo(function ReportCalculationCharts({ charts, language = "zh" }: { charts: ReportDocumentV2["charts"]; language?: ReportLanguage }) {
|
||||
/**
|
||||
* Structured charts the report body did not already draw (BUG-1203): the same
|
||||
* rule as the export. A current report draws every chart in its body, so this
|
||||
* renders nothing; an older report without chart fences still gets its charts.
|
||||
* The birth-time caveat lives under the body's chart heading.
|
||||
*/
|
||||
export const ReportCalculationCharts = memo(function ReportCalculationCharts({ charts, markdown, language = "zh" }: {
|
||||
charts: ReportDocumentV2["charts"];
|
||||
/** The body shown on the page, whose chart fences are already drawn. */
|
||||
markdown: string;
|
||||
language?: ReportLanguage;
|
||||
}) {
|
||||
const copy = COPY[language];
|
||||
const actual = charts.filter(chart => hasRealChartData(chart as ChartV1));
|
||||
const drawn = useMemo(() => fencedReportChartIds(markdown), [markdown]);
|
||||
const actual = charts.filter(chart => hasRealChartData(chart as ChartV1) && !drawn.has(chart.id));
|
||||
if (!actual.length) return null;
|
||||
return <section className="personal-report-section" aria-label={copy.heading}>
|
||||
<h2>{copy.heading}</h2>
|
||||
<p>{copy.note}</p>
|
||||
<div className="personal-report-chart-grid">
|
||||
{actual.map(chart => <article className="personal-report-chart-card" key={chart.id}>
|
||||
<h3>{language === "en" ? `${chart.id} chart` : chart.title}</h3>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { CHART_BIRTH_FAILURE_REASONS } from "./chart-birth-truth.ts";
|
||||
import { PLANET_GLYPH_KEYS } from "./planet-glyphs.ts";
|
||||
import { VARGA_DIGNITY_LEVELS } from "./chart-view-varga.ts";
|
||||
|
||||
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
@@ -82,7 +83,7 @@ export const chartViewVargaRowSchema = z.object({
|
||||
house: z.number().int().min(1).max(12),
|
||||
natalSign: z.string().min(1),
|
||||
natalSignLabel: z.string().min(1),
|
||||
dignity: z.enum(["exalted", "own", "debilitated"]).nullable(),
|
||||
dignity: z.enum(VARGA_DIGNITY_LEVELS).nullable(),
|
||||
vargottama: z.boolean(),
|
||||
retrograde: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
signZh,
|
||||
} from "./chart-view-labels.ts";
|
||||
import { traditionalSymbol } from "./chart-view-symbols.ts";
|
||||
import { vargaDignity } from "./chart-view-varga.ts";
|
||||
import { vargaDignityLevel } from "./chart-view-varga.ts";
|
||||
import { transitsFromEngine } from "./ephemeris-view.ts";
|
||||
import { TRANSIT_LOCAL_TIME } from "./transit-chart-body.ts";
|
||||
import {
|
||||
@@ -209,9 +209,8 @@ function houseFromSigns(sign: string, ascSign: string): number {
|
||||
|
||||
/**
|
||||
* The divisional chart's own rows: the engine's divisional sign, in-varga
|
||||
* degree and house (counted from the divisional ascendant), plus the D1 sign
|
||||
* each body came from. Nothing is recomputed here except the sign-dignity
|
||||
* lookup, whose tables are the engine's (`chart-view-varga.ts`).
|
||||
* degree, house (counted from the divisional ascendant) and dignity level,
|
||||
* plus the D1 sign each body came from. Nothing is recomputed here.
|
||||
*/
|
||||
function vargaRows(
|
||||
ascSign: string,
|
||||
@@ -236,7 +235,7 @@ function vargaRows(
|
||||
house: engineHouse !== null && engineHouse >= 1 && engineHouse <= 12 ? Math.trunc(engineHouse) : houseFromSigns(sign, ascSign),
|
||||
natalSign,
|
||||
natalSignLabel: signZh(natalSign),
|
||||
dignity: vargaDignity(name, sign),
|
||||
dignity: vargaDignityLevel(planet.dignity_level),
|
||||
vargottama: sign === natalSign,
|
||||
retrograde: natalRetro.get(name) ?? false,
|
||||
});
|
||||
@@ -323,9 +322,10 @@ function buildVargas(
|
||||
chart: mapped.chart,
|
||||
...(table ? {
|
||||
...table,
|
||||
// A null dignity or false same-sign result is still a valid result.
|
||||
// Availability follows validated source rows, never positive hits.
|
||||
supportsDignity: table.rows.length > 0,
|
||||
// The engine gives every body a level; a row without one is an
|
||||
// older cached payload, and then the column is hidden (BUG-1202).
|
||||
supportsDignity: table.rows.length > 0 && table.rows.every((row) => row.dignity !== null),
|
||||
supportsVargottama: division === 9 && (table.rows.length > 0 || table.ascendant !== undefined),
|
||||
} : {}),
|
||||
});
|
||||
@@ -383,9 +383,9 @@ function planetRows(chart: Record<string, unknown>, vargaPacket: Record<string,
|
||||
}
|
||||
|
||||
/**
|
||||
* The ascendant as a planet-table row. Sign and degree come straight from the
|
||||
* engine's `ascendant`; nakshatra, pada and their lord stay null because the
|
||||
* engine does not return them for a chart point and they must not be guessed.
|
||||
* The ascendant as a planet-table row, straight from the engine's `ascendant`
|
||||
* (nakshatra, pada and lord since BUG-1201). An older cached chart without
|
||||
* them leaves the three cells empty; they are never derived here.
|
||||
*/
|
||||
function ascendantRow(chart: Record<string, unknown>) {
|
||||
const ascendant = record(chart.ascendant);
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import tables from "./vedic-chart-tables.json";
|
||||
|
||||
export type VargaDignity = "exalted" | "own" | "debilitated";
|
||||
/**
|
||||
* The engine's dignity levels (`jyotish_engine._get_dignity_level`). A
|
||||
* divisional row carries the level `/api/varga_full` computed with
|
||||
* `varga_dignity_level`, the Vimsopaka function; the page never recomputes it.
|
||||
*/
|
||||
export const VARGA_DIGNITY_LEVELS = [
|
||||
"EXALTED",
|
||||
"MOOLATRIKONA",
|
||||
"OWN_SIGN",
|
||||
"GREAT_FRIEND",
|
||||
"FRIEND",
|
||||
"NEUTRAL",
|
||||
"ENEMY",
|
||||
"GREAT_ENEMY",
|
||||
"DEBILITATED",
|
||||
"NEECHA_BHANGA",
|
||||
] as const;
|
||||
|
||||
export type VargaDignity = (typeof VARGA_DIGNITY_LEVELS)[number];
|
||||
|
||||
const EXALTATION: Readonly<Record<string, string>> = tables.exaltation;
|
||||
const DEBILITATION: Readonly<Record<string, string>> = tables.debilitation;
|
||||
const SIGN_LORDS: Readonly<Record<string, string>> = tables.signLords;
|
||||
const NODE_OWN_SIGN: Readonly<Record<string, string>> = tables.nodeOwnSign;
|
||||
const VARGA_MEANING: Readonly<Record<string, string>> = tables.vargaMeaning;
|
||||
|
||||
/**
|
||||
* Sign dignity of a body inside a divisional chart.
|
||||
*
|
||||
* Same precedence as `jyotish_engine._get_dignity_level` (exalted, then own
|
||||
* sign, then debilitated) without the parts that need a D1 degree or the whole
|
||||
* chart: no moolatrikona band, no neecha bhanga, no friendship. The tables are
|
||||
* the engine's; `tests/test_chart_page_vedic_tables_contract.py` pins them.
|
||||
*/
|
||||
export function vargaDignity(planet: string, sign: string): VargaDignity | null {
|
||||
if (EXALTATION[planet] === sign) return "exalted";
|
||||
if (SIGN_LORDS[sign] === planet || NODE_OWN_SIGN[planet] === sign) return "own";
|
||||
if (DEBILITATION[planet] === sign) return "debilitated";
|
||||
return null;
|
||||
}
|
||||
/** Same wording as the D1 "状态" column (`jyotish_engine.DIGNITY_LABELS`). */
|
||||
export const VARGA_DIGNITY_LABEL: Readonly<Record<VargaDignity, string>> = tables.dignityLabels;
|
||||
|
||||
export const VARGA_DIGNITY_LABEL: Record<VargaDignity, string> = {
|
||||
exalted: "入旺",
|
||||
own: "自宫",
|
||||
debilitated: "落陷",
|
||||
};
|
||||
export function vargaDignityLevel(value: unknown): VargaDignity | null {
|
||||
return typeof value === "string" && (VARGA_DIGNITY_LEVELS as readonly string[]).includes(value)
|
||||
? value as VargaDignity
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The engine's own one-line use of a D-number chart (`VargaType.meaning`). */
|
||||
export function vargaMeaning(division: number): string | null {
|
||||
|
||||
@@ -203,6 +203,19 @@ export function toNorthIndianChart(block: ReportChartBlock, language: ReportLang
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of the valid chart fences a report body draws. The reader page and the
|
||||
* export both skip a structured chart whose id is already here (BUG-1203).
|
||||
*/
|
||||
export function fencedReportChartIds(markdown: string): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const match of markdown.matchAll(/```jyotish-chart\r?\n([\s\S]*?)```/g)) {
|
||||
const block = parseReportChartBlock(match[1]);
|
||||
if (block) ids.add(block.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function stripReportChartBlocks(markdown: string): string {
|
||||
const stripped = markdown.replace(FENCE_RE, "\n\n");
|
||||
return stripped.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReportDocumentV2 } from "./personal-report-contract";
|
||||
import { reportChartExportSvg } from "./report-chart-export";
|
||||
import { buildLongformOutline, personalReportMarkdownFilename } from "./personal-report-longform-outline";
|
||||
import { chartAriaLabel, parseReportChartBlock, stripReportChartBlocks, toNorthIndianChart, type ReportChartBlock } from "./report-chart-block";
|
||||
import { chartAriaLabel, fencedReportChartIds, parseReportChartBlock, stripReportChartBlocks, toNorthIndianChart, type ReportChartBlock } from "./report-chart-block";
|
||||
import type { ReportFactTable } from "./report-fact-table-schema";
|
||||
import { factCellText, factStatusLabel } from "./report-fact-table-display";
|
||||
import type { ReportLanguage } from "./report-language";
|
||||
@@ -71,12 +71,11 @@ export function buildReportExportBlocks({ markdown, charts = [], factTables = []
|
||||
for (const section of outline.sections) {
|
||||
add(`body:${section.id}`, "body", section.title, stripReportChartBlocks(section.sourceMarkdown));
|
||||
}
|
||||
const chartIds = new Set<string>();
|
||||
const chartIds = fencedReportChartIds(projected);
|
||||
for (const match of projected.matchAll(/```jyotish-chart\r?\n([\s\S]*?)```/g)) {
|
||||
const block = parseReportChartBlock(match[1]);
|
||||
if (!block) continue;
|
||||
const occurrence = blocks.filter(item => item.group === "chart" && item.id.startsWith(`chart:${block.id}:`)).length + 1;
|
||||
chartIds.add(block.id);
|
||||
// Only the immediately preceding SVG belongs to this occurrence (D9 may repeat).
|
||||
// The public projection has already rejected unsafe SVG; never recover raw HTML.
|
||||
const preceding = projected.slice(0, match.index);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_comment": "Chart-page reference tables. Mirrors jyotish_engine.SIGN_LORDS / EXALTATION / DEBILITATION, the Rahu-Virgo / Ketu-Pisces own-sign rule in jyotish_engine._get_dignity_level, and divisional_charts_extended.VargaType meanings. tests/test_chart_page_vedic_tables_contract.py fails if either side drifts.",
|
||||
"_comment": "Engine tables the chart page reads. vargaMeaning = divisional_charts_extended.VargaType meaning; dignityLabels = jyotish_engine.DIGNITY_LABELS (the D1 status wording). Pinned by tests/test_chart_page_vedic_tables_contract.py.",
|
||||
"vargaMeaning": {
|
||||
"1": "本命盘",
|
||||
"2": "财富",
|
||||
@@ -22,40 +22,16 @@
|
||||
"45": "全面判断",
|
||||
"60": "前世业力"
|
||||
},
|
||||
"signLords": {
|
||||
"Aries": "Mars",
|
||||
"Taurus": "Venus",
|
||||
"Gemini": "Mercury",
|
||||
"Cancer": "Moon",
|
||||
"Leo": "Sun",
|
||||
"Virgo": "Mercury",
|
||||
"Libra": "Venus",
|
||||
"Scorpio": "Mars",
|
||||
"Sagittarius": "Jupiter",
|
||||
"Capricorn": "Saturn",
|
||||
"Aquarius": "Saturn",
|
||||
"Pisces": "Jupiter"
|
||||
},
|
||||
"exaltation": {
|
||||
"Sun": "Aries",
|
||||
"Moon": "Taurus",
|
||||
"Mars": "Capricorn",
|
||||
"Mercury": "Virgo",
|
||||
"Jupiter": "Cancer",
|
||||
"Venus": "Pisces",
|
||||
"Saturn": "Libra"
|
||||
},
|
||||
"debilitation": {
|
||||
"Sun": "Libra",
|
||||
"Moon": "Scorpio",
|
||||
"Mars": "Cancer",
|
||||
"Mercury": "Pisces",
|
||||
"Jupiter": "Capricorn",
|
||||
"Venus": "Virgo",
|
||||
"Saturn": "Aries"
|
||||
},
|
||||
"nodeOwnSign": {
|
||||
"Rahu": "Virgo",
|
||||
"Ketu": "Pisces"
|
||||
"dignityLabels": {
|
||||
"EXALTED": "入旺(Exalted)",
|
||||
"MOOLATRIKONA": "本垣(Moolatrikona)",
|
||||
"OWN_SIGN": "入庙(Own Sign)",
|
||||
"GREAT_FRIEND": "极友(Great Friend)",
|
||||
"FRIEND": "入友(Friendly Sign)",
|
||||
"NEUTRAL": "中性(Neutral)",
|
||||
"ENEMY": "入敌(Enemy Sign)",
|
||||
"GREAT_ENEMY": "极敌(Great Enemy)",
|
||||
"DEBILITATED": "落陷(Debilitated)",
|
||||
"NEECHA_BHANGA": "落陷取消(Neecha Bhanga)"
|
||||
}
|
||||
}
|
||||
@@ -90,13 +90,34 @@ test("a divisional chart's table carries that chart's signs, degrees and houses"
|
||||
assert.match(ascD9, /射手/, "D9 ascendant is Sagittarius");
|
||||
});
|
||||
|
||||
test("the D1 table is byte-for-byte the table the page drew before BUG-1076", async () => {
|
||||
test("the D1 table is byte-for-byte the table the page drew before BUG-1076, ascendant nakshatra aside", async () => {
|
||||
const view = await vargaView();
|
||||
const markup = render(view, "D1");
|
||||
const table = markup.match(/<div class="chart-page-planet-table-wrap">[\s\S]*?<\/table><\/div>/)?.[0];
|
||||
// Rendered from the baseline commit 5dc977ef with the same golden.
|
||||
// Rendered from the baseline commit 5dc977ef with the same golden. The only
|
||||
// change since is BUG-1201: the engine now gives the ascendant its nakshatra
|
||||
// (Leo 29°23′ = Uttara Phalguni pada 1, lord Sun), where the baseline had dashes.
|
||||
const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8");
|
||||
assert.equal(table, baseline);
|
||||
const dashes = '<td>—</td><td class="chart-page-planet-mid">—</td><td class="chart-page-planet-mid chart-page-planet-num">—</td><td>—</td>';
|
||||
const filled = '<td>Uttara Phalguni</td><td class="chart-page-planet-mid"><span class="chart-page-planet-glyph is-sun">☉</span></td>'
|
||||
+ '<td class="chart-page-planet-mid chart-page-planet-num">1</td><td>—</td>';
|
||||
assert.equal(baseline.split(dashes).length, 2, "the baseline's ascendant row is the only all-dash row");
|
||||
assert.equal(table, baseline.replace(dashes, filled));
|
||||
});
|
||||
|
||||
// BUG-1201: the ascendant row's nakshatra cells were always dashes.
|
||||
test("the D1 ascendant row shows the engine's nakshatra, lord and pada", async () => {
|
||||
const view = await vargaView();
|
||||
assert.equal(view.vedic.ascendant?.nakshatra, "Uttara Phalguni");
|
||||
assert.equal(view.vedic.ascendant?.pada, 1);
|
||||
const ascendant = row(render(view, "D1"), "上升");
|
||||
assert.match(ascendant, /<td>Uttara Phalguni<\/td>/);
|
||||
assert.match(ascendant, /is-sun/, "Uttara Phalguni's lord is the Sun");
|
||||
// An older cached chart without the fields still renders, with dashes.
|
||||
const chart = structuredClone(golden.chart);
|
||||
for (const key of ["nakshatra", "nakshatra_pada", "nakshatra_lord"]) delete (chart.ascendant as Record<string, unknown>)[key];
|
||||
const old = buildChartView({ chart, varga: null, chara: null, western: null, profile, asOf: "2026-09-15" });
|
||||
assert.equal(old.vedic.ascendant?.nakshatra, null);
|
||||
});
|
||||
|
||||
test("divisional rows carry engine dignity and vargottama, and drop nakshatra columns", async () => {
|
||||
@@ -106,18 +127,23 @@ test("divisional rows carry engine dignity and vargottama, and drop nakshatra co
|
||||
const saturn = d9.rows.find((item) => item.name === "Saturn");
|
||||
// D1 Saturn is in Capricorn and D9 Saturn is in Capricorn: own sign, vargottama.
|
||||
assert.equal(saturn?.sign, "Capricorn");
|
||||
assert.equal(saturn?.dignity, "own");
|
||||
assert.equal(saturn?.dignity, "OWN_SIGN");
|
||||
assert.equal(saturn?.vargottama, true);
|
||||
const sun = d9.rows.find((item) => item.name === "Sun");
|
||||
assert.equal(sun?.sign, "Libra");
|
||||
assert.equal(sun?.dignity, "debilitated");
|
||||
assert.equal(sun?.dignity, "DEBILITATED");
|
||||
assert.equal(sun?.vargottama, false);
|
||||
const markup = render(view, "D9");
|
||||
const header = markup.match(/<thead>([\s\S]*?)<\/thead>/)?.[1] ?? "";
|
||||
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
|
||||
assert.doesNotMatch(header, /星宿|宿主|pada/);
|
||||
assert.match(row(markup, "土星"), />自宫<[\s\S]*>同位</);
|
||||
assert.match(row(markup, "太阳"), />落陷</);
|
||||
// Same wording as the D1 status column (BUG-1202), friendship levels included.
|
||||
assert.match(row(markup, "土星"), />入庙\(Own Sign\)<[\s\S]*>同位</);
|
||||
assert.match(row(markup, "太阳"), />落陷\(Debilitated\)</);
|
||||
assert.match(row(markup, "月亮"), />入友\(Friendly Sign\)</);
|
||||
assert.match(row(markup, "火星"), />中性\(Neutral\)</);
|
||||
assert.match(row(markup, "水星"), />入敌\(Enemy Sign\)</);
|
||||
assert.match(row(markup, "木星"), />极敌\(Great Enemy\)</);
|
||||
assert.match(row(markup, "上升"), />不适用</, "the ascendant has no sign dignity");
|
||||
// Every divisional chart the engine returned has its own rows, D1 has none.
|
||||
for (const varga of view.vedic.vargas) {
|
||||
@@ -181,7 +207,7 @@ test("valid false and null results never turn supported columns off", async () =
|
||||
copy.ascendant!.vargottama = false;
|
||||
const markup = tableMarkup(chartViewVargaSchema.parse(copy));
|
||||
assertColumns(markup, true, true);
|
||||
assert.doesNotMatch(markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|>入旺<|>自宫<|>落陷</);
|
||||
assert.doesNotMatch(markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|\((?:Exalted|Own Sign|Debilitated|Neutral)\)</);
|
||||
assert.match(row(markup, "上升"), />不适用</);
|
||||
});
|
||||
|
||||
@@ -200,6 +226,24 @@ test("capability schema rejects wrong types and direct cache rendering requires
|
||||
}
|
||||
});
|
||||
|
||||
// BUG-1202: the dignity column is the engine's; it is never recomputed here.
|
||||
test("a divisional payload without engine dignity hides the column instead of deriving it", () => {
|
||||
const varga = structuredClone(golden.varga_full);
|
||||
for (const item of Object.values(varga.result)) {
|
||||
for (const planet of Object.values(item.planets)) delete planet.dignity_level;
|
||||
}
|
||||
const view = buildChartView({ chart: structuredClone(golden.chart), varga: varga as unknown as Record<string, unknown>, chara: null, western: null, profile, asOf: "2026-09-15" });
|
||||
const d9 = view.vedic.vargas.find((item) => item.id === "D9")!;
|
||||
assert.equal(d9.supportsDignity, false);
|
||||
assert.equal(d9.supportsVargottama, true);
|
||||
assert.ok(d9.rows!.every((item) => item.dignity === null));
|
||||
assertColumns(tableMarkup(d9), false, true);
|
||||
const unknown = structuredClone(golden.varga_full);
|
||||
unknown.result.D9_Navamsa!.planets.Sun!.dignity_level = "SOMETHING_NEW";
|
||||
const mixed = buildChartView({ chart: structuredClone(golden.chart), varga: unknown as unknown as Record<string, unknown>, chara: null, western: null, profile, asOf: "2026-09-15" });
|
||||
assert.equal(mixed.vedic.vargas.find((item) => item.id === "D9")!.supportsDignity, false);
|
||||
});
|
||||
|
||||
test("missing or invalid source positions are not manufactured as negative same-sign results", () => {
|
||||
const chart = structuredClone(golden.chart);
|
||||
const varga = structuredClone(golden.varga_full);
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { vargaDignity, vargaMeaning } from "../src/lib/chart-view-varga.ts";
|
||||
import { VARGA_DIGNITY_LABEL, VARGA_DIGNITY_LEVELS, vargaDignityLevel, vargaMeaning } from "../src/lib/chart-view-varga.ts";
|
||||
|
||||
// The tables are pinned to the engine by tests/test_chart_page_vedic_tables_contract.py;
|
||||
// this pins the precedence (exalted, own, debilitated) the Python test mirrors.
|
||||
test("sign dignity follows the engine's precedence and node own signs", () => {
|
||||
assert.equal(vargaDignity("Sun", "Aries"), "exalted");
|
||||
assert.equal(vargaDignity("Sun", "Leo"), "own");
|
||||
assert.equal(vargaDignity("Sun", "Libra"), "debilitated");
|
||||
assert.equal(vargaDignity("Mercury", "Virgo"), "exalted", "exaltation wins over own sign");
|
||||
assert.equal(vargaDignity("Rahu", "Virgo"), "own");
|
||||
assert.equal(vargaDignity("Ketu", "Pisces"), "own");
|
||||
assert.equal(vargaDignity("Rahu", "Taurus"), null, "the engine has no node exaltation");
|
||||
assert.equal(vargaDignity("Moon", "Leo"), null);
|
||||
// BUG-1202: the level is the engine's (`/api/varga_full` dignity_level); the
|
||||
// page only validates it and labels it with the D1 status wording, pinned to
|
||||
// jyotish_engine.DIGNITY_LABELS by tests/test_chart_page_vedic_tables_contract.py.
|
||||
test("engine dignity levels are accepted as-is and anything else is unknown", () => {
|
||||
for (const level of VARGA_DIGNITY_LEVELS) assert.equal(vargaDignityLevel(level), level);
|
||||
assert.equal(vargaDignityLevel("exalted"), null, "the retired three-level values are not engine levels");
|
||||
assert.equal(vargaDignityLevel(undefined), null);
|
||||
assert.equal(vargaDignityLevel(1), null);
|
||||
});
|
||||
|
||||
test("every engine level has the D1 status wording", () => {
|
||||
assert.equal(VARGA_DIGNITY_LABEL.EXALTED, "入旺(Exalted)");
|
||||
assert.equal(VARGA_DIGNITY_LABEL.OWN_SIGN, "入庙(Own Sign)");
|
||||
assert.equal(VARGA_DIGNITY_LABEL.ENEMY, "入敌(Enemy Sign)");
|
||||
assert.equal(VARGA_DIGNITY_LABEL.NEUTRAL, "中性(Neutral)");
|
||||
assert.deepEqual(Object.keys(VARGA_DIGNITY_LABEL).sort(), [...VARGA_DIGNITY_LEVELS].sort());
|
||||
});
|
||||
|
||||
test("varga meanings come from the engine table including D40, D45 and D60", () => {
|
||||
|
||||
+365
-182
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import test from "node:test";
|
||||
|
||||
import { ReportCalculationCharts } from "../src/components/personal-report/report-calculation-charts.tsx";
|
||||
import { buildReportExportBlocks } from "../src/lib/report-export-blocks.ts";
|
||||
import { fencedReportChartIds } from "../src/lib/report-chart-block.ts";
|
||||
import { assembleLongformCharts } from "../src/lib/personal-report-generation.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
// Real engine output and its real reader body (fictional chart).
|
||||
const fixture = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8"));
|
||||
const engine = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8"));
|
||||
const charts = assembleLongformCharts(engine, "ev-cover-longform");
|
||||
|
||||
function render(markdown: string, language: "zh" | "en" = "zh"): string {
|
||||
return renderToStaticMarkup(React.createElement(ReportCalculationCharts, { charts, markdown, language }));
|
||||
}
|
||||
|
||||
// BUG-1203: the body already drew these charts; the tail drew them again.
|
||||
test("a report whose body draws every chart has no trailing chart section", () => {
|
||||
const drawn = fencedReportChartIds(fixture.markdown);
|
||||
assert.ok(charts.length > 0);
|
||||
for (const chart of charts) assert.ok(drawn.has(chart.id), chart.id);
|
||||
assert.equal(render(fixture.markdown), "");
|
||||
assert.equal(render(fixture.markdown, "en"), "");
|
||||
});
|
||||
|
||||
test("an older body without chart fences keeps its charts, without the moved caveat", () => {
|
||||
const markup = render("# 报告\n\n正文");
|
||||
assert.match(markup, /<h2>分盘核对<\/h2>/);
|
||||
assert.equal((markup.match(/personal-report-chart-card/g) ?? []).length, charts.filter(chart => chart.houses.some(house => house.occupants.length)).length);
|
||||
assert.doesNotMatch(markup, /出生时间敏感/);
|
||||
});
|
||||
|
||||
test("the page and the export skip the same charts", () => {
|
||||
const first = /```jyotish-chart\n[\s\S]*?```/.exec(fixture.markdown)![0];
|
||||
const drawn = [...fencedReportChartIds(first)];
|
||||
assert.equal(drawn.length, 1);
|
||||
const markup = render(first);
|
||||
const pageIds = charts.filter(chart => !drawn.includes(chart.id) && chart.houses.some(house => house.occupants.length)).map(chart => chart.id);
|
||||
assert.equal((markup.match(/personal-report-chart-card/g) ?? []).length, pageIds.length);
|
||||
const exported = buildReportExportBlocks({ markdown: first, charts }).filter(block => block.group === "chart" && !/^chart:[^:]+:\d+$/.test(block.id)); // fence charts carry an occurrence suffix
|
||||
assert.deepEqual(exported.map(block => block.id.replace(/^chart:/, "")), pageIds);
|
||||
});
|
||||
@@ -2575,7 +2575,7 @@ def _vedastro_runtime_fingerprint() -> dict:
|
||||
|
||||
def _build_api_chart_cache_payload(body: dict) -> dict:
|
||||
return {
|
||||
'cache_schema_version': 4,
|
||||
'cache_schema_version': 5,
|
||||
'birth': {
|
||||
'year': body.get('year'),
|
||||
'month': body.get('month'),
|
||||
@@ -7820,7 +7820,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
|
||||
result = {}
|
||||
for varga in selected:
|
||||
key = f'D{varga.division}_{varga.varga_name}'
|
||||
result[key] = calc._calculate_single_varga(varga, planet_lons, asc_lon)
|
||||
result[key] = _load_local_module('varga_dignity').annotate_varga_dignity(calc._calculate_single_varga(varga, planet_lons, asc_lon))
|
||||
except KeyError as e:
|
||||
raise BadRequest('unsupported standard division; use custom mode for arbitrary D-N') from e
|
||||
except ValueError as e:
|
||||
|
||||
+33
-12
@@ -701,6 +701,20 @@ def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
|
||||
return 'NEUTRAL'
|
||||
|
||||
|
||||
def varga_dignity_level(planet, varga_planets):
|
||||
"""Dignity of a body inside a divisional chart (Vimsopaka and the chart page alike).
|
||||
|
||||
The temporary half of the compound relationship is read from that varga's
|
||||
own positions, as the Vimsopaka path always has. Returns None when the body
|
||||
has no usable sign.
|
||||
"""
|
||||
pinfo = varga_planets.get(planet) if isinstance(varga_planets, dict) else None
|
||||
if not isinstance(pinfo, dict) or pinfo.get('sign') not in SIGN_LORDS:
|
||||
return None
|
||||
deg_in_sign = pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30)
|
||||
return _get_dignity_level(planet, pinfo['sign'], deg_in_sign, _build_dignity_context(varga_planets))
|
||||
|
||||
|
||||
def _get_planet_status_label(planet, sign, deg_in_sign=None, planets_data=None):
|
||||
"""Return the user-facing D1 dignity label for chart output."""
|
||||
return DIGNITY_LABELS.get(_get_dignity_level(planet, sign, deg_in_sign, planets_data), '中性')
|
||||
@@ -12228,6 +12242,15 @@ def _load_strict_evidence_collector():
|
||||
# ============================================================================
|
||||
# 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取)
|
||||
# ============================================================================
|
||||
def _nakshatra_fields(sidereal_lon):
|
||||
"""Nakshatra, pada and lord of a sidereal longitude — planets and the ascendant alike."""
|
||||
nak_span = 360.0 / 27
|
||||
lon = sidereal_lon % 360
|
||||
pada = int((lon % nak_span) / (nak_span / 4)) + 1
|
||||
name, lord, _ = NAKSHATRA_LIST[int(lon / nak_span) % 27]
|
||||
return {"nakshatra": name, "nakshatra_pada": pada, "nakshatra_lord": lord}
|
||||
|
||||
|
||||
def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='mean', second=0, ayanamsa_name=None):
|
||||
"""计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)。node_mode: mean|true。"""
|
||||
if not HAS_SWE:
|
||||
@@ -12264,7 +12287,8 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
|
||||
"degree_in_sign": round(deg_in_sign, 4),
|
||||
"degree_in_sign_raw": deg_in_sign,
|
||||
"lon": round(asc_deg, 4),
|
||||
"lord": SIGN_LORDS[asc_sign]}
|
||||
"lord": SIGN_LORDS[asc_sign],
|
||||
**_nakshatra_fields(asc_deg)}
|
||||
|
||||
for i in range(12):
|
||||
c = (asc_lon[i] - ayanamsa) % 360
|
||||
@@ -12273,7 +12297,6 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
|
||||
"cusp_sign_cn": SIGNS_CN[SIGNS[si]], "cusp_degree": round(c, 4),
|
||||
"lord": SIGN_LORDS[SIGNS[si]]}
|
||||
|
||||
nak_span = 360.0 / 27
|
||||
planets_swe = {**BASE_PLANETS_SWE, 'Rahu': node_pid}
|
||||
calc_flags = getattr(swe, 'FLG_SWIEPH', 2)
|
||||
for pname, pid in planets_swe.items():
|
||||
@@ -12283,26 +12306,22 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='
|
||||
si = int(lon_p / 30); d_in_s = lon_p - si * 30; sign = SIGNS[si]
|
||||
retro = spd < 0
|
||||
house = ((si - asc_idx) % 12) + 1
|
||||
ni = int(lon_p / nak_span); pada = int((lon_p % nak_span) / (nak_span / 4)) + 1
|
||||
nak_n, nak_l, _ = NAKSHATRA_LIST[ni % 27]
|
||||
result["planets"][pname] = {
|
||||
"sign": sign, "sign_cn": SIGNS_CN[sign], "degree": round(lon_p, 4),
|
||||
"degree_raw": lon_p,
|
||||
"degree_in_sign": round(d_in_s, 4), "degree_in_sign_raw": d_in_s,
|
||||
"house": house, "status": "",
|
||||
"retrograde": retro, "speed": round(spd, 6),
|
||||
"nakshatra": nak_n, "nakshatra_pada": pada, "nakshatra_lord": nak_l}
|
||||
**_nakshatra_fields(lon_p)}
|
||||
if pname == 'Rahu':
|
||||
klon = (lon_p + 180) % 360; ksi = int(klon / 30); kd = klon - ksi * 30
|
||||
kni = int(klon / nak_span); kp = int((klon % nak_span) / (nak_span / 4)) + 1
|
||||
kn, kl, _ = NAKSHATRA_LIST[kni % 27]
|
||||
result["planets"]["Ketu"] = {
|
||||
"sign": SIGNS[ksi], "sign_cn": SIGNS_CN[SIGNS[ksi]],
|
||||
"degree": round(klon, 4), "degree_raw": klon,
|
||||
"degree_in_sign": round(kd, 4), "degree_in_sign_raw": kd,
|
||||
"house": ((ksi - asc_idx) % 12) + 1, "status": "",
|
||||
"retrograde": True, "speed": round(spd, 6),
|
||||
"nakshatra": kn, "nakshatra_pada": kp, "nakshatra_lord": kl}
|
||||
**_nakshatra_fields(klon)}
|
||||
except Exception as e:
|
||||
result["planets"][pname] = {"error": str(e)}
|
||||
|
||||
@@ -15689,10 +15708,12 @@ def cmd_full_reading(args):
|
||||
# 不使用 _dignity 字典(只有 Exalted/Debilitated/Own Sign)
|
||||
pinfo = vdata.get(pn, {})
|
||||
if isinstance(pinfo, dict) and 'sign' in pinfo:
|
||||
dignity_context = planets if div == 1 else _build_dignity_context(vdata)
|
||||
dl_key = _get_dignity_level(pn, pinfo['sign'],
|
||||
pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
|
||||
dignity_context)
|
||||
if div == 1:
|
||||
dl_key = _get_dignity_level(pn, pinfo['sign'],
|
||||
pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
|
||||
planets)
|
||||
else:
|
||||
dl_key = varga_dignity_level(pn, vdata)
|
||||
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
|
||||
elif div == 1:
|
||||
# D1 直接用本命盘数据
|
||||
|
||||
@@ -35,6 +35,8 @@ _PHRASES: dict[str, str] = {
|
||||
"特殊上升与派生点补充": "Special Lagnas and Points (supplement)",
|
||||
"敏感点与特殊落点": "Sensitive Points and Special Placements",
|
||||
"本命与分盘北印度图盘": "Natal and Divisional Charts (North Indian)",
|
||||
"分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。":
|
||||
"Divisional charts are sensitive to birth time. The raw calculations are here for checking and do not by themselves make a conclusion more certain.",
|
||||
" 数值位置表": " Position Table",
|
||||
"Shadbala / Bhava Bala 摘要": "Shadbala / Bhava Bala Summary",
|
||||
"Avastha 行星状态": "Avastha Planetary States",
|
||||
@@ -84,7 +86,7 @@ _PHRASES: dict[str, str] = {
|
||||
"Sign lord, star lord, sub lord and sub-sub lord: the rulers of the planet's sign, nakshatra and KP subdivisions. Strength ratio: Shadbala against the required strength; 1 or above meets it.",
|
||||
"| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |": "| Body | Sign | Degree in sign | Longitude | Whole-sign house |",
|
||||
"| House | Sign | Lord | Score | 关键因素 |": "| House | Sign | Lord | Score | Key factors |",
|
||||
"| House | Sign | SAV | BAV/承接 | Full with Lagna |": "| House | Sign | SAV | BAV / carry | Full with Lagna |",
|
||||
"| 合计 | - |": "| Total | - |",
|
||||
"| 序号 | 主运 | 起始 | 结束 | 年数 |": "| No. | Mahadasha | Start | End | Years |",
|
||||
"| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |": "| Vimshottari AD | Mahadasha | Antardasha | Start | End |",
|
||||
"| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |": "| Vimshottari PD | Mahadasha | Antardasha | Pratyantardasha | Start | End |",
|
||||
|
||||
@@ -1008,7 +1008,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
]
|
||||
ready_natal_specs = [spec for spec in natal_specs if spec[2]]
|
||||
if ready_natal_specs:
|
||||
lines.extend(['### p6-p17 本命与分盘北印度图盘', ''])
|
||||
lines.extend(['### p6-p17 本命与分盘北印度图盘', '', '分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。', ''])
|
||||
for page, label, _ready in ready_natal_specs:
|
||||
lines.extend([f'##### p{page} · {label}', ''])
|
||||
# The original PL9 charts are visual, but their sign placements are
|
||||
@@ -1140,12 +1140,13 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
)
|
||||
lines.append('')
|
||||
lines.extend([
|
||||
'### p33 Declination / Kranti / Speed',
|
||||
'### p33 Declination / Speed',
|
||||
'',
|
||||
'| Planet | Degree | Declination | Kranti | Speed |',
|
||||
'|--------|--------|-------------|--------|-------|',
|
||||
'| Planet | Degree | Declination | Speed |',
|
||||
'|--------|--------|-------------|-------|',
|
||||
])
|
||||
kranti_planets = _dict(_dict(strengths.get('kranti_local')).get('planets'))
|
||||
# Kranti has no local producer (materials.kranti stays blocked), so
|
||||
# the reader shows no Kranti column rather than a column of dashes.
|
||||
speed_planets = _dict(_dict(strengths.get('speed_display_local')).get('planets'))
|
||||
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
|
||||
row = _dict(shadbala_planets.get(planet))
|
||||
@@ -1153,11 +1154,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
continue
|
||||
kala = _dict(row.get('kala_bala'))
|
||||
planet_row = _dict((packet.get('core_chart') or {}).get('planets', {}).get(planet)) if isinstance((packet.get('core_chart') or {}).get('planets'), dict) else {}
|
||||
kranti_degrees = _dict(kranti_planets.get(planet)).get('kranti_degrees')
|
||||
speed_dms = _dict(speed_planets.get(planet)).get('speed_dms') or _num(planet_row.get('speed'), 6)
|
||||
lines.append(
|
||||
f"| {_cell(_token(planet))} | {_cell(_degree(planet_row.get('degree_in_sign') or planet_row.get('longitude')))} | "
|
||||
f"{_cell(_degree(kala.get('declination')))} | {_cell(_degree(kranti_degrees))} | {_cell(speed_dms)} |"
|
||||
f"{_cell(_degree(kala.get('declination')))} | {_cell(speed_dms)} |"
|
||||
)
|
||||
lines.append('')
|
||||
avasthas = _dict(advanced.get('avasthas'))
|
||||
@@ -1329,15 +1329,38 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
lines.extend([
|
||||
'### p53 Ashtakavarga 完整宫位分数',
|
||||
'',
|
||||
'| House | Sign | SAV | BAV/承接 | Full with Lagna |',
|
||||
'|-------|------|-----|----------|-----------------|',
|
||||
'| House | Sign | SAV | Lagna BAV | SAV + Lagna |',
|
||||
'|-------|------|-----|-----------|-------------|',
|
||||
])
|
||||
# `house_scores_full[*].sav_score` is the with-Lagna total (386
|
||||
# scale), never plain SAV (BUG-1200). Split it from the engine's
|
||||
# per-sign SAV and the Lagna BAV; ashtakavarga.py stays untouched
|
||||
# because it belongs to the frozen rectification identity.
|
||||
sign_list = list(globals().get('SIGNS', []))
|
||||
lagna_bindus = _dict(_dict(ashtakavarga.get('bav')).get('Lagna')).get('bindus')
|
||||
splits = []
|
||||
for row in full_rows:
|
||||
sign = row.get('sign')
|
||||
sav_value = sav_scores.get(sign)
|
||||
lagna_value = None
|
||||
if isinstance(lagna_bindus, list) and len(lagna_bindus) == 12 and sign in sign_list:
|
||||
lagna_value = lagna_bindus[sign_list.index(sign)]
|
||||
consistent = (
|
||||
isinstance(sav_value, int) and isinstance(lagna_value, int)
|
||||
and sav_value + lagna_value == row.get('sav_score')
|
||||
)
|
||||
splits.append((sav_value, lagna_value) if consistent else None)
|
||||
split = all(splits)
|
||||
for row, parts in zip(full_rows, splits):
|
||||
sav_value, lagna_value = parts if split else (None, None)
|
||||
lines.append(
|
||||
f"| {_cell(row.get('house'))} | {_cell(_token(row.get('sign')))} | "
|
||||
f"{_cell(row.get('sav') or row.get('sav_score') or row.get('score'))} | "
|
||||
f"{_cell(row.get('bav') or row.get('bav_score') or row.get('bindus'))} | "
|
||||
f"{_cell(row.get('full_with_lagna') or row.get('full_with_lagna_house_score'))} |"
|
||||
f"{_cell(sav_value)} | {_cell(lagna_value)} | {_cell(row.get('sav_score'))} |"
|
||||
)
|
||||
if split:
|
||||
lines.append(
|
||||
f"| 合计 | - | {sum(parts[0] for parts in splits)} | "
|
||||
f"{sum(parts[1] for parts in splits)} | {sum(row['sav_score'] for row in full_rows)} |"
|
||||
)
|
||||
lines.append('')
|
||||
bav = _dict(ashtakavarga.get('bav'))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Engine dignity on each body of a `/api/varga_full` divisional chart (BUG-1202).
|
||||
|
||||
The chart page's divisional tables show the same ten-level dignity as the D1
|
||||
"状态" column. The level is `jyotish_engine.varga_dignity_level`, the function
|
||||
the Vimsopaka path uses, so the page never carries a second rule set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jyotish_engine import varga_dignity_level
|
||||
|
||||
|
||||
def annotate_varga_dignity(varga: dict) -> dict:
|
||||
"""Add `dignity_level` to every body of one `_calculate_single_varga` result."""
|
||||
planets = varga.get("planets") if isinstance(varga, dict) else None
|
||||
if not isinstance(planets, dict):
|
||||
return varga
|
||||
for name, row in planets.items():
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
level = varga_dignity_level(name, planets)
|
||||
if level:
|
||||
row["dignity_level"] = level
|
||||
return varga
|
||||
@@ -3413,6 +3413,30 @@ def test_varga_full_endpoint_returns_extended_standard_divisions() -> None:
|
||||
assert result['result']['D81_Navamsa-Navamsa']['planets']['Moon']['house'] in range(1, 13)
|
||||
|
||||
|
||||
def test_varga_full_standard_divisions_carry_the_vimsopaka_dignity_level() -> None:
|
||||
"""BUG-1202: the chart page shows this level; it is the Vimsopaka function's."""
|
||||
import jyotish_engine
|
||||
from varga import calc_all_vargas
|
||||
|
||||
handler = _handler()
|
||||
planets = _sample_planets()
|
||||
result = handler._compute_varga_full({
|
||||
'planets': planets,
|
||||
'ascendant': {'lon': 92.0},
|
||||
'divisions': ['D9', 'D10'],
|
||||
})
|
||||
d9 = result['result']['D9_Navamsa']['planets']
|
||||
assert {name: row['dignity_level'] for name, row in d9.items()} == {
|
||||
name: jyotish_engine.varga_dignity_level(name, d9) for name in d9
|
||||
}
|
||||
assert all(row['dignity_level'] in jyotish_engine.DIGNITY_LABELS for row in d9.values())
|
||||
# The Vimsopaka path reads the same positions from varga.calc_all_vargas.
|
||||
vimsopaka_d9 = calc_all_vargas({name: row['lon'] for name, row in planets.items()}, 92.0, None)['D9_Navamsa']
|
||||
for name, row in d9.items():
|
||||
assert vimsopaka_d9[name]['sign'] == row['sign'], name
|
||||
assert jyotish_engine.varga_dignity_level(name, vimsopaka_d9) == row['dignity_level'], name
|
||||
|
||||
|
||||
def test_varga_full_endpoint_supports_custom_and_composite_modes() -> None:
|
||||
handler = _handler()
|
||||
custom = handler._compute_varga_full({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""The chart page's dignity and varga-meaning tables must be the engine's.
|
||||
|
||||
`frontend/src/lib/vedic-chart-tables.json` is read by the chart-page mapper so
|
||||
the divisional tables can show 入旺 / 自宫 / 落陷 without a second hand-written
|
||||
rule set. This test pins it to the constants `jyotish_engine._get_dignity_level`
|
||||
uses and to `divisional_charts_extended.VargaType`.
|
||||
`frontend/src/lib/vedic-chart-tables.json` is read by the chart page: varga
|
||||
meanings (`divisional_charts_extended.VargaType`) and the dignity wording
|
||||
(`jyotish_engine.DIGNITY_LABELS`). The dignity level itself comes from
|
||||
`/api/varga_full` (`varga_dignity_level`, BUG-1202); the page never derives it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,33 +27,11 @@ TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_tex
|
||||
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
|
||||
|
||||
|
||||
def _table_dignity(planet: str, sign: str) -> str | None:
|
||||
"""Same precedence as `vargaDignity` in frontend/src/lib/chart-view-varga.ts."""
|
||||
if TABLES["exaltation"].get(planet) == sign:
|
||||
return "EXALTED"
|
||||
if TABLES["signLords"].get(sign) == planet or TABLES["nodeOwnSign"].get(planet) == sign:
|
||||
return "OWN_SIGN"
|
||||
if TABLES["debilitation"].get(planet) == sign:
|
||||
return "DEBILITATED"
|
||||
return None
|
||||
|
||||
|
||||
def test_tables_equal_engine_constants() -> None:
|
||||
assert TABLES["signLords"] == jyotish_engine.SIGN_LORDS
|
||||
assert TABLES["exaltation"] == jyotish_engine.EXALTATION
|
||||
assert TABLES["debilitation"] == jyotish_engine.DEBILITATION
|
||||
|
||||
|
||||
def test_every_planet_sign_pair_matches_engine_dignity_level() -> None:
|
||||
for planet in PLANETS:
|
||||
for sign in jyotish_engine.SIGN_LORDS:
|
||||
# No degree and no chart context: the varga table shows the sign
|
||||
# dignity only (no moolatrikona band, no neecha bhanga, no friendship).
|
||||
engine = jyotish_engine._get_dignity_level(planet, sign)
|
||||
expected = engine if engine in {"EXALTED", "OWN_SIGN", "DEBILITATED"} else None
|
||||
if engine == "NEECHA_BHANGA":
|
||||
expected = "DEBILITATED"
|
||||
assert _table_dignity(planet, sign) == expected, (planet, sign, engine)
|
||||
def test_dignity_labels_equal_engine_status_wording() -> None:
|
||||
# BUG-1202: divisional rows carry the engine's level and the page labels it
|
||||
# with the D1 status wording; the three-level page table is retired.
|
||||
assert TABLES["dignityLabels"] == jyotish_engine.DIGNITY_LABELS
|
||||
assert {"signLords", "exaltation", "debilitation", "nodeOwnSign"}.isdisjoint(TABLES)
|
||||
|
||||
|
||||
def test_varga_meanings_equal_engine_varga_types() -> None:
|
||||
@@ -131,3 +109,17 @@ def test_d9_projected_ascendant_matches_native_summary(d9_projection: tuple[dict
|
||||
assert projected["ascendant"]["sign"] == native["lagna"]
|
||||
assert projected["ascendant"]["natalSign"] == golden["chart"]["ascendant"]["sign"]
|
||||
assert projected["ascendant"]["vargottama"] is native["lagna_vargottama"]
|
||||
|
||||
|
||||
def test_d9_projected_dignity_is_the_engine_level(d9_projection: tuple[dict, dict]) -> None:
|
||||
golden, projected = d9_projection
|
||||
d9_planets = golden["varga_full"]["result"]["D9_Navamsa"]["planets"]
|
||||
levels = set()
|
||||
for row in projected["rows"]:
|
||||
expected = jyotish_engine.varga_dignity_level(row["name"], d9_planets)
|
||||
assert expected is not None
|
||||
assert row["dignity"] == expected == d9_planets[row["name"]]["dignity_level"]
|
||||
levels.add(expected)
|
||||
# The golden must show that friendship levels reach the page, not only
|
||||
# exalted / own / debilitated.
|
||||
assert {"OWN_SIGN", "DEBILITATED", "NEUTRAL", "ENEMY", "FRIEND"} <= levels
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Blank report and chart columns (TASK-report-chart-blank-columns-20261003).
|
||||
|
||||
BUG-1199 Kranti column with no producer; BUG-1200 Ashtakavarga house table
|
||||
labelled the with-Lagna total as SAV (split in the reader: ashtakavarga.py is
|
||||
in the frozen rectification identity and stays byte-identical); BUG-1201 ascendant without nakshatra;
|
||||
BUG-1203 the divisional-chart caveat moved to the chart section. Every chart
|
||||
here is fictional.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.ashtakavarga import BAV_TOTALS, EXPECTED_SAV_TOTAL, SIGNS
|
||||
from scripts.jyotish_engine import NAKSHATRA_LIST, _nakshatra_fields, compute_chart_data
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CAVEAT_ZH = "分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。"
|
||||
|
||||
|
||||
def _independent_nakshatra(lon: float) -> tuple[str, int, str]:
|
||||
minutes = (lon % 360) * 60
|
||||
index = int(minutes // 800) # 13°20′ = 800′
|
||||
pada = int((minutes % 800) // 200) + 1 # 3°20′ = 200′
|
||||
name, lord, _ = NAKSHATRA_LIST[index]
|
||||
return name, pada, lord
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lon,expected", [
|
||||
(0.0, ("Ashwini", 1, "Ketu")),
|
||||
(13.33, ("Ashwini", 4, "Ketu")),
|
||||
(13.34, ("Bharani", 1, "Venus")),
|
||||
(134.53, ("Purva Phalguni", 1, "Venus")),
|
||||
(149.3994, ("Uttara Phalguni", 1, "Sun")),
|
||||
(359.99, ("Revati", 4, "Mercury")),
|
||||
(360.0, ("Ashwini", 1, "Ketu")),
|
||||
])
|
||||
def test_nakshatra_fields_at_boundaries(lon, expected) -> None:
|
||||
fields = _nakshatra_fields(lon)
|
||||
assert (fields["nakshatra"], fields["nakshatra_pada"], fields["nakshatra_lord"]) == expected
|
||||
|
||||
|
||||
def test_chart_ascendant_carries_its_nakshatra_and_planets_are_unchanged() -> None:
|
||||
golden = json.loads((ROOT / "frontend/tests/fixtures/chart-view-golden.json").read_text(encoding="utf-8"))
|
||||
chart, *_ = compute_chart_data(1990, 6, 15, 12, 0, 39.9042, 116.4074, 8.0, ayanamsa_name="raman")
|
||||
asc = chart["ascendant"]
|
||||
assert (asc["nakshatra"], asc["nakshatra_pada"], asc["nakshatra_lord"]) == _independent_nakshatra(asc["lon"])
|
||||
assert {k: asc[k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")} == {
|
||||
k: golden["chart"]["ascendant"][k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")
|
||||
}
|
||||
for name, planet in chart["planets"].items():
|
||||
stored = golden["chart"]["planets"][name]
|
||||
assert (planet["nakshatra"], planet["nakshatra_pada"], planet["nakshatra_lord"]) == (
|
||||
stored["nakshatra"], stored["nakshatra_pada"], stored["nakshatra_lord"]), name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def editions() -> tuple[str, str, dict]:
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
return zh, _pl9_export_markdown_for_edition(english, "reader_main"), packet
|
||||
|
||||
|
||||
def _table_after(markdown: str, heading: str) -> list[list[str]]:
|
||||
lines = markdown.splitlines()
|
||||
start = next(i for i, line in enumerate(lines) if line.startswith("### ") and heading in line)
|
||||
rows = []
|
||||
for line in lines[start + 1:]:
|
||||
if line.startswith("### ") or (rows and not line.startswith("|")):
|
||||
break
|
||||
if line.startswith("|") and not set(line) <= set("|- "):
|
||||
rows.append([cell.strip() for cell in line.strip("|").split("|")])
|
||||
return rows
|
||||
|
||||
|
||||
def test_declination_table_has_no_kranti_column(editions) -> None:
|
||||
for markdown, heading in ((editions[0], "Declination / Speed"), (editions[1], "Declination / Speed")):
|
||||
assert "Kranti" not in markdown
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == ["Planet", "Degree", "Declination", "Speed"]
|
||||
assert len(rows) == 8 and all(len(row) == 4 and "-" not in row for row in rows)
|
||||
|
||||
|
||||
def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
|
||||
zh, en, packet = editions
|
||||
expected_sav = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sav"]["scores"]
|
||||
for markdown, heading, header, total in (
|
||||
(zh, "Ashtakavarga 完整宫位分数", ["House", "Sign", "SAV", "上升 BAV", "SAV + 上升"], "合计"),
|
||||
(en, "Ashtakavarga Full House Scores", ["House", "Sign", "SAV", "Lagna BAV", "SAV + Lagna"], "Total"),
|
||||
):
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == header
|
||||
body, totals = rows[1:13], rows[13]
|
||||
for row in body:
|
||||
sav, lagna, full = (int(cell) for cell in row[2:])
|
||||
assert sav + lagna == full
|
||||
assert totals == [total, "-", str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])]
|
||||
# SAV equals the per-sign SAV table, Lagna BAV the Lagna bindus, and the last
|
||||
# column the engine's `house_scores_full` total that rectification reads.
|
||||
ashtakavarga = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]
|
||||
full = {row["sign"]: row["sav_score"] for row in ashtakavarga["house_scores_full"].values()}
|
||||
for row in _table_after(en, "Ashtakavarga Full House Scores")[1:13]:
|
||||
assert int(row[2]) == expected_sav[row[1]]
|
||||
assert int(row[3]) == ashtakavarga["bav"]["Lagna"]["bindus"][SIGNS.index(row[1])]
|
||||
assert int(row[4]) == full[row[1]]
|
||||
|
||||
|
||||
def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> None:
|
||||
packet = copy.deepcopy(editions[2])
|
||||
del packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["bav"]["Lagna"]
|
||||
rows = _table_after(_pl9_export_markdown_for_edition(packet, "reader_main"), "Ashtakavarga 完整宫位分数")
|
||||
assert len(rows) == 13, "no total row without the split"
|
||||
for row in rows[1:]:
|
||||
assert row[2:4] == ["-", "-"]
|
||||
assert row[4].isdigit()
|
||||
|
||||
|
||||
def test_chart_section_carries_the_divisional_caveat(editions) -> None:
|
||||
zh, en, _ = editions
|
||||
assert re.search(r"### 本命与分盘北印度图盘\n\n" + re.escape(CAVEAT_ZH) + r"\n", zh)
|
||||
assert re.search(r"### Natal and Divisional Charts \(North Indian\)\n\nDivisional charts are sensitive to birth time\.", en)
|
||||
Reference in New Issue
Block a user