fix(qizheng): pass school options into the vendored chart and stabilize gates
Independent Staging Quality Gate / validate (push) Failing after 8m1s
Independent Staging Quality Gate / publish (push) Skipped

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:
jesse-ux
2026-09-15 17:54:20 +08:00
parent 6bb892b826
commit a38a941522
12 changed files with 314 additions and 31 deletions
@@ -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()
+35 -14
View File
@@ -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)
+47
View File
@@ -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 }));