Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a09fbfd3e3 | ||
|
|
fab3ebf573 | ||
|
|
d411abbc42 | ||
|
|
3562171be4 | ||
|
|
530a030217 | ||
|
|
17df8fef7b | ||
|
|
3911967269 | ||
|
|
aaddf7835b |
@@ -1,5 +1,9 @@
|
||||
# BLOCKED
|
||||
|
||||
## #257 年运 Muntha 验收环境
|
||||
|
||||
- Windows Docker Desktop Linux 引擎不可用。碎片扫描和预检定向套件超时,远端和适配器诊断通过。替代证据见 [#257 进度](docs/tasks/PROGRESS-annual-muntha-records-20261113.md)。
|
||||
|
||||
## #256 年运 Tajika 验收环境
|
||||
|
||||
- Windows Docker Desktop 的 Linux 引擎不可用,碎片扫描和预检定向套件超时。替代证据见 [#256 进度](docs/tasks/PROGRESS-annual-tajika-yoga-states-20261113.md)。
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-11-14 — 年运附录分列 Muntha 四项记录
|
||||
|
||||
- 计算依据新增年盘结构、Muntha 主状态、慢星换座与停滞、逐年推进。沿用当前 Skill 版本(BUG-1880)。
|
||||
|
||||
## 2026-11-13 — 年运报告可导出 Markdown
|
||||
|
||||
- 写完后从顶栏导出整篇或选取章节,计算依据可整组带走。旧报告也可导出。
|
||||
|
||||
@@ -25395,3 +25395,15 @@
|
||||
- 验证及防复发:公开盘、形成条件定向测试、旧快照与渲染回归。十段写作证据哈希逐段对比。详见 [#256 进度](tasks/PROGRESS-annual-tajika-yoga-states-20261113.md)。
|
||||
- 关联记录:#256、BUG-1652、BUG-1655、BUG-1868。
|
||||
- 修复版本:`codex/annual-tajika-yoga-states-20261113`,待发布。
|
||||
|
||||
## BUG-1880 | 年运未分列 Muntha 四项记录及慢星换座
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
- 首次发现 / 最近更新:2026-11-13 / 2026-11-14。
|
||||
- 影响面:年运报告计算依据和 Markdown 附录。
|
||||
- 现象和触发:读取年运报告时,Muntha 事实仅给位置及其主星,缺少同星座结构、主星状态、覆盖期内慢星事件及逐年推进。
|
||||
- 根因:原写作事实只投影年盘结构摘要。慢星扫描结果未按每张年盘的 Muntha 星座和有效区间投影。
|
||||
- 修复:附录分四组,复用年盘字段和慢星扫描,按口径与有效区间过滤事件。逐年推进复用原生产者。
|
||||
- 验证及防复发:公开盘和虚构盘核对四项、逆行往返、停滞、空区间、岁差与交点、旧快照及写作证据。详见 [#257 进度](tasks/PROGRESS-annual-muntha-records-20261113.md)。
|
||||
- 关联记录:#257、BUG-1212、BUG-1219、BUG-1817、BUG-1868。
|
||||
- 修复版本:`codex/annual-muntha-records-20261113`,待发布。
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# #260 开工前补充:做成动态题库,一直问到收敛(2026-11-14,Claude)
|
||||
|
||||
本文件补充 `TASK-rect-adaptive-20261114.md`。两份冲突时,以本文件为准。
|
||||
|
||||
## 产品负责人的意见(2026-11-14)
|
||||
|
||||
现在这套是固定题:一共 5 道,答完是什么就是什么。用户每答一题,系统其实就多知道了一些,应该据此更新后面的题,动态加题或减题,直到收敛。
|
||||
|
||||
## 改法:取代任务书第 1 条的「一题一题按固定顺序问」,以及第 4 条的「再确认 X 盘」入口
|
||||
|
||||
1. **每张盘准备多道候选题。** 每张关键盘不止一道题,候选题包括:
|
||||
- D9:感情开始、结婚、分开,各算一个类别。
|
||||
- D10:入职或升职、换工作或离职、创业或开店,各算一个类别。
|
||||
- D2:收入明显增加、较大的支出或损失,各算一个类别。
|
||||
- 同一类别里,可以用第二突出的年龄段再出一道题。
|
||||
- D1:外表、胎记。
|
||||
- D6、D8:补充题。补充题每张盘最多一道。
|
||||
|
||||
所有候选题都由 #241 已有的打分方式生成,不得新增评分规则。D6、D8 的事件题仍要等占星师定义,本单不加。
|
||||
2. **每答完一题,就重新挑下一题。**
|
||||
- 先拿当前时间段,算出每张关键盘还剩几种可能。
|
||||
- 在还没问过的候选题里,选「最能把剩下的可能分开」的那一道。分得开的程度可以用信息量或最大分组数来衡量,具体怎么算写进 PROGRESS。
|
||||
- 并列时,先选事件题,再按 D1 → D9 → D10 → D2 → D6 → D8 的顺序选。
|
||||
- 已经定下来的盘,不再出它的题。
|
||||
3. **什么时候停**:下面任何一条满足就停。
|
||||
- 六张关键盘都已经定下来。
|
||||
- 剩下的候选题都分不开剩下的可能。这时照实把那几张盘标为「还没定」。
|
||||
- 问满 **12 题**。
|
||||
- 用户点了「先到这里」。
|
||||
4. **页面**
|
||||
- 进度改为「已确认 n / 6 张盘」,配一条细进度条,不再写「第 n 题,共 N 题」。
|
||||
- 结果页去掉「再确认 X 盘」入口:没定的盘会在停下前自动接着问到底。
|
||||
- 首页的「继续」只对两种情况开放:中途点了「先到这里」的记录,以及旧的 v1 记录。点了之后,从已保存的那一段接着动态问。
|
||||
5. **记录**:每道题都写进 `rounds`(或 v2 的 `asked`)。内容包括题目 id、它要区分的那张盘、用户的回答、答完后时间段的变化。不记原文以外的推断。
|
||||
|
||||
## 验收补充
|
||||
|
||||
- 测试盘(±15 分钟和 ±30 分钟各一组,用合成答案):
|
||||
- 题目会根据前面的回答变化。同一张盘答了「记不清」后,会换一类事件或换一个年龄段再问。
|
||||
- 收敛后会自动停下。
|
||||
- 总题数不超过 12。把实际题数记进 PROGRESS。
|
||||
- 单测要覆盖:
|
||||
- 下一题的选择规则,包括并列时的顺序;
|
||||
- 已经定下的盘会被跳过;
|
||||
- 四种停止条件;
|
||||
- 「记不清」之后会换题。
|
||||
@@ -1,26 +0,0 @@
|
||||
# #260 开工前补充 2:首页(当前使用)只留结果,不留「继续」和历史(2026-11-14,Claude)
|
||||
|
||||
本文件补充 `TASK-rect-adaptive-20261114.md` 和 `FIX-rect-adaptive-0.md`。几份文件冲突时,以本文件为准。
|
||||
|
||||
## 产品负责人的意见(2026-11-14,看了真机首页)
|
||||
|
||||
- 「继续」没有用。上次答过的题,再进去还是同样的题,用户只会给同样的答案。不能让用户为了凑出一个结果去改答案:那是用户的生日,不是系统的生日。
|
||||
- 「已定 4 张盘,提到 2 件事」这类统计不需要显示。用户只要看到上次拿到的时间。
|
||||
- 既然选中了代表时刻,首页就要把代表时刻显示出来。只显示时间范围没有用。
|
||||
- 不需要历史列表(「历史 2 次」)。
|
||||
|
||||
## 改法(`ChartChoiceHome`,即 `chart-choice-page.tsx` 约 1091–1175 行)
|
||||
|
||||
1. **主标题改成代表时刻。** 大字显示代表时刻,比如 `05:01`;有秒就显示秒,比如 `05:01:12`。紧接着下面一行小字显示 `范围 04:59:43–05:15:00`。
|
||||
2. **删掉这一行:** 「代表时刻 05:01,你填的是 05:00」。刻度尺上的 `▲ 05:00` 已经标出了用户填的时间。
|
||||
3. **刻度尺和六张盘的行保持不变。** 没定的盘照实写「还没定(05:07:55 换)」。
|
||||
4. **删掉「还有 n 张盘没定」和「继续」按钮。** 同时删掉首页上 `onResume` 这条入口,v1 旧记录和「先到这里」停下的记录也一样。按照 FIX-0,答题时已经一路问到收敛;剩下没定的盘就是没定,不再提供重答入口。想重来就点「重新纠正」。
|
||||
5. **整块删掉「历史 / n 次」列表**,包括 `chart-choice-history`、旧记录的打开入口和 `historySubline`(「已定 n 张盘,提到 n 件事」)。后端照常保存历史记录,只是不再显示,接口不改。
|
||||
6. **底部两个按钮保持不变:** 「看星盘」和「重新纠正」。
|
||||
7. **结果页**也删掉事件计数句:「你提到 n 件有日期的事,n 件和这一段的大运对得上」,以及「你提到的事,n 件」这个标签。结果页只说时间、各盘、哪些还没定。
|
||||
|
||||
## 验收
|
||||
|
||||
- 截图:一条已全部定下的记录、一条有两张盘没定的记录,宽度 390 和 1280 各一张。
|
||||
- 首页上不出现以下文字:「继续」「历史」「件事」「你填的是」。
|
||||
- 测试改为断言以上四个字串都不出现,并断言代表时刻显示为主标题。
|
||||
@@ -1,72 +0,0 @@
|
||||
# #260 补充 3:按上游方法对齐动态题库(2026-11-14,Claude)
|
||||
|
||||
本文件补充 `TASK-rect-adaptive-20261114.md`、`FIX-rect-adaptive-0.md` 和 `FIX-rect-adaptive-1.md`。几份文件冲突时,以本文件为准。
|
||||
|
||||
产品负责人的规定:生时纠正跟上游占星师的方法走,不能自己另起一套。本文件里列出的上游依据都在 `/workspace/yinduzhanxing`(5fb8379c):
|
||||
|
||||
- `references/birth-time-rectification-decision-tree.md`
|
||||
- `scripts/active_rectification_questions.py`
|
||||
- `scripts/active_rectification_selector.py`
|
||||
- `scripts/active_rectification_candidates.py`
|
||||
- `scripts/active_rectification_events.py`
|
||||
|
||||
## 1. 题目按「事件领域」出,不按「一张盘一道题」出
|
||||
|
||||
上游的题库是按生活领域写的,每个领域绑定一张主分盘(见 `QUESTION_BINDINGS` 和决策树第 4 节)。一道题只要能把当前剩下的候选时间段分开,就有用,不管它属于哪张盘。我们的 6 张关键盘(D1、D9、D10、D2、D6、D8)只用来判断什么时候停,不决定问什么。
|
||||
|
||||
候选题按领域准备。年龄沿用产品规则,从 18 岁起问:
|
||||
|
||||
| 领域 | 主分盘 | 题目内容(示例) | 可靠度(上游 `DOMAIN_RELIABILITY`) |
|
||||
|---|---|---|---|
|
||||
| 学业 | D24 | 升学、毕业、读研、换专业 | 1.0 |
|
||||
| 搬迁 | D4 | 搬家、离乡、出国、长期异地 | 0.95 |
|
||||
| 感情 | D9 | 开始恋爱、结婚、分开 | 0.95 |
|
||||
| 工作 | D10 | 入职、换工作、创业、职责明显加重 | 0.9 |
|
||||
| 公开身份 | D10 / A10 | 升职、作品公开、身份变化 | 0.82 |
|
||||
| 钱财 | D2 | 收入明显增加、较大支出或损失 | 0.8 |
|
||||
| 子女 | D7 | 孩子出生 | 0.85 |
|
||||
| 父母 | D12 | 父母的重大变故 | 0.75 |
|
||||
|
||||
- 每道题的选项年龄段,仍然用现有的 `_dasha_score` / `_varga_score`(Vimshottari MD/AD/PD + Narayana MD/AD)算出来。领域对应的宫位和事件星,用 `astrologer.EVENT_CATEGORIES`;没有的类别,从上游 `active_rectification_event_engine.py` 补上。
|
||||
- **D30 健康题,这一轮不做。** 上游规定:只有用户有明确的健康事件时才用,而且要等候选窗口收窄以后才能问(`_question_gating`,候选数 ≤ 24)。这件事需要产品负责人拍板,这一轮不要做。
|
||||
- 主盘(D1)的外貌和胎记题保留,仍然只在 D1 线存在时问(上游 advanced 第二阶段「外貌和胎记预筛」)。
|
||||
- **删掉 D6 / D8 的「哪种更像你」补充题。** 上游的生时纠正里没有这类题。D6 和 D8 只能靠事件题顺带定下来;定不下来,照实标「还没定」。
|
||||
|
||||
## 2. 选下一题的打分,照搬上游 `_score_question`
|
||||
|
||||
在 FIX-0 的「最能把剩下可能分开」之外,再加上上游的几个分量:
|
||||
|
||||
- 分开程度 × 3
|
||||
- 可靠度 × 2.5
|
||||
- 用户答题的省力程度 × 1.5
|
||||
- 领域多样:同一领域问过一次,再问时扣 1.5(`_question_domain_priority`),另外再扣 1(`_score_question`)
|
||||
- 最高分低于门槛就停(上游门槛是 `best < 4.0`,停止原因 `no_answer_can_improve_separation`)。我们的分值尺度不同时,在 `docs/tasks` 里写清楚换算方法。
|
||||
|
||||
FIX-0 的「最多 12 题」继续保留,作为产品上限。上游没有这个数字,题库本身只有 8 个领域。
|
||||
|
||||
## 3. 结果要带上游的把握分级
|
||||
|
||||
依据上游 `active_rectification_events.py` 约 325–357 行:
|
||||
|
||||
- **低:** 有日期的事件少于 3 件;或者领域少于 2 个;或者选中的段宽超过 15 分钟;或者领先幅度不到 10%。
|
||||
- **高:** 事件至少 4 件,领域至少 3 个,段宽不超过 5 分钟,领先幅度至少 20%。
|
||||
- **其余为中。**
|
||||
- **Narayana 降级:** 如果 Vimshottari 支持选中的段,但 Narayana 在这一段是反对的,把握最高只能是低(上游 `score_answers` 里 `downgrade_reasons` 的 narayana 一项)。
|
||||
|
||||
显示方式:首页和结果页的代表时刻旁边,只放一个词「把握 高 / 中 / 低」。FIX-1 第 7 条删的是事件计数句,**把握分级要保留**。
|
||||
|
||||
有日期的事件少于 3 件时,结果只叫「倾向」。这是之前已经定下的规则,不变。
|
||||
|
||||
结果永远不说「已验证」:上游是 `claim_status=candidate`、`not_birth_time_truth`。档案里的出生时间也不改,这一条维持现状。
|
||||
|
||||
## 4. 验收新增:用 77 个公开案例做回放(不调模型)
|
||||
|
||||
- 用 holdout v5 的 77 个 AA 案例(脚本参照 `docs/research/artifacts/rectification-width-funnel-2026-10-16/seg_placebo.py`)。用每个案例已知的、有日期的事件,模拟用户回答新流程。
|
||||
- 报告以下几项,写在 `docs/tasks/rect-adaptive-replay.md`:
|
||||
- 选中的段包含真实时间的比例;
|
||||
- 随机选一段的比例;
|
||||
- 直接用用户填的时间所在段的比例(11-06 的基线是 19% / 20% / 34%);
|
||||
- 平均问了几题;
|
||||
- 各种停止原因的占比;
|
||||
- 把握高 / 中 / 低各占多少,以及每一档的实际命中率。
|
||||
- 结果不好也要如实报告,不许调参调到好看。上线与否由产品负责人看完数字再决定。
|
||||
@@ -1,33 +0,0 @@
|
||||
# #260 补充 4:加上健康题(D30),按上游门槛来问(2026-11-14,Claude)
|
||||
|
||||
产品负责人 11-14 拍板:要加健康题。本条取代 FIX-2 第 1 节里「D30 健康题这一轮不做」那一句。
|
||||
|
||||
上游依据:
|
||||
|
||||
- `references/birth-time-rectification-decision-tree.md` 第 4 节和第 6 节:D30 只在有明确的健康事件时使用。
|
||||
- `scripts/active_rectification_questions.py` 里 `health_crisis_or_low_period` 的绑定:event_categories health / loss,dasha_tracks Vimshottari + Narayana + Transit。
|
||||
- `scripts/active_rectification_selector.py` 的 `_question_gating`:候选窗口收窄以后才能问。
|
||||
|
||||
## 怎么问
|
||||
|
||||
1. **领域:** 健康 / 意外,主分盘 D30,可靠度 0.72(上游 `DOMAIN_RELIABILITY`)。年龄段的算法和其他事件题一样:用 `_dasha_score` / `_varga_score` 算出来,宫位和事件星按上游 health 类别(6、8 宫,参照 `active_rectification_event_engine.py`)。
|
||||
2. **门槛:**
|
||||
- 至少答过一道非健康的事件题,并且当前剩下的候选段不止一段,才能问;
|
||||
- 第一题永远不能是健康题;
|
||||
- 整个流程里健康题最多问 1 道。
|
||||
3. **题面用中性语言,** 只问发生过什么:「你在哪个年龄段有过比较大的健康变故,比如手术、住院、意外受伤?」不写疾病名称,不写「危机」「灾」这类字眼。
|
||||
4. **选项:** 年龄段(带年份)、「都没有」,外加一个与「记不清」同样大小的「不想回答」。选「不想回答」等同于「记不清」,不算作否定。
|
||||
5. **展开后的细项按钮:** 手术 / 住院 / 意外受伤,三选一,加上年份。
|
||||
6. **隐私:**
|
||||
- 健康答案只用于这次纠正打分,不写进报告和咨询的上下文,不显示在首页,也不进日志原文;
|
||||
- 记录里只存年份和类别,不存自由文字。
|
||||
7. **D30 不加入 6 张关键盘。** 结果页和首页不出现 D30 这一行。健康题的作用只是帮忙把候选段分开。
|
||||
|
||||
## 验收补充
|
||||
|
||||
- 回放报告里单列一行:有健康题和没有健康题时的选中率对比。
|
||||
- 测试覆盖以下几点:
|
||||
- 第一题不是健康题;
|
||||
- 健康题最多 1 道;
|
||||
- 「不想回答」不影响打分;
|
||||
- 健康答案不出现在报告或咨询的输入里。
|
||||
@@ -0,0 +1,23 @@
|
||||
# #257 年运 Muntha 四项记录
|
||||
|
||||
执行者 Codex。开工基线 `881fb75ef`,可信任务书提交 `aaddf783`。沿用预留编号 BUG-1880。
|
||||
|
||||
## 实现
|
||||
|
||||
- 年盘结构记录 Muntha 星座、从年盘上升数的宫位、同星座行星及起盘地点方案。核对 `annual_tajika_pack.py`、`solar_return.py` 及上游 `3509ccd7` 的 `skills/tajika-annual`。这些入口没有本场景可用的 Muntha 星座相位规则或 Muntha 度数,故相位留为 `not_built`。
|
||||
- Muntha 主的落座、落宫、尊贵、逆行、五分力使用同一年盘字段。返照生产者当前没有给出燃烧状态,显示未取得。
|
||||
- 慢星行运从现有完整区间扫描中筛选每张年盘与报告重叠区间内的进出和停滞,左含右不含。保存时刻和逆行标记。岁差与交点对照返照引擎回传的年盘口径,月亮和快星不在本轮范围。上游 11 月 6 日审查未批准通用行运容许度,本单使用实际星座边界和停滞。
|
||||
- 扫描沿用逐日取样后二分的精度,同日内的多次过界可能未被生产者捕获。计都沿用扫描返回的罗睺停滞时刻,只有对宫是 Muntha 星座时才列入该年盘。
|
||||
- 逐年推进沿用本命上升加已满年数的生产者,列出覆盖的各年及下一年。报告和 Markdown 共用同一附录投影。
|
||||
|
||||
## 验证
|
||||
|
||||
- 同一虚构盘在 staging 和本分支以 `PYTHONHASHSEED=0` 实算。十个写作区段的 evidence 哈希一致。区段名和哈希清单 SHA-256 为 `0fbe3e4b5c89dc9330794f4a754e7b19aac262a39899318d184ccc096a24ae78`。
|
||||
- 公开盘、现居地虚构盘和事件过滤控制覆盖四项独立、跨年、逆行重复、停滞、空事件、口径不匹配与旧附录。既有断言未改。
|
||||
- Python 年运定向 39 项通过,最终 Muntha 回归及仓库隐私 66 项通过。前端附录及导出定向 10 项通过,ESLint 0 错误、62 条既有警告。
|
||||
- 同机无 Docker 前端全量 3731 项,3648 通过、46 失败、37 跳过、0 取消。与 `881fb75ef` 对比,保留全部旧用例,新增 2 项通过,失败清单逐项一致。
|
||||
- TypeScript 与生产构建通过,首页仍为静态,首屏 gzip JS 516100 字节、CSS 92069 字节,与开工 staging 相同。预检远端和适配器通过,碎片扫描与定向预检超时。
|
||||
- 补齐计都停滞后,最终 Muntha 回归及隐私 66 项、前端定向 10 项均通过。最终同机无 Docker 前端全量 3731 项,3649 通过、45 失败、37 跳过、0 取消。与开工基线逐名对比,新增 2 项通过,失败无新增。基线中失败的一项咨询计时测试本次通过。
|
||||
- 本机 Python 快速门退出 1,1490 项通过、2 项跳过、29 个子测试通过。3 项 Kaala / PyJHora 失败在开工 staging 定向复现。完整快速门待独立验收环境复核。
|
||||
- 同步已验收的 #256 和后续相位显示修正(staging `cbc7fdccf`),保留 Tajika、Mudda 和 Muntha 各自的附录字段。以同步后的 staging 重算,十个写作 evidence 哈希全部一致。
|
||||
- 同步后 Python 定向与隐私检查 92 项、前端定向 12 项、TypeScript 和生产构建通过,ESLint 0 错误。无 Docker 前端全量 3733 项,3650 通过、46 失败、37 跳过、0 取消;同期 staging 3731 项,3648 通过。两项新增用例通过,旧用例及失败清单无缺失或新增失败。最新 staging 同样复现快速门的 3 项 Kaala / PyJHora 失败。
|
||||
@@ -0,0 +1,86 @@
|
||||
# TASK:年运 Muntha 四项分开记录,加行运扫描(2026-11-13)
|
||||
|
||||
- Gitea:#257
|
||||
- 分支:`codex/annual-muntha-records-20261113`,从 staging `097d5b3a` 切出。
|
||||
- Bug 编号:BUG-1880。开工时核对 `docs/BUG_HISTORY.md` 末尾;已被占用就顺延,并在 PROGRESS 写明。
|
||||
- 负责验收:Claude(jyotisha-99)。
|
||||
|
||||
任务书没写到的取舍,不得写成"产品决定"或"裁定",一律标「需拍板」。不得修改或删除既有断言来让测试通过。确需修改的,在 PROGRESS 逐条写明原值、新值和原因。
|
||||
|
||||
同时在做的还有 #233、#255、#256。各单只改自己的函数;后合入的那张负责 rebase。
|
||||
|
||||
## 背景
|
||||
|
||||
占星师 2026-11-12 回复(年运 Q6),原意:
|
||||
|
||||
- 分别记录以下四项。四者不是同一件事,不能重复计证。
|
||||
1. 年盘内与 Muntha 的结构关系;
|
||||
2. 年盘 Munthesh(Muntha 主)的状态;
|
||||
3. 实际行运经过该年固定的 Muntha 点或星座;
|
||||
4. Muntha 自身的逐年推进。
|
||||
- 不设「只有土星、罗睺才算」的全局白名单。每条要说明是同宫、同星座、精确度数接触还是某种相位,以及用哪套角距和容许度,不能自己发明统一阈值。只有星座位置时,不得输出精确到日的度数交会。
|
||||
- 行运要扫描整个覆盖区间,保留逆行造成的重复命中、停滞、进入和离开边界的实际日期;单一观察日不代表全年。
|
||||
- ayanamsa、node mode、年盘位置方案要统一,不能把另一种回归盘里的点不加说明地混算。
|
||||
- 月亮和快星如果用,只是对应时间尺度上的触发候选,不自动获得年度主导权。
|
||||
- 她 2026-11-06 的来源审查(上游 `3509ccd7` 的 `docs/research/tajika_source_adjudication_2026_11_06.md`,「Future transit influence windows」节):没有批准任何本命—行运的数值容许度,只保留精确根(0° 残差)以及离散的进入、停滞事件。
|
||||
|
||||
现状:
|
||||
- `package.py` 的 `_muntha_block`:Muntha 星座、从年盘上升数的宫位、Muntha 主及其落座和落宫。Muntha 取本命上升加已过整年数(BUG-1212)。
|
||||
- `package.py` 的 `slow_transits`:慢星(木星、土星、罗睺、计都)在区间内的进入、停滞等事件。`_transit_context` 用它算 Sade Sati、Ashtakavarga 和 Vedha。
|
||||
- facts 的 `cards.*.muntha` 只有星座、宫位、主星、主星位置。
|
||||
|
||||
## 要做的
|
||||
|
||||
在 facts 每年新增 `muntha_records`,四项各占一个键,互不引用。
|
||||
|
||||
1. **`structure`:年盘内的结构关系**
|
||||
- Muntha 星座,从年盘上升数的宫位。
|
||||
- 年盘里落在 Muntha 星座的星,标为「同星座」。
|
||||
- 星座层面的相位关系:只有在仓里找到有出处的 Muntha 相位规则时才做(先查 `annual_tajika_pack.py`、`solar_return.py`、上游 `skills/tajika-annual`),并写明出处。找不到就写 `not_built` 和理由,不自己发明。
|
||||
2. **`munthesh`:Muntha 主的状态**
|
||||
- 落座、落宫、尊贵、燃烧、逆行、五分力分数。都从已有字段取,缺的写缺。
|
||||
3. **`transits`:行运经过 Muntha 星座**
|
||||
- 用 `slow_transits` 的事件,列出每颗慢星进入和离开 Muntha 星座的实际时刻,以及在星座内的停滞。逆行造成的重复进出都保留,按时间排序。
|
||||
- Muntha 只有星座、没有度数,所以只做星座级,不输出度数交会。若仓里 Muntha 有定义好的度数,可以另加精确根(0° 残差),不加容许度。
|
||||
- 月亮和快星这次不做,在 note 写明。
|
||||
- 写明用的 ayanamsa 和 node mode,必须和年盘一致,用测试断言。
|
||||
4. **`progression`:逐年推进**
|
||||
- 报告覆盖范围内每一年的 Muntha 星座,加上下一年的。
|
||||
- 取法和 `_muntha_block` 一致(本命上升加已过整年数)。
|
||||
|
||||
页面附录新增「Muntha 四项记录」小节,按四项分四组列出。行运按时间列,例如「土星 2026-05-03 进入 Muntha 所在的摩羯座」「2026-09-10 逆行退出」。没有事件就写「本期间没有慢星进出 Muntha 星座」。
|
||||
|
||||
写作输入不变:十个写作区段的 evidence 哈希不变。`muntha_records` 不进写作输入。
|
||||
|
||||
## 不要碰
|
||||
|
||||
- 冻结文件、`references/**`、`consult-preset-prompts.ts`。
|
||||
- 不改 Muntha 的取法,不加容许度,不加快星扫描。
|
||||
|
||||
## 要改的文件
|
||||
|
||||
- `scripts/annual_service/report_facts.py`,视情况改 `package.py`
|
||||
- `frontend/src/lib/annual-report-generate.ts`,只加 Muntha 一个小节
|
||||
- 测试:`tests/test_annual_muntha_records_20261113.py`、`frontend/tests/annual-muntha-records-20261113.test.tsx`
|
||||
- `docs/BUG_HISTORY.md`,只在末尾追加
|
||||
- `docs/tasks/PROGRESS-annual-muntha-records-20261113.md`
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 公开名人盘和一个合成盘上,四项都有,各自独立。
|
||||
2. 行运测试覆盖以下几种情况:
|
||||
- 有逆行重复进出的情况,用合成数据;
|
||||
- 停滞;
|
||||
- 区间内没有事件;
|
||||
- ayanamsa 和 node mode 与年盘一致。
|
||||
3. 输出里没有度数交会、容许度和精确到日的星座外推。
|
||||
4. 写作输入哈希不变;旧快照没有这一节时不报错。
|
||||
5. 用户可见文字里没有「·」、字段名或下划线。
|
||||
6. tsc、eslint(0 错)、`npm test`(基线失败除外)、quick gate 全部通过。
|
||||
|
||||
## 自测命令
|
||||
|
||||
```
|
||||
cd frontend && npx tsc --noEmit -p . && npx eslint . && npm test
|
||||
PATH=<venv>/bin:$PATH PYTHONHASHSEED=0 python scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
|
||||
```
|
||||
@@ -1,69 +0,0 @@
|
||||
# TASK:生时纠正改为边答边缩小;年龄选项带年份;选中样式重做;结果页去掉「对不上」「再补几件事」和回测说明(2026-11-14)
|
||||
|
||||
- Gitea:#260(接 #239)
|
||||
- 分支:`codex/rect-adaptive-20261114`,基于 staging `cbc7fdccf58674b81cdac36bf6cbb36116fce5ec`
|
||||
- Bug 编号:BUG-1881
|
||||
- 验收:Claude(jyotisha-e6)
|
||||
|
||||
不得自行写「产品决定/裁定」;任务书没写到的取舍,一律选需拍板。不得修改或删除既有断言来让测试通过;确需修改的,写明原值、新值和原因。
|
||||
|
||||
## 产品负责人真机反馈(2026-11-14)
|
||||
|
||||
1. 旧记录的首页上,时间尺的「分界 04:59:42」和「存 05:00」叠在一起。
|
||||
2. 年龄选项只写「20 岁」,用户还得自己去算是哪一年。
|
||||
3. 选中后整块涂上暗红,深色模式下很难看。
|
||||
4. 结果页列出四条「你的回答有一处对不上……」,用户看了也没法处理。原话:「你就告诉对不上,你有解决办法吗?你没有啊,那你说这个有啥用」。
|
||||
5. 「再补几件事」点进去,问的还是那 5 道题,没有用。
|
||||
6. 「这个方法有多准」(77 个案例)整段不要。
|
||||
|
||||
## 要做的
|
||||
|
||||
### 1. 引擎:边答边缩小(`scripts/chart_choice/`,接口 `chart_answers`)
|
||||
|
||||
- 按 D1 → D9 → D10 → D2 → D6 → D8 的顺序**一题一题来**。每答完一题,就用这个答案把当前时间段缩小(取交集)。
|
||||
- 下一题**只列出在当前时间段里还可能的选项**;年龄段按这几种可能重新算。某张盘在当前段里只剩一种可能,**直接跳过,不出题**。
|
||||
- 「都没有」「记不清」不缩小时间段,这张盘照旧记为「还没定」。
|
||||
- 因为每题只给还可能的选项,正常作答不会再出现冲突。万一出现(例如改答案后),冲突只记进后台的 `conflict`,**接口不再返回给页面展示**。
|
||||
- 接口改成一题一请求:带上已答的内容,返回下一题,或者返回结果。前端「第 n 题,共 N 题」里的 N,按当前预计还要问的题数动态显示。
|
||||
- D6、D8 的补充题也照此处理。通常前面几题答完,它们已经被定下来,不会再问。
|
||||
|
||||
### 2. 年龄选项带年份
|
||||
|
||||
- 选项写成「20 岁(2017 年前后)」「27–28 岁(2024–2025 年)」。年份按出生日期折算,跨两年时写成年份区间。
|
||||
- 展开后「哪一年」的默认值,取这个年龄段的第一年(已经是这样,保留)。
|
||||
|
||||
### 3. 选中样式(浅色、深色都按这个)
|
||||
|
||||
- 选中的选项块**不再整块换底色**:底色和其他选项一样,外加 1.5px 的强调色描边(用 inset ring,不占布局),选项文字和迷你时间带改成强调色。
|
||||
- 展开后,「什么事」的按钮未选中时只有 1px 细边框、透明底,选中时填实心强调色加白字。「哪一年」的加减器也只用细边框,不加底色块。
|
||||
- 深色模式下,所有底色用 `--color-surface-dark*` 这套 token,不要叠半透明的红。
|
||||
- 展开动画照旧。
|
||||
|
||||
### 4. 结果页
|
||||
|
||||
- 删掉所有「你的回答有一处对不上……」这类句子。
|
||||
- 「依据」只留一行,写对得上的事件,例如「2020 年入职或升职、2023 年收入明显增加,和这段时间的大运对得上。」有日期的事少于 3 件时,在同一句末尾加「先作参考」,不另起一行。
|
||||
- **「再补几件事」改成按条件显示。** 只有当某张盘还没定,**并且**引擎还有一道**没问过的新题**能区分它(例如另一个事件类别,或第二个年龄段)时,才显示「再确认婚恋盘」这类入口,点进去只问这道新题。没有新题就不显示。引擎接口要返回 `followups: [{chart, question_id}]`。
|
||||
- **整段删掉「这个方法有多准」**。页面上不再出现 77 个案例和那组百分比。顶部标题照旧写「按你的回答,出生时间在 X 到 Y 之间」。
|
||||
- 结果页和首页的六张盘那几行保持现状,不变。
|
||||
|
||||
### 5. 时间尺标签不重叠
|
||||
|
||||
- 所有时间尺(首页的旧 v1 记录、范围页、答题页、结果页)都要处理:两个标签离得太近时,一个放在尺上方,另一个放在尺下方。下方已有端点时间时,靠近的那个端点时间隐藏。
|
||||
- 首页的旧 v1 记录(「线后这张盘」那种)要专门修好这一处。
|
||||
|
||||
## 验收
|
||||
|
||||
- 测试盘(±15 分钟、±30 分钟各一组):
|
||||
- 实际出题数要比修订五更少(写进 PROGRESS);
|
||||
- 正常作答不出现冲突;
|
||||
- 年龄选项带年份;
|
||||
- 选中样式浅色、深色各截一张图;
|
||||
- 结果页没有冲突句、没有回测说明,「再确认 X 盘」只在有新题时出现;
|
||||
- 首页 v1 记录的时间尺标签不再重叠。
|
||||
- 单测要覆盖:
|
||||
- 答完上一题后,下一题的选项被正确缩小;
|
||||
- 某张盘只剩一种可能时被跳过;
|
||||
- 改答案后冲突只出现在后台;
|
||||
- `followups` 在有新题时出现、没有新题时为空。
|
||||
- 质量门、前端测试、tsc、eslint 全部通过。截图先给产品负责人看过,才能标 `待真机`。
|
||||
@@ -1408,6 +1408,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
|
||||
- **本命瑜伽(BUG-1857):** 年运写作材料中的瑜伽按原有顺序提供完整清单及状态词,沿用本命段落的版式和篇幅。
|
||||
- **年运附录(BUG-1855):** Narayana 标为条件参考。种子未决时在口径说明中列出候选数,每支单独一行。
|
||||
- **年运附录(BUG-1878):** 「这份报告的口径」在年内展开说明下列出 Mudda 主结果和两种对照口径。跨年结果不同时按年盘分行。技法执行表保留西方占星各行,计算栏显示「不适用」,使用栏附理由。
|
||||
- **年运 Muntha(#257,BUG-1880):** 计算依据按年盘列「Muntha 四项记录」。年盘结构、Muntha 主、慢星进出与停滞、逐年推进各自列出,导出沿用同一顺序。
|
||||
- **入口**:「我的报告」最上面两个横条并排(手机上下排,2026-11-03 方案 A,见 Personal report centre):「年运」横条是 `Sun` 图标方块、标题、一行「这一年每段怎么安排,几个要留意的时间点」,右边描边按钮「打开年运」(手机「打开」),直接打开 `/reports/annual`。没有单独的入口页。个人报告横条上不写份数。
|
||||
- **地址**:`/reports/annual`(`?report=<id>` 打开某一份),放在 `/reports` 下,侧栏仍亮「我的报告」。沿用二级页外壳,标题「年运」,标题旁是人物切换。正文一栏 `min(720px, 100%)` 居中。
|
||||
- **遮罩**:报告的章节骨架在后面虚着(灰标题 + 两条微光条),上面一层 `--color-canvas` 72% 半透明 + 6px 模糊,正中一张卡片(`min(520px, 100%)`、圆角 20px、细边、`--shadow-elevated`,超出时卡内滚动):范围小字「YYYY-MM-DD 到 YYYY-MM-DD」,衬线标题「某人的这一年」;三个胶囊切换「生日年度 / 公历年 / 未来十二个月」(`ToggleGroup`,选中描边 `--color-action-strong`、文字 `--color-action`);三条注意事项(主色勾);出生时间没确认时一行浅底提示,本人和别人都带「先做生时校正」链接(#160 起可以给档案里的其他人做校正);「补充现在的情况(可以不填)」折叠,展开后是关系状态、工作 / 学习、最关心的(最多三项)、已有的计划(一行输入)、不想看的;居中实心按钮「开始写年运」(最小 220×52,手机占满宽),下面一行「本次消耗 N 点,写到一半断掉不扣」。价格读不到时这一行写「计费配置不可用」,按钮不可点。没有别的提示小字。
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* second failure stops the report (nothing is charged, see the worker).
|
||||
*/
|
||||
|
||||
import { AYANAMSA_OPTIONS } from "./ayanamsa.ts";
|
||||
import { planetZh, signZh } from "./chart-view-labels.ts";
|
||||
import { displayTechniqueAuditLabel, techniqueAuditAxes, techniqueAuditStatusText, techniqueCalculationStatusLabel, techniqueInterpretationStatusLabel } from "./consultation-technique-audit.ts";
|
||||
import {
|
||||
@@ -251,6 +252,56 @@ const YEAR_LORD_ROLES: Readonly<Partial<Record<string, string>>> = {
|
||||
varsha_lagna_lord: "年盘上升主", tri_rasi_lord: "Tri-Rasi 主",
|
||||
};
|
||||
|
||||
const AYANAMSA_NAMES: Readonly<Record<string, string>> = Object.fromEntries(
|
||||
AYANAMSA_OPTIONS.map((option) => [option.value, option.label.replace(/(默认)$/, "")]),
|
||||
);
|
||||
|
||||
function munthaRecordLines(value: unknown): string[] {
|
||||
const data = record(value);
|
||||
if (!Object.keys(data).length) return [];
|
||||
const structure = record(data.structure);
|
||||
const lord = record(data.munthesh);
|
||||
const transits = record(data.transits);
|
||||
const progression = record(data.progression);
|
||||
const zodiac = (value: unknown) => str(value) ? `${signZh(str(value))}座` : "未取得星座";
|
||||
const house = (value: unknown) => typeof value === "number" ? `第 ${value} 宫` : "宫位未取得";
|
||||
const sameSign = rows(structure.same_sign).map((row) => planetZh(str(row.planet))).filter(Boolean);
|
||||
const site = structure.return_location_mode === "current_city" ? "按现居地起盘。" : structure.return_location_mode === "birth_place" ? "按出生地起盘。" : "";
|
||||
const aspects = str(record(structure.aspects).reason);
|
||||
const dignity = str(lord.dignity).replace(/\([^)]*\)$/, "").trim();
|
||||
const flags = [
|
||||
// The Sun cannot be combust; only other planets carry the combustion flag.
|
||||
lord.planet === "Sun" ? "" : lord.combust === true ? "燃烧" : lord.combust === false ? "未燃烧" : "燃烧状态未取得",
|
||||
lord.retrograde === true ? "逆行" : lord.retrograde === false ? "顺行" : "顺逆状态未取得",
|
||||
typeof lord.panchavargiya === "number" ? `五分力 ${lord.panchavargiya}` : "五分力未取得",
|
||||
].filter(Boolean);
|
||||
const node = transits.node_mode === "true" ? "真交点" : transits.node_mode === "mean" ? "平均交点" : "交点未标明";
|
||||
const events = rows(transits.events).flatMap((event) => {
|
||||
const when = localMinute(event.instant);
|
||||
const planet = str(event.body);
|
||||
if (!when || !planet) return [];
|
||||
const motion = event.retrograde === true ? "逆行" : "";
|
||||
const sign = zodiac(event.sign);
|
||||
const station = event.motion === "direct_to_retrograde" ? "停滞转逆行"
|
||||
: event.motion === "retrograde_to_direct" ? "停滞转顺行" : "停滞";
|
||||
const action = event.kind === "entry" ? `${motion}进入 Muntha 所在的${sign}`
|
||||
: event.kind === "exit" ? `${motion}退出 Muntha 所在的${sign}`
|
||||
: event.kind === "station" ? `在 Muntha 所在的${sign}${station}` : "";
|
||||
const offset = str(record(event.instant).utc_offset);
|
||||
return action ? [`${planetZh(planet)} ${when}${offset ? `(UTC${offset})` : ""} ${action}。`] : [];
|
||||
});
|
||||
const transitLines = transits.status === "computed"
|
||||
? (events.length ? events : ["本期间没有慢星进出 Muntha 星座,也没有在该星座停滞。"])
|
||||
: [transits.reason === "muntha_scan_frame_mismatch" ? "行运与年盘口径不一致,本期间的行运未列出。" : "本期间的行运未算出。"];
|
||||
return [
|
||||
`年盘结构:Muntha 在${zodiac(structure.sign)},从年盘上升数为${house(structure.house)}。${site}${sameSign.length ? `同星座:${sameSign.join("、")}。` : structure.sign ? "没有同星座的行星。" : ""}${aspects}`,
|
||||
`Muntha 主:${str(lord.planet) ? planetZh(str(lord.planet)) : "未取得主星"},${zodiac(lord.sign)},${house(lord.house)},${dignity || "尊贵状态未取得"},${flags.join(",")}。`,
|
||||
`慢星行运:岁差 ${AYANAMSA_NAMES[str(transits.ayanamsa).toLowerCase()] ?? (str(transits.ayanamsa) || "未标明")},${node}。${str(transits.note)}`,
|
||||
...transitLines,
|
||||
`逐年推进:${rows(progression.years).map((row) => `${row.target_year} 年${zodiac(row.sign)}`).join("、") || "未算出"}。`,
|
||||
];
|
||||
}
|
||||
|
||||
const TAJIKA_MOTION: Readonly<Partial<Record<string, string>>> = {
|
||||
applying: "入相", separating: "出相", exact: "精确成相", stationary_relative: "相对停滞",
|
||||
};
|
||||
@@ -299,6 +350,7 @@ function appendicesByYear(root: Row): AnnualAppendixBlock[] {
|
||||
});
|
||||
if (str(lord.selection_basis_text)) candidates.push(`年主为${planetZh(str(lord.planet))}。${str(lord.selection_basis_text)}`);
|
||||
add("year-lord", "年主星怎么选出", candidates);
|
||||
add("muntha-records", "Muntha 四项记录", munthaRecordLines(year.muntha_records));
|
||||
const aspects = tajikaAspectLines(year.tajika_aspect_layer);
|
||||
if (aspects.length) result.push({
|
||||
id: `tajika-aspects-${year.target_year}`, title: "Tajika 相位(相位联系,不是具名瑜伽)" + suffix,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { AnnualReportView } from "../src/components/annual/annual-report-page.tsx";
|
||||
import { annualAppendixMarkdown, buildAppendix, type AnnualSnapshot } from "../src/lib/annual-report-generate.ts";
|
||||
|
||||
const { year, yearWithEvents } = JSON.parse(readFileSync(new URL("./fixtures/annual-muntha-records-20261113.json", import.meta.url), "utf8"));
|
||||
const range = { kind: "birthday_year", key: "annual:birthday_year:2026", label: "生日年度",
|
||||
span: "2026 到 2027", crossesBirthday: false, birthdays: [] } as const;
|
||||
const birth = { known: false, beforeMinutes: null, afterMinutes: null, source: null, rectification: "none" } as const;
|
||||
const makeAppendix = (annualYear: unknown) => buildAppendix({
|
||||
cards: { year_overview: [{ target_year: year.target_year }] },
|
||||
page_appendix: { years: [annualYear] },
|
||||
}, range, birth);
|
||||
|
||||
test("Muntha appendix shows four distinct records with no-event and missing-data wording", () => {
|
||||
const appendix = makeAppendix(year);
|
||||
const block = appendix.find((entry) => entry.id === "muntha-records-2026");
|
||||
assert.ok(block);
|
||||
assert.equal(block.title, "Muntha 四项记录");
|
||||
const markdown = annualAppendixMarkdown(appendix);
|
||||
const snapshot: AnnualSnapshot = {
|
||||
kind: "annual_report_snapshot", version: 1, status: "ready", range, appendix,
|
||||
subject: { name: "公开盘", isSelf: false }, plan: [], sections: {}, windows: [], remaining: [], factsSha256: "",
|
||||
};
|
||||
const html = renderToStaticMarkup(createElement(AnnualReportView, { snapshot, generating: false }));
|
||||
const section = html.split("<h4>Muntha 四项记录</h4>")[1]?.split("</section>")[0] ?? "";
|
||||
const positions = ["年盘结构", "Muntha 主", "慢星行运", "逐年推进"].map((label) => section.indexOf(label));
|
||||
assert.ok(positions[0]! >= 0 && positions.every((position, i) => i === 0 || position > positions[i - 1]!));
|
||||
assert.match(section, /同星座:火星/);
|
||||
assert.match(section, /燃烧状态未取得/);
|
||||
assert.match(section, /本期间没有慢星进出 Muntha 星座/);
|
||||
assert.match(section, /平均交点/);
|
||||
assert.doesNotMatch(section, /[·_]/);
|
||||
assert.ok(block.lines.every((line) => markdown.includes(line)));
|
||||
});
|
||||
|
||||
test("Muntha appendix formats actual sign boundaries and keeps old snapshots readable", () => {
|
||||
const block = makeAppendix(yearWithEvents).find((entry) => entry.id === "muntha-records-2026");
|
||||
assert.ok(block);
|
||||
const lines = block.lines.join("\n");
|
||||
assert.match(lines, /真交点/);
|
||||
assert.match(lines, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}(UTC[+-]\d{2}:\d{2}) (?:逆行)?进入 Muntha 所在的/);
|
||||
assert.match(lines, /退出 Muntha 所在的/);
|
||||
assert.doesNotMatch(lines, /度数交会|容许度|[·_]/);
|
||||
assert.equal(makeAppendix({ target_year: 2026 }).some((entry) => entry.id.startsWith("muntha-records-")), false);
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"year": {
|
||||
"target_year": 2026,
|
||||
"muntha_records": {
|
||||
"structure": {
|
||||
"sign": "Gemini",
|
||||
"house": 10,
|
||||
"return_location_mode": "birth_place",
|
||||
"same_sign": [
|
||||
{
|
||||
"planet": "Mars",
|
||||
"relation": "same_sign"
|
||||
}
|
||||
],
|
||||
"aspects": {
|
||||
"status": "not_built",
|
||||
"reason": "现有年盘规则没有可用于此处的 Muntha 星座相位口径。"
|
||||
}
|
||||
},
|
||||
"munthesh": {
|
||||
"planet": "Mercury",
|
||||
"sign": "Cancer",
|
||||
"house": 11,
|
||||
"dignity": "中性(Neutral)",
|
||||
"combust": null,
|
||||
"retrograde": false,
|
||||
"panchavargiya": 8.91,
|
||||
"missing": [
|
||||
"combust"
|
||||
]
|
||||
},
|
||||
"transits": {
|
||||
"basis": "sign_boundary",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"coverage": {
|
||||
"start": {
|
||||
"utc": "2026-08-05T21:09:24Z",
|
||||
"local": "2026-08-05T11:09:24-10:00",
|
||||
"utc_offset": "-10:00",
|
||||
"timezone_id": "Pacific/Honolulu",
|
||||
"jd_ut": 2461258.38152408
|
||||
},
|
||||
"end": {
|
||||
"utc": "2027-08-06T03:16:30Z",
|
||||
"local": "2027-08-05T17:16:30-10:00",
|
||||
"utc_offset": "-10:00",
|
||||
"timezone_id": "Pacific/Honolulu",
|
||||
"jd_ut": 2461623.63645874
|
||||
}
|
||||
},
|
||||
"events": [],
|
||||
"note": "按星座边界记录木星、土星、罗睺和计都的换座及停滞。本次不扫描月亮和快星。",
|
||||
"status": "computed"
|
||||
},
|
||||
"progression": {
|
||||
"status": "computed",
|
||||
"basis": "natal_ascendant_plus_completed_years",
|
||||
"years": [
|
||||
{
|
||||
"target_year": 2026,
|
||||
"sign": "Gemini"
|
||||
},
|
||||
{
|
||||
"target_year": 2027,
|
||||
"sign": "Cancer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"yearWithEvents": {
|
||||
"target_year": 2026,
|
||||
"muntha_records": {
|
||||
"structure": {
|
||||
"sign": "Leo",
|
||||
"house": 10,
|
||||
"return_location_mode": "birth_place",
|
||||
"same_sign": [
|
||||
{
|
||||
"planet": "Ketu",
|
||||
"relation": "same_sign"
|
||||
}
|
||||
],
|
||||
"aspects": {
|
||||
"status": "not_built",
|
||||
"reason": "现有年盘规则没有可用于此处的 Muntha 星座相位口径。"
|
||||
}
|
||||
},
|
||||
"munthesh": {
|
||||
"planet": "Sun",
|
||||
"sign": "Gemini",
|
||||
"house": 8,
|
||||
"dignity": "入敌(Enemy Sign)",
|
||||
"combust": null,
|
||||
"retrograde": false,
|
||||
"panchavargiya": 8.3,
|
||||
"missing": [
|
||||
"combust"
|
||||
]
|
||||
},
|
||||
"transits": {
|
||||
"basis": "sign_boundary",
|
||||
"ayanamsa": "lahiri",
|
||||
"node_mode": "true",
|
||||
"coverage": {
|
||||
"start": {
|
||||
"utc": "2026-06-15T09:29:25Z",
|
||||
"local": "2026-06-15T17:29:25+08:00",
|
||||
"utc_offset": "+08:00",
|
||||
"timezone_id": "Asia/Shanghai",
|
||||
"jd_ut": 2461206.89543255
|
||||
},
|
||||
"end": {
|
||||
"utc": "2027-06-15T15:32:28Z",
|
||||
"local": "2027-06-15T23:32:28+08:00",
|
||||
"utc_offset": "+08:00",
|
||||
"timezone_id": "Asia/Shanghai",
|
||||
"jd_ut": 2461572.14754354
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{"body": "Ketu", "instant": {"utc": "2026-06-19T17:54:23Z", "local": "2026-06-20T01:54:23+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461211.24610329}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-06-21T21:48:43Z", "local": "2026-06-22T05:48:43+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461213.40882778}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-07-04T07:48:34Z", "local": "2026-07-04T15:48:34+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461225.82539463}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-07-07T19:38:41Z", "local": "2026-07-08T03:38:41+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461229.31852531}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-07-17T00:25:56Z", "local": "2026-07-17T08:25:56+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461238.518013}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-07-21T10:57:20Z", "local": "2026-07-21T18:57:20+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461242.95648193}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-07-31T11:50:56Z", "local": "2026-07-31T19:50:56+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461252.99369812}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-08-06T02:30:07Z", "local": "2026-08-06T10:30:07+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461258.60424423}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-08-13T09:52:23Z", "local": "2026-08-13T17:52:23+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461265.911376}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-08-20T02:41:52Z", "local": "2026-08-20T10:41:52+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461272.61241245}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-08-22T21:39:39Z", "local": "2026-08-23T05:39:39+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461275.40253925}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-08-27T18:51:38Z", "local": "2026-08-28T02:51:38+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461280.28585815}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-09-04T07:57:34Z", "local": "2026-09-04T15:57:34+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461287.83164787}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-09-09T19:18:45Z", "local": "2026-09-10T03:18:45+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461293.30468369}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-09-18T20:33:14Z", "local": "2026-09-19T04:33:14+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461302.35641098}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-09-24T02:40:20Z", "local": "2026-09-24T10:40:20+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461307.61134529}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-10-03T13:32:44Z", "local": "2026-10-03T21:32:44+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461317.06439972}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-10-07T01:20:14Z", "local": "2026-10-07T09:20:14+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461320.55571842}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-10-18T16:01:24Z", "local": "2026-10-19T00:01:24+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461332.16763401}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-10-21T08:53:34Z", "local": "2026-10-21T16:53:34+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461334.87052822}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Jupiter", "instant": {"utc": "2026-10-31T06:32:44Z", "local": "2026-10-31T14:32:44+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461344.77272701}, "sign": "Leo", "kind": "entry", "retrograde": false},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-11-01T20:33:38Z", "local": "2026-11-02T04:33:38+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461346.35668468}, "sign": "Leo", "kind": "station", "motion": "retrograde_to_direct"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-11-03T03:05:07Z", "local": "2026-11-03T11:05:07+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461347.62855434}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Ketu", "instant": {"utc": "2026-11-25T12:16:07Z", "local": "2026-11-25T20:16:07+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461370.01119518}, "sign": "Leo", "kind": "exit", "retrograde": true},
|
||||
{"body": "Jupiter", "instant": {"utc": "2026-12-13T00:56:40Z", "local": "2026-12-13T08:56:40+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461387.53935623}, "sign": "Leo", "kind": "station", "motion": "direct_to_retrograde"},
|
||||
{"body": "Jupiter", "instant": {"utc": "2027-01-24T20:01:29Z", "local": "2027-01-25T04:01:29+08:00", "utc_offset": "+08:00", "timezone_id": "Asia/Shanghai", "jd_ut": 2461430.33436108}, "sign": "Leo", "kind": "exit", "retrograde": true}
|
||||
],
|
||||
"note": "按星座边界记录木星、土星、罗睺和计都的换座及停滞。本次不扫描月亮和快星。",
|
||||
"status": "computed"
|
||||
},
|
||||
"progression": {
|
||||
"status": "computed",
|
||||
"basis": "natal_ascendant_plus_completed_years",
|
||||
"years": [
|
||||
{
|
||||
"target_year": 2026,
|
||||
"sign": "Leo"
|
||||
},
|
||||
{
|
||||
"target_year": 2027,
|
||||
"sign": "Virgo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -948,6 +948,7 @@ def _chart_block(report: dict, houses: dict, sr: dict) -> dict[str, Any]:
|
||||
chart = report.get("sr_chart") if isinstance(report.get("sr_chart"), dict) else None
|
||||
if not chart or houses.get("status") == "blocked":
|
||||
return block("blocked", "solar_return.calc_solar_return_chart", reason=str(houses.get("reason") or "annual_chart_missing"))
|
||||
frame = chart.get("birth_info") or {}
|
||||
asc_lon = float(houses["ascendant_longitude"])
|
||||
asc_idx = _sign_idx(asc_lon)
|
||||
planets = {}
|
||||
@@ -962,6 +963,8 @@ def _chart_block(report: dict, houses: dict, sr: dict) -> dict[str, Any]:
|
||||
"house": _house(_sign_idx(lon), asc_idx),
|
||||
"speed": row.get("speed"),
|
||||
"retrograde": row.get("retrograde"),
|
||||
"dignity": row.get("status"),
|
||||
"combust": row.get("combust"),
|
||||
"nakshatra": row.get("nakshatra"),
|
||||
}
|
||||
lord = SIGN_LORDS[SIGNS[asc_idx]]
|
||||
@@ -974,7 +977,8 @@ def _chart_block(report: dict, houses: dict, sr: dict) -> dict[str, Any]:
|
||||
planets=planets,
|
||||
houses=_plain(houses.get("houses")),
|
||||
house_system="whole_sign",
|
||||
node_mode_of_chart=report.get("node_mode", "mean"),
|
||||
ayanamsa=frame.get("ayanamsa_name"),
|
||||
node_mode_of_chart=frame.get("node_mode") or report.get("node_mode", "mean"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1893,6 +1893,96 @@ def _page_year_lord(blocks: dict, natal: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _muntha_transits(slow: dict, annual_chart: dict, sign: str | None, coverage: dict) -> dict:
|
||||
frame = {"ayanamsa": annual_chart.get("ayanamsa"), "node_mode": annual_chart.get("node_mode_of_chart")}
|
||||
note = "按星座边界记录木星、土星、罗睺和计都的换座及停滞。本次不扫描月亮和快星。"
|
||||
result = {"basis": "sign_boundary", **frame, "coverage": coverage, "events": [], "note": note}
|
||||
if slow.get("status") != "computed" or sign not in ap.SIGNS or not coverage:
|
||||
return {"status": "blocked", "reason": "muntha_scan_inputs_missing", **result}
|
||||
if not all(value and slow.get(key) == value for key, value in frame.items()):
|
||||
return {"status": "blocked", "reason": "muntha_scan_frame_mismatch", **result}
|
||||
start, end = _jd(coverage.get("start")), _jd(coverage.get("end"))
|
||||
if start is None or end is None or start >= end:
|
||||
return {"status": "blocked", "reason": "muntha_scan_coverage_missing", **result}
|
||||
bodies = set(slow.get("bodies") or [])
|
||||
opposite = ap.SIGNS[(ap.SIGNS.index(sign) + 6) % 12]
|
||||
events = []
|
||||
for row in slow.get("events") or []:
|
||||
at = _jd(row.get("instant"))
|
||||
body = row.get("body")
|
||||
if at is None or not start <= at < end or body not in bodies:
|
||||
continue
|
||||
if row.get("kind") == "ingress":
|
||||
entered, exited = row.get("to_sign") == sign, row.get("from_sign") == sign
|
||||
if entered == exited:
|
||||
continue
|
||||
detail = {"kind": "entry" if entered else "exit", "retrograde": row.get("retrograde")}
|
||||
elif row.get("kind") == "station":
|
||||
# The producer emits the shared node station once, under Rahu.
|
||||
if body == "Rahu" and "Ketu" in bodies and row.get("sign") == opposite:
|
||||
body = "Ketu"
|
||||
elif row.get("sign") != sign:
|
||||
continue
|
||||
detail = {"kind": "station", "motion": row.get("motion")}
|
||||
else:
|
||||
continue
|
||||
events.append({"body": body, "instant": row["instant"], "sign": sign, **detail})
|
||||
events.sort(key=lambda row: (row["instant"]["jd_ut"], row["body"], row["kind"]))
|
||||
return {**result, "status": "computed", "events": events}
|
||||
|
||||
|
||||
def _muntha_progression(charts: list[dict]) -> dict:
|
||||
years, anchor = [], None
|
||||
for chart in charts:
|
||||
field = (chart.get("blocks") or {}).get("muntha") or {}
|
||||
data = (field.get("data") or {}) if field.get("status") == "computed" else {}
|
||||
years.append({"target_year": chart["target_year"], "sign": data.get("muntha_sign")})
|
||||
if isinstance(data.get("natal_asc_sign_idx"), int) and isinstance(data.get("age"), int):
|
||||
anchor = chart["target_year"], data["natal_asc_sign_idx"], data["age"]
|
||||
if not years:
|
||||
return {"status": "blocked", "years": []}
|
||||
next_year = charts[-1]["target_year"] + 1
|
||||
next_sign = None
|
||||
# A natal ascendant from any available covered return supplies the same annual progression.
|
||||
if anchor is not None:
|
||||
anchor_year, natal_ascendant, age = anchor
|
||||
next_sign = ap._module("solar_return")._muntha_from_natal_ascendant(
|
||||
natal_ascendant, age + next_year - anchor_year)["muntha_sign"]
|
||||
years.append({"target_year": next_year, "sign": next_sign})
|
||||
return {"status": "computed" if all(row["sign"] for row in years) else "partial",
|
||||
"basis": "natal_ascendant_plus_completed_years", "years": years}
|
||||
|
||||
|
||||
def _muntha_records(chart: dict, slow: dict, progression: dict) -> dict:
|
||||
blocks = chart["blocks"]
|
||||
field = blocks["muntha"]
|
||||
muntha = (field.get("data") or {}) if field.get("status") == "computed" else {}
|
||||
sign, lord = muntha.get("muntha_sign"), muntha.get("muntha_lord")
|
||||
annual = blocks["chart"]
|
||||
planets = annual.get("planets") or {}
|
||||
same_sign = [{"planet": planet, "relation": "same_sign"} for planet, row in planets.items()
|
||||
if sign and row.get("sign") == sign]
|
||||
placement = muntha.get("muntha_lord_placement") or {}
|
||||
lord_row = planets.get(lord) or {}
|
||||
strength = (blocks["annual_strength"].get("data") or {}).get("panchavargiya_bala") or {}
|
||||
munthesh = {
|
||||
"planet": lord, "sign": placement.get("sign"), "house": placement.get("house_from_annual_ascendant"),
|
||||
"dignity": lord_row.get("dignity"), "combust": lord_row.get("combust"), "retrograde": lord_row.get("retrograde"),
|
||||
"panchavargiya": (strength.get(lord) or {}).get("score"),
|
||||
}
|
||||
return {
|
||||
"structure": {
|
||||
"sign": sign, "house": muntha.get("muntha_house_from_annual_ascendant"),
|
||||
"return_location_mode": (chart.get("return_location") or {}).get("mode"),
|
||||
"same_sign": same_sign,
|
||||
"aspects": {"status": "not_built", "reason": "现有年盘规则没有可用于此处的 Muntha 星座相位口径。"},
|
||||
},
|
||||
"munthesh": {**munthesh, "missing": [key for key, value in munthesh.items() if value is None]},
|
||||
"transits": _muntha_transits(slow, annual, sign, chart.get("in_report_range") or {}),
|
||||
"progression": progression,
|
||||
}
|
||||
|
||||
|
||||
_TAJIKA_COMMON_CONDITIONS = {
|
||||
"orb_profile": "双方容许度的来源口径",
|
||||
"future_perfection": "未来能否成相",
|
||||
@@ -2030,8 +2120,12 @@ def _page_tajika(field: dict) -> dict:
|
||||
|
||||
|
||||
def _page_appendix(package: dict, natal: dict) -> dict:
|
||||
|
||||
charts = package.get("year_charts") or []
|
||||
slow = package.get("slow_transits") or {}
|
||||
progression = _muntha_progression(charts)
|
||||
years = []
|
||||
for chart in package.get("year_charts") or []:
|
||||
for chart in charts:
|
||||
if chart.get("status") != "computed":
|
||||
continue
|
||||
blocks = chart["blocks"]
|
||||
@@ -2041,6 +2135,7 @@ def _page_appendix(package: dict, natal: dict) -> dict:
|
||||
years.append({
|
||||
"target_year": chart["target_year"],
|
||||
"year_lord": _page_year_lord(blocks, natal),
|
||||
"muntha_records": _muntha_records(chart, slow, progression),
|
||||
"mudda_balance": {
|
||||
"status": mudda.get("status"),
|
||||
"balance_profile": mudda.get("balance_profile"),
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""The four Muntha appendix records use year-chart and slow-transit evidence."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.test_annual_audit_axes_20261112 import public_facts
|
||||
from tests.test_annual_report_facts import _body, _facts, annual_api, annual_report_facts as rf
|
||||
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
|
||||
TSX = FRONTEND / "node_modules/tsx/dist/cli.mjs"
|
||||
|
||||
|
||||
def test_public_muntha_records_are_independent(public_facts):
|
||||
year = public_facts["page_appendix"]["years"][0]
|
||||
records = year["muntha_records"]
|
||||
fixture = FRONTEND / "tests/fixtures/annual-muntha-records-20261113.json"
|
||||
assert json.loads(fixture.read_text(encoding="utf-8"))["year"] == {
|
||||
key: year[key] for key in ("target_year", "muntha_records")
|
||||
}
|
||||
assert set(records) == {"structure", "munthesh", "transits", "progression"}
|
||||
structure, lord = records["structure"], records["munthesh"]
|
||||
assert structure["aspects"]["status"] == "not_built"
|
||||
assert all(row["relation"] == "same_sign" for row in structure["same_sign"])
|
||||
overview = public_facts["cards"]["year_overview"][0]
|
||||
assert structure["sign"] == overview["muntha"]["sign"]
|
||||
assert structure["house"] == overview["muntha"]["annual_house"]
|
||||
assert lord["planet"] == overview["muntha"]["lord"]
|
||||
assert lord["dignity"] and type(lord["retrograde"]) is bool and isinstance(lord["panchavargiya"], (int, float))
|
||||
assert lord["combust"] is None and "combust" in lord["missing"]
|
||||
assert "muntha_records" not in overview
|
||||
assert [row["target_year"] for row in records["progression"]["years"]] == [2026, 2027]
|
||||
|
||||
|
||||
def test_fictional_cross_year_report_matches_annual_chart_frame_and_progression():
|
||||
single_year = _facts(ayanamsa="lahiri", node_mode="true")["page_appendix"]["years"][0]
|
||||
fixture = FRONTEND / "tests/fixtures/annual-muntha-records-20261113.json"
|
||||
assert json.loads(fixture.read_text(encoding="utf-8"))["yearWithEvents"] == {
|
||||
key: single_year[key] for key in ("target_year", "muntha_records")
|
||||
}
|
||||
inputs = dict(report_range={"kind": "calendar_year", "year": 2026}, ayanamsa="lahiri", node_mode="true",
|
||||
return_location={"mode": "current_city", "latitude": 51.5074, "longitude": -0.1278,
|
||||
"timezone_id": "Europe/London"})
|
||||
facts = _facts(**inputs)
|
||||
package = annual_api.compute_annual_package(_body(**inputs))["package"]
|
||||
years = facts["page_appendix"]["years"]
|
||||
assert [year["target_year"] for year in years] == [2025, 2026]
|
||||
for year, annual in zip(years, package["year_charts"], strict=True):
|
||||
records = year["muntha_records"]
|
||||
assert set(records) == {"structure", "munthesh", "transits", "progression"}
|
||||
assert records["structure"]["return_location_mode"] == "current_city"
|
||||
scan = records["transits"]
|
||||
assert scan["status"] == "computed" and scan["basis"] == "sign_boundary"
|
||||
assert scan["coverage"] == annual["in_report_range"]
|
||||
assert scan["ayanamsa"] == annual["blocks"]["chart"]["ayanamsa"] == package["slow_transits"]["ayanamsa"] == "lahiri"
|
||||
assert scan["node_mode"] == annual["blocks"]["chart"]["node_mode_of_chart"] == package["slow_transits"]["node_mode"] == "true"
|
||||
assert all(event["sign"] == records["structure"]["sign"] for event in scan["events"])
|
||||
assert all("longitude" not in event and "orb" not in event for event in scan["events"])
|
||||
progression = records["progression"]["years"]
|
||||
assert [row["target_year"] for row in progression] == [2025, 2026, 2027]
|
||||
assert progression[year["target_year"] - 2025]["sign"] == records["structure"]["sign"]
|
||||
first = rf.ap.SIGNS.index(progression[0]["sign"])
|
||||
assert [row["sign"] for row in progression] == [rf.ap.SIGNS[(first + offset) % 12] for offset in range(3)]
|
||||
|
||||
|
||||
def test_muntha_sign_filter_preserves_repeated_entries_exits_and_station():
|
||||
frame = {"ayanamsa": "raman", "node_mode_of_chart": "true"}
|
||||
scan = {"status": "computed", "ayanamsa": "raman", "node_mode": "true", "bodies": ["Saturn"], "events": []}
|
||||
def instant(n):
|
||||
return {"jd_ut": n}
|
||||
|
||||
def crossing(n, source, target, retrograde):
|
||||
return {"kind": "ingress", "body": "Saturn", "instant": instant(n),
|
||||
"from_sign": source, "to_sign": target, "retrograde": retrograde}
|
||||
|
||||
scan["events"] = [
|
||||
crossing(5, "Capricorn", "Aquarius", False),
|
||||
crossing(3, "Aquarius", "Capricorn", True),
|
||||
crossing(1, "Capricorn", "Aquarius", False),
|
||||
{"kind": "station", "body": "Saturn", "instant": instant(2), "sign": "Aquarius",
|
||||
"motion": "direct_to_retrograde", "longitude": 305.0},
|
||||
{"kind": "station", "body": "Saturn", "instant": instant(4), "sign": "Capricorn", "motion": "retrograde_to_direct"},
|
||||
crossing(7, "Aquarius", "Pisces", False),
|
||||
]
|
||||
coverage = {"start": instant(1), "end": instant(7)}
|
||||
result = rf._muntha_transits(scan, frame, "Aquarius", coverage)
|
||||
assert [(row["instant"]["jd_ut"], row["kind"]) for row in result["events"]] == [
|
||||
(1, "entry"), (2, "station"), (3, "exit"), (5, "entry"),
|
||||
]
|
||||
assert result["events"][2]["retrograde"] is True
|
||||
assert "longitude" not in result["events"][1]
|
||||
empty = rf._muntha_transits(scan, frame, "Leo", coverage)
|
||||
assert empty["status"] == "computed" and empty["events"] == []
|
||||
blocked = rf._muntha_transits(scan, {**frame, "node_mode_of_chart": "mean"}, "Aquarius", coverage)
|
||||
assert blocked["status"] == "blocked" and blocked["events"] == []
|
||||
assert rf._muntha_transits(scan, {**frame, "ayanamsa": "lahiri"}, "Aquarius", coverage)["status"] == "blocked"
|
||||
|
||||
node_scan = {**scan, "bodies": ["Rahu", "Ketu"], "events": [
|
||||
{"kind": "station", "body": "Rahu", "instant": instant(4), "sign": "Leo", "motion": "retrograde_to_direct"},
|
||||
]}
|
||||
assert rf._muntha_transits(node_scan, frame, "Aquarius", coverage)["events"] == [
|
||||
{"kind": "station", "body": "Ketu", "instant": instant(4), "sign": "Aquarius", "motion": "retrograde_to_direct"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSX.is_file(), reason="frontend dependencies are required for the writer contract")
|
||||
def test_muntha_appendix_leaves_ten_writer_evidence_hashes_unchanged(public_facts):
|
||||
program = """
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { EMPTY_ANNUAL_SITUATION as situation } from "./src/lib/annual-report-shared.ts";
|
||||
import { buildAnnualPlan, sectionEvidence } from "./src/lib/annual-report-sections.ts";
|
||||
const after = JSON.parse(readFileSync(0, "utf8"));
|
||||
const before = structuredClone(after);
|
||||
for (const year of before.page_appendix.years) delete year.muntha_records;
|
||||
const hashes = facts => buildAnnualPlan(facts, situation).map(entry => [
|
||||
entry.id, createHash("sha256").update(JSON.stringify(sectionEvidence(facts, entry, situation))).digest("hex"),
|
||||
]);
|
||||
assert.equal(hashes(after).length, 10);
|
||||
assert.deepEqual(hashes(after), hashes(before));
|
||||
"""
|
||||
subprocess.run(["node", str(TSX), "-e", program], cwd=FRONTEND, check=True, timeout=45,
|
||||
input=json.dumps(public_facts, ensure_ascii=False).encode("utf-8"))
|
||||
Reference in New Issue
Block a user