Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d6c99dac | ||
|
|
394c39ce33 | ||
|
|
6bf20b8dbf | ||
|
|
34f7793e52 | ||
|
|
b9504d758a |
@@ -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(待独立验收)。
|
||||
|
||||
@@ -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,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`,路由和裁定里都没说清。
|
||||
- 要让某一行如实反映状态,必须新增计算。
|
||||
@@ -1,38 +0,0 @@
|
||||
# TASK:最后仍不通过的节,改用计算直接生成的内容;页面不再出现「没有通过核对」(2026-11-13)
|
||||
|
||||
- Gitea:#243
|
||||
- 分支:`codex/reading-computed-fallback-20261113`。**开工条件:#242(重绑与重写)合入 staging**,开工前先 rebase。
|
||||
- Bug 编号:BUG-1877
|
||||
- 验收:Claude(jyotisha-e6)
|
||||
|
||||
不得自行写「产品决定/裁定」,任务书没写到的取舍一律选需拍板。不得修改或删除既有断言来让测试通过;确需修改的,写明原值/新值/原因。
|
||||
|
||||
## 产品决定(产品负责人 2026-11-13)
|
||||
|
||||
一节经过系统修正标注、模型重写一次之后仍不通过时,页面上这一节显示**由计算直接生成的内容**,不再写「这一节没有通过数据核对」,也不写原因。原因只留在后台的 `transform_log` 里。「依据与边界」被拦时生成的依据清单(#218),也并入这套做法。
|
||||
|
||||
## 要做的
|
||||
|
||||
1. **每节的计算兜底**:放在新文件 `frontend/src/lib/personal-report-reading-fallback.ts`,只读事实包,不调用模型。每节 2–4 段,句子固定,只陈述盘面事实;每段按所属结论类型的上限标等级(C 或 D)。
|
||||
- 性格:上升星座与命宫星宿;命主星落宫与尊贵;月亮落宫与尊贵;功能吉星、凶星(按 `functional.functional_profile`)。
|
||||
- 事业:第十宫星座、宫主落宫与尊贵;D10 上升(D10 在边界上时只写「出生时间误差内会换座,暂不判断」);A10、AmK。
|
||||
- 财富:第二宫、第十一宫的星座、宫主落宫;D2 上升(处理同上);第二宫、第十一宫的 SAV 分数(只报分数,不写强弱)。
|
||||
- 感情:第七宫星座、宫主落宫与尊贵;D9 上升(处理同上);UL、DK。
|
||||
- 健康:第六、第八、第十二宫的宫主落宫;Badhaka 宫和 Badhaka 宫主(标注「产品计算,待占星师复核」);不写病症。
|
||||
- 现在这几年:Vimshottari 当前的大运、小运、细运起止;Narayana 当前段(标注「参数敏感,只作条件窗口」)。
|
||||
- 总评:取性格节第一段,再加各领域第一句。
|
||||
- 几条建议:**整节不显示**。建议没法由计算生成;这节失败时,目录里也去掉它。
|
||||
- 依据与边界:沿用 #218 的依据清单。
|
||||
|
||||
文案风格:短句,中文,术语第一次出现时用半句话说明它看什么,与模型稿保持一致。
|
||||
2. **存储**:兜底的节记为 `status: "computed"`,段落带 `source: "computed"` 和 `grade`。存储版本加一,旧版本照常读取。
|
||||
3. **页面与导出**:`computed` 的节和普通的节显示方式一致,段落下面照样有等级行。删去对用户显示的 `READING_SECTION_WITHHELD_TEXT` 和 `WITHHELD_REASONS`;旧存储里已有的 withheld 节,读取时也改用兜底生成(读取时手里有事实包就生成,没有就整节不显示)。
|
||||
4. **验收工具**:`postcheck.json` 统计兜底节数。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 合成事实包:每节都能生成兜底;D10、D2、D9 在边界上时,相应句子写成「暂不判断」;健康节没有病症词(跑一遍 #218 的 health_outcome 检查,必须通过)。
|
||||
2. 页面和 MD 导出里,不再出现「没有通过数据核对」,也不出现任何退回原因。
|
||||
3. 旧 v3/v4 存储里的 withheld 节能正常显示(兜底或不显示)。
|
||||
4. 原有测试全部通过;tsc 无错误、eslint 0 个错误,Python 快速门通过。
|
||||
5. 截图(手机宽度):兜底的事业节和普通节放在一起的效果,附在 PR 里。
|
||||
@@ -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")),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user