From 63f56f94575b74efa665405ff0f787b503a83c61 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Tue, 14 Jul 2026 18:36:50 +0800 Subject: [PATCH] expose formula-only prashna sphuta evidence --- docs/research/pre_work_error_ledger.md | 1 + references/technique_registry.json | 12 +++---- scripts/prashna_context.py | 23 ++++++++++++-- scripts/prashna_sphuta.py | 44 ++++++++++++++++++++++++++ tests/test_prashna_entry_contract.py | 3 +- tests/test_prashna_sphuta.py | 23 ++++++++++++++ 6 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 scripts/prashna_sphuta.py create mode 100644 tests/test_prashna_sphuta.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index a304f239..c1cf8120 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -90,6 +90,7 @@ For large architecture or release work, also read: | ERR-057 | The release quality profile checked untracked files but did not execute the privacy AST scan or the real Chromium report-isolation probe. | mitigated 2026-07-14 | `release_hygiene_check()` now requires `public_release_privacy_scan.py --json` and `report_renderer_isolation_poc.py --strict`; parity manifest validation also runs as a contract check. | | ERR-058 | Formula-based Sahams used the day/night operand rules but omitted the documented zodiacal-order `+30°` exception. | mitigated 2026-07-14 | `_calc_formula_saham()` applies the `references/saham_rules.json` forward-arc condition and one-sign correction; keep external numeric oracle parity as a separate `partial` requirement. | | ERR-059 | Gulika was either an approximate fallback or falsely implied as a chart module output. | mitigated 2026-07-14 | `scripts/gulika.py` computes Prasna Marga Ghatika segment Ascendant with Swiss sunrise/sunset and Lahiri sidereal houses. It is exposed only as `prashna_context.supporting_indicators.gulika`, remains `partial`, and cannot unlock Sphuta or verdict layers until external numeric parity exists. | +| ERR-060 | Legacy Sphuta functions combined approximate Gulika with interpretive signals, while the exact formula could not be inspected in the production question context. | mitigated 2026-07-14 | `prashna_sphuta.py` exposes formula-only Trisphuta/Catusphuta/Pancasphuta from the partial Gulika evidence. It is supporting-only; Kunda, life-sensitive Sphutas and Prashna verdicts remain blocked pending external numeric parity. | ## Fragment Sweep Command Set diff --git a/references/technique_registry.json b/references/technique_registry.json index 68058797..182b9ff1 100644 --- a/references/technique_registry.json +++ b/references/technique_registry.json @@ -452,7 +452,7 @@ "timing", "life" ], - "status": "blocked", + "status": "partial", "knowledge_refs": [ "references/prashna-complete-guide.md", "references/single-event-inquiry-protocol.md" @@ -463,18 +463,16 @@ "scripts/jyotish_engine.py --mode life" ], "output_paths": [ - "scripts/prashna.py:calc_sphutas", - "scripts/prashna.py:calc_life_sphutas", - "kp_horary.sphutas" + "prashna_context.supporting_indicators.sphuta" ], "audit_label": "Sphuta/Trisphuta", - "missing_impact": "Prashna energy convergence and life-sensitivity markers are absent, leaving horary and life-risk reading thinner than classical toolchains.", + "missing_impact": "Formula-only Trisphuta/Catusphuta/Pancasphuta evidence is available from exact Gulika. Life-sensitive Sphutas, external numeric parity and all verdict use remain unavailable.", "entry_type": "supporting_indicator", "evidence_role": "secondary", "user_visibility": "expert_audit", "verification_level": { - "calculation": "verified", - "rule": "verified", + "calculation": "partial", + "rule": "partial", "prediction": "support_only" }, "conclusion_policy": "Supporting evidence only; it can raise/lower confidence but cannot by itself decide an event or timing claim." diff --git a/scripts/prashna_context.py b/scripts/prashna_context.py index b4b4492d..38a58caf 100644 --- a/scripts/prashna_context.py +++ b/scripts/prashna_context.py @@ -13,6 +13,10 @@ try: from scripts.gulika import calculate_gulika except ModuleNotFoundError: # pragma: no cover - CLI execution path from gulika import calculate_gulika +try: + from scripts.prashna_sphuta import calculate_sphuta_evidence +except ModuleNotFoundError: # pragma: no cover - CLI execution path + from prashna_sphuta import calculate_sphuta_evidence class PrashnaContextError(ValueError): @@ -69,6 +73,19 @@ def build_prashna_context(payload: dict[str, Any]) -> dict[str, Any]: "status": "blocked", "reason": f"gulika_supporting_indicator_failed:{type(exc).__name__}", } + if gulika.get("status") == "partial": + longitudes = { + name: item.get("degree_raw", item.get("lon")) + for name, item in chart["planets"].items() + if isinstance(item, dict) + } + sphuta = calculate_sphuta_evidence( + ascendant_longitude=chart["ascendant"].get("degree_raw", chart["ascendant"].get("lon")), + planet_longitudes=longitudes, + gulika_longitude=gulika["longitude"], + ) + else: + sphuta = {"status": "blocked", "reason": "gulika_supporting_indicator_unavailable"} return { "scope": "prashna_context", "status": "computed", @@ -82,7 +99,7 @@ def build_prashna_context(payload: dict[str, Any]) -> dict[str, Any]: "planets": chart["planets"], "calculation_contract": chart["calculation_contract"], "result_hash": chart["result_hash"], - "supporting_indicators": {"gulika": gulika}, - "blocked_layers": ["Trisphuta", "Kunda", "Prashna verdict"], - "boundary": "No client-supplied planets or ascendant are accepted. Gulika is supporting-only pending external numeric parity; Sphuta and verdict layers remain blocked.", + "supporting_indicators": {"gulika": gulika, "sphuta": sphuta}, + "blocked_layers": ["Kunda", "Prashna verdict"], + "boundary": "No client-supplied planets or ascendant are accepted. Gulika and formula-only Sphuta are supporting-only pending external numeric parity; verdict layers remain blocked.", } diff --git a/scripts/prashna_sphuta.py b/scripts/prashna_sphuta.py new file mode 100644 index 00000000..e7c1f552 --- /dev/null +++ b/scripts/prashna_sphuta.py @@ -0,0 +1,44 @@ +"""Formula-only Prasna Marga Sphuta evidence, without verdict interpretation.""" +from __future__ import annotations + +from typing import Any + + +def _norm(value: float) -> float: + return float(value) % 360.0 + + +def calculate_sphuta_evidence( + *, + ascendant_longitude: float, + planet_longitudes: dict[str, Any], + gulika_longitude: float, +) -> dict[str, Any]: + required = ("Sun", "Moon", "Rahu") + missing = [name for name in required if name not in planet_longitudes] + if missing: + return {"status": "blocked", "reason": "missing_sphuta_planets", "missing": missing} + asc = _norm(ascendant_longitude) + moon = _norm(planet_longitudes["Moon"]) + sun = _norm(planet_longitudes["Sun"]) + rahu = _norm(planet_longitudes["Rahu"]) + gulika = _norm(gulika_longitude) + trisphuta = _norm(asc + moon + gulika) + catusphuta = _norm(trisphuta + sun) + pancasphuta = _norm(catusphuta + rahu) + return { + "scope": "prasna_marga_sphuta_evidence", + "status": "partial", + "points": { + "trisphuta": trisphuta, + "catusphuta": catusphuta, + "pancasphuta": pancasphuta, + }, + "formula_trace": { + "trisphuta": "Lagna + Moon + Gulika", + "catusphuta": "Trisphuta + Sun", + "pancasphuta": "Catusphuta + Rahu", + }, + "rule_source": "references/prashna-complete-guide.md#3.2-3.3", + "boundary": "Formula-only supporting evidence. No health, event, or Prashna verdict is permitted without external numeric parity and adjudication rules.", + } diff --git a/tests/test_prashna_entry_contract.py b/tests/test_prashna_entry_contract.py index 21acbf22..62185733 100644 --- a/tests/test_prashna_entry_contract.py +++ b/tests/test_prashna_entry_contract.py @@ -22,7 +22,8 @@ def test_cli_prashna_uses_question_moment_swiss_context_only(): assert payload["status"] == "computed" assert payload["chart_source"] == "swiss_ephemeris_backend" assert payload["supporting_indicators"]["gulika"]["status"] == "partial" - assert "Trisphuta" in payload["blocked_layers"] + assert payload["supporting_indicators"]["sphuta"]["status"] == "partial" + assert "Kunda" in payload["blocked_layers"] def test_cli_prashna_blocks_legacy_approximation_modes(): diff --git a/tests/test_prashna_sphuta.py b/tests/test_prashna_sphuta.py new file mode 100644 index 00000000..469902db --- /dev/null +++ b/tests/test_prashna_sphuta.py @@ -0,0 +1,23 @@ +from scripts.prashna_sphuta import calculate_sphuta_evidence + + +def test_sphuta_formula_evidence_uses_exact_gulika_input() -> None: + result = calculate_sphuta_evidence( + ascendant_longitude=10, + planet_longitudes={"Moon": 20, "Sun": 30, "Rahu": 40}, + gulika_longitude=50, + ) + + assert result["status"] == "partial" + assert result["points"] == {"trisphuta": 80.0, "catusphuta": 110.0, "pancasphuta": 150.0} + + +def test_sphuta_evidence_blocks_missing_required_planet() -> None: + result = calculate_sphuta_evidence( + ascendant_longitude=10, + planet_longitudes={"Moon": 20, "Sun": 30}, + gulika_longitude=50, + ) + + assert result["status"] == "blocked" + assert result["missing"] == ["Rahu"]