fix: restore ayanamsa state and keep tests hermetic
This commit is contained in:
@@ -7904,3 +7904,18 @@
|
||||
- 相关记录:BUG-511
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
## BUG-513 | Gulika 计算泄漏进程级岁差模式,Raman 请求回执与后续计算可能漂移
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-03
|
||||
- 最近更新:2026-09-03
|
||||
- 影响面:`scripts/gulika.py`、`scripts/jyotish_api_server.py`、`scripts/prashna_context.py`
|
||||
- 用户现象:请求选择 Raman 时 Gulika 曾按其他岁差返回或污染后续恒星黄道计算;同一常驻进程的下一次计算可能继承错误模式。
|
||||
- 触发条件:Gulika / Upagraha 计算在已设置另一岁差的进程内调用 `houses_ex(..., FLG_SIDEREAL)`。
|
||||
- 根因:Swiss Ephemeris 岁差模式是进程全局状态;Gulika 设置请求岁差后没有恢复调用前模式,API 与 Prashna 调用方也没有始终传入本次请求岁差。
|
||||
- 修复:`ayanamsa_utils.temporary_ayanamsa` 在 `finally` 恢复前一模式;Gulika 的恒星上升计算统一使用该上下文;API 与 Prashna 显式透传并回执实际岁差。
|
||||
- 验证:Raman→Lahiri、Lahiri→Raman 两向同进程回归测试均保持调用前 Moon 黄经;模式设置所有权测试锁定只有 `ayanamsa_utils.py` 可直调 `set_sid_mode`;API Raman smoke 回执为 `raman` / `Raman`。
|
||||
- 防复发:临时切换进程级岁差必须通过 `temporary_ayanamsa`;业务模块不得直接调用 `set_sid_mode`。
|
||||
- 相关记录:BUG-511、BUG-512
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# PROGRESS · 仓库整备修复单(2026-09-03)
|
||||
|
||||
工作树:`.worktrees/repo-hygiene-20260903`
|
||||
分支:`codex/repo-hygiene-fix-20260903`
|
||||
基线:最新 `origin/staging`(提交前重放)
|
||||
|
||||
本轮完成修复与本地验证;交付目标为 `origin/staging`,提交/推送状态以 Git 远端为准;不手动部署。其他工作树的既有脏改动未触碰。
|
||||
|
||||
## 已完成
|
||||
|
||||
- BUG-512:Gulika / Upagraha 临时切换岁差后恢复进程原模式;API 与 Prashna 透传同一请求岁差,回执使用实际标准化名称。
|
||||
- 设置 UI:提示语明确咨询/报告/星盘库/每日星语跟随设置,生时校正目前仍固定 Raman;无样式类名由最新 staging 删除,不重复增加空样式。
|
||||
- 默认值:修复单列出的 Prashna、校正扫描、Muhurta、Solar Return CLI、ephemeris contract 改用 `DEFAULT_AYANAMSA_NAME`;五个参考/外部对照脚本保留显式 Lahiri 并标注参考口径。
|
||||
- 仓库卫生:三个 oracle 生成器支持 `--output-dir`,对应测试写入 pytest 临时目录,不再改写 `references/oracle`。
|
||||
- 记录:新增 `BUG-512`,更新本状态板。
|
||||
|
||||
## 验证
|
||||
|
||||
```text
|
||||
python -m pytest -q \
|
||||
tests/test_gulika.py \
|
||||
tests/test_gulika_ayanamsa_mode.py \
|
||||
tests/test_ayanamsa_mode_ownership.py \
|
||||
tests/test_prashna_context.py \
|
||||
tests/test_standalone_ayanamsa_defaults.py \
|
||||
tests/test_full_technique_invocation_matrix.py \
|
||||
tests/test_muhurta_factor_only_scoring_packet.py \
|
||||
tests/test_prashna_sphuta_oss_case_probe.py
|
||||
# 24 passed
|
||||
|
||||
node --test --import tsx \
|
||||
tests/ayanamsa.test.ts \
|
||||
tests/settings-mvp-contract.test.ts \
|
||||
tests/class-name-definition-contract.test.ts
|
||||
# 8 passed
|
||||
|
||||
./node_modules/.bin/tsc --noEmit
|
||||
# exit 0
|
||||
|
||||
npm run lint
|
||||
# exit 0; 73 个既有 warning,0 error
|
||||
|
||||
npm run build
|
||||
# exit 0;5 个既有 Turbopack 动态文件访问 warning
|
||||
|
||||
JYOTISH_PRE_WORK_PYTHON=/Users/jesse/Downloads/Copse/astrology/yinduzhanxing/.venv/bin/python \
|
||||
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
|
||||
# pass
|
||||
```
|
||||
|
||||
额外 smoke:`_attach_gulika_observation` 输入 `ayanamsa=raman`,回执为 `ayanamsa=raman`、`ayanamsa_display=Raman`。测试后 `git diff -- references/oracle` 为空。
|
||||
|
||||
## 明确未做
|
||||
|
||||
- 可选任务 5(机器相关计数测试加 `external` marker):任务书要求产品同意,本轮没有该授权,未改。
|
||||
- Python quick gate:503 passed、1 skipped。前端全量 `npm test` 在本机数据库/Docker 与缺少 PyYAML 等既有环境项失败;本轮相关的 8 项定向测试通过。
|
||||
- 最新 staging 另有不在修复单表内的显式 Lahiri:Solar Return / Transit 的独立 helper 仍有现成合同测试要求 Lahiri,VedAstro、PyJHora、三引擎 parity 等为外部参考口径;因此旧的宽泛 grep 验收命令仍会列出这些预存行,本轮没有越界改写其业务口径。
|
||||
@@ -92,7 +92,7 @@
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-repo-hygiene-20260903.md` | `PROGRESS-repo-hygiene-20260903.md` | 岁差默认 Raman + 用户设置可选、参考集锁口径、发布门本地可复现、注册表白名单、仓库地址、早期日志搬家、远端分支清理 | 已验收(带修复单) | `b6a70aa7`、`2415e751`、`01a45363`(BUG-511);验收结论见修复单附录 |
|
||||
| `TASK-repo-hygiene-fix-20260903.md` | — | Gulika 全局岁差泄漏(P1)、设置类名无样式、提示语对齐校正现状、剩余 lahiri 字面量、测试不写仓库 | 待领取 | 分支 `codex/repo-hygiene-fix-20260903` |
|
||||
| `TASK-repo-hygiene-fix-20260903.md` | `PROGRESS-repo-hygiene-fix-20260903.md` | Gulika 全局岁差泄漏(P1)、设置类名无样式、提示语对齐校正现状、剩余 lahiri 字面量、测试不写仓库 | 修复完成 | 分支 `codex/repo-hygiene-fix-20260903`;交付状态以 Git 远端为准 |
|
||||
|
||||
### 上游同步与能力接入
|
||||
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ The self-profile editor in 星盘资料 (`ChartLibraryPanel`) owns the ayanamsa
|
||||
|
||||
- **Structure:** four 44px stacked radios reused from `.theme-preference-option`, plus a caption under the group. Raman is marked 默认. Each option has a one-line description; selected rows show the same `Check` as the theme panel.
|
||||
- **Values:** `raman` (default) / `lahiri` / `kp` / `true_pushya`. Copy lives in `frontend/src/lib/ayanamsa.ts`.
|
||||
- **Hint:** “切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。”
|
||||
- **Hint:** “之后的咨询、报告、星盘库和每日星语会按新岁差算。生时校正目前仍固定按 Raman,之后的版本才会跟着这项设置走。”
|
||||
- **Placement:** after `ProfileFields` on the self-profile form only. Not on other-chart save, not on `page.tsx`.
|
||||
- **States:** default, hover, selected (`aria-checked` only), keyboard focus. Switching does not clear a confirmed birth minute.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const AYANAMSA_OPTIONS = [
|
||||
}>;
|
||||
|
||||
export const AYANAMSA_SWITCH_HINT =
|
||||
"切换只影响之后的新计算;已生成的报告和校正结果保持当时的口径。";
|
||||
"之后的咨询、报告、星盘库和每日星语会按新岁差算。生时校正目前仍固定按 Raman,之后的版本才会跟着这项设置走。";
|
||||
|
||||
export function isAyanamsaName(value: unknown): value is AyanamsaName {
|
||||
return typeof value === "string" && (AYANAMSA_VALUES as readonly string[]).includes(value);
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
AYANAMSA_SWITCH_HINT,
|
||||
AYANAMSA_VALUES,
|
||||
DEFAULT_AYANAMSA,
|
||||
isAyanamsaName,
|
||||
@@ -21,4 +22,6 @@ test("resolveAyanamsa defaults to Raman and accepts the four product values", ()
|
||||
assert.equal(resolveAyanamsa({ ayanamsa: "fagan" }), "raman");
|
||||
assert.equal(isAyanamsaName("raman"), true);
|
||||
assert.equal(isAyanamsaName("sidereal"), false);
|
||||
assert.match(AYANAMSA_SWITCH_HINT, /咨询、报告、星盘库和每日星语/);
|
||||
assert.match(AYANAMSA_SWITCH_HINT, /生时校正目前仍固定按 Raman/);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,8 @@ comparison fixtures must pass an explicit `lahiri` name.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Iterator
|
||||
|
||||
try:
|
||||
import swisseph as swe
|
||||
@@ -91,6 +92,7 @@ def current_ayanamsa_name(args: Any = None) -> str:
|
||||
|
||||
def apply_ayanamsa(name: Any = None, swe_module: Any = None) -> bool:
|
||||
"""Set Swiss Ephemeris sidereal mode explicitly."""
|
||||
global ACTIVE_AYANAMSA_NAME
|
||||
module = swe_module or swe
|
||||
if module is None:
|
||||
return False
|
||||
@@ -99,9 +101,24 @@ def apply_ayanamsa(name: Any = None, swe_module: Any = None) -> bool:
|
||||
module.set_sid_mode(AYANAMSA_MODES[key])
|
||||
except Exception:
|
||||
return False
|
||||
ACTIVE_AYANAMSA_NAME = key
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_ayanamsa(name: Any = None, swe_module: Any = None) -> Iterator[str]:
|
||||
"""Apply a sidereal mode and restore the previous process-wide mode."""
|
||||
previous = ACTIVE_AYANAMSA_NAME
|
||||
target = normalize_ayanamsa_name(name)
|
||||
if not apply_ayanamsa(target, swe_module):
|
||||
raise RuntimeError(f"failed to apply ayanamsa {target}")
|
||||
try:
|
||||
yield target
|
||||
finally:
|
||||
if not apply_ayanamsa(previous, swe_module):
|
||||
raise RuntimeError(f"failed to restore ayanamsa {previous}")
|
||||
|
||||
|
||||
def sidereal_flags(swe_module: Any, ayanamsa_name: Any = None) -> int:
|
||||
apply_ayanamsa(ayanamsa_name, swe_module)
|
||||
return getattr(swe_module, 'FLG_SWIEPH', 2) | getattr(swe_module, 'FLG_SIDEREAL', 65536)
|
||||
|
||||
@@ -11,6 +11,10 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from scripts.ayanamsa_utils import DEFAULT_AYANAMSA_NAME
|
||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
|
||||
try:
|
||||
from scripts.rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
@@ -109,7 +113,7 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
|
||||
"version": "rectification-input-v1",
|
||||
"center_input_fingerprint": candidate_input_fingerprint(payload),
|
||||
"settings": {
|
||||
"ayanamsa": str(payload.get("ayanamsa") or "lahiri").strip().lower(),
|
||||
"ayanamsa": str(payload.get("ayanamsa") or DEFAULT_AYANAMSA_NAME).strip().lower(),
|
||||
"node_mode": str(
|
||||
payload.get("node_mode", payload.get("nodeMode", "mean"))
|
||||
).strip().lower(),
|
||||
|
||||
@@ -17,7 +17,7 @@ import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from ayanamsa_utils import sidereal_flags
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, sidereal_flags
|
||||
|
||||
|
||||
def _weekday_to_vara(py_weekday: int) -> int:
|
||||
@@ -26,7 +26,7 @@ def _weekday_to_vara(py_weekday: int) -> int:
|
||||
|
||||
|
||||
def _get_sun_moon_lons(year: int, month: int, day: int,
|
||||
hour: int = 12, ayanamsa_name: str = 'lahiri') -> tuple:
|
||||
hour: int = 12, ayanamsa_name: str = DEFAULT_AYANAMSA_NAME) -> tuple:
|
||||
"""获取太阳/月亮恒星黄经。优先 swisseph,退化为近似算法。"""
|
||||
def _primary_longitude(value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
@@ -112,7 +112,7 @@ def cmd_muhurta(args, chart_data: Optional[Dict] = None) -> int:
|
||||
hour_from_sunrise = getattr(args, 'hour_from_sunrise', 6.0)
|
||||
target_activity = getattr(args, 'activity', None)
|
||||
scan_days = getattr(args, 'scan_days', 1)
|
||||
ayanamsa_name = getattr(args, 'ayanamsa', 'lahiri')
|
||||
ayanamsa_name = getattr(args, 'ayanamsa', None) or DEFAULT_AYANAMSA_NAME
|
||||
|
||||
activities = list(ACTIVITY_RULES.keys())
|
||||
if target_activity and target_activity not in activities:
|
||||
|
||||
@@ -28,6 +28,7 @@ def cmd_solar_return(args: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
try:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
|
||||
from solar_return import solar_return_full_report
|
||||
except ImportError as e:
|
||||
return {"error": f"solar_return 模块导入失败: {e}"}
|
||||
@@ -41,7 +42,7 @@ def cmd_solar_return(args: Any) -> Dict[str, Any]:
|
||||
args.hour, args.minute,
|
||||
args.lat, args.lon, args.tz,
|
||||
args.target_year,
|
||||
ayanamsa_name=getattr(args, 'ayanamsa', 'lahiri'),
|
||||
ayanamsa_name=getattr(args, 'ayanamsa', None) or DEFAULT_AYANAMSA_NAME,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ def collect_chart_core(
|
||||
settings = {
|
||||
"case_id": case_id,
|
||||
"birth": {key: case.get(key) for key in ("year", "month", "day", "hour", "minute", "second", "lat", "lon", "tz")},
|
||||
"ayanamsa_policy": case.get("ayanamsa_policy") or case.get("ayanamsa") or "lahiri",
|
||||
"ayanamsa_policy": case.get("ayanamsa_policy") or case.get("ayanamsa") or "lahiri", # 参考口径,勿改为默认
|
||||
"node_policy": case.get("node_policy") or case.get("node_mode") or "mean",
|
||||
"endpoint_host": adapter._endpoint_host(endpoint),
|
||||
"calculator": "AllPlanetData",
|
||||
|
||||
@@ -22,6 +22,7 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from jyotish_engine import HAS_SWE, compute_chart_data # noqa: E402
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME # noqa: E402
|
||||
|
||||
|
||||
BODY_KEYS = ["Sun", "Moon", "Ascendant", "Rahu", "Ketu"]
|
||||
@@ -40,7 +41,7 @@ class EphemerisAdapterContract:
|
||||
lat: float
|
||||
lon: float
|
||||
tz: float
|
||||
ayanamsa_policy: str = "lahiri"
|
||||
ayanamsa_policy: str = DEFAULT_AYANAMSA_NAME
|
||||
node_policy: str = "mean"
|
||||
body_list: tuple[str, ...] = tuple(BODY_KEYS)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
@@ -9,7 +10,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "references/oracle/full_technique_invocation_matrix_2026_07_22.json"
|
||||
OUTPUT_NAME = "full_technique_invocation_matrix_2026_07_22.json"
|
||||
|
||||
SCAN_ROOTS = {
|
||||
"skill": ["SKILL.md", "skills"],
|
||||
@@ -339,13 +340,18 @@ def _claim_boundary(tech: Technique, claim_status: str) -> str:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-dir", type=Path, default=ROOT / "references/oracle")
|
||||
args = parser.parse_args()
|
||||
packet = build_matrix()
|
||||
without_hash = dict(packet)
|
||||
without_hash["content_hash"] = ""
|
||||
digest = hashlib.sha256(json.dumps(without_hash, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
packet["content_hash"] = digest
|
||||
OUTPUT.write_text(json.dumps(packet, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"path": str(OUTPUT.relative_to(ROOT)), "sha256": digest, "summary": packet["summary"]}, ensure_ascii=False, indent=2))
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output = args.output_dir / OUTPUT_NAME
|
||||
output.write_text(json.dumps(packet, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"path": str(output), "sha256": digest, "summary": packet["summary"]}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+9
-9
@@ -12,9 +12,9 @@ try:
|
||||
except ImportError:
|
||||
from scripts.saham_daynight import determine_daytime
|
||||
try:
|
||||
from ayanamsa_utils import ayanamsa_display_name, normalize_ayanamsa_name, sidereal_flags
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, ayanamsa_display_name, normalize_ayanamsa_name, temporary_ayanamsa
|
||||
except ImportError:
|
||||
from scripts.ayanamsa_utils import ayanamsa_display_name, normalize_ayanamsa_name, sidereal_flags
|
||||
from scripts.ayanamsa_utils import DEFAULT_AYANAMSA_NAME, ayanamsa_display_name, normalize_ayanamsa_name, temporary_ayanamsa
|
||||
|
||||
|
||||
# Monday=0, matching datetime.weekday(). Values are the end of Saturn's share
|
||||
@@ -72,10 +72,10 @@ TEMPORAL_UPAGRAHAS = {
|
||||
}
|
||||
|
||||
|
||||
def _sidereal_ascendant(jd_ut: float, lat: float, lon: float, ayanamsa: str = "raman") -> float:
|
||||
flags = sidereal_flags(swe, ayanamsa)
|
||||
cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", flags)
|
||||
return float(ascmc[0]) % 360
|
||||
def _sidereal_ascendant(jd_ut: float, lat: float, lon: float, ayanamsa: str = DEFAULT_AYANAMSA_NAME) -> float:
|
||||
with temporary_ayanamsa(ayanamsa, swe):
|
||||
_, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", swe.FLG_SWIEPH | swe.FLG_SIDEREAL)
|
||||
return float(ascmc[0]) % 360
|
||||
|
||||
|
||||
def calculate_gulika(
|
||||
@@ -85,7 +85,7 @@ def calculate_gulika(
|
||||
lon: float,
|
||||
tz: float,
|
||||
method: str = "saturn_part_start",
|
||||
ayanamsa: str = "raman",
|
||||
ayanamsa: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> dict[str, Any]:
|
||||
"""Return Gulika from local moment/location using Swiss sunrise and sunset."""
|
||||
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
|
||||
@@ -140,7 +140,7 @@ def _temporal_upagraha(
|
||||
tz: float,
|
||||
planet_index: int,
|
||||
segment_part: str,
|
||||
ayanamsa: str = "raman",
|
||||
ayanamsa: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> dict[str, Any]:
|
||||
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
|
||||
daynight = determine_daytime(moment, lat=lat, lon=lon, tz=tz)
|
||||
@@ -198,7 +198,7 @@ def calculate_upagrahas(
|
||||
lon: float,
|
||||
tz: float,
|
||||
sun_longitude: float,
|
||||
ayanamsa: str = "raman",
|
||||
ayanamsa: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> dict[str, Any]:
|
||||
"""Calculate the D1 Upagraha packet using local Swiss Ephemeris inputs."""
|
||||
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
|
||||
|
||||
@@ -1046,7 +1046,9 @@ def _attach_gulika_observation(chart: dict, birth_payload: dict) -> None:
|
||||
int(float(birth_payload.get('minute', 0) or 0)),
|
||||
int(float(birth_payload.get('second', 0) or 0)),
|
||||
)
|
||||
result = _load_local_module('gulika').calculate_gulika(moment, lat=lat, lon=lon, tz=tz)
|
||||
result = _load_local_module('gulika').calculate_gulika(
|
||||
moment, lat=lat, lon=lon, tz=tz, ayanamsa=_request_ayanamsa(birth_payload)
|
||||
)
|
||||
result['claim_boundary'] = 'observation_only; not a verified event claim'
|
||||
modules['gulika'] = result
|
||||
except Exception as exc:
|
||||
|
||||
@@ -227,7 +227,7 @@ def build_report() -> dict[str, Any]:
|
||||
}
|
||||
return {
|
||||
"scope": "local_jyotish_accuracy_report",
|
||||
"reference_ayanamsa": "lahiri",
|
||||
"reference_ayanamsa": "lahiri", # 参考口径,勿改为默认
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"summary": summary,
|
||||
"checks": checks,
|
||||
|
||||
+6
-6
@@ -18,7 +18,7 @@ from datetime import datetime, timedelta
|
||||
import calendar
|
||||
import math
|
||||
|
||||
from ayanamsa_utils import sidereal_flags
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, sidereal_flags
|
||||
|
||||
# ── Vara(周日行星)──────────────────────────────────────────────────
|
||||
VARA_LORDS = {
|
||||
@@ -2138,7 +2138,7 @@ def _local_time_from_jd(jd: float, tz: float) -> str:
|
||||
return local_dt.strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
|
||||
def _swisseph_sun_moon_lon(jd: float, ayanamsa_name: str = 'lahiri') -> Optional[Tuple[float, float]]:
|
||||
def _swisseph_sun_moon_lon(jd: float, ayanamsa_name: str = DEFAULT_AYANAMSA_NAME) -> Optional[Tuple[float, float]]:
|
||||
try:
|
||||
import swisseph as swe
|
||||
flags = sidereal_flags(swe, ayanamsa_name)
|
||||
@@ -2192,7 +2192,7 @@ def calc_panchanga_end_times(
|
||||
day: int,
|
||||
tz: float,
|
||||
local_hours: float,
|
||||
ayanamsa_name: str = 'raman',
|
||||
ayanamsa_name: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> Optional[Dict]:
|
||||
"""Calculate current Tithi/Nakshatra/Yoga end times from SwissEph positions."""
|
||||
jd_start = _jd_for_local_time(year, month, day, local_hours, tz)
|
||||
@@ -2903,7 +2903,7 @@ def panchanga_range_report(
|
||||
lat: Optional[float] = None,
|
||||
lon: Optional[float] = None,
|
||||
tz: Optional[float] = None,
|
||||
ayanamsa_name: str = 'raman',
|
||||
ayanamsa_name: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> Dict:
|
||||
"""Build a date-range Panchanga calendar with daytime inauspicious windows."""
|
||||
start_dt = datetime.strptime(start_date[:10], '%Y-%m-%d')
|
||||
@@ -3034,7 +3034,7 @@ def muhurta_range_search(
|
||||
lon: Optional[float] = None,
|
||||
tz: Optional[float] = None,
|
||||
avoid_inauspicious_periods: bool = True,
|
||||
ayanamsa_name: str = 'raman',
|
||||
ayanamsa_name: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> Dict:
|
||||
"""Search a date range for ranked Muhurta candidates for a selected activity."""
|
||||
limit = max(1, min(int(limit or 5), 20))
|
||||
@@ -3106,7 +3106,7 @@ def build_muhurta_sidecar(
|
||||
lat: Optional[float] = None,
|
||||
lon: Optional[float] = None,
|
||||
tz: Optional[float] = None,
|
||||
ayanamsa_name: str = 'raman',
|
||||
ayanamsa_name: str = DEFAULT_AYANAMSA_NAME,
|
||||
) -> Dict:
|
||||
"""Compact Muhurta/Panchanga packet for unified workflow consumers."""
|
||||
activity = activity if activity in ACTIVITY_RULES else 'business'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""Capture a Muhurta factor-only scoring observation packet."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -11,7 +12,7 @@ from muhurta_factor_probe import build_probe
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "references/oracle/muhurta_factor_only_scoring_packet_2026_07_23.json"
|
||||
OUTPUT_NAME = "muhurta_factor_only_scoring_packet_2026_07_23.json"
|
||||
|
||||
|
||||
def _stable_json(data: Any) -> str:
|
||||
@@ -63,8 +64,14 @@ def build() -> dict[str, Any]:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-dir", type=Path, default=ROOT / "references/oracle")
|
||||
args = parser.parse_args()
|
||||
packet = build()
|
||||
OUTPUT.write_text(json.dumps(packet, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(args.output_dir / OUTPUT_NAME).write_text(
|
||||
json.dumps(packet, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(json.dumps(packet, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ def _namespace_from_template(case: dict[str, Any]) -> argparse.Namespace:
|
||||
birth["node_mode"] = settings["node_mode"]
|
||||
return _namespace_from_birth(
|
||||
birth,
|
||||
ayanamsa=settings.get("ayanamsa", "lahiri"),
|
||||
ayanamsa=settings.get("ayanamsa", "lahiri"), # 参考口径,勿改为默认
|
||||
moon_lon=None,
|
||||
nakshatra=None,
|
||||
pada=None,
|
||||
|
||||
@@ -9,6 +9,10 @@ try:
|
||||
from scripts.domain_calculation_service import CalculationError, compute_chart
|
||||
except ModuleNotFoundError: # pragma: no cover - CLI execution path
|
||||
from domain_calculation_service import CalculationError, compute_chart
|
||||
try:
|
||||
from scripts.ayanamsa_utils import DEFAULT_AYANAMSA_NAME, normalize_ayanamsa_name
|
||||
except ModuleNotFoundError: # pragma: no cover - CLI execution path
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, normalize_ayanamsa_name
|
||||
try:
|
||||
from scripts.gulika import calculate_gulika
|
||||
except ModuleNotFoundError: # pragma: no cover - CLI execution path
|
||||
@@ -39,6 +43,10 @@ def _timezone_offset(value: Any, moment: datetime) -> float:
|
||||
raise PrashnaContextError("timezone must be a numeric UTC offset or present in question_timestamp")
|
||||
|
||||
|
||||
def _payload_ayanamsa(payload: dict[str, Any]) -> str:
|
||||
return normalize_ayanamsa_name(payload.get("ayanamsa") or DEFAULT_AYANAMSA_NAME)
|
||||
|
||||
|
||||
def build_prashna_context(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
required = ("question_text", "question_timestamp", "lat", "lon", "timezone")
|
||||
missing = [field for field in required if payload.get(field) in (None, "")]
|
||||
@@ -56,18 +64,21 @@ def build_prashna_context(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if abs(timestamp_tz - tz) > 0.001:
|
||||
raise PrashnaContextError("timezone conflicts with question_timestamp offset")
|
||||
moment = moment.replace(tzinfo=None)
|
||||
ayanamsa = _payload_ayanamsa(payload)
|
||||
try:
|
||||
chart = compute_chart({
|
||||
"year": moment.year, "month": moment.month, "day": moment.day,
|
||||
"hour": moment.hour, "minute": moment.minute, "second": moment.second,
|
||||
"lat": float(payload["lat"]), "lon": float(payload["lon"]), "tz": tz,
|
||||
"ayanamsa": str(payload.get("ayanamsa") or "lahiri"),
|
||||
"ayanamsa": ayanamsa,
|
||||
"node_mode": str(payload.get("node_mode") or "mean"),
|
||||
})
|
||||
except (CalculationError, ValueError, TypeError) as exc:
|
||||
raise PrashnaContextError(f"Swiss Prashna chart blocked: {exc}") from exc
|
||||
try:
|
||||
gulika = calculate_gulika(moment, lat=float(payload["lat"]), lon=float(payload["lon"]), tz=tz)
|
||||
gulika = calculate_gulika(
|
||||
moment, lat=float(payload["lat"]), lon=float(payload["lon"]), tz=tz, ayanamsa=ayanamsa
|
||||
)
|
||||
except Exception as exc:
|
||||
gulika = {
|
||||
"status": "blocked",
|
||||
@@ -92,7 +103,7 @@ def build_prashna_context(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"question_text": str(payload["question_text"])[:500],
|
||||
"question_timestamp": str(payload["question_timestamp"]),
|
||||
"location": {"lat": float(payload["lat"]), "lon": float(payload["lon"]), "timezone": tz, "location_convention": "wgs84"},
|
||||
"ayanamsa": str(payload.get("ayanamsa") or "lahiri"),
|
||||
"ayanamsa": ayanamsa,
|
||||
"node_mode": str(payload.get("node_mode") or "mean"),
|
||||
"chart_source": "swiss_ephemeris_backend",
|
||||
"ascendant": chart["ascendant"],
|
||||
|
||||
@@ -37,5 +37,5 @@ def build(date):
|
||||
return data
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser(); ap.add_argument('--date',default='2026-07-20'); args=ap.parse_args(); data=build(args.date); OUT.write_text(json.dumps(data,ensure_ascii=False,indent=2,sort_keys=True)+'\n'); print(json.dumps(data,ensure_ascii=False,indent=2,sort_keys=True))
|
||||
ap=argparse.ArgumentParser(); ap.add_argument('--date',default='2026-07-20'); ap.add_argument('--output-dir',type=Path,default=OUT.parent); args=ap.parse_args(); data=build(args.date); args.output_dir.mkdir(parents=True,exist_ok=True); (args.output_dir/OUT.name).write_text(json.dumps(data,ensure_ascii=False,indent=2,sort_keys=True)+'\n'); print(json.dumps(data,ensure_ascii=False,indent=2,sort_keys=True))
|
||||
if __name__=='__main__': main()
|
||||
|
||||
@@ -53,7 +53,7 @@ def build_report() -> dict[str, Any]:
|
||||
"abhijit_muhurta": drik.abhijit_muhurta(local_jd, place),
|
||||
}
|
||||
utc_jd = local_jd - tz / 24
|
||||
sun_moon = _swisseph_sun_moon_lon(utc_jd, "lahiri")
|
||||
sun_moon = _swisseph_sun_moon_lon(utc_jd, "lahiri") # 参考口径,勿改为默认
|
||||
local = calc_panchanga(*sun_moon, weekday=(datetime(y, m, d).weekday() + 1) % 7)
|
||||
local_gulika = calculate_gulika(datetime(y, m, d, hh, mm, ss), lat=lat, lon=lon, tz=tz)
|
||||
py_gulika_deg = raw["gulika"][0] * 30 + raw["gulika"][1]
|
||||
|
||||
@@ -70,7 +70,7 @@ def _namespace_from_template(case: dict[str, Any]) -> Any:
|
||||
"lat": birth["lat"],
|
||||
"lon": birth["lon"],
|
||||
"tz": birth["tz"],
|
||||
"ayanamsa": settings.get("ayanamsa", "lahiri"),
|
||||
"ayanamsa": settings.get("ayanamsa", "lahiri"), # 参考口径,勿改为默认
|
||||
"node_mode": settings.get("node_mode", "mean"),
|
||||
},
|
||||
)()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_only_ayanamsa_utils_sets_process_sidereal_mode() -> None:
|
||||
violations = []
|
||||
for path in (ROOT / "scripts").rglob("*.py"):
|
||||
if path.name == "ayanamsa_utils.py":
|
||||
continue
|
||||
if re.search(r"\bset_sid_mode\s*\(", path.read_text(encoding="utf-8")):
|
||||
violations.append(str(path.relative_to(ROOT)))
|
||||
assert violations == []
|
||||
@@ -14,15 +14,16 @@ INDEX = ROOT / "references/oracle/evidence_packet_index_2026_07_19.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def matrix_packet() -> dict:
|
||||
def matrix_packet(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
output_dir = tmp_path_factory.mktemp("full-technique-invocation-matrix")
|
||||
subprocess.run(
|
||||
[sys.executable, "scripts/full_technique_invocation_matrix.py"],
|
||||
[sys.executable, "scripts/full_technique_invocation_matrix.py", "--output-dir", str(output_dir)],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(PACKET.read_text(encoding="utf-8"))
|
||||
return json.loads((output_dir / PACKET.name).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_full_matrix_scans_all_required_runtime_layers(matrix_packet: dict) -> None:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import swisseph as swe
|
||||
|
||||
from scripts.gulika import calculate_gulika
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, apply_ayanamsa
|
||||
|
||||
|
||||
def _moon_longitude() -> float:
|
||||
jd = swe.julday(2000, 1, 1, 12.0)
|
||||
return float(swe.calc_ut(jd, swe.MOON, swe.FLG_SWIEPH | swe.FLG_SIDEREAL)[0][0])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("previous", "requested"), [("lahiri", "raman"), ("raman", "lahiri")])
|
||||
def test_gulika_restores_process_ayanamsa(previous: str, requested: str) -> None:
|
||||
try:
|
||||
assert apply_ayanamsa(previous, swe)
|
||||
before = _moon_longitude()
|
||||
|
||||
result = calculate_gulika(
|
||||
datetime(1990, 1, 1, 12, 0),
|
||||
lat=39.9042,
|
||||
lon=116.4074,
|
||||
tz=8,
|
||||
ayanamsa=requested,
|
||||
)
|
||||
|
||||
assert result["ayanamsa"] == requested
|
||||
assert _moon_longitude() == pytest.approx(before, abs=1e-10)
|
||||
finally:
|
||||
apply_ayanamsa(DEFAULT_AYANAMSA_NAME, swe)
|
||||
@@ -7,10 +7,10 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
PACKET = ROOT / "references/oracle/muhurta_factor_only_scoring_packet_2026_07_23.json"
|
||||
|
||||
|
||||
def test_muhurta_factor_only_packet_generator_captures_ten_factors():
|
||||
def test_muhurta_factor_only_packet_generator_captures_ten_factors(tmp_path: Path):
|
||||
data = json.loads(
|
||||
subprocess.check_output(
|
||||
["python3", "scripts/muhurta_factor_only_scoring_packet.py"],
|
||||
["python3", "scripts/muhurta_factor_only_scoring_packet.py", "--output-dir", str(tmp_path)],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@ def test_prashna_context_uses_backend_chart_from_question_moment():
|
||||
assert packet["ascendant"]["degree"] >= 0
|
||||
assert "Sun" in packet["planets"]
|
||||
assert "question_timestamp" in packet
|
||||
assert packet["ayanamsa"] == "lahiri"
|
||||
assert packet["supporting_indicators"]["gulika"]["ayanamsa"] == "lahiri"
|
||||
|
||||
|
||||
def test_prashna_context_rejects_missing_time_or_non_wgs84_location():
|
||||
|
||||
@@ -7,8 +7,8 @@ ARTIFACT = ROOT / "references/oracle/prashna_sphuta_oss_case_probe_2026_07_20.js
|
||||
INDEX = ROOT / "references/oracle/evidence_packet_index_2026_07_19.json"
|
||||
|
||||
|
||||
def test_prashna_sphuta_oss_case_probe_runs_pyjhora_case_in_isolation():
|
||||
data = json.loads(subprocess.check_output(["python3", "scripts/prashna_sphuta_oss_case_probe.py", "--date", "2026-07-20"], cwd=ROOT, text=True))
|
||||
def test_prashna_sphuta_oss_case_probe_runs_pyjhora_case_in_isolation(tmp_path: Path):
|
||||
data = json.loads(subprocess.check_output(["python3", "scripts/prashna_sphuta_oss_case_probe.py", "--date", "2026-07-20", "--output-dir", str(tmp_path)], cwd=ROOT, text=True))
|
||||
assert data["scope"] == "prashna_sphuta_oss_case_probe"
|
||||
assert data["claim_status"] == "tooling_observation_only"
|
||||
assert data["license_boundary"] == "agpl_observation_only_do_not_vendor"
|
||||
|
||||
@@ -79,10 +79,10 @@ def test_solar_return_julian_datetime_helpers_work_without_greg_flag_constant():
|
||||
assert roundtrip.minute == 30
|
||||
|
||||
|
||||
def test_muhurta_sun_moon_helper_defaults_to_lahiri_after_global_raman_switch():
|
||||
def test_muhurta_sun_moon_helper_defaults_to_product_ayanamsa():
|
||||
jd = swe.julday(2026, 6, 25, 4.0)
|
||||
expected_sun = _sidereal_lon(jd, swe.SUN, swe.SIDM_LAHIRI)
|
||||
expected_moon = _sidereal_lon(jd, swe.MOON, swe.SIDM_LAHIRI)
|
||||
expected_sun = _sidereal_lon(jd, swe.SUN, swe.SIDM_RAMAN)
|
||||
expected_moon = _sidereal_lon(jd, swe.MOON, swe.SIDM_RAMAN)
|
||||
|
||||
_apply_ayanamsa('raman')
|
||||
actual = _swisseph_sun_moon_lon(jd)
|
||||
@@ -92,10 +92,10 @@ def test_muhurta_sun_moon_helper_defaults_to_lahiri_after_global_raman_switch():
|
||||
assert _angular_diff(actual[1], expected_moon) < 1e-9
|
||||
|
||||
|
||||
def test_cmd_muhurta_helper_defaults_to_lahiri_after_global_raman_switch():
|
||||
def test_cmd_muhurta_helper_defaults_to_product_ayanamsa():
|
||||
jd = swe.julday(2026, 6, 25, 4)
|
||||
expected_sun = _sidereal_lon(jd, swe.SUN, swe.SIDM_LAHIRI)
|
||||
expected_moon = _sidereal_lon(jd, swe.MOON, swe.SIDM_LAHIRI)
|
||||
expected_sun = _sidereal_lon(jd, swe.SUN, swe.SIDM_RAMAN)
|
||||
expected_moon = _sidereal_lon(jd, swe.MOON, swe.SIDM_RAMAN)
|
||||
|
||||
_apply_ayanamsa('raman')
|
||||
sun, moon, has_swe = _get_sun_moon_lons(2026, 6, 25, hour=4)
|
||||
|
||||
Reference in New Issue
Block a user