Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efdb67906c | ||
|
|
44a93d0539 | ||
|
|
5041521322 | ||
|
|
cfa8e0ff58 | ||
|
|
e5d6c99dac | ||
|
|
1f5367d09b | ||
|
|
c2f295fd82 | ||
|
|
394c39ce33 | ||
|
|
6bf20b8dbf | ||
|
|
34f7793e52 | ||
|
|
b9504d758a |
@@ -1,5 +1,10 @@
|
||||
# BLOCKED
|
||||
|
||||
## #237 · Windows 全量门禁与预检
|
||||
|
||||
- 快速门的三项 PyJHora 检查在同机基线复现。咨询 VedAstro 对照在门禁内波动,分支与基线单独重跑均通过。目录预检超时,定向预检复现既有历史镜像路径断言失败。
|
||||
- 替代证据:报告与选盘定向回归、真实 PostgreSQL 报告档案测试和仓库隐私检查通过。详见 [#237 进度](docs/tasks/PROGRESS-reading-window-edge-20261113.md)。
|
||||
|
||||
## BUG-1865 · 选盘保存的计算时刻可能落在所选区间外(#227,2026-11-13)
|
||||
|
||||
- ~~旧流程区间缺少采纳凭据。~~ 修订任务书已明确优先读取档案的选盘采纳记录,并限定旧流程的凭据。
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-11-13 — 个人报告按区间内部的换座核对证据
|
||||
|
||||
- 生时纠正选定的区间、填写范围及默认范围,只在区间内部发生换座时标记分盘敏感。报告摘要和证据等级按同一规则判定。沿用当前 Skill 版本(BUG-1871)。
|
||||
|
||||
## 2026-11-13 — 个人报告显示章节拦截原因
|
||||
|
||||
- 页面和 Markdown 导出会解释对应章节为何未通过核对。新解读保存生成快照和逐节转换记录,便于复查和回放。
|
||||
|
||||
@@ -25086,6 +25086,20 @@
|
||||
- 关联记录:#209、#190、BUG-1601、BUG-1650、BUG-1732、BUG-1830、BUG-1842。
|
||||
- 修复版本:`codex/annual-narayana-conditional-20261112`。
|
||||
|
||||
## BUG-1856 | 年运审计计算与解读状态合用,未完成会被现有附录显示为阻塞
|
||||
|
||||
- 状态:fixed-pending-verify(后台数据待独立验收,附录接入由 #212 处理)。
|
||||
- 首次发现 / 最近更新:2026-11-12 / 2026-11-13。
|
||||
- 影响面:年运报告的后台技法审计与现有「技法执行情况」附录。
|
||||
- 现象:旧审计行的单值区分不了计算与解读。加入拟议的两轴和外部验证行后,现有附录会立即显示这些行,且把 `not_built` 标成「计算阻塞」。
|
||||
- 触发条件:把新行直接放入既有 `technique_audit` 数组。
|
||||
- 根因:现有附录直接枚举该数组,只按两个旧名称过滤。计算状态解析的未知值回退到 `blocked`。BUG-1753 与 BUG-1840 的两轴覆盖早于本轮新增的 `not_built`,旧验证未覆盖这一值。
|
||||
- 修复:按工单评论 42704 新增后台 `technique_audit_axes`,保留旧审计数组。新字段逐行记计算与解读状态、原因、profile 和原始路径,给外部验证四行加附录标记。
|
||||
- 验证:公开盘核对新增行、状态与方法配置。合成盘旧数组与 staging 逐字节一致,十个写作区段的证据一致。详见 `docs/tasks/PROGRESS-annual-audit-axes-20261112.md`。
|
||||
- 防复发:锁定旧数组完整内容,检查计算阻塞和未完成的不同状态,并以写作入口验证新字段不进入模型输入。
|
||||
- 关联记录:#210、#212、BUG-1753、BUG-1794、BUG-1840、BUG-1855。
|
||||
- 修复版本:`codex/annual-audit-axes-20261112`,待独立验收。
|
||||
|
||||
## BUG-1858 | 报告用途稳定性由一分钟分盘边界检查推定
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
@@ -25215,3 +25229,16 @@
|
||||
- 防复发:测试分别锁定档案权限、请求转交、缓存隔离与公开盘的候选瑜伽结果。
|
||||
- 关联记录:#228、#189、BUG-1063。
|
||||
- 修复版本:`codex/report-gender-20261113`,待发布。
|
||||
|
||||
## BUG-1871 | 个人报告把区间端点当成区间内换座
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
- 首次发现 / 最近更新:2026-11-13 / 2026-11-13。
|
||||
- 影响面:个人报告的分盘换座摘要和证据等级上限。
|
||||
- 现象:已采纳侧起点的换座线被报为区间内换座,依赖相应分盘的结论降级。
|
||||
- 触发条件:距最近换座的时间恰等于出生时间区间一侧的宽度。
|
||||
- 根因:BUG-1864 的区间检查用包含端点的比较,既有测试也锁定了该行为。BUG-1865 让已采纳区间到达报告后,选择过的换座线经常成为端点,摘要与证据等级两处都将其判入区间。
|
||||
- 修复:两处共用区间判定,未确认分钟时只计严格落在端点容差以内的内部换座。确认分钟仍按原有前后各一分钟的规则。
|
||||
- 验证及防复发:合成事实覆盖只在端点换座、区间内有后续换座和两侧容差,分别检查分盘状态、摘要及各领域上限。确认分钟的端点和端点外也有回归覆盖。
|
||||
- 关联记录:#237、BUG-1864、BUG-1865、BUG-1858。详见 [执行进度](tasks/PROGRESS-reading-window-edge-20261113.md)。
|
||||
- 修复版本:`codex/reading-window-edge-20261113`,待发布。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility.
|
||||
|
||||
## 2026-11-13 · #237 Windows 报告预检
|
||||
|
||||
- 远端可见性、Python 和适配器检查通过。首次目录扫描及定向测试超时。外层命令时限延长至 180 秒后,目录扫描仍在 90 秒超时,定向测试仅余既有 `.workbuddy` 镜像路径断言失败。见 [#237 进度](../tasks/PROGRESS-reading-window-edge-20261113.md)。
|
||||
|
||||
## 2026-11-13 · #220 报告解读快照
|
||||
|
||||
- Windows 预检可见 Gitea 远端,碎片扫描与适配器步骤通过。既有 `test_preflight_fragment_scan_reports_authority_layers_and_risk_buckets` 仍要求本机不存在的 `.workbuddy` 历史镜像路径。预检退出 1。
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# 年运审计两轴 · 2026-11-12
|
||||
|
||||
Gitea #210,BUG-1856。按 Claude 评论 42309 的任务书认领,认领评论 42331。基线 `2f374fff281622f342151a97fa6f6d758de74cc8`。
|
||||
|
||||
## 已核对
|
||||
|
||||
- 年运事实包的 `technique_audit` 目前既是后台审计数组,也是现有年运附录「技法执行情况」的数据源。`buildAppendix` 只按两个旧技法名排除外部行,会渲染数组里的其他行。`visible_in_appendix` 当前没有参与过滤。
|
||||
- `techniqueAuditAxes` 已读取两轴字段。现有 `parseTechniqueCalculationStatus` 没有 `not_built` 枚举,未知值落到 `blocked`。
|
||||
- `#212` 的可信评论 42236 指明新「计算依据」附录显示外部验证,正文仍不写。该工单仍排队,等 #210 等前置单合入。
|
||||
- 在与 staging 一致的年运附录和写作投影代码中,给现有事实夹具注入四条拟议外部行和一条 Chara Dasha 示例。四条外部行均进入当前附录,三条 `not_built` 被显示成「计算阻塞」。十个写作区段的证据文本均未带入这四行。注入仅用于复现显示,示例状态不代表真实计算。
|
||||
- 现有 Python 年运审计合同 1 项通过,年运前端定向 36 项通过。复现实验及合同检查未修改产品代码。
|
||||
|
||||
## 已确认的交付契约
|
||||
|
||||
Claude 在评论 42704 放行独立字段,第二次认领为评论 42726。执行分支合入了 staging `7670cf54`,冲突处保留 BUG-1856 和 staging 的其他记录。新字段为 `technique_audit_axes`。原 `technique_audit` 保持行数、顺序及取值。附录切换和「未完成」显示由 #212 处理。
|
||||
|
||||
## 路由核对与状态
|
||||
|
||||
对照 `references/strict-workflow-router.md` §0.7 的印度与西方技法、§8 的相关年运行,以及 `references/product_runtime_contract.md` 的两轴和外部验证约定。状态随实际块结果更新。已计算的年盘块缺部分年份时写 `partial`,同时记录缺项原因。计算失败的层给出阻碍,未接入的层写 `not_built`。未计算的层无原始结果路径。
|
||||
|
||||
| 技法 | 计算 / 解读 | 证据与边界 |
|
||||
| --- | --- | --- |
|
||||
| D1、D2、D4、D9、D10、D24、Functional Benefic/Malefic | executed / primary | 本命与分领域分盘。功能吉凶来自实际 profile。 |
|
||||
| D11、A10、A7、UL、DK、Natal Yogas | executed / supporting | D11 是财富辅助盘,A10、A7、UL、DK 与本命瑜伽按领域作为辅助依据。 |
|
||||
| Jaimini / Arudha | partial / supporting | 上述专用点有结果,完整套件未跑。 |
|
||||
| Vimshottari、Transits | executed / primary | 本命时间轴与慢星行运;上游计算失败时保留 blocked 和原因。 |
|
||||
| Narayana Dasha、Varsha Narayana、Annual Sahams、Vedha | executed / conditional | 来源版本、年盘太阳角距、回归昼夜公式和慢星 Vedha 分开记;本命 Narayana 依 #209 作条件对照,计算被阻塞时保留阻碍。 |
|
||||
| Tajika Annual Chart、Mudda Dasha、Patyayini Dasha | executed / primary | 年盘及其中的年内运限。 |
|
||||
| Muntha、Tajika Year Lord、Panchavargiya Bala、Annual Shadbala、Transit Ashtakavarga | executed / supporting | 年盘位置、年主、年盘力量及慢星位置的本命 BAV/SAV。 |
|
||||
| Sade Sati、Tajika Aspects | executed / observation_only | 保留土星相对月亮的位置与相位联系。 |
|
||||
| Tajika Yogas | executed 或 partial / not_used | 从具名瑜伽块读取结果;存在缺条件的具名规则时记 partial。 |
|
||||
| Tajika Monthly Charts、Tithi Pravesha、Tripataki Chakra、Annual Special Points、Mudda Alternative Profiles | executed / not_used | 计算保留在年包或后台卡片;不送进写作区段。替代 Mudda 口径与写作用的默认序列分别列出。 |
|
||||
| Chara、Yogini、Ashtottari、Kalachakra、Bhrigu Pada、KP、Double Transit PAC、Transit LL / 7L、Bhava Chalit、Sudarshana、Nakshatra Overlay、Natal Shadbala、Argala | not_built / not_used | 年包未执行这些专用层,分行说明当前覆盖范围。 |
|
||||
| 回归黄道本命盘、西占行运持续区间、西占太阳回归、次限、太阳弧、逆向推运或太阳弧、中点、月亮回归、Parans | not_built / not_used | 年运链路没有运行这些西方方法。 |
|
||||
| Yoga / Nakshatra Pravesha | not_applicable / not_used | 目前只用于研究,已在年运范围中排除。 |
|
||||
| MEVG / Global Web Evidence、Real Case Calibration | not_built / not_used | 保留旧名称对应的审计行,记录本轮没有执行。 |
|
||||
| 本地计算、预置来源匹配、实时联网、真实案例校准 | 本地按计算状态;其余 not_built / not_used | 四行都有附录标记。预置来源匹配证据不足,实时联网和案例校准本轮未做。 |
|
||||
|
||||
公开回归使用仓库收录的 Barack Obama AA 记录。两轴状态按实际返回,保留该盘中本命年运数据的阻碍以及具名 Tajika 瑜伽的部分覆盖。回归同时用合成盘核对 staging 的旧数组字节和全部十个写作区段的证据投影。没有改动既有断言。
|
||||
|
||||
## 验证
|
||||
|
||||
- 新增四项回归、现有后台事实测试合计 18 项通过。现有前端年运定向 36 项通过、取消 0。隐私检查 62 项通过。
|
||||
- 年运专项套件 163 项通过、5 项失败。用未改动的 staging `7670cf54` 单独复现同名五项,涉及 PyJHora 年运参考、报告参考行和既有年运时间轴连续性。
|
||||
- Python 快速门 1,463 项通过、2 跳过、3 项 PyJHora 失败,失败项与同机基线一致。最新 staging `3e6077f75` 已合入 #228,本单年运范围的文件未受影响。
|
||||
- 开工预检的碎片扫描、外部适配器和远端可见性通过;预检定向套件仍有一个要求本机存在旧镜像路径的失败,错误台账已有记录。
|
||||
@@ -0,0 +1,23 @@
|
||||
# #237 执行进度 · 分盘换座的区间端点
|
||||
|
||||
Codex 基于 `origin/staging` 的 `151159927` 实施。任务书提交 `c2f295fd8` 相对规划基线 `3e6077f75` 只新增任务书。交付版本中的任务书移除原案例的具体时刻,保留验收用的相对分钟数。
|
||||
|
||||
报告摘要和未确认分钟的证据等级共用 `crosses_birth_window`。采纳区间、填写范围和默认范围的两侧都使用 `换座距离 < 该侧宽度 − 0.01 分钟`。0.01 分钟约为 0.6 秒,可吸收小于一秒的取整差异。合成回归分别检查离端点 0.005 分钟和 0.02 分钟的位置,后者仍判为区间内部。已确认分钟仍按包含端点的前后各一分钟规则判断。
|
||||
|
||||
合成事实包中,D2、D6、D8 只有起点换座,状态为 `clear_at_tolerance`,摘要写「区间内不换座」。D10、D9 在区间内仍有换座,状态为 `boundary_sensitive`。财富收入、财富留存和健康压力的上限为 C,事业和婚恋结构仍为 D。
|
||||
|
||||
| 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 填写区间后端点的财富收入上限 `D` | `C` | 后端点的换座不属于区间内部。 |
|
||||
| 摘要对前端点写「区间内会换座」 | 「区间内不换座」 | 前侧换座距离等于前侧宽度。 |
|
||||
| 摘要对后端点写「区间内会换座」 | 「区间内不换座」 | 后侧换座距离等于后侧宽度,前侧在搜索上限内未换座。 |
|
||||
| 默认区间端点的事业上限 `D` | `C` | 默认窗口同样排除端点,用途稳定性仍为 `not_evaluated`。 |
|
||||
|
||||
## 验证
|
||||
|
||||
- 新增两项回归均在修复前复现端点误判。
|
||||
- Python 定向 53 项通过,覆盖任务书的四个文件和既有选盘测试。报告出生时间范围与 worker 的前端定向 34 项通过,真实 PostgreSQL 的报告档案测试 1 项通过。Node 两组测试均无失败和取消。
|
||||
- Python 快速门退出 1,1,462 项通过、2 项跳过、29 项子测试通过、4 项失败。其中 3 项 PyJHora 检查在同机基线逐项复现。另一项是既有咨询 VedAstro 对照波动,分支和基线各单独重跑一次均通过。
|
||||
- Windows 预检确认 Python、Gitea 远端和适配器可用。目录扫描超时。延长定向测试时限后,只有既有 `.workbuddy` 镜像路径断言失败。详见 [预检台账](../research/pre_work_error_ledger.md)。
|
||||
- 仓库隐私检查 62 项通过。
|
||||
- 同步含 #210 的 staging `e5d6c99da` 后,年运审计、分盘范围、证据等级、用途稳定性和隐私合跑 89 项通过。
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [#237 任务书](TASK-reading-window-edge-20261113.md) | [执行进度](PROGRESS-reading-window-edge-20261113.md) | 个人报告区间端点换座判定(BUG-1871) | 待验收 | `codex/reading-window-edge-20261113` |
|
||||
| [#220 修订任务书](FIX-reading-snapshot-binding-1.md) | [执行进度](PROGRESS-reading-snapshot-binding-20261112.md) | 个人报告生成快照、逐节拦截原因和离线验收包(BUG-1863) | 待验收 | `codex/reading-snapshot-binding-20261112` |
|
||||
|
||||
截至 2026-09-03,`main` 停在 2026-08-16 的 `7b620c7a`,下表**没有任何一项已上线**。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# TASK:年运报告技法审计改为两个状态轴,并补齐路由技法行(后台,2026-11-12)
|
||||
|
||||
- Gitea:#210
|
||||
- 分支:`codex/annual-audit-axes-20261112`(基于 staging `2f374fff281622f342151a97fa6f6d758de74cc8`,#209 已合入)
|
||||
- Bug 编号:BUG-1856。开工时核对 `docs/BUG_HISTORY.md` 末尾;若已被占用,顺延,并在 PROGRESS 写明。
|
||||
- 负责验收:Claude(jyotisha-de)
|
||||
|
||||
不得自行写“产品决定/裁定”;任务书没写的取舍一律选需拍板。
|
||||
|
||||
不得修改或删除既有断言来让测试通过;确需修改必须写原值/新值/原因,并在 PROGRESS 里逐条列出。
|
||||
|
||||
## 背景
|
||||
|
||||
占星师有以下几条裁定:
|
||||
|
||||
- **第五轮裁定 7(2026-11-08)**:审计表每行分两个状态轴写。
|
||||
- Calculation:`executed` / `partial` / `blocked` / `not_applicable`
|
||||
- Interpretation:`primary` / `supporting` / `conditional` / `observation_only` / `blocked`,再加产品自己的 `not_used`
|
||||
- 个人报告和聊天已经这样做了,见 BUG-1753,以及 `references/product_runtime_contract.md` 第五轮裁定 7 一条。
|
||||
- **她的路由 `references/strict-workflow-router.md` 第 70–77 行(Full-Spectrum 合同)**:每个相关技法都要写明 executed、blocked 或 not_applicable,不得静默省略。路由第 365 行要求 Tajika Yogas 单独一行。
|
||||
- **Q3(2026-11-07)**:交付了文件,不等于执行了方法。
|
||||
|
||||
年运报告现在的情况(行号是大约位置):
|
||||
|
||||
- `scripts/annual_service/report_facts.py` 的 `_technique_audit`(约 :1581–1629)每行只有一个状态。
|
||||
- 月盘、Tithi Pravesh 算了却没有被读用,审计里仍写“executed”,约 :1613。
|
||||
- 路由要求的一些技法没有行,例如 Double Transit、Chara、Yogini、Ashtottari、Kalachakra、KP。
|
||||
- 没有单独的 Tajika Yogas 行。
|
||||
|
||||
**本单只改后台数据,也就是审计行的结构和内容。页面怎么显示另开单,等产品负责人看过原型图再定。**
|
||||
|
||||
## 要做的
|
||||
|
||||
1. `_technique_audit` 每一行增加 `calculation_status` 和 `interpretation_status`,取值按上面的两轴。保留原来的单值字段,前端现在还在读它。
|
||||
2. 新旧值映射照 `references/product_runtime_contract.md` 第五轮裁定 7 一条的写法:
|
||||
- executed → executed + primary
|
||||
- candidate → executed + conditional
|
||||
- blocked → blocked + blocked
|
||||
- not_applicable → not_applicable + not_used
|
||||
3. **算了但没读用的层**,即月盘、Tithi Pravesh,以及其他符合这个情况的层,标 `executed` + `not_used`。所有这类层逐个列进 PROGRESS。
|
||||
4. 新增单独的 **Tajika Yogas** 行,从现有的具名瑜伽结果取状态。
|
||||
5a. **「未完成」与「阻塞」分开**(占星师 2026-11-12 验收回复 §4、§5 Q3):Calculation 轴新增 `not_built`(中文「未完成」)——技法相关、但网站尚未开发或这次没有运行。`blocked` 只用于有具体阻碍(缺输入、调用失败、验证失败),并写明阻碍;`not_applicable` 必须写明适用条件或范围理由。`not_in_domain_route` 不能自动当作不适用。
|
||||
5. **路由技法补行**:
|
||||
- 对照她的路由第 70–77 行,以及年运相关路由,把没有行的技法补上;
|
||||
- 年运报告没有运行的技法,按 5a 写 `not_built`、`blocked` 或 `not_applicable`,并给出原因;不能写 executed;
|
||||
- 不新增任何计算,本单只写行。
|
||||
6. 每行写上实际用的 profile 名,例如 `pvr_table30_v1`、Narayana 的 Rath profile,以及原始数据路径。没有就写 `null`。
|
||||
7. **外部验证四行**:「本地计算」「预置来源匹配」「实时联网」「真实案例校准」。
|
||||
- 写进审计数据,取值照实(本轮「实时联网」「真实案例校准」为未完成);
|
||||
- 不进给模型的正文要求(模型正文仍不写,BUG-1794);
|
||||
- 产品负责人 2026-11-12 决定(回应占星师验收回复 §4):这几行要在用户可展开的「计算依据」附录里如实显示。本单只保证数据里有、并标 `visible_in_appendix: true`;页面显示由 #212 做。
|
||||
8. Narayana 一行的 Interpretation 写 `conditional`。这一点与 #209 一致。
|
||||
|
||||
## 要改的文件
|
||||
|
||||
- `scripts/annual_service/report_facts.py`:只改 `_technique_audit` 及其辅助函数。
|
||||
- 新建 `tests/test_annual_audit_axes_20261112.py`
|
||||
- `docs/BUG_HISTORY.md`:只在末尾追加一条。
|
||||
- `docs/tasks/PROGRESS-annual-audit-axes-20261112.md`
|
||||
|
||||
## 不要碰
|
||||
|
||||
- `frontend/**`:页面显示另开单。
|
||||
- 冻结文件,以及 `references/**`。
|
||||
- `report_facts.py` 里窗口、阶段和 Narayana 的逻辑:属于 #209。
|
||||
|
||||
## 验收标准(写进新测试)
|
||||
|
||||
1. 用一张公开名人盘生成年包:每一行审计都有两个轴,取值在规定范围内。
|
||||
2. 月盘、Tithi Pravesh 两行是 `executed` + `not_used`。
|
||||
3. 有 Tajika Yogas 行。
|
||||
4. 路由列出的每个技法都有一行,没有静默缺失。测试里列出期望的技法名单,名单来自你对照路由整理的结果。
|
||||
5. 外部验证四行在审计数据里存在、带 `visible_in_appendix: true`,并且不出现在给模型的证据文本里。
|
||||
6. 现有年运测试全部通过。
|
||||
|
||||
## 自测命令
|
||||
|
||||
```
|
||||
python -m pytest -q tests/test_annual_audit_axes_20261112.py tests/ -k annual
|
||||
python scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
|
||||
```
|
||||
|
||||
## 进度记录
|
||||
|
||||
写在 `docs/tasks/PROGRESS-annual-audit-axes-20261112.md`,内容包括:
|
||||
|
||||
- 对照路由整理的技法清单,以及每一项的两轴取值和原因;
|
||||
- “算了没读用”的层的清单;
|
||||
- 测试结果,以及在 staging 上也复现的基线失败。
|
||||
|
||||
## 何时停下来选“需拍板”
|
||||
|
||||
- 某个技法该写 `not_built`、`blocked` 还是 `not_applicable`,路由和裁定里都没说清。
|
||||
- 要让某一行如实反映状态,必须新增计算。
|
||||
@@ -0,0 +1,55 @@
|
||||
# TASK:生时纠正选定一侧的起点就是已解决的换盘线,不能再算成「区间内会换座」(2026-11-13)
|
||||
|
||||
- Gitea:#237
|
||||
- 分支:`codex/reading-window-edge-20261113`,基于 staging `3e6077f752b606fee69a9eb15853fe328801901b`
|
||||
- Bug 编号:BUG-1871
|
||||
- 验收:Claude(jyotisha-e6)
|
||||
|
||||
不得自行写「产品决定/裁定」;任务书没写的取舍一律选需拍板。不得修改或删除既有断言来让测试通过;确需修改的,写明原值、新值、原因。
|
||||
|
||||
## 现象(产品负责人 2026-11-13 实测报告)
|
||||
|
||||
选盘页采纳换盘线后一侧后,#227 部署的报告正确识别了生时纠正选定的区间。下表用相对计算时刻的合成测试输入复现误判,窗口前侧 0.3 分钟、后侧 15 分钟。事业、财富、健康三节都降成 D:
|
||||
|
||||
| 分盘 | 提前多少分钟换座 | 推后多少分钟换座 | 实际情况 |
|
||||
|---|---|---|---|
|
||||
| 财富分盘 D2 | 0.3 | 147.167 | 线后一侧整段都不换座 |
|
||||
| 健康分盘 D6 | 0.3 | 24.367 | 线后一侧整段都不换座 |
|
||||
| D8 | 0.3 | 18.2 | 线后一侧整段都不换座 |
|
||||
| 事业分盘 D10 | 0.3 | 14.5 | 区间内会再换一次,这次判断是对的 |
|
||||
| 九分盘 D9 | 8.517 | 7.917 | 区间内会换,这次判断是对的 |
|
||||
|
||||
「提前 0.3 分钟就换座」指的正是区间起点,也就是用户在选盘页已经选过的那条线。区间就是从这条线开始的,线后的盘是固定的。可是代码判断用的是 `before <= window.before_minutes`(0.3 ≤ 0.3),把这条线本身也算成了「区间内换座」。
|
||||
|
||||
## 要改的地方
|
||||
|
||||
1. `scripts/report_render/evidence_grade.py::_varga_boundary` 和 `scripts/report_render/birth_precision_margins.py`(生成 `summary_zh` 里「区间内会/不会换座」那一句)用同一条规则。抽成一个共享函数,两处都调用它。
|
||||
2. 规则:只有换座点**严格落在区间内部**,才算区间内会换座。
|
||||
- 前侧:`before < window.before_minutes - ε`
|
||||
- 后侧:`after < window.after_minutes - ε`
|
||||
- ε 取 0.01 分钟,用来吸收秒级的取整误差。在 PROGRESS 里写明为什么取这个值。
|
||||
- 这条规则对 `rectified_range`、`user_stated` 和默认窗口都适用:换座点正好落在区间端点上,本来就不算区间内。
|
||||
3. 已确认到分钟(±1 分钟)的旧规则不受影响,原有测试保持通过。
|
||||
|
||||
## 不要碰
|
||||
|
||||
- `frontend/**`
|
||||
- `references/**`
|
||||
- 选盘页本身(#229 负责)
|
||||
- 冻结文件
|
||||
|
||||
## 验收(新测试)
|
||||
|
||||
1. 按上表构造合成事实包(`rectified_range`,前 0.3、后 15):
|
||||
- D2、D6、D8 判为 `clear_at_tolerance`,`summary_zh` 写「区间内不换座」;
|
||||
- D10、D9 判为 `boundary_sensitive`;
|
||||
- `wealth_earning`、`wealth_retention`、`health_pressure` 的上限回到 C,`career_promise` 和 `relationship_structure` 仍然是 D。
|
||||
2. 换座点比区间端点早 0.02 分钟(落在区间内)时,判为会换座。
|
||||
3. #224、#227、#215 的测试全部通过。
|
||||
|
||||
## 自测命令
|
||||
|
||||
```
|
||||
python3 -m pytest -q tests/test_report_birth_window_20261113.py tests/test_report_evidence_grade_20261112.py tests/test_evidence_grade_purpose_stability_20261112.py tests/test_report_reading_facts.py
|
||||
python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
|
||||
```
|
||||
@@ -1654,6 +1654,199 @@ def _technique_audit(package: dict, natal: dict) -> list[dict[str, str]]:
|
||||
return rows
|
||||
|
||||
|
||||
_AUDIT_AXES = {
|
||||
"computed": ("executed", "primary"),
|
||||
"executed": ("executed", "primary"),
|
||||
"candidate": ("executed", "conditional"),
|
||||
"parameter_sensitive": ("executed", "conditional"),
|
||||
"partial": ("partial", "conditional"),
|
||||
"blocked": ("blocked", "blocked"),
|
||||
"not_applicable": ("not_applicable", "not_used"),
|
||||
"not_built": ("not_built", "not_used"),
|
||||
}
|
||||
|
||||
|
||||
def _audit_axes_row(name, status, note, *, raw_path=None, profile=None, interpretation=None):
|
||||
calculation, usage = _AUDIT_AXES[status]
|
||||
if calculation in ("executed", "partial") and interpretation is not None:
|
||||
usage = interpretation
|
||||
return {
|
||||
"technique": name,
|
||||
"calculation_status": calculation,
|
||||
"interpretation_status": usage,
|
||||
"note": note,
|
||||
"profile": profile,
|
||||
"raw_path": raw_path,
|
||||
}
|
||||
|
||||
|
||||
def _audit_axes_from_block(name, block, note, path, *, profile=None, interpretation=None):
|
||||
status = block.get("status")
|
||||
if status not in _AUDIT_AXES:
|
||||
status = "blocked"
|
||||
reason = block.get("reason")
|
||||
if reason or status in ("blocked", "not_applicable"):
|
||||
note = f"{note};{reason or '本轮缺少对应的计算结果'}"
|
||||
return _audit_axes_row(name, status, note, raw_path=path if block else None,
|
||||
profile=profile or block.get("profile"), interpretation=interpretation)
|
||||
|
||||
|
||||
def _technique_audit_axes(package: dict, natal: dict) -> list[dict[str, Any]]:
|
||||
"""Project existing results into the appendix's two axes without rerunning techniques."""
|
||||
natal_ready = natal.get("status") == "computed"
|
||||
periods = package.get("natal_periods") or {}
|
||||
narayana = periods.get("narayana") or {}
|
||||
transit = package.get("transit_context") or {}
|
||||
annual_views = package.get("independent_annual_views") or {}
|
||||
rows = []
|
||||
|
||||
def add(name, block, path, note, **options):
|
||||
rows.append(_audit_axes_from_block(name, block or {}, note, path, **options))
|
||||
|
||||
functional = natal.get("functional") or {}
|
||||
add("Functional Benefic/Malefic", functional, "natal_base.functional",
|
||||
"本命功能吉凶用于各领域判断" if natal_ready else natal["reason"], profile=functional.get("functional_profile"))
|
||||
add("Vimshottari", periods.get("vimshottari"), "package.natal_periods.vimshottari",
|
||||
"大运作背景,小运与细运用于分领域判断")
|
||||
method = CONFIG["narayana_skeleton_profile"]
|
||||
add("Narayana Dasha", narayana.get(method) or {"reason": narayana.get("reason")},
|
||||
f"package.natal_periods.narayana.{method}" if narayana.get(method) else None,
|
||||
"Sanjay Rath 版用于条件对照,旧算法只作对照",
|
||||
profile=CONFIG["narayana_method"].get("profile"), interpretation="conditional")
|
||||
add("Transits", package.get("slow_transits"), "package.slow_transits", "慢星换宫、停滞与精确合相")
|
||||
for name, key, usage, note in (
|
||||
("Sade Sati", "sade_sati", "observation_only", "土星相对本命月亮的位置"),
|
||||
("Transit Ashtakavarga", "ashtakavarga", "supporting", "慢星所在座的本命 BAV 与 SAV"),
|
||||
("Vedha", "vedha", "conditional", "只核对慢星 Vedha"),
|
||||
):
|
||||
block = transit.get(key)
|
||||
add(name, block or {"reason": transit.get("reason")}, f"package.transit_context.{key}" if block else None,
|
||||
note, interpretation=usage)
|
||||
|
||||
charts = package.get("year_charts") or []
|
||||
annual_blocks = (
|
||||
("Tajika Annual Chart", "chart", "primary", "太阳回归年盘"),
|
||||
("Muntha", "muntha", "supporting", "年盘 Muntha 及其宫主"),
|
||||
("Tajika Year Lord", "year_lord", "supporting", "Panchadhikari 年主"),
|
||||
("Tajika Aspects / Panchavargiya Bala", "annual_strength", "supporting", "五分力用于评估力量,Tajika 相位另列"),
|
||||
("Tajika Aspects", "tajika_yogas", "observation_only", "只记录 Deeptamsa 内的相位联系"),
|
||||
("Tajika Yogas", "tajika_yogas", "not_used", "具名瑜伽只列出,部分规则的必要条件未齐"),
|
||||
("Annual Sahams", "sahams", "conditional", "按回归时刻的昼夜取 PVR 表 74 公式"),
|
||||
("Annual Shadbala", "annual_shadbala", "supporting", "年盘力量,独立于本命 Shadbala"),
|
||||
("Varsha Narayana", "varsha_narayana", "conditional", "年盘太阳角距版"),
|
||||
("Mudda Dasha", "mudda_dasha", "primary", "按年盘解释"),
|
||||
("Patyayini Dasha", "patyayini", "primary", "按年盘解释"),
|
||||
("Tajika Monthly Charts", "monthly_charts", "not_used", "计算月盘,年运写作与候选窗口均未使用"),
|
||||
("Tripataki Chakra", "tripataki", "not_used", "年包计算,年运写作未读取"),
|
||||
("Annual Special Points", "special_points", "not_used", "年盘特殊点已计算,年运写作未读取"),
|
||||
("Mudda Alternative Profiles", "mudda_profiles", "not_used", "年包保留并列口径,年运写作仍使用默认序列"),
|
||||
)
|
||||
for name, key, usage, note in annual_blocks:
|
||||
blocks = [(chart.get("blocks") or {}).get(key) or {"reason": chart.get("reason")} for chart in charts]
|
||||
successful = [block for block in blocks if block.get("status") == "computed"]
|
||||
if successful:
|
||||
representative = successful[-1]
|
||||
data = representative.get("data") or {}
|
||||
partial = len(successful) < len(blocks) or (key == "tajika_yogas" and
|
||||
any((block.get("data") or {}).get("blocked_named_yogas") for block in successful))
|
||||
block = {**representative, "status": "partial" if partial else "computed"}
|
||||
if len(successful) < len(blocks):
|
||||
gaps = dict.fromkeys(b.get("reason") or "缺少计算结果" for b in blocks if b.get("status") != "computed")
|
||||
note += ";部分年盘未完成:" + "、".join(gaps)
|
||||
profile = representative.get("balance_profile") or representative.get("default_profile") or data.get("profile")
|
||||
if key == "year_lord":
|
||||
profile = "Panchadhikari"
|
||||
else:
|
||||
block, profile = (blocks[-1] if blocks else package.get("report_range") or {}), None
|
||||
raw_path = f"package.year_charts[*].blocks.{key}" if any(key in (chart.get("blocks") or {}) for chart in charts) else None
|
||||
add(name, block, raw_path, note, profile=profile, interpretation=usage)
|
||||
|
||||
tithi = annual_views.get("tithi_pravesha") or {}
|
||||
tithi_years = tithi.get("years") or []
|
||||
tithi_ready = sum(year.get("status") == "computed" for year in tithi_years)
|
||||
if tithi_years and tithi_ready < len(tithi_years):
|
||||
tithi = {**tithi, "status": "partial" if tithi_ready else "blocked",
|
||||
"reason": "、".join(dict.fromkeys(year.get("reason") or "缺少候选" for year in tithi_years
|
||||
if year.get("status") != "computed"))}
|
||||
add("Tithi Pravesha", tithi, "package.independent_annual_views.tithi_pravesha",
|
||||
"独立年度视角,未纳入年运写作和候选窗口", interpretation="not_used")
|
||||
|
||||
natal_path = "natal_base" if natal_ready else None
|
||||
rows.append(_audit_axes_row("D1", natal["status"], "本命盘及宫位" if natal_ready else natal.get("reason"),
|
||||
raw_path=natal_path, profile=(package.get("parameters") or {}).get("house_system")))
|
||||
for domain, divisions in DOMAIN_VARGAS.items():
|
||||
for index, division in enumerate(divisions):
|
||||
usage = "supporting" if VARGA_ROLES.get((domain, division)) == "auxiliary" else "primary"
|
||||
rows.append(_audit_axes_row(
|
||||
f"D{division}", natal["status"], f"本命 {domain} 分盘" if natal_ready else natal.get("reason"),
|
||||
raw_path=f"natal_base.domains.{domain}.vargas[{index}]" if natal_ready else None, interpretation=usage,
|
||||
))
|
||||
for name, domain in (("A10", "career"), ("A7", "relationship"), ("UL", "relationship"), ("DK", "relationship")):
|
||||
special = (natal.get("domains") or {}).get(domain, {}).get("special") or {}
|
||||
rows.append(_audit_axes_row(
|
||||
name, "executed" if special.get(name) else "blocked",
|
||||
"分领域辅助核对" if special.get(name) else f"本轮缺少 {name} 计算结果",
|
||||
raw_path=f"natal_base.domains.{domain}.special.{name}" if special.get(name) else None,
|
||||
interpretation="supporting",
|
||||
))
|
||||
add("Natal Yogas", natal.get("yogas"), "natal_base.yogas",
|
||||
"本命候选瑜伽" if natal_ready else natal["reason"], interpretation="supporting")
|
||||
for name, note in (
|
||||
("Chara Dasha", "年包只计算 Vimshottari 和 Narayana 本命运限"),
|
||||
("Yogini Dasha", "年包未运行 Yogini 运限"),
|
||||
("Ashtottari Dasha", "年包未核对适用条件或运行 Ashtottari 运限"),
|
||||
("Kalachakra Dasha", "年包未运行 Kalachakra 运限"),
|
||||
("Bhrigu Pada", "本命年运证据未接入 Bhrigu Pada 计算"),
|
||||
("KP / Sub-lord", "年运未计算 KP 宫头与子主"),
|
||||
("Double Transit PAC", "现有慢星行运与相位未执行 PAC 条件核对"),
|
||||
("Transit LL / 7L", "未单独追踪上升主与七宫主的行运窗口"),
|
||||
("Bhava Chalit", "年运本命宫位采用整宫,未另算 Bhava Chalit"),
|
||||
("Sudarshana", "年运未运行 Sudarshana 多参考点"),
|
||||
("Nakshatra Overlay", "本命月宿字段已保存,未运行叠加核对"),
|
||||
("Natal Shadbala", "已计算年盘 Shadbala,未单独计算本命 Shadbala"),
|
||||
("Argala", "年运未执行 Argala 支持与阻碍核对"),
|
||||
("Western Tropical Natal Chart", "年运链路未接入回归黄道本命盘"),
|
||||
("Western Transit-to-Natal Duration Scan", "未运行西占行运持续区间扫描"),
|
||||
("Western Solar Return", "已计算恒星黄道 Tajika 年盘,未运行西占回归盘"),
|
||||
("Secondary Progressions", "年运未运行次限"),
|
||||
("Solar Arc", "年运未运行太阳弧"),
|
||||
("Converse Progressions / Arcs", "年运未运行逆向推运或逆向太阳弧"),
|
||||
("Midpoints", "年运未运行西占中点"),
|
||||
("Lunar Return", "已计算 Tajika 月盘,未运行西占月亮回归"),
|
||||
("Parans", "年运未运行同升同落核对"),
|
||||
("MEVG / Global Web Evidence", "本轮未运行外部实时联网"),
|
||||
("Real Case Calibration", "本轮未运行独立案例校准"),
|
||||
):
|
||||
rows.append(_audit_axes_row(name, "not_built", note))
|
||||
rows.append(_audit_axes_row("Jaimini / Arudha", "partial" if natal_ready else "blocked",
|
||||
"已单列 A10、A7、UL、DK;未运行完整 Jaimini / Arudha 组" if natal_ready else natal["reason"],
|
||||
raw_path="natal_base.domains" if natal_ready else None, interpretation="supporting"))
|
||||
rows.append(_audit_axes_row("Yoga / Nakshatra Pravesha", "not_applicable", "仅研究使用,现行年运范围排除"))
|
||||
calculated_charts = sum(chart.get("status") == "computed" for chart in charts)
|
||||
local_status = "blocked"
|
||||
core_ready = natal_ready and (package.get("parameters", {}).get("natal_chart") or {}).get("status") == "computed"
|
||||
if core_ready and calculated_charts and calculated_charts == len(charts):
|
||||
local_status = "executed"
|
||||
elif natal_ready or calculated_charts:
|
||||
local_status = "partial"
|
||||
local_note = "本命与年盘的本地计算状态"
|
||||
if local_status != "executed":
|
||||
gaps = [chart.get("reason") for chart in charts if chart.get("status") != "computed"]
|
||||
reasons = dict.fromkeys(reason for reason in [natal.get("reason"), *gaps] if reason)
|
||||
local_note += ";" + ("、".join(reasons) or "缺少可用年盘")
|
||||
external = (
|
||||
("本地计算", local_status, local_note),
|
||||
("预置来源匹配", "not_built", "年运报告未接入预置来源匹配,当前证据不足"),
|
||||
("实时联网", "not_built", "本轮未执行实时联网"),
|
||||
("真实案例校准", "not_built", "本轮未执行独立案例校准"),
|
||||
)
|
||||
for name, status, note in external:
|
||||
row = _audit_axes_row(name, status, note, raw_path="package.block_index" if name == "本地计算" else None,
|
||||
interpretation="not_used")
|
||||
rows.append({**row, "visible_in_appendix": True})
|
||||
return rows
|
||||
|
||||
|
||||
def build_annual_report_facts(request: dict[str, Any], *, birth_window: Any = None) -> dict[str, Any]:
|
||||
package = ap.build_annual_package(request)
|
||||
ctx = ap._Context(request)
|
||||
@@ -1681,6 +1874,7 @@ def build_annual_report_facts(request: dict[str, Any], *, birth_window: Any = No
|
||||
"natal_base": public_natal,
|
||||
"cards": cards,
|
||||
"technique_audit": audit,
|
||||
"technique_audit_axes": _technique_audit_axes(package, natal),
|
||||
# 占星师第三轮审查 2026-11-07:起点含、终点不含;跨年 / 跨表的断口与重叠。
|
||||
"timeline_continuity": _timeline_summary(package.get("timeline_continuity")),
|
||||
})
|
||||
|
||||
@@ -8,6 +8,8 @@ from domain_calculation_service import swiss_ephemeris_lock
|
||||
from jyotish_engine import _birth_hour_decimal, _sidereal_calculation_profile, swe
|
||||
from varga import calc_varga
|
||||
|
||||
from .birth_precision_window import crosses_birth_window
|
||||
|
||||
|
||||
_CAP_SECONDS = 240 * 60
|
||||
_STEP_SECONDS = 30
|
||||
@@ -53,10 +55,7 @@ def _margin_summary_zh(before: float | None, after: float | None, cap: float, wi
|
||||
else f"出生时间提前 {cap:g} 分钟内上升未换座")
|
||||
later = (f"推后 {after:g} 分钟才换座" if after is not None
|
||||
else f"推后 {cap:g} 分钟内未换座")
|
||||
crosses = (
|
||||
before is not None and 0 < window["before_minutes"] and before <= window["before_minutes"]
|
||||
or after is not None and 0 < window["after_minutes"] and after <= window["after_minutes"]
|
||||
)
|
||||
crosses = crosses_birth_window(before, after, window)
|
||||
conclusion = "区间内会换座" if crosses else "区间内不换座"
|
||||
return f"{earlier};{later}。{conclusion}。"
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
|
||||
CONFIRMED_TOLERANCE_MINUTES = 1.0
|
||||
DEFAULT_TOLERANCE_MINUTES = 30.0
|
||||
_WINDOW_EDGE_TOLERANCE_MINUTES = 0.01
|
||||
WINDOW_POLICY = "birth_record_window_v1"
|
||||
_DECLARED_SOURCES = {"user_stated", "shichen", "period", "rectified_range"}
|
||||
_MAX_SIDE_MINUTES = 720
|
||||
@@ -64,6 +65,16 @@ def report_birth_window(value: object, *, minute_confirmed: bool) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def crosses_birth_window(before: float | None, after: float | None, window: dict) -> bool:
|
||||
"""Check for a crossing in either direction, keeping the confirmed-minute rule inclusive."""
|
||||
before_limit, after_limit = window["before_minutes"], window["after_minutes"]
|
||||
if window.get("source") == "confirmed_minute":
|
||||
return (before is not None and before <= before_limit
|
||||
or after is not None and after <= after_limit)
|
||||
return (before is not None and before < before_limit - _WINDOW_EDGE_TOLERANCE_MINUTES
|
||||
or after is not None and after < after_limit - _WINDOW_EDGE_TOLERANCE_MINUTES)
|
||||
|
||||
|
||||
def birth_window_label_zh(window: dict) -> str:
|
||||
source = window["source"]
|
||||
before, after = window["before_minutes"], window["after_minutes"]
|
||||
|
||||
@@ -30,7 +30,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, report_birth_window
|
||||
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, crosses_birth_window, report_birth_window
|
||||
|
||||
try:
|
||||
from scripts.consult_card_domain_facts import PRECISION_VARGA
|
||||
@@ -165,8 +165,7 @@ def _varga_boundary(facts: dict, code: str) -> tuple[str, dict[str, Any]]:
|
||||
before = float(margin["before_minutes"])
|
||||
after = float(margin["after_minutes"])
|
||||
detail.update(before_change_minutes=before, after_change_minutes=after, nearest_change_minutes=min(before, after))
|
||||
crosses = (0 < window["before_minutes"] and before <= window["before_minutes"]
|
||||
or 0 < window["after_minutes"] and after <= window["after_minutes"])
|
||||
crosses = crosses_birth_window(before, after, window)
|
||||
return ("boundary_sensitive" if crosses else "clear_at_tolerance"), detail
|
||||
if not isinstance(margin, dict):
|
||||
return "missing", {"policy": PRECISION_POLICY, "reason": "no ascendant margin for this varga"}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Annual audit axes and compatibility with the existing reader and writer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.annual_service import api
|
||||
from scripts.annual_service.report_facts import _technique_audit_axes
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FRONTEND = ROOT / "frontend"
|
||||
TSX = FRONTEND / "node_modules/tsx/dist/cli.mjs"
|
||||
|
||||
CALCULATION_STATES = {"executed", "partial", "blocked", "not_applicable", "not_built"}
|
||||
INTERPRETATION_STATES = {"primary", "supporting", "conditional", "observation_only", "blocked", "not_used"}
|
||||
EXTERNAL = {"本地计算", "预置来源匹配", "实时联网", "真实案例校准"}
|
||||
# Full-Spectrum Invocation Contract §0.7 and Technique Audit Table §8.
|
||||
ROUTE_TECHNIQUES = {
|
||||
"D1", "D2", "D4", "D9", "D10", "D11", "D24", "Functional Benefic/Malefic",
|
||||
"Vimshottari", "Narayana Dasha", "Chara Dasha", "Yogini Dasha", "Ashtottari Dasha",
|
||||
"Kalachakra Dasha", "Bhrigu Pada", "Jaimini / Arudha", "A10", "A7", "UL", "DK",
|
||||
"KP / Sub-lord", "Double Transit PAC", "Transit LL / 7L", "Bhava Chalit", "Sudarshana",
|
||||
"Tajika Annual Chart", "Muntha", "Tajika Year Lord", "Nakshatra Overlay",
|
||||
"Natal Shadbala", "Annual Shadbala", "Transit Ashtakavarga", "Natal Yogas", "Argala",
|
||||
"Tajika Yogas", "Tajika Aspects", "Annual Sahams", "Transits", "Sade Sati", "Vedha",
|
||||
"Western Tropical Natal Chart", "Western Transit-to-Natal Duration Scan", "Western Solar Return",
|
||||
"Secondary Progressions", "Solar Arc", "Converse Progressions / Arcs", "Midpoints",
|
||||
"Lunar Return", "Parans", "MEVG / Global Web Evidence", "Real Case Calibration",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def public_facts():
|
||||
source = ROOT / "references/real_case_calibration/minute_rectification_holdout_v4.json"
|
||||
cases = json.loads(source.read_text(encoding="utf-8"))["cases"]
|
||||
birth = next(row["birth"] for row in cases if row["case_id"] == "barack_obama_1961_aa_v4_holdout")
|
||||
dt = datetime.fromisoformat(f"{birth['date']}T{birth['time']}")
|
||||
return api.compute_annual_report_facts({
|
||||
"year": dt.year, "month": dt.month, "day": dt.day, "hour": dt.hour, "minute": dt.minute,
|
||||
"lat": birth["latitude"], "lon": birth["longitude"], "tz": birth["timezone_offset"],
|
||||
"timezone_id": "Pacific/Honolulu", "report_range": {"kind": "birthday_year", "year": 2026},
|
||||
"observation_date": "2026-11-01",
|
||||
})["facts"]
|
||||
|
||||
|
||||
def test_annual_audit_lists_route_techniques_and_distinguishes_unused_from_blocked(public_facts):
|
||||
rows = public_facts["technique_audit_axes"]
|
||||
by_name = {row["technique"]: row for row in rows}
|
||||
assert len(by_name) == len(rows)
|
||||
assert ROUTE_TECHNIQUES | EXTERNAL <= by_name.keys()
|
||||
for row in rows:
|
||||
assert row["calculation_status"] in CALCULATION_STATES
|
||||
assert row["interpretation_status"] in INTERPRETATION_STATES
|
||||
assert row["note"]
|
||||
assert row["profile"] is None or isinstance(row["profile"], str)
|
||||
assert row["raw_path"] is None or isinstance(row["raw_path"], str)
|
||||
for name in ("Tajika Monthly Charts", "Tithi Pravesha", "Tripataki Chakra", "Annual Special Points", "Mudda Alternative Profiles"):
|
||||
assert (by_name[name]["calculation_status"], by_name[name]["interpretation_status"]) == ("executed", "not_used")
|
||||
assert by_name["Functional Benefic/Malefic"]["profile"] == "pvr_table30_v1"
|
||||
assert by_name["Tajika Yogas"]["raw_path"] == "package.year_charts[*].blocks.tajika_yogas"
|
||||
assert by_name["Tajika Yogas"]["interpretation_status"] == "not_used"
|
||||
assert by_name["Double Transit PAC"]["calculation_status"] == "not_built"
|
||||
assert by_name["Yoga / Nakshatra Pravesha"]["calculation_status"] == "not_applicable"
|
||||
assert {row["technique"] for row in rows if row.get("visible_in_appendix")} == EXTERNAL
|
||||
assert by_name["实时联网"]["calculation_status"] == "not_built"
|
||||
assert by_name["真实案例校准"]["calculation_status"] == "not_built"
|
||||
|
||||
|
||||
def test_annual_audit_carries_source_status_and_reason_on_partial_or_blocked_blocks():
|
||||
package = {
|
||||
"parameters": {"natal_chart": {"status": "computed"}},
|
||||
"natal_periods": {"narayana": {"rath": {"status": "computed"}}},
|
||||
"year_charts": [
|
||||
{"status": "computed", "blocks": {"tajika_yogas": {"status": "computed"}}},
|
||||
{"status": "blocked", "reason": "solar_return_unavailable"},
|
||||
],
|
||||
"transit_context": {"status": "blocked", "reason": "natal_moon_missing"},
|
||||
}
|
||||
by_name = {row["technique"]: row for row in _technique_audit_axes(package, {"status": "computed"})}
|
||||
assert by_name["Narayana Dasha"]["interpretation_status"] == "conditional"
|
||||
assert by_name["Narayana Dasha"]["profile"]
|
||||
assert by_name["Tajika Yogas"]["calculation_status"] == "partial"
|
||||
assert "solar_return_unavailable" in by_name["Tajika Yogas"]["note"]
|
||||
assert by_name["Sade Sati"]["calculation_status"] == "blocked"
|
||||
assert by_name["Sade Sati"]["raw_path"] is None
|
||||
assert "natal_moon_missing" in by_name["Sade Sati"]["note"]
|
||||
assert by_name["本地计算"]["calculation_status"] == "partial"
|
||||
|
||||
|
||||
def test_annual_legacy_audit_matches_staging_byte_for_byte():
|
||||
# Capture from the unchanged staging implementation with its fictional fixture.
|
||||
from tests.test_annual_report_facts import _facts
|
||||
|
||||
legacy = json.dumps(_facts()["technique_audit"], ensure_ascii=False, separators=(",", ":")).encode()
|
||||
assert hashlib.sha256(legacy).hexdigest() == "8cfbaba4cfb4aef84eb1913b4bc88780c48aefa258bd3fd4bcdaab84fd3a531f"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSX.is_file(), reason="frontend dependencies are required for the writer contract")
|
||||
def test_annual_writer_evidence_does_not_read_the_new_audit(public_facts):
|
||||
without_axes = deepcopy(public_facts)
|
||||
without_axes.pop("technique_audit_axes")
|
||||
program = """
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { EMPTY_ANNUAL_SITUATION } from "./src/lib/annual-report-shared.ts";
|
||||
import { buildAnnualPlan, sectionEvidence } from "./src/lib/annual-report-sections.ts";
|
||||
const [before, after] = JSON.parse(readFileSync(0, "utf8"));
|
||||
const plan = buildAnnualPlan(after, EMPTY_ANNUAL_SITUATION);
|
||||
assert.equal(plan.length, 10);
|
||||
const evidence = (facts) => plan.map((entry) => sectionEvidence(facts, entry, EMPTY_ANNUAL_SITUATION));
|
||||
assert.deepEqual(evidence(before), evidence(after));
|
||||
assert.doesNotMatch(JSON.stringify(evidence(after)), /technique_audit_axes|本地计算|预置来源匹配|实时联网|真实案例校准/);
|
||||
"""
|
||||
subprocess.run(
|
||||
["node", str(TSX), "-e", program], cwd=FRONTEND,
|
||||
input=json.dumps([without_axes, public_facts], ensure_ascii=False).encode("utf-8"),
|
||||
check=True, timeout=45,
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from scripts.professional_report_reference import _reading_request
|
||||
from scripts.report_render.birth_precision_margins import _margin_summary_zh
|
||||
from scripts.report_render.evidence_grade import evidence_grades
|
||||
from scripts.report_render.evidence_grade import _varga_boundary, evidence_grades
|
||||
from scripts.report_render.reading_facts import _birth_precision
|
||||
from tests.test_report_evidence_grade_20261112 import _facts
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_default_window_checks_required_and_optional_vargas():
|
||||
assert _row(claims, "health_pressure", "D30")["state"] == "not_evaluated"
|
||||
|
||||
|
||||
def test_declared_window_uses_each_side_and_includes_the_endpoint():
|
||||
def test_declared_window_uses_each_side_and_excludes_the_endpoint():
|
||||
five = {"before_minutes": 5, "after_minutes": 5, "source": "user_stated"}
|
||||
claims = evidence_grades(_unconfirmed_facts(five))["claims"]
|
||||
assert claims["relationship_structure"]["cap"] == "C"
|
||||
@@ -51,7 +51,7 @@ def test_declared_window_uses_each_side_and_includes_the_endpoint():
|
||||
margin.update(before_minutes=0.3, after_minutes=21)
|
||||
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
|
||||
margin["after_minutes"] = 20
|
||||
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "D"
|
||||
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
|
||||
|
||||
|
||||
def test_shichen_request_reaches_facts_even_with_a_single_calculation_clock():
|
||||
@@ -119,11 +119,51 @@ def test_rejected_rectified_range_stays_marked_for_intake_and_default():
|
||||
|
||||
def test_margin_summary_distinguishes_the_two_directions_and_search_cap():
|
||||
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0.3, "after_minutes": 19}) == (
|
||||
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内会换座。"
|
||||
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
|
||||
)
|
||||
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0, "after_minutes": 19}) == (
|
||||
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
|
||||
)
|
||||
assert _margin_summary_zh(None, 19, 240, {"before_minutes": 5, "after_minutes": 19}) == (
|
||||
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内会换座。"
|
||||
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内不换座。"
|
||||
)
|
||||
|
||||
|
||||
def test_rectified_window_only_caps_claims_for_crossings_inside_the_range():
|
||||
window = {"before_minutes": 0.3, "after_minutes": 15, "source": "rectified_range"}
|
||||
facts = _unconfirmed_facts(window)
|
||||
margins = facts["birth_precision"]["ascendant_margins"]
|
||||
for code, before, after in (
|
||||
("D2", 0.3, 147.167), ("D6", 0.3, 24.367), ("D8", 0.3, 18.2),
|
||||
("D10", 0.3, 14.5), ("D9", 8.517, 7.917),
|
||||
):
|
||||
margins[code] = {"before_minutes": before, "after_minutes": after}
|
||||
|
||||
claims = evidence_grades(facts)["claims"]
|
||||
for code in ("D2", "D6", "D8", "D10", "D9"):
|
||||
crosses = code in ("D10", "D9")
|
||||
assert _varga_boundary(facts, code)[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
|
||||
margin = margins[code]
|
||||
summary = _margin_summary_zh(margin["before_minutes"], margin["after_minutes"], 240, window)
|
||||
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
|
||||
|
||||
assert {name: claims[name]["cap"] for name in (
|
||||
"wealth_earning", "wealth_retention", "health_pressure", "career_promise", "relationship_structure",
|
||||
)} == {"wealth_earning": "C", "wealth_retention": "C", "health_pressure": "C",
|
||||
"career_promise": "D", "relationship_structure": "D"}
|
||||
|
||||
|
||||
def test_window_edge_tolerance_applies_on_each_side_and_preserves_confirmed_minute():
|
||||
for source, width in (("user_stated", 5), ("default_unknown_30", 30), ("rectified_range", 15), ("confirmed_minute", 1)):
|
||||
window = {"before_minutes": width, "after_minutes": width, "source": source}
|
||||
facts = _unconfirmed_facts(window)
|
||||
facts["minute_confirmed"] = source == "confirmed_minute"
|
||||
for offset, unconfirmed_crosses, confirmed_crosses in (
|
||||
(0.02, False, False), (0, False, True), (-0.005, False, True), (-0.01, False, True), (-0.02, True, True),
|
||||
):
|
||||
crosses = confirmed_crosses if facts["minute_confirmed"] else unconfirmed_crosses
|
||||
for before, after in ((width + offset, 73), (73, width + offset)):
|
||||
facts["birth_precision"]["ascendant_margins"]["D2"] = {"before_minutes": before, "after_minutes": after}
|
||||
assert _varga_boundary(facts, "D2")[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
|
||||
summary = _margin_summary_zh(before, after, 240, window)
|
||||
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_unconfirmed_birth_time_at_default_boundary_is_not_assumed_stable():
|
||||
facts = _facts(minute_confirmed=False)
|
||||
claim = claim_grade(facts, "career_promise")
|
||||
assert claim["ledger"]["purpose_stability"] == "not_evaluated"
|
||||
assert claim["cap"] == "D"
|
||||
assert claim["cap"] == "C"
|
||||
|
||||
|
||||
def test_parameter_sensitive_timing_is_flagged_not_hidden():
|
||||
|
||||
Reference in New Issue
Block a user