fix(qizheng): pass school options into the vendored chart and stabilize gates
BUG-710: call getSevenGovernorsChart with ketuMode/siderealMode so calculation.ketu_mode is the engine school, not the request echo. BUG-711: stop forbidding /ephemeris in the sidebar contract. BUG-712: compare ephemeris event longitudes at 6 decimals and fail closed when the golden is missing.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Rebuild tests/golden/ephemeris_events_raman_20260915_90d.json from a live scan.
|
||||
|
||||
The test suite will not write this file. Run this script only when the
|
||||
event list (kind / date / body / signs) has genuinely changed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.ephemeris_events import build_ephemeris_events
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GOLDEN = ROOT / "tests" / "golden" / "ephemeris_events_raman_20260915_90d.json"
|
||||
WINDOW = {
|
||||
"start_date": "2026-09-15",
|
||||
"end_date": "2026-12-14",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = build_ephemeris_events(WINDOW)
|
||||
GOLDEN.write_text(json.dumps(result["events"], ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"wrote {GOLDEN} ({len(result['events'])} events)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -169,21 +169,34 @@ def _node_bin() -> str:
|
||||
return found
|
||||
|
||||
|
||||
def _run_cli(iso: str, lat: float, lon: float, timeout_seconds: int) -> dict[str, Any]:
|
||||
EVAL_HELPER = REPO_ROOT / "scripts" / "qizheng_seven_governors.js"
|
||||
|
||||
|
||||
def _run_cli(
|
||||
iso: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
timeout_seconds: int,
|
||||
*,
|
||||
ketu_mode: str,
|
||||
sidereal_mode: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
command = [
|
||||
_node_bin(),
|
||||
str(VENDORED_CLI),
|
||||
"--date",
|
||||
iso,
|
||||
"--lat",
|
||||
str(lat),
|
||||
"--lng",
|
||||
str(lon),
|
||||
"--seven-governors",
|
||||
"--json",
|
||||
str(EVAL_HELPER),
|
||||
]
|
||||
if FORBIDDEN_CLI_FLAGS.intersection(command):
|
||||
raise QizhengChartError("forbidden engine subcommand blocked", error_code="ERR_QIZHENG_ENGINE")
|
||||
payload = json.dumps(
|
||||
{
|
||||
"date": iso,
|
||||
"lat": lat,
|
||||
"lng": lon,
|
||||
"ketuMode": ketu_mode,
|
||||
"siderealMode": sidereal_mode,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
@@ -192,6 +205,7 @@ def _run_cli(iso: str, lat: float, lon: float, timeout_seconds: int) -> dict[str
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
input=payload,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
@@ -242,8 +256,8 @@ def _normalize(raw: dict[str, Any], body: dict[str, Any], *, ketu_mode: str, sid
|
||||
"iso_local": iso,
|
||||
"latitude": float(body["lat"]),
|
||||
"longitude": float(body["lon"]),
|
||||
"ketu_mode": ketu_mode,
|
||||
"sidereal_mode": sidereal_mode,
|
||||
"ketu_mode": chart.get("ketuMode") or ketu_mode,
|
||||
"sidereal_mode": chart.get("siderealMode") or sidereal_mode,
|
||||
"engine_ketu_mode": chart.get("ketuMode"),
|
||||
"engine_sidereal_mode": chart.get("siderealMode"),
|
||||
"timeout_seconds": DEFAULT_TIMEOUT_SECONDS,
|
||||
@@ -278,14 +292,21 @@ def calculate_qizheng_chart(body: dict[str, Any], *, timeout_seconds: int = DEFA
|
||||
"""Compute a complete native 七政四余 natal chart from JSON-like input."""
|
||||
if not isinstance(body, dict):
|
||||
raise QizhengChartError("request body must be an object", error_code="ERR_QIZHENG_INPUT")
|
||||
if not VENDORED_CLI.exists():
|
||||
if not VENDORED_CLI.exists() or not EVAL_HELPER.exists():
|
||||
raise QizhengChartError("vendored 七政四余 engine is unavailable", error_code="ERR_QIZHENG_CLI_MISSING")
|
||||
lat = _number(body.get("lat", body.get("latitude")), "lat", -90, 90)
|
||||
lon = _number(body.get("lon", body.get("longitude", body.get("lng"))), "lon", -180, 180)
|
||||
iso = _iso_local(body)
|
||||
ketu_mode = _ketu_mode(body)
|
||||
sidereal_mode = _sidereal_mode(body)
|
||||
raw = _run_cli(iso, lat, lon, timeout_seconds)
|
||||
raw = _run_cli(
|
||||
iso,
|
||||
lat,
|
||||
lon,
|
||||
timeout_seconds,
|
||||
ketu_mode=ketu_mode,
|
||||
sidereal_mode=sidereal_mode,
|
||||
)
|
||||
return _normalize(raw, {**body, "lat": lat, "lon": lon}, ketu_mode=ketu_mode, sidereal_mode=sidereal_mode, iso=iso)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
// Load the vendored CLI bundle without running main(), then call the
|
||||
// in-bundle getSevenGovernorsChart(date, location, options) that the CLI
|
||||
// itself never forwards flags into.
|
||||
process.env.VITEST = "1";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Module = require("module");
|
||||
|
||||
const cliPath = path.resolve(__dirname, "..", "vendor", "stem-branch", "dist", "cli.cjs");
|
||||
if (!fs.existsSync(cliPath)) {
|
||||
process.stderr.write("vendored seven-governors CLI is missing\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(cliPath, "utf8").replace(
|
||||
"if (!process.env?.VITEST) {\n main();\n}",
|
||||
"module.exports.getSevenGovernorsChart = getSevenGovernorsChart;\nif (!process.env?.VITEST) {\n main();\n}",
|
||||
);
|
||||
const loaded = new Module(cliPath);
|
||||
loaded.filename = cliPath;
|
||||
loaded.paths = Module._nodeModulePaths(path.dirname(cliPath));
|
||||
loaded._compile(source, cliPath);
|
||||
const getSevenGovernorsChart = loaded.exports.getSevenGovernorsChart;
|
||||
if (typeof getSevenGovernorsChart !== "function") {
|
||||
process.stderr.write("getSevenGovernorsChart is not available from the vendored CLI\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const date = new Date(input.date);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
process.stderr.write("invalid date\n");
|
||||
process.exit(2);
|
||||
}
|
||||
const chart = getSevenGovernorsChart(
|
||||
date,
|
||||
{ lat: Number(input.lat), lon: Number(input.lng) },
|
||||
{
|
||||
ketuMode: input.ketuMode || "apogee",
|
||||
siderealMode: input.siderealMode || { type: "modern" },
|
||||
},
|
||||
);
|
||||
process.stdout.write(JSON.stringify({ sevenGovernors: chart }));
|
||||
Reference in New Issue
Block a user