Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e895b75ad | ||
|
|
d136b76a46 | ||
|
|
831e6708a8 | ||
|
|
079f3d06e3 | ||
|
|
b9343d7cc5 | ||
|
|
67045a12a9 |
@@ -10,6 +10,10 @@
|
||||
|
||||
- Docker Desktop 的 Linux 引擎返回 HTTP 500,容器用例无法在本机验证。目录预检扫描超时。生产模型对两张公开盘的实跑留给独立验收。
|
||||
- 替代证据:假时钟、报告快照和离线回放通过,前端用同配置与 staging 对比。详见 [#240 进度](docs/tasks/PROGRESS-reading-timeout-20261113.md)。
|
||||
## #211 · 年运验证及附录地点文案
|
||||
|
||||
- Windows 全量的年运 5 项和快速门 3 项失败在同机 staging 复现。Docker Desktop 报 500,前端改用无 Docker 的同机基线对照。所有名称和失败项一致,详情见 [#211 进度](docs/tasks/PROGRESS-annual-tithi-yogas-20261112.md)。
|
||||
- 年运附录的固定文字仍将 Tithi Pravesha 描述为仅按出生地。任务书把前端例外限定在本命瑜伽材料组装处,地点文案修订待授权。
|
||||
|
||||
## #237 · Windows 全量门禁与预检
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
## 2026-11-13 — 选盘可分轮核对并保存到秒
|
||||
|
||||
- 新版选盘接口按关键换盘线逐处确认,最多三轮。仍会换座的分盘列为未定。短区间可保存段内秒级时刻,历史记录可读回。沿用当前 Skill 版本(BUG-1872)。
|
||||
## 2026-11-13 — 年运独立计算加入现居地版本
|
||||
|
||||
- Tithi Pravesha 有现居地时同时给出出生地和现居地版,缺少现居地时显示说明。年运写作材料中的本命瑜伽携带各自的核对状态。沿用当前 Skill 版本(BUG-1857)。
|
||||
|
||||
## 2026-11-13 — 个人报告按区间内部的换座核对证据
|
||||
|
||||
|
||||
@@ -25282,3 +25282,15 @@
|
||||
- 验证及防复发:覆盖有与无可区分时段、D1 问题范围、日期跨轮、平分兜底、候选段内评分和三件事件门槛。见 [#241 进度](tasks/PROGRESS-chart-choice-questions-20261113.md)。
|
||||
- 关联记录:#229、#238、#241、BUG-405、BUG-1703、BUG-1759、BUG-1872。
|
||||
- 修复版本:`codex/chart-choice-questions-20261113`,待发布。
|
||||
## BUG-1857 | 年运 Tithi Pravesha 缺现居地,本命瑜伽证据截断
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
- 首次发现 / 最近更新:2026-11-12 / 2026-11-13。
|
||||
- 影响面:年运计算包的 Tithi Pravesha 及本命段写作材料。
|
||||
- 现象:填写现居地后,年运仍只有出生地版的 Tithi Pravesha。本命瑜伽超过八条时,后续瑜伽没有进入写作材料,已列行也缺少状态。
|
||||
- 触发条件:年运选用现居城市,或原生瑜伽检测返回八条以上。
|
||||
- 根因:Tithi Pravesha 原生模块只用出生地起盘。年运瑜伽投影在排序后截取八条,丢掉状态字段。
|
||||
- 修复:沿用年盘的现居地输入,用同一个回归时刻起两张盘,缺少现居地时只保留出生地版。瑜伽保留原排序,逐行调用共享状态词映射并传到写作端。
|
||||
- 验证及防复发:合成输入核对两地、出生地回退和上游数值。公开名人盘从引擎返回到写作入口逐条核对名称、数量和状态词。
|
||||
- 关联记录:#211、BUG-1856、BUG-1839。详见 [执行进度](tasks/PROGRESS-annual-tithi-yogas-20261112.md)。
|
||||
- 修复版本:`codex/annual-tithi-yogas-20261112`,待发布。
|
||||
|
||||
@@ -5,6 +5,10 @@ Purpose: read this file before substantial project work. It exists to stop repea
|
||||
## 2026-11-13 · #240 Windows 报告预检
|
||||
|
||||
- 本轮复现目录扫描 90 秒超时。延长预检外层时限后,预检测试中的独立目录扫描仍在 120 秒超时。Python 和适配器诊断通过,Gitea 分支用官方 API 与 Git 核对。构建遇到指向工作树外的依赖联接时,改用从同锁文件安装结果复制到本工作树的依赖。见 [#240 进度](../tasks/PROGRESS-reading-timeout-20261113.md)。
|
||||
## 2026-11-13 · #211 Windows 年运验证
|
||||
|
||||
- 并发运行多个前端构建与完整测试时,Windows 资源不足,Docker Desktop 返回 500,基线 Turbopack 报 OS 1450。停止重叠的默认并行测试后,基线构建复跑通过。随后用相同的并行度和 Docker 可用性重做前端对照。
|
||||
- 确认上游 `8aaf6099` 在本机独立 Git 检出中存在。使用随仓星历和等价的位置口径重跑上游函数,避免将星历回退和旧岁差别名造成的差异归为现居地算法差异。
|
||||
|
||||
## 2026-11-13 · #237 Windows 报告预检
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# #211 执行进度 · 年运现居地与本命瑜伽
|
||||
|
||||
Codex 在 `origin/staging` 的 `e5d6c99da` 上快进到任务书提交 `67045a12a` 后实施。BUG-1857 尚未使用,按任务书编号登记。
|
||||
|
||||
## 计算与写作材料
|
||||
|
||||
- 从本地上游检出验证 `8aaf60991c32a84ac30d6d7b6a62c4f166507bce` 的原始 `scripts/tithi_pravesh.py`。用出生地和出生时刻查找回归点。现居地沿用年盘的经纬度和偏移,在同一回归点另起一盘。两地各自标记来源,缺现居地时只给出生地版,并标明「未提供现居地」。保持独立年运视角和窗口打分原有边界。
|
||||
- 生产仍使用本仓的求根和星历封装。对拍用本仓随附 Swiss Ephemeris 星历,在上游选 `apparent`,与本仓 `legacy` 别名一致。上游旧 `legacy` 的岁差换算会让上升相差约 0.00231°。既有求根仍用相位跨周期排除条件,现居地的相位按上游在精确回归时刻计算。出生地候选延用原有字段和计算,增加来源与上升。上游缺现居地时可复制出生地行,产品版按任务书只返回一份。
|
||||
- 固定合成输入的现居地结果与指定上游对拍一致。JD UT `2461228.2685106047`,相位 `257.1125057401731°`,现居地上升 `235.60228037337504°`,Tithi `22`。详细输入在回归测试中。
|
||||
- 保留 `_yogas` 原排序。结构观察排在命名瑜伽之后,同组按既有强度及类别排列。公开盘检测返回 41 条,写作材料从 8 条增至 41 条,其中 3 条为结构观察。状态词由 `yoga_status` 提供。前端改动在年运本命节的写作材料组装处,旧事实包缺状态词时沿用原文本。没有修改已有断言。
|
||||
- 年运页附录有一条既有固定地点文案。任务书允许的前端改动限本命瑜伽写作材料,本轮留待另行确认修订该文案。
|
||||
|
||||
## 验证
|
||||
|
||||
- 新增 4 项通过,覆盖两地候选、缺现居地、公开盘瑜伽行和写作入口的数量与状态词。
|
||||
- 年运前端定向 36 项通过,失败和取消均为 0。TypeScript 检查与 Next 构建通过,ESLint 为 0 错误、62 条既有警告。
|
||||
- 首页保持静态。对照同机 staging 构建,首页 39 个资源的 gzip-9 大小一致,JS 515,860 字节、CSS 90,906 字节。
|
||||
- 年运全量 217 项通过、5 项失败。同机 staging 定向复现全部 5 项,包括两项 PyJHora 年运进程超时、两项专业参考版年运表检查及一项年盘期间连续性检查。
|
||||
- Python 快速门 1,463 项通过、2 项跳过、29 项子测试通过、3 项失败。失败均为 `test_upstream_technique_layers.py`,同机 staging 逐条复现。
|
||||
- 合入 #237 后,新增回归、年运审计及隐私守卫合跑 70 项通过。
|
||||
- Windows 前端全量首次运行遇到 Docker Desktop 500 和系统资源不足。改用相同的并行度并在测试进程中停用 Docker 后,分支和基线各收集 3,692 项,均为 3,611 通过、45 失败、36 跳过、0 取消。TAP 的全部名称及失败名称逐项相同。
|
||||
- Windows 预检的 Python、远端可见性和适配器步骤通过。目录扫描与定向测试触及命令时限,见既有 [预检台账](../research/pre_work_error_ledger.md)。
|
||||
@@ -36,6 +36,7 @@
|
||||
| [#241 修订任务书](FIX-chart-choice-questions-1.md) | [执行进度](PROGRESS-chart-choice-questions-20261113.md) | 多轮选盘事件题与判边(BUG-1875) | 待验收 | `codex/chart-choice-questions-20261113` |
|
||||
| [#240 任务书](TASK-reading-timeout-20261113.md) | [执行进度](PROGRESS-reading-timeout-20261113.md) | 报告解读超时与租期内重试(BUG-1874) | 待验收 | `codex/reading-timeout-20261113` |
|
||||
| [#238 任务书](TASK-chart-choice-rounds-engine-20261113.md) | [执行进度](PROGRESS-chart-choice-rounds-engine-20261113.md) | 多轮选盘与秒级采纳(BUG-1872) | 待验收 | `codex/chart-choice-rounds-engine-20261113` |
|
||||
| [#211 任务书](TASK-annual-tithi-yogas-20261112.md) | [执行进度](PROGRESS-annual-tithi-yogas-20261112.md) | 年运 Tithi Pravesha 现居地与本命瑜伽状态(BUG-1857) | 待验收 | `codex/annual-tithi-yogas-20261112` |
|
||||
| [#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` |
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# TASK:年运报告的 Tithi Pravesh 按现居地计算,本命瑜伽加状态词并取消 8 条上限(2026-11-12)
|
||||
|
||||
- Gitea:#211
|
||||
- 分支:`codex/annual-tithi-yogas-20261112`,从 staging `e5d6c99d`(#210 已合入)切出。
|
||||
- Bug 编号:BUG-1857。开工时核对 `docs/BUG_HISTORY.md` 末尾;若已被占用,顺延,并在 PROGRESS 写明。
|
||||
- 负责验收:Claude(jyotisha-de)
|
||||
|
||||
不得自行写“产品决定/裁定”;任务书没写的取舍一律选需拍板。
|
||||
|
||||
不得修改或删除既有断言来让测试通过;确需修改必须写原值/新值/原因,并在 PROGRESS 里逐条列出。
|
||||
|
||||
## 背景
|
||||
|
||||
### 1. Tithi Pravesh
|
||||
|
||||
- 占星师的 `scripts/tithi_pravesh.py` 第 46–60 行,在她仓库的提交 `8aaf60991c32a84ac30d6d7b6a62c4f166507bce`,仓库地址 `https://github.com/732642856/yinduzhanxing`:同时用出生地和现居地计算 Tithi Pravesh。
|
||||
- 我们的 `scripts/annual_native/tithi_pravesh.py` 约第 42–44 行:只用出生地。
|
||||
- 年运报告已经有现居地字段,即年盘返照地点的那一个。用户没填现居地时,退回出生地,并标明是退回的。
|
||||
- 退回的逻辑见 `scripts/annual_service/package.py` 约 :58–60、:760–827,以及 `frontend/src/lib/annual-report-generate.ts` 约 :164–167。
|
||||
|
||||
### 2. 本命瑜伽
|
||||
|
||||
`scripts/annual_service/report_facts.py` 约 :152、:469–493 有两个问题:
|
||||
|
||||
- 只取前 8 条瑜伽;
|
||||
- 不带状态词。
|
||||
|
||||
这违反两条裁定:
|
||||
|
||||
- 她第七轮的瑜伽状态映射:检测命中不等于成立,状态词用产品里已有的映射,见 `scripts/report_render/yoga_status_words.py`(#189)。
|
||||
- 第五轮裁定 6:白话可以减少术语,不能减少信息。
|
||||
|
||||
## 要做的
|
||||
|
||||
1. **Tithi Pravesh:照搬她的计算。**
|
||||
- 把她 `tithi_pravesh.py` 46–60 行的现居地计算方法,搬进我们的 `scripts/annual_native/tithi_pravesh.py`。
|
||||
- 同时输出出生地版和现居地版两份结果,各自注明地点来源。
|
||||
- 用户没填现居地时,只输出出生地版,并写明“未提供现居地”。
|
||||
- 不改她的公式。算法和她的有任何不同,都列进 PROGRESS。
|
||||
- Tithi Pravesh 仍不参与窗口打分,这是产品现有规则,保持不变。
|
||||
2. **本命瑜伽。**
|
||||
- 去掉 8 条上限,送给模型的证据里列出全部瑜伽。
|
||||
- 每条带上状态词,用 `scripts/report_render/yoga_status_words.py` 的映射,不另造词。
|
||||
- 排序沿用第五轮裁定里的瑜伽优先级。代码里如果已有排序函数,就复用;没有就选需拍板。
|
||||
3. **页面。** 年运报告页面上瑜伽的显示,本单只做到“数据完整”:
|
||||
- 页面原来显示几条,现在照样显示几条;
|
||||
- 显示的那几条换成带状态词的版本;
|
||||
- 显示条数的改动另开单,等原型图。
|
||||
|
||||
## 要改的文件
|
||||
|
||||
- `scripts/annual_native/tithi_pravesh.py`
|
||||
- `scripts/annual_service/report_facts.py`:只改瑜伽那一段。
|
||||
- `scripts/annual_service/package.py`:只在需要把现居地传给 Tithi Pravesh 时改。
|
||||
- 新建 `tests/test_annual_tithi_yogas_20261112.py`
|
||||
- `docs/BUG_HISTORY.md`:只在末尾追加一条。
|
||||
- `docs/tasks/PROGRESS-annual-tithi-yogas-20261112.md`
|
||||
|
||||
## 不要碰
|
||||
|
||||
- 冻结文件(`scripts/yoga_engine.py` 只许 import),以及 `references/**`。
|
||||
- `report_facts.py` 的审计表和 Narayana 部分。
|
||||
- `frontend/**`。唯一例外:如果给模型的证据组装在前端,只改瑜伽那一段,并在 PROGRESS 写明。
|
||||
|
||||
## 验收标准(写进新测试)
|
||||
|
||||
1. 给出现居地时,Tithi Pravesh 输出两份结果,地点来源各自正确;不给时,只有出生地版,并带“未提供现居地”标记。
|
||||
2. 用一个固定的合成输入,现居地版的结果与她 `tithi_pravesh.py` 同一输入的结果一致。把她的函数作为参照,算一次并把数值写进测试,不 import 她的仓库。
|
||||
3. 一张瑜伽多于 8 条的公开名人盘,给模型的瑜伽条数等于引擎返回的条数,而且每条都有状态词。
|
||||
4. 现有年运测试全部通过。
|
||||
|
||||
## 自测命令
|
||||
|
||||
```
|
||||
python -m pytest -q tests/test_annual_tithi_yogas_20261112.py tests/ -k "annual or tithi"
|
||||
python scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
|
||||
```
|
||||
|
||||
## 进度记录
|
||||
|
||||
写在 `docs/tasks/PROGRESS-annual-tithi-yogas-20261112.md`,内容包括:
|
||||
|
||||
- 和她算法的差异;
|
||||
- 参照数值;
|
||||
- 瑜伽条数改动前后的对比;
|
||||
- 测试结果,以及在 staging 上也复现的基线失败。
|
||||
|
||||
## 何时停下来选“需拍板”
|
||||
|
||||
- 她的现居地算法依赖我们没有的输入。
|
||||
- 瑜伽优先级排序在代码里找不到现成实现。
|
||||
@@ -1394,6 +1394,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
|
||||
|
||||
## 年运报告与开始遮罩(2026-11-01,issue #153 / #154)
|
||||
|
||||
- **本命瑜伽(BUG-1857):** 年运写作材料中的瑜伽按原有顺序提供完整清单及状态词,沿用本命段落的版式和篇幅。
|
||||
- **年运附录(BUG-1855):** Narayana 标为条件参考。种子未决时在口径说明中列出候选数,每支单独一行。
|
||||
- **入口**:「我的报告」最上面两个横条并排(手机上下排,2026-11-03 方案 A,见 Personal report centre):「年运」横条是 `Sun` 图标方块、标题、一行「这一年每段怎么安排,几个要留意的时间点」,右边描边按钮「打开年运」(手机「打开」),直接打开 `/reports/annual`。没有单独的入口页。个人报告横条上不写份数。
|
||||
- **地址**:`/reports/annual`(`?report=<id>` 打开某一份),放在 `/reports` 下,侧栏仍亮「我的报告」。沿用二级页外壳,标题「年运」,标题旁是人物切换。正文一栏 `min(720px, 100%)` 居中。
|
||||
|
||||
@@ -419,9 +419,15 @@ function natalEvidence(facts: unknown): Row {
|
||||
// 新值:structural_observation 行单列在 结构观察,不进 格局
|
||||
// 原因:产品 2026-11-07 同步占星师 Yoga 规则批次;结构观察只记录宫主关联、位置或尊贵,不是已成立的格局。
|
||||
格局: yogas.filter((yoga) => yoga.claim_kind !== "structural_observation")
|
||||
.map((yoga) => `${str(yoga.name_cn) || str(yoga.name)}:${str(yoga.combination)}`),
|
||||
.map((yoga) => {
|
||||
const state = str(yoga.state_text);
|
||||
return `${str(yoga.name_cn) || str(yoga.name)}:${str(yoga.combination)}${state ? `(${state})` : ""}`;
|
||||
}),
|
||||
结构观察: yogas.filter((yoga) => yoga.claim_kind === "structural_observation")
|
||||
.map((yoga) => `${str(yoga.name_cn) || str(yoga.name)}:${str(yoga.combination)}(不单独构成 Yoga,不据此推断结果)`),
|
||||
.map((yoga) => {
|
||||
const state = str(yoga.state_text);
|
||||
return `${str(yoga.name_cn) || str(yoga.name)}:${str(yoga.combination)}(${state ? `${state},` : ""}不单独构成 Yoga,不据此推断结果)`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ from typing import Any
|
||||
|
||||
try:
|
||||
from annual_native.sidereal_bodies import sun_moon_longitudes
|
||||
from ayanamsa_utils import configure_ephemeris_path
|
||||
except ImportError: # pragma: no cover
|
||||
from scripts.annual_native.sidereal_bodies import sun_moon_longitudes
|
||||
from scripts.ayanamsa_utils import configure_ephemeris_path
|
||||
|
||||
SCHEMA = "jyotish.tithi_pravesh.v1"
|
||||
_NAMES = (
|
||||
@@ -22,6 +24,9 @@ _NAMES = (
|
||||
|
||||
|
||||
def build_tithi_pravesh(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
import swisseph as swe
|
||||
|
||||
configure_ephemeris_path(swe)
|
||||
birth = dict(payload["birth"])
|
||||
settings = dict(payload.get("settings") or {})
|
||||
target_year = int(payload["target_year"])
|
||||
@@ -41,8 +46,22 @@ def build_tithi_pravesh(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
)
|
||||
birth_place = _place(birth, tz)
|
||||
birth_rows = [
|
||||
_chart_row(jd, birth_place, node_mode, target_phase, ayanamsa) for jd in candidate_jds
|
||||
_chart_row(jd, birth_place, node_mode, target_phase, ayanamsa, location_source="birth_place")
|
||||
for jd in candidate_jds
|
||||
]
|
||||
# The upstream location calculation at 8aaf6099 casts the same roots at
|
||||
# the residence's coordinates and offset. The report retains its natal-only
|
||||
# fallback when the residence is absent.
|
||||
current_location = settings.get("current_location")
|
||||
current_rows = []
|
||||
current_location_reason = "current_location_missing"
|
||||
if isinstance(current_location, dict) and current_location:
|
||||
residence = _place(current_location, 0.0)
|
||||
current_rows = [
|
||||
_chart_row(jd, residence, node_mode, target_phase, ayanamsa, location_source="current_location")
|
||||
for jd in candidate_jds
|
||||
]
|
||||
current_location_reason = None if current_rows else "no_tithi_pravesh_candidate"
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": "partial_verified" if birth_rows else "blocked",
|
||||
@@ -52,6 +71,8 @@ def build_tithi_pravesh(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"natal_sun_longitude": birth_sun,
|
||||
"natal_moon_longitude": birth_moon,
|
||||
"rows": birth_rows,
|
||||
"current_location_rows": current_rows,
|
||||
"current_location_reason": current_location_reason,
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +122,7 @@ def _phase_delta(jd, target_phase, ayanamsa, node_mode) -> float:
|
||||
return ((moon - sun - target_phase + 180.0) % 360.0) - 180.0
|
||||
|
||||
|
||||
def _chart_row(jd, place, node_mode, target_phase, ayanamsa) -> dict[str, Any]:
|
||||
def _chart_row(jd, place, node_mode, target_phase, ayanamsa, *, location_source: str) -> dict[str, Any]:
|
||||
try:
|
||||
from jyotish_engine import compute_chart_data
|
||||
except ImportError:
|
||||
@@ -117,17 +138,22 @@ def _chart_row(jd, place, node_mode, target_phase, ayanamsa) -> dict[str, Any]:
|
||||
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
||||
sun = float(planets["Sun"]["degree_raw"]) % 360.0
|
||||
moon = float(planets["Moon"]["degree_raw"]) % 360.0
|
||||
if location_source == "current_location":
|
||||
sun, moon = sun_moon_longitudes(jd, node_mode=node_mode, ayanamsa_name=ayanamsa)
|
||||
reported_planets = planets if location_source == "current_location" else ("Sun", "Moon")
|
||||
phase = (moon - sun) % 360.0
|
||||
return {
|
||||
"jd_ut": jd,
|
||||
"local_datetime": local.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"location_source": location_source,
|
||||
"ascendant": chart.get("ascendant"),
|
||||
"tithi": _tithi_payload(sun, moon),
|
||||
"phase_degrees": phase,
|
||||
"phase_delta_degrees": ((phase - target_phase + 180.0) % 360.0) - 180.0,
|
||||
"planet_longitudes": {
|
||||
name: planets[name].get("degree_raw")
|
||||
for name in ("Sun", "Moon")
|
||||
if isinstance(planets.get(name), dict)
|
||||
for name in reported_planets
|
||||
if isinstance(planets.get(name), dict) and planets[name].get("degree_raw") is not None
|
||||
},
|
||||
"status": "partial_verified",
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@ def build_annual_package(request: dict[str, Any], *, detail: str = "full") -> di
|
||||
"annual_native.tithi_pravesh.build_tithi_pravesh",
|
||||
reason=None if tithi else "report_range_blocked",
|
||||
start_rule="Sun and Moon return to the natal Sun-Moon phase near the birthday (own start rule, not the solar return)",
|
||||
location_rule="birth place, birth UTC offset (the producer's own rule; not the Tajika return-location option)",
|
||||
location_rule="birth place and, if supplied, the current city used for the solar return",
|
||||
years=tithi,
|
||||
),
|
||||
},
|
||||
@@ -630,7 +630,7 @@ def _return_location_parameters(ctx: _Context) -> dict[str, Any]:
|
||||
"timezone_id": location.get("timezone_id"),
|
||||
"utc_offset_hours": location.get("utc_offset_hours"),
|
||||
"note": "solar return, year chart, monthly charts and annual Shadbala cast for the current city; "
|
||||
"natal chart, dashas and Tithi Pravesha stay at the birth place",
|
||||
"natal chart and dashas stay at the birth place; Tithi Pravesha uses both locations",
|
||||
}
|
||||
return {
|
||||
"mode": "birth_place",
|
||||
@@ -1439,12 +1439,9 @@ def _av_block(natal: dict, lagna_idx: int, ingresses: list[dict], start_position
|
||||
|
||||
# ── independent annual view ─────────────────────────────────────────────────
|
||||
|
||||
def _tithi_pravesha(ctx: _Context, year: int, range_start, range_end) -> dict[str, Any]:
|
||||
producer = "annual_native.tithi_pravesh.build_tithi_pravesh"
|
||||
raw = _module("annual_native.tithi_pravesh").build_tithi_pravesh(ctx.pack_payload(year))
|
||||
zone = timezone(timedelta(hours=ctx.tz_hours))
|
||||
def _tithi_candidates(source_rows, zone, range_start, range_end) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for row in raw.get("rows") or []:
|
||||
for row in source_rows or []:
|
||||
if not isinstance(row, dict) or not isinstance(row.get("jd_ut"), (int, float)):
|
||||
continue
|
||||
moment = _utc_from_jd(row["jd_ut"])
|
||||
@@ -1455,15 +1452,33 @@ def _tithi_pravesha(ctx: _Context, year: int, range_start, range_end) -> dict[st
|
||||
"phase_degrees": row.get("phase_degrees"),
|
||||
"phase_delta_degrees": row.get("phase_delta_degrees"),
|
||||
"planet_longitudes": row.get("planet_longitudes"),
|
||||
"location_source": row.get("location_source"),
|
||||
"ascendant": row.get("ascendant"),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def _tithi_pravesha(ctx: _Context, year: int, range_start, range_end) -> dict[str, Any]:
|
||||
producer = "annual_native.tithi_pravesh.build_tithi_pravesh"
|
||||
raw = _module("annual_native.tithi_pravesh").build_tithi_pravesh(ctx.pack_payload(year))
|
||||
zone = timezone(timedelta(hours=ctx.tz_hours))
|
||||
rows = _tithi_candidates(raw.get("rows"), zone, range_start, range_end)
|
||||
if raw.get("status") == "blocked" or not rows:
|
||||
return {"target_year": year, **block("blocked", producer, reason=str(raw.get("reason") or "no_candidate"))}
|
||||
if raw.get("current_location_rows"):
|
||||
current_zone = timezone(timedelta(hours=ctx.current_city_offset(year)))
|
||||
residence = {"current_location_candidates": _tithi_candidates(
|
||||
raw["current_location_rows"], current_zone, range_start, range_end,
|
||||
)}
|
||||
else:
|
||||
residence = {"current_location_note": "未提供现居地"}
|
||||
return {
|
||||
"target_year": year,
|
||||
**block("computed", producer, producer_status=raw.get("status")),
|
||||
"natal_phase_degrees": raw.get("natal_phase_degrees"),
|
||||
"candidate_count": len(rows),
|
||||
"candidates": rows,
|
||||
**residence,
|
||||
"candidate_note": "every candidate the producer found is listed; none is chosen here",
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,6 @@ DOMAIN_SAHAMS = {
|
||||
"learning": ("vidya_saham", "jnana_saham"),
|
||||
}
|
||||
YEAR_SAHAMS = ("punya_saham",)
|
||||
MAX_YOGAS = 8
|
||||
|
||||
|
||||
def _jd(boundary: Any) -> float | None:
|
||||
@@ -472,6 +471,11 @@ def _varga_cue(domain: str, division: int, asc_lon: float, lons: dict[str, float
|
||||
|
||||
|
||||
def _yogas(planets: dict, lagna_sign: str) -> dict[str, Any]:
|
||||
try:
|
||||
from report_render.yoga_status_words import yoga_status
|
||||
except ModuleNotFoundError: # pragma: no cover - package import
|
||||
from scripts.report_render.yoga_status_words import yoga_status
|
||||
|
||||
try:
|
||||
rows = ap._module("yoga_engine").detect_yogas(planets, lagna_sign) or []
|
||||
except Exception as exc: # noqa: BLE001 - a yoga failure is a blocked block
|
||||
@@ -484,16 +488,23 @@ def _yogas(planets: dict, lagna_sign: str) -> dict[str, Any]:
|
||||
# 新值:structural_observation 排在所有命名 Yoga 之后
|
||||
# 原因:产品 2026-11-07 同步占星师 Yoga 规则批次;结构观察不是命名 Yoga,不占格局名额。
|
||||
return (int(row.get("claim_kind") == "structural_observation"), strength, first)
|
||||
picked = sorted((row for row in rows if isinstance(row, dict) and row.get("name")), key=rank)[:MAX_YOGAS]
|
||||
ordered = sorted((row for row in rows if isinstance(row, dict) and row.get("name")), key=rank)
|
||||
# Structure only: name, category and the combination. The producer's
|
||||
# effect phrases are not facts the report may repeat. A structural
|
||||
# observation carries its kind verbatim so the report lists it apart.
|
||||
return ap.block("computed", "yoga_engine.detect_yogas", total=len(rows), rows=[
|
||||
{"name": row.get("name"), "name_cn": row.get("name_cn"), "category": row.get("category"),
|
||||
"combination": row.get("combination"), "strength": row.get("strength"),
|
||||
**({"claim_kind": row["claim_kind"]} if row.get("claim_kind") == "structural_observation" else {})}
|
||||
for row in picked
|
||||
])
|
||||
evidence = []
|
||||
for row in ordered:
|
||||
formation = row.get("formation") or row.get("formation_assessment")
|
||||
state, state_text = yoga_status({**row, "formation": formation})
|
||||
if not (row.get("status") or row.get("claim_status") or formation):
|
||||
state, state_text = yoga_status({"status": "computed"})
|
||||
evidence.append({
|
||||
"name": row.get("name"), "name_cn": row.get("name_cn"), "category": row.get("category"),
|
||||
"combination": row.get("combination"), "strength": row.get("strength"),
|
||||
"state": state, "state_text": state_text,
|
||||
**({"claim_kind": row["claim_kind"]} if row.get("claim_kind") == "structural_observation" else {}),
|
||||
})
|
||||
return ap.block("computed", "yoga_engine.detect_yogas", total=len(rows), rows=evidence)
|
||||
|
||||
|
||||
def _role(functional: dict, planet: str | None) -> str:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Annual Tithi Pravesha location and natal yoga evidence (#211)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.annual_native.tithi_pravesh import build_tithi_pravesh
|
||||
from scripts.annual_service import api, package, report_facts
|
||||
from scripts.report_render.yoga_status_words import YOGA_STATUS_MAPPING
|
||||
from tests.test_annual_package import _body
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
|
||||
TSX = FRONTEND / "node_modules" / "tsx" / "dist" / "cli.mjs"
|
||||
|
||||
|
||||
def _context(location=None):
|
||||
return package._Context(api._request(_body(
|
||||
ayanamsa="lahiri", **({"return_location": location} if location else {}),
|
||||
)))
|
||||
|
||||
|
||||
def _tithi_view(context):
|
||||
return package._tithi_pravesha(context, 2026, datetime(2026, 1, 1, tzinfo=UTC), datetime(2027, 1, 1, tzinfo=UTC))
|
||||
|
||||
|
||||
def test_annual_tithi_pravesh_casts_both_locations_at_the_same_root():
|
||||
ctx = _context({"mode": "current_city", "latitude": 51.5074, "longitude": -0.1278, "timezone_id": "Europe/London"})
|
||||
raw = build_tithi_pravesh(ctx.pack_payload(2026))
|
||||
birth, = raw["rows"]
|
||||
city, = raw["current_location_rows"]
|
||||
assert birth["location_source"] == "birth_place" and city["location_source"] == "current_location"
|
||||
assert birth["jd_ut"] == city["jd_ut"]
|
||||
assert birth["ascendant"]["sign"] != city["ascendant"]["sign"]
|
||||
# Upstream 8aaf6099, with the product's bundled Swiss Ephemeris files and
|
||||
# the apparent frame matching the product engine's legacy alias.
|
||||
assert city["jd_ut"] == pytest.approx(2461228.2685106047, rel=0, abs=1e-8)
|
||||
assert city["local_datetime"] == "2026-07-06 19:26:39"
|
||||
assert city["phase_degrees"] == pytest.approx(257.1125057401731, rel=0, abs=1e-8)
|
||||
assert city["ascendant"]["degree_raw"] == pytest.approx(235.60228037337504, rel=0, abs=1e-8)
|
||||
assert city["tithi"]["tithi_num"] == 22
|
||||
|
||||
view = _tithi_view(ctx)
|
||||
assert view["candidate_count"] == 1 and "current_location_note" not in view
|
||||
birth_view, = view["candidates"]
|
||||
city_view, = view["current_location_candidates"]
|
||||
assert birth_view["location_source"] == "birth_place" and city_view["location_source"] == "current_location"
|
||||
assert birth_view["instant"]["jd_ut"] == city_view["instant"]["jd_ut"]
|
||||
assert birth_view["instant"]["utc_offset"] == "+08:00" and city_view["instant"]["utc_offset"] == "+01:00"
|
||||
|
||||
|
||||
def test_annual_tithi_pravesh_omits_the_residence_view_when_missing():
|
||||
ctx = _context()
|
||||
raw = build_tithi_pravesh(ctx.pack_payload(2026))
|
||||
assert len(raw["rows"]) == 1 and not raw["current_location_rows"]
|
||||
assert raw["current_location_reason"] == "current_location_missing"
|
||||
view = _tithi_view(ctx)
|
||||
assert view["candidate_count"] == 1 and "current_location_candidates" not in view
|
||||
assert view["current_location_note"] == "未提供现居地"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def natal_yogas():
|
||||
profiles = json.loads(Path(__file__).with_name("celebrity_cases.json").read_text(encoding="utf-8"))
|
||||
birth = next(row for row in profiles if row["id"] == "jobs")
|
||||
chart, *_ = package._engine().compute_chart_data(
|
||||
*(birth[key] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")),
|
||||
second=birth["second"], ayanamsa_name="lahiri",
|
||||
)
|
||||
planets, sign = chart["planets"], chart["ascendant"]["sign"]
|
||||
detected = package._module("yoga_engine").detect_yogas(planets, sign)
|
||||
return detected, report_facts._yogas(planets, sign)
|
||||
|
||||
|
||||
def test_annual_yoga_evidence_keeps_all_detected_rows_and_canonical_status(natal_yogas):
|
||||
detected, evidence = natal_yogas
|
||||
assert evidence["total"] == len(evidence["rows"]) == len(detected) > 8
|
||||
assert Counter(row["name"] for row in evidence["rows"]) == Counter(row["name"] for row in detected)
|
||||
assert all(row["state_text"] == YOGA_STATUS_MAPPING[row["state"]]["text"] for row in evidence["rows"])
|
||||
assert any(row["state"] == "candidate" for row in evidence["rows"])
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSX.is_file(), reason="frontend dependencies are required for the writer contract")
|
||||
def test_annual_model_evidence_includes_every_yoga_and_its_status(natal_yogas):
|
||||
_, yogas = natal_yogas
|
||||
program = """
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { sectionEvidence } from "./src/lib/annual-report-sections.ts";
|
||||
import { EMPTY_ANNUAL_SITUATION } from "./src/lib/annual-report-shared.ts";
|
||||
const yogas = JSON.parse(readFileSync(0, "utf8"));
|
||||
const entry = { id: "person", title: "本命", kind: "person", disposition: "write" };
|
||||
const { 本命 } = sectionEvidence({ natal_base: { yogas } }, entry, EMPTY_ANNUAL_SITUATION);
|
||||
const observations = yogas.rows.filter((row) => row.claim_kind === "structural_observation");
|
||||
const named = yogas.rows.filter((row) => row.claim_kind !== "structural_observation");
|
||||
assert.equal(本命.格局.length + 本命.结构观察.length, yogas.total);
|
||||
for (const [input, output] of [[named, 本命.格局], [observations, 本命.结构观察]]) {
|
||||
input.forEach((row, index) => {
|
||||
assert.ok(output[index].startsWith(row.name_cn || row.name));
|
||||
assert.ok(output[index].includes(row.state_text));
|
||||
});
|
||||
}
|
||||
"""
|
||||
subprocess.run(["node", str(TSX), "-e", program], cwd=FRONTEND,
|
||||
input=json.dumps(yogas, ensure_ascii=False).encode("utf-8"), check=True, timeout=45)
|
||||
Reference in New Issue
Block a user