Compare commits

...
Author SHA1 Message Date
ClaudeandClaude Opus 5.5 efdb67906c Merge #237 window edge is not a crossing (BUG-1871)
Independent Staging Quality Gate / validate-python (push) Waiting to run
Independent Staging Quality Gate / validate-frontend (push) Waiting to run
Independent Staging Quality Gate / publish (push) Blocked by required conditions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXozsiu43ob7B8UZCmDM
2026-11-13 10:39:27 +08:00
jesse-ux 44a93d0539 Record #237 checks against updated staging
Independent Staging Quality Gate / validate-frontend (pull_request) Failing after 14m50s
Independent Staging Quality Gate / validate-python (pull_request) Failing after 16m12s
Independent Staging Quality Gate / publish (pull_request) Skipped
2026-11-13 10:29:02 +08:00
jesse-ux 5041521322 Merge remote-tracking branch 'origin/staging' into codex/reading-window-edge-20261113 2026-11-13 10:27:50 +08:00
jesse-ux cfa8e0ff58 Exclude report birth-window endpoints from varga crossings (#237) 2026-11-13 10:27:50 +08:00
Claude e5d6c99dac Merge remote-tracking branch 'origin/staging' into HEAD
Independent Staging Quality Gate / validate-frontend (push) Successful in 5m40s
Independent Staging Quality Gate / validate-python (push) Successful in 7m46s
Independent Staging Quality Gate / publish (push) Canceled after 6m1s
2026-11-13 10:15:48 +08:00
jesse-ux 1f5367d09b Merge remote-tracking branch 'origin/codex/reading-window-edge-20261113' into codex/reading-window-edge-20261113 2026-11-13 10:04:44 +08:00
ClaudeandClaude Opus 5.5 c2f295fd82 docs(tasks): brief for #237 reading-window-edge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXozsiu43ob7B8UZCmDM
2026-11-13 09:49:44 +08:00
jesse-ux 394c39ce33 Add annual audit calculation and interpretation axes (#210)
Independent Staging Quality Gate / validate-python (pull_request) Successful in 6m39s
Independent Staging Quality Gate / validate-frontend (pull_request) Successful in 7m21s
Independent Staging Quality Gate / publish (pull_request) Skipped
2026-11-13 09:30:04 +08:00
jesse-ux 6bf20b8dbf Sync annual audit worktree with staging for #210 2026-11-13 08:53:20 +08:00
jesse-ux 34f7793e52 docs(annual): record #210 audit appendix compatibility decision 2026-11-12 21:52:19 +08:00
ClaudeandClaude Opus 5.5 b9504d758a docs(task): brief for annual audit two axes and route rows (#210, BUG-1856, 2026-11-12)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-12 21:28:58 +08:00
16 changed files with 641 additions and 13 deletions
+5
View File
@@ -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)
- ~~旧流程区间缺少采纳凭据。~~ 修订任务书已明确优先读取档案的选盘采纳记录,并限定旧流程的凭据。
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-11-13 — 个人报告按区间内部的换座核对证据
- 生时纠正选定的区间、填写范围及默认范围,只在区间内部发生换座时标记分盘敏感。报告摘要和证据等级按同一规则判定。沿用当前 Skill 版本(BUG-1871)。
## 2026-11-13 — 个人报告显示章节拦截原因
- 页面和 Markdown 导出会解释对应章节为何未通过核对。新解读保存生成快照和逐节转换记录,便于复查和回放。
+27
View File
@@ -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`,待发布。
+4
View File
@@ -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 项通过。
+1
View File
@@ -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
```
+194
View File
@@ -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"]
+2 -3
View File
@@ -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"}
+126
View File
@@ -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,
)
+45 -5
View File
@@ -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 "区间内不换座。")
+1 -1
View File
@@ -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():