From 9bb054b0aeab832e7b5d993fff7fc3a6aac221ba Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Fri, 17 Jul 2026 01:03:50 +0800 Subject: [PATCH] Add Rangacharya comparison variant --- references/rangacharya_source_cards.json | 88 ++++++++++++ references/rangacharya_source_manifest.json | 127 ++++++++++++++++ references/technique_registry.json | 34 +++++ scripts/jyotish_api_server.py | 13 ++ scripts/rangacharya.py | 143 +++++++++++++++++++ scripts/rangacharya_readiness.py | 58 ++++++++ tests/test_jaimini_rangacharya_api.py | 50 +++++++ tests/test_rangacharya_adjudication_guard.py | 16 +++ tests/test_rangacharya_readiness.py | 14 ++ tests/test_rangacharya_registry.py | 17 +++ tests/test_rangacharya_source_cards.py | 50 +++++++ tests/test_rangacharya_source_manifest.py | 50 +++++++ tests/test_rangacharya_variant.py | 42 ++++++ 13 files changed, 702 insertions(+) create mode 100644 references/rangacharya_source_cards.json create mode 100644 references/rangacharya_source_manifest.json create mode 100644 scripts/rangacharya.py create mode 100644 scripts/rangacharya_readiness.py create mode 100644 tests/test_jaimini_rangacharya_api.py create mode 100644 tests/test_rangacharya_adjudication_guard.py create mode 100644 tests/test_rangacharya_readiness.py create mode 100644 tests/test_rangacharya_registry.py create mode 100644 tests/test_rangacharya_source_cards.py create mode 100644 tests/test_rangacharya_source_manifest.py create mode 100644 tests/test_rangacharya_variant.py diff --git a/references/rangacharya_source_cards.json b/references/rangacharya_source_cards.json new file mode 100644 index 00000000..cbac20db --- /dev/null +++ b/references/rangacharya_source_cards.json @@ -0,0 +1,88 @@ +{ + "schema_version": 1, + "created": "2026-07-16", + "cards": [ + { + "id": "rangacharya_core_arudha", + "title": "Rangacharya Arudha core counting", + "status": "transcribed", + "adjudication_enabled": false, + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Pending formula-level transcription and source verification.", + "notes": "Covers AL, A7, A10, UL, and A1-A12 variant counting." + } + }, + { + "id": "active_effective_lagna", + "title": "Active Lagna and Effective Lagna", + "status": "transcribed", + "adjudication_enabled": false, + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Pending formula-level transcription and source verification.", + "notes": "Runtime may report placeholder metadata only." + } + }, + { + "id": "prakriti_sanmukha", + "title": "Prakriti Chakra and Sanmukha", + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "formula direction and exception rules need source-card verification", + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Blocked until rule text is verified.", + "notes": "No runtime calculation allowed yet." + } + }, + { + "id": "rangacharya_special_mappings", + "title": "Special divisional and graha mappings", + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "Krishnamisra/Somanatha Navamsa, Parivritti/Somanatha Drekkana, Pancansa, and Graha Chakra need separate fixtures", + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Blocked until mapping tables are verified.", + "notes": "Raw descriptive output only after fixtures exist." + } + }, + { + "id": "rangacharya_named_yogas", + "title": "Named Rangacharya yogas", + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "named yogas need formula-level source cards and golden fixtures", + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Blocked until each yoga has its own source card.", + "notes": "Includes Dhana, Nirdhana, Kemadruma, Vahana, Bandhana, Dustamarana, Saukhya, Buddhi, Raja, Manipravala, Amatya, Senadhipatya, Chandradhi/Lagnadhi, Karakamsa, Yogada, Kevala, and Aspecting Graha." + } + }, + { + "id": "rangacharya_ul_family_rules", + "title": "UL relationship and family rules", + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "relationship, child, miscarriage, and adoption claims need real-case calibration", + "evidence": { + "source_ids": ["uploaded_screenshots_20260716"], + "formula_text": "Blocked until source and case validation.", + "notes": "May only become rule-hit reporting before calibration." + } + }, + { + "id": "article_warehouse_future_tracks", + "title": "Future article-warehouse tracks", + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "registered only; out of Rangacharya runtime scope", + "evidence": { + "source_ids": ["local_article_warehouse_20260716"], + "formula_text": "Blocked until hash, license, and technique-level extraction.", + "notes": "Includes Tithi Lord, Panchapakshi, Rashi Tulya Navamsa, Bhrigu Pada Dasha, Tajika, Darakaraka, and spouse rules." + } + } + ] +} diff --git a/references/rangacharya_source_manifest.json b/references/rangacharya_source_manifest.json new file mode 100644 index 00000000..f3223aa4 --- /dev/null +++ b/references/rangacharya_source_manifest.json @@ -0,0 +1,127 @@ +{ + "schema_version": 1, + "created": "2026-07-16", + "sources": [ + { + "id": "uploaded_screenshots_20260716", + "kind": "user_uploaded_screenshots", + "paths": [ + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3502.PNG", + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3503.PNG", + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3504.PNG", + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3505.PNG", + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3506.PNG", + "/Users/wuyongnaren/文件仓库/印度占星文章/260716/IMG_3507.PNG" + ], + "sha256": { + "IMG_3502.PNG": "1cf628c3f5dfac372c719eba442f0cc8325114fabea4cc01855e00247bc9c031", + "IMG_3503.PNG": "f3c1f3c67a3add9c6c586e60e18186e86549ca4a5b5a1e6a6c07cc4fe223e1de", + "IMG_3504.PNG": "6c884011b1fe458da2f23a4e3e5acaf48c6ea72cd402b51b2942b3a28cee5de1", + "IMG_3505.PNG": "16af004f553fde2c1ab5d0bc7968deba46040a02e7726f735f174523e6bdccee", + "IMG_3506.PNG": "a8732bc73767983130683152079eb44697d4686435452e71d2e742f8b16ed888", + "IMG_3507.PNG": "1ff2bbc69a3fe1dab17d742437000d9938d9b5bb5e818831dafc9bf0e05a152e" + }, + "license": "user_private_reference", + "privacy": "private", + "runtime_use": "reference_only_until_formula_verified", + "extraction_status": "located_and_hashed_ocr_blocked_tesseract_missing" + }, + { + "id": "local_article_warehouse_20260716", + "kind": "local_research_archive", + "path": "/Users/wuyongnaren/文件仓库/印度占星文章", + "license": "unknown", + "privacy": "local_research", + "runtime_use": "manifest_only_until_hash_and_license_review" + }, + { + "id": "kimi_agent_archive_20260716", + "kind": "local_training_archive", + "path": "/Users/wuyongnaren/Downloads/_整理候选/安装包与压缩包/Kimi_Agent_高维印度占星师.zip", + "license": "unknown", + "privacy": "local_research", + "runtime_use": "reference_only_until_license_review" + }, + { + "id": "vedastro_official", + "kind": "external_oracle", + "url": "https://github.com/VedAstro/VedAstro", + "license": "MIT", + "privacy": "public", + "runtime_use": "oracle_raw_reference" + }, + { + "id": "pyjhora_official", + "kind": "external_oracle", + "url": "https://github.com/naturalstupid/PyJHora", + "license": "AGPL-3.0", + "privacy": "public", + "runtime_use": "isolated_external_process_only" + }, + { + "id": "jyotishganit_official", + "kind": "external_oracle", + "url": "https://github.com/northtara/jyotishganit", + "license": "MIT", + "privacy": "public", + "runtime_use": "oracle_raw_reference" + }, + { + "id": "dashaflow_official", + "kind": "external_formula_reference", + "url": "https://github.com/adarshj322/dashaflow", + "license": "MIT", + "privacy": "public", + "runtime_use": "formula_reference_only" + } + ], + "validation_ladder": [ + "transcribed", + "source_verified", + "golden_verified", + "engine_cross_checked", + "case_calibrated", + "adjudication_enabled", + "blocked" + ], + "rules": [ + { + "id": "rangacharya_core_arudha", + "label": "Rangacharya Arudha core counting", + "source_ids": ["uploaded_screenshots_20260716"], + "status": "transcribed", + "adjudication_enabled": false + }, + { + "id": "active_effective_lagna", + "label": "Active Lagna and Effective Lagna", + "source_ids": ["uploaded_screenshots_20260716"], + "status": "transcribed", + "adjudication_enabled": false + }, + { + "id": "rangacharya_special_mappings", + "label": "Krishnamisra/Somanatha Navamsa, Parivritti/Somanatha Drekkana, Pancansa Graha, Graha Chakra", + "source_ids": ["uploaded_screenshots_20260716"], + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "needs formula-level source cards before runtime use" + }, + { + "id": "rangacharya_named_yogas", + "label": "Dhana/Nirdhana/Kemadruma and other named yogas", + "source_ids": ["uploaded_screenshots_20260716"], + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "needs formula-level source cards before runtime use" + }, + { + "id": "article_warehouse_future_tracks", + "label": "Tithi Lord, Panchapakshi, Rashi Tulya Navamsa, Bhrigu Pada Dasha, Tajika, Darakaraka, spouse rules", + "source_ids": ["local_article_warehouse_20260716"], + "status": "blocked", + "adjudication_enabled": false, + "blocked_reason": "registered for later source governance; out of Rangacharya Phase 1 runtime scope" + } + ] +} diff --git a/references/technique_registry.json b/references/technique_registry.json index a49cafe1..6ab387c1 100644 --- a/references/technique_registry.json +++ b/references/technique_registry.json @@ -1695,6 +1695,40 @@ }, "conclusion_policy": "Supporting evidence only; it can raise/lower confidence but cannot by itself decide an event or timing claim." }, + "rangacharya_jaimini_variant": { + "audit_label": "Rangacharya Jaimini Variant", + "commands": [ + "jaimini" + ], + "conclusion_policy": "Display current-vs-variant differences only; do not use for verdicts or timing.", + "domains": [ + "jaimini", + "arudha", + "experimental", + "audit" + ], + "entry_type": "experimental_variant", + "evidence_role": "comparison_only", + "knowledge_refs": [ + "references/rangacharya_source_manifest.json", + "references/rangacharya_source_cards.json", + "docs/superpowers/specs/2026-07-16-rangacharya-vedastro-design.md" + ], + "missing_impact": "Rangacharya-specific Arudha and named-yoga rules remain unavailable for adjudication until source cards, golden fixtures, oracle comparison, and case calibration pass.", + "name": "Rangacharya / Iranganti Jaimini Variant", + "note": "Rangacharya variant has a tested current-vs-variant diff path and API variant output, but adjudication remains disabled until source-card validation and golden fixtures pass.", + "output_paths": [ + "result.rangacharya", + "result.rangacharya_diff" + ], + "status": "comparison-only", + "user_visibility": "expert_audit", + "verification_level": { + "calculation": "experimental", + "prediction": "blocked", + "rule": "blocked" + } + }, "rashi_tulya_navamsa": { "name": "Rashi Tulya Navamsa / 本命对分盘分析", "domains": [ diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 6fc58409..7aa4602f 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -5947,6 +5947,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): allowed_modes = {'all', 'karaka', 'dasha', 'karakamsha', 'arudha', 'special'} if mode not in allowed_modes: raise BadRequest(f'mode must be one of: {", ".join(sorted(allowed_modes))}') + variant = body.get('variant', 'current') + if not isinstance(variant, str): + raise BadRequest('variant must be a string') + variant = variant.strip().lower() or 'current' + allowed_variants = {'current', 'rangacharya', 'all'} + if variant not in allowed_variants: + raise BadRequest(f'variant must be one of: {", ".join(sorted(allowed_variants))}') antardasha = bool(body.get('antardasha', False)) year = self._get_int(body, 'year', datetime.now().year, 1800, 2400) month = self._get_int(body, 'month', 1, 1, 12) @@ -5976,6 +5983,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if mode in ('all', 'arudha'): result['arudha_padas'] = jaimini.calc_arudha_padas(asc_sign_idx, planet_lons) result['graha_padas'] = jaimini.calc_graha_padas(planet_lons) + if variant in ('rangacharya', 'all'): + rangacharya = _load_local_module('rangacharya') + rangacharya_result = rangacharya.calc_rangacharya_variant(asc_sign_idx, planet_lons) + result['rangacharya'] = rangacharya_result + current_arudha = result.get('arudha_padas') or jaimini.calc_arudha_padas(asc_sign_idx, planet_lons) + result['rangacharya_diff'] = rangacharya.diff_current_vs_rangacharya(current_arudha, rangacharya_result) if mode in ('all', 'special'): result['special_lagnas'] = jaimini.calc_special_lagnas(asc_sign_idx, hour, minute + second / 60.0) return { diff --git a/scripts/rangacharya.py b/scripts/rangacharya.py new file mode 100644 index 00000000..a8f32cba --- /dev/null +++ b/scripts/rangacharya.py @@ -0,0 +1,143 @@ +"""Experimental Rangacharya/Jaimini variant. + +All outputs are blocked from adjudication until formula-level validation passes. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, Mapping + + +SIGNS = [ + "Aries", + "Taurus", + "Gemini", + "Cancer", + "Leo", + "Virgo", + "Libra", + "Scorpio", + "Sagittarius", + "Capricorn", + "Aquarius", + "Pisces", +] + +SOURCE_CARDS_PATH = Path(__file__).resolve().parent.parent / "references" / "rangacharya_source_cards.json" + + +class RangacharyaValidationError(RuntimeError): + pass + + +def _source_cards() -> Dict[str, Dict[str, Any]]: + try: + data = json.loads(SOURCE_CARDS_PATH.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return {} + return {str(card.get("id")): dict(card) for card in data.get("cards", []) if card.get("id")} + + +def _card_meta(card_id: str) -> Dict[str, Any]: + card = _source_cards().get(card_id, {}) + status = str(card.get("status") or "blocked") + meta = { + "source_card_id": card_id, + "source_card_status": status, + "validation_status": status, + "adjudication_enabled": False, + } + if status != "source_verified": + meta["blocked_reason"] = card.get("blocked_reason") or "source card is not verified for adjudication" + return meta + + +def _sign_name(index: int) -> str: + return SIGNS[index % 12] + + +def _placeholder_pada(label: str, asc_sign_idx: int, source_house: int) -> Dict[str, Any]: + sign_idx = (asc_sign_idx + source_house - 1) % 12 + return { + "label": label, + "sign": _sign_name(sign_idx), + "sign_index": sign_idx, + "source_house": source_house, + "note": "Rangacharya formula pending source-card implementation", + **_card_meta("rangacharya_core_arudha"), + } + + +def calc_rangacharya_variant(asc_sign_idx: int, planet_longitudes: Mapping[str, float]) -> Dict[str, Any]: + asc_sign_idx %= 12 + arudha_padas = { + "AL": _placeholder_pada("AL", asc_sign_idx, 1), + "A7": _placeholder_pada("A7", asc_sign_idx, 7), + "A10": _placeholder_pada("A10", asc_sign_idx, 10), + "UL": _placeholder_pada("UL", asc_sign_idx, 12), + } + return { + "variant": "rangacharya", + "status": "experimental_not_for_adjudication", + "adjudication_enabled": False, + "source_status": "transcribed", + "active_lagna": { + "sign": _sign_name(asc_sign_idx), + **_card_meta("active_effective_lagna"), + }, + "effective_lagna": { + "sign": _sign_name(asc_sign_idx), + **_card_meta("active_effective_lagna"), + }, + "arudha_padas": arudha_padas, + "input_planets_present": sorted(planet_longitudes), + } + + +def _flatten(prefix: str, value: Any) -> Dict[str, Any]: + if not isinstance(value, dict): + return {prefix: value} + rows: Dict[str, Any] = {} + for key, child in value.items(): + child_key = f"{prefix}.{key}" if prefix else str(key) + rows.update(_flatten(child_key, child)) + return rows + + +def diff_current_vs_rangacharya(current: Mapping[str, Any], variant: Mapping[str, Any]) -> Dict[str, Any]: + current_flat = _flatten("", dict(current)) + variant_flat = _flatten("", dict(variant.get("arudha_padas", variant))) + differences = [] + for key in sorted(set(current_flat) | set(variant_flat)): + current_value = current_flat.get(key) + variant_value = variant_flat.get(key) + if current_value != variant_value: + differences.append({"key": key, "current": current_value, "rangacharya": variant_value}) + return { + "current_algorithm": "current_jaimini", + "variant_algorithm": "rangacharya", + "adjudication_enabled": False, + "differences": differences, + } + + +def validation_summary(result: Mapping[str, Any]) -> Dict[str, Any]: + statuses = [] + for key, value in _flatten("", dict(result)).items(): + if key.endswith("validation_status"): + statuses.append(str(value)) + blocking = sorted({status for status in statuses if status != "adjudication_enabled"}) + return { + "adjudication_enabled": bool(result.get("adjudication_enabled")) and not blocking, + "blocking_statuses": blocking, + } + + +def assert_adjudication_allowed(result: Mapping[str, Any]) -> None: + summary = validation_summary(result) + if not summary["adjudication_enabled"]: + raise RangacharyaValidationError( + "Rangacharya variant is not adjudication-enabled; validation gates are incomplete" + ) diff --git a/scripts/rangacharya_readiness.py b/scripts/rangacharya_readiness.py new file mode 100644 index 00000000..874acced --- /dev/null +++ b/scripts/rangacharya_readiness.py @@ -0,0 +1,58 @@ +"""Readiness report for the experimental Rangacharya variant.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict + + +ROOT = Path(__file__).resolve().parent.parent +CARDS_PATH = ROOT / "references" / "rangacharya_source_cards.json" +MANIFEST_PATH = ROOT / "references" / "rangacharya_source_manifest.json" + + +def _load_json(path: Path) -> Dict[str, Any]: + try: + return json.loads(path.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def build_report() -> Dict[str, Any]: + cards_payload = _load_json(CARDS_PATH) + manifest_payload = _load_json(MANIFEST_PATH) + cards = {} + blocked = [] + transcribed = [] + for card in cards_payload.get("cards", []): + card_id = str(card.get("id") or "") + if not card_id: + continue + status = str(card.get("status") or "blocked") + adjudication_enabled = bool(card.get("adjudication_enabled")) + cards[card_id] = { + "status": status, + "adjudication_enabled": adjudication_enabled, + "blocked_reason": card.get("blocked_reason") or "", + } + if status == "blocked" or not adjudication_enabled: + blocked.append(card_id) + if status == "transcribed": + transcribed.append(card_id) + return { + "scope": "rangacharya_readiness", + "manifest_available": bool(manifest_payload), + "source_cards_available": bool(cards_payload), + "adjudication_enabled": bool(cards) and not blocked, + "card_count": len(cards), + "blocked_count": len(blocked), + "transcribed_count": len(transcribed), + "blocked_cards": blocked, + "transcribed_cards": transcribed, + "cards": cards, + } + + +if __name__ == "__main__": + print(json.dumps(build_report(), ensure_ascii=False, indent=2, sort_keys=True)) diff --git a/tests/test_jaimini_rangacharya_api.py b/tests/test_jaimini_rangacharya_api.py new file mode 100644 index 00000000..1f649542 --- /dev/null +++ b/tests/test_jaimini_rangacharya_api.py @@ -0,0 +1,50 @@ +import os +import sys + + +SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts") +if SCRIPTS not in sys.path: + sys.path.insert(0, SCRIPTS) + +from jyotish_api_server import JyotishAPIHandler # noqa: E402 + + +def _handler() -> JyotishAPIHandler: + return JyotishAPIHandler.__new__(JyotishAPIHandler) + + +def _planets(): + return { + "Sun": {"lon": 10.0}, + "Moon": {"lon": 45.0}, + "Mars": {"lon": 80.0}, + "Mercury": {"lon": 110.0}, + "Jupiter": {"lon": 145.0}, + "Venus": {"lon": 200.0}, + "Saturn": {"lon": 250.0}, + "Rahu": {"lon": 300.0}, + "Ketu": {"lon": 120.0}, + } + + +def test_jaimini_default_does_not_include_rangacharya(): + result = _handler()._compute_jaimini({ + "mode": "arudha", + "ascendant": {"lon": 0.0}, + "planets": _planets(), + }) + assert "rangacharya" not in result["result"] + assert "rangacharya_diff" not in result["result"] + + +def test_jaimini_variant_all_includes_current_variant_and_diff(): + result = _handler()._compute_jaimini({ + "mode": "arudha", + "variant": "all", + "ascendant": {"lon": 0.0}, + "planets": _planets(), + }) + assert result["result"]["rangacharya"]["adjudication_enabled"] is False + assert result["result"]["rangacharya_diff"]["adjudication_enabled"] is False + assert result["result"]["rangacharya"]["arudha_padas"]["AL"]["source_card_status"] == "transcribed" + assert result["result"]["rangacharya"]["active_lagna"]["source_card_id"] == "active_effective_lagna" diff --git a/tests/test_rangacharya_adjudication_guard.py b/tests/test_rangacharya_adjudication_guard.py new file mode 100644 index 00000000..0e624bdd --- /dev/null +++ b/tests/test_rangacharya_adjudication_guard.py @@ -0,0 +1,16 @@ +import pytest + +from scripts import rangacharya + + +def test_assert_adjudication_allowed_rejects_default_variant(): + result = rangacharya.calc_rangacharya_variant(0, {"Sun": 10.0}) + with pytest.raises(rangacharya.RangacharyaValidationError): + rangacharya.assert_adjudication_allowed(result) + + +def test_validation_summary_lists_blocking_rules(): + result = rangacharya.calc_rangacharya_variant(0, {"Sun": 10.0}) + summary = rangacharya.validation_summary(result) + assert summary["adjudication_enabled"] is False + assert summary["blocking_statuses"] diff --git a/tests/test_rangacharya_readiness.py b/tests/test_rangacharya_readiness.py new file mode 100644 index 00000000..0aaf82db --- /dev/null +++ b/tests/test_rangacharya_readiness.py @@ -0,0 +1,14 @@ +from scripts import rangacharya_readiness + + +def test_readiness_reports_blocked_until_no_adjudication_cards(): + report = rangacharya_readiness.build_report() + assert report["scope"] == "rangacharya_readiness" + assert report["adjudication_enabled"] is False + assert report["blocked_count"] >= 1 + assert "rangacharya_core_arudha" in report["cards"] + + +def test_readiness_does_not_expose_secrets(): + report = rangacharya_readiness.build_report() + assert "sk_live_" not in str(report) diff --git a/tests/test_rangacharya_registry.py b/tests/test_rangacharya_registry.py new file mode 100644 index 00000000..5965c181 --- /dev/null +++ b/tests/test_rangacharya_registry.py @@ -0,0 +1,17 @@ +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY = ROOT / "references" / "technique_registry.json" + + +def test_rangacharya_variant_registered_as_comparison_only(): + techniques = json.loads(REGISTRY.read_text(encoding="utf-8"))["techniques"] + entry = techniques["rangacharya_jaimini_variant"] + assert entry["status"] == "comparison-only" + assert entry["verification_level"]["calculation"] == "experimental" + assert entry["verification_level"]["prediction"] == "blocked" + assert entry["conclusion_policy"] == "Display current-vs-variant differences only; do not use for verdicts or timing." + assert entry["evidence_role"] == "comparison_only" + assert "references/rangacharya_source_cards.json" in entry["knowledge_refs"] diff --git a/tests/test_rangacharya_source_cards.py b/tests/test_rangacharya_source_cards.py new file mode 100644 index 00000000..2fb570c7 --- /dev/null +++ b/tests/test_rangacharya_source_cards.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path + + +CARDS = Path("references/rangacharya_source_cards.json") + + +REQUIRED_CARD_IDS = { + "rangacharya_core_arudha", + "active_effective_lagna", + "prakriti_sanmukha", + "rangacharya_special_mappings", + "rangacharya_named_yogas", + "rangacharya_ul_family_rules", + "article_warehouse_future_tracks", +} + + +def _cards(): + return json.loads(CARDS.read_text(encoding="utf-8")) + + +def test_source_cards_exist_with_required_schema(): + data = _cards() + assert data["schema_version"] == 1 + assert isinstance(data["cards"], list) + for card in data["cards"]: + assert card["id"] + assert card["status"] in { + "transcribed", + "source_verified", + "golden_verified", + "engine_cross_checked", + "case_calibrated", + "blocked", + } + assert card["adjudication_enabled"] is False + assert "evidence" in card + + +def test_source_cards_cover_phase2_rule_groups(): + data = _cards() + found = {card["id"] for card in data["cards"]} + assert REQUIRED_CARD_IDS <= found + + +def test_source_cards_do_not_contain_secrets(): + text = CARDS.read_text(encoding="utf-8") + assert "sk_live_" not in text + assert "VEDASTRO_API_KEY" not in text diff --git a/tests/test_rangacharya_source_manifest.py b/tests/test_rangacharya_source_manifest.py new file mode 100644 index 00000000..0f13d149 --- /dev/null +++ b/tests/test_rangacharya_source_manifest.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path + + +MANIFEST = Path("references/rangacharya_source_manifest.json") + + +def test_manifest_exists_and_has_required_sections(): + data = json.loads(MANIFEST.read_text(encoding="utf-8")) + assert data["schema_version"] == 1 + assert "sources" in data + assert "rules" in data + assert "validation_ladder" in data + + +def test_manifest_does_not_contain_secrets(): + text = MANIFEST.read_text(encoding="utf-8") + assert "sk_live_" not in text + assert "api_key" not in text.lower() + + +def test_rules_default_below_adjudication(): + data = json.loads(MANIFEST.read_text(encoding="utf-8")) + for rule in data["rules"]: + assert rule["status"] in { + "transcribed", + "source_verified", + "golden_verified", + "engine_cross_checked", + "case_calibrated", + "blocked", + } + assert rule["status"] != "adjudication_enabled" + assert rule["adjudication_enabled"] is False + + +def test_screenshot_source_paths_are_located_and_hashed(): + data = json.loads(MANIFEST.read_text(encoding="utf-8")) + source = next(item for item in data["sources"] if item["id"] == "uploaded_screenshots_20260716") + assert source["extraction_status"] == "located_and_hashed_ocr_blocked_tesseract_missing" + assert len(source["paths"]) == 6 + assert all("/文件仓库/印度占星文章/260716/" in path for path in source["paths"]) + assert set(source["sha256"]) == { + "IMG_3502.PNG", + "IMG_3503.PNG", + "IMG_3504.PNG", + "IMG_3505.PNG", + "IMG_3506.PNG", + "IMG_3507.PNG", + } diff --git a/tests/test_rangacharya_variant.py b/tests/test_rangacharya_variant.py new file mode 100644 index 00000000..a263a848 --- /dev/null +++ b/tests/test_rangacharya_variant.py @@ -0,0 +1,42 @@ +from scripts import rangacharya + + +SAMPLE_LONGS = { + "Sun": 10.0, + "Moon": 45.0, + "Mars": 80.0, + "Mercury": 110.0, + "Jupiter": 145.0, + "Venus": 200.0, + "Saturn": 250.0, + "Rahu": 300.0, + "Ketu": 120.0, +} + + +def test_variant_result_is_experimental_and_not_for_adjudication(): + result = rangacharya.calc_rangacharya_variant(0, SAMPLE_LONGS) + assert result["variant"] == "rangacharya" + assert result["adjudication_enabled"] is False + assert result["status"] == "experimental_not_for_adjudication" + + +def test_variant_includes_core_sections(): + result = rangacharya.calc_rangacharya_variant(0, SAMPLE_LONGS) + assert "source_status" in result + assert "arudha_padas" in result + assert "active_lagna" in result + assert "effective_lagna" in result + assert result["arudha_padas"]["AL"]["source_card_id"] == "rangacharya_core_arudha" + assert result["arudha_padas"]["AL"]["source_card_status"] == "transcribed" + assert result["active_lagna"]["source_card_id"] == "active_effective_lagna" + assert result["effective_lagna"]["source_card_id"] == "active_effective_lagna" + + +def test_diff_marks_algorithm_names(): + current = {"AL": {"sign": "Aries"}} + variant = {"arudha_padas": {"AL": {"sign": "Taurus"}}} + diff = rangacharya.diff_current_vs_rangacharya(current, variant) + assert diff["current_algorithm"] == "current_jaimini" + assert diff["variant_algorithm"] == "rangacharya" + assert diff["differences"][0]["key"] == "AL.sign"