Files
Jyotisha/docs/superpowers/plans/2026-07-16-rangacharya-vedastro-phase1.md
T
2026-07-16 23:22:18 +08:00

18 KiB

Rangacharya Variant + VedAstro Closure Phase 1 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the first safe Rangacharya variant slice: source manifest, core variant skeleton, current-vs-variant diff output, non-adjudication guard, and VedAstro raw-closure status hooks.

Architecture: Keep current Jaimini unchanged. Add a small rangacharya module that consumes the same chart inputs, emits experimental outputs, and is blocked from verdict/timing use by explicit validation status. Add source-manifest governance before importing any screenshot/archive/article rules into runtime.

Tech Stack: Python 3 standard library, existing scripts/jaimini.py, existing scripts/jyotish_api_server.py, existing VedAstro gateway/archive contracts, pytest.

Global Constraints

  • Do not store or echo the VedAstro key.
  • Do not copy AGPL PyJHora code.
  • Do not use .workbuddy as runtime.
  • Do not change existing calc_arudha_padas() behavior.
  • Every new Rangacharya rule starts transcribed, blocked, or experimental_not_for_adjudication.
  • Keep unrelated dirty-tree files untouched.

File Map

  • Create references/rangacharya_source_manifest.json: source inventory and rule validation states.
  • Create scripts/rangacharya.py: independent variant functions and diff helpers.
  • Create tests/test_rangacharya_source_manifest.py: manifest schema/privacy tests.
  • Create tests/test_rangacharya_variant.py: core variant and non-adjudication tests.
  • Modify scripts/jyotish_api_server.py: optional variant=rangacharya|all in Jaimini endpoint.
  • Modify tests/test_jaimini.py or create tests/test_jaimini_rangacharya_api.py: endpoint contract tests.
  • Modify existing VedAstro tests only if needed to expose raw-closure status, not to run real credentials in CI.

Task 1: Source Manifest

Files:

  • Create: references/rangacharya_source_manifest.json

  • Create: tests/test_rangacharya_source_manifest.py

  • Step 1: Write failing manifest tests

Create tests/test_rangacharya_source_manifest.py:

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
  • Step 2: Run test, verify failure

Run:

python3 -m pytest tests/test_rangacharya_source_manifest.py -q

Expected: FAIL because manifest is missing.

  • Step 3: Add minimal manifest

Create references/rangacharya_source_manifest.json with no secrets:

{
  "schema_version": 1,
  "created": "2026-07-16",
  "sources": [
    {
      "id": "uploaded_screenshots_20260716",
      "kind": "user_uploaded_screenshots",
      "paths": [
        "/Users/wuyongnaren/Downloads/IMG_3502.PNG",
        "/Users/wuyongnaren/Downloads/IMG_3503.PNG",
        "/Users/wuyongnaren/Downloads/IMG_3504.PNG",
        "/Users/wuyongnaren/Downloads/IMG_3505.PNG",
        "/Users/wuyongnaren/Downloads/IMG_3506.PNG",
        "/Users/wuyongnaren/Downloads/IMG_3507.PNG"
      ],
      "license": "user_private_reference",
      "privacy": "private",
      "runtime_use": "reference_only_until_formula_verified"
    },
    {
      "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": "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"
    }
  ],
  "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_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"
    }
  ]
}
  • Step 4: Run test, verify pass

Run:

python3 -m pytest tests/test_rangacharya_source_manifest.py -q
  • Step 5: Commit

Stage only manifest and its test.


Task 2: Rangacharya Variant Skeleton

Files:

  • Create: scripts/rangacharya.py

  • Create: tests/test_rangacharya_variant.py

  • Step 1: Write failing core tests

Create tests/test_rangacharya_variant.py:

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


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"
  • Step 2: Run test, verify failure

Run:

python3 -m pytest tests/test_rangacharya_variant.py -q

Expected: FAIL because module is missing.

  • Step 3: Add minimal module

Create scripts/rangacharya.py:

"""Experimental Rangacharya/Jaimini variant.

All outputs are blocked from adjudication until formula-level validation passes.
"""

from __future__ import annotations

from typing import Any, Dict, Mapping


SIGNS = [
    "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
    "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]


def _sign_index(longitude: float) -> int:
    return int((longitude % 360) // 30)


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,
        "validation_status": "transcribed",
        "adjudication_enabled": False,
        "note": "Rangacharya formula pending source-card implementation",
    }


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),
            "validation_status": "transcribed",
            "adjudication_enabled": False,
        },
        "effective_lagna": {
            "sign": _sign_name(asc_sign_idx),
            "validation_status": "transcribed",
            "adjudication_enabled": False,
        },
        "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,
    }
  • Step 4: Run tests

Run:

python3 -m pytest tests/test_rangacharya_variant.py -q
  • Step 5: Commit

Stage only scripts/rangacharya.py and tests/test_rangacharya_variant.py.


Task 3: API Exposure Without Changing Current Defaults

Files:

  • Modify: scripts/jyotish_api_server.py

  • Create: tests/test_jaimini_rangacharya_api.py

  • Step 1: Write endpoint contract tests

Create tests/test_jaimini_rangacharya_api.py using the existing API test helper style in nearby tests. Assert:

def test_jaimini_default_does_not_include_rangacharya(client):
    response = client.post("/api/jaimini", json={"ascendant": 0, "planets": []})
    data = response.get_json()
    assert "rangacharya" not in data["result"]


def test_jaimini_variant_all_includes_current_variant_and_diff(client):
    response = client.post("/api/jaimini", json={
        "ascendant": 0,
        "variant": "all",
        "mode": "arudha",
        "planets": [
            {"name": "Sun", "longitude": 10},
            {"name": "Moon", "longitude": 45},
            {"name": "Mars", "longitude": 80},
            {"name": "Mercury", "longitude": 110},
            {"name": "Jupiter", "longitude": 145},
            {"name": "Venus", "longitude": 200},
            {"name": "Saturn", "longitude": 250}
        ]
    })
    data = response.get_json()
    assert data["result"]["rangacharya"]["adjudication_enabled"] is False
    assert data["result"]["rangacharya_diff"]["adjudication_enabled"] is False

If existing test fixtures use a different client factory, copy that local pattern exactly.

  • Step 2: Run test, verify failure

Run:

python3 -m pytest tests/test_jaimini_rangacharya_api.py -q
  • Step 3: Add optional variant handling

In scripts/jyotish_api_server.py::_compute_jaimini:

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))}')

After current arudha calculation:

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)

Preserve default variant=current, so existing clients do not see new fields.

  • Step 4: Run focused tests

Run:

python3 -m pytest tests/test_jaimini_rangacharya_api.py tests/test_jaimini.py -q
  • Step 5: Commit

Stage only API/test files touched in this task.


Task 4: Adjudication Guard

Files:

  • Create: tests/test_rangacharya_adjudication_guard.py

  • Modify: scripts/rangacharya.py

  • Step 1: Write guard tests

Create tests/test_rangacharya_adjudication_guard.py:

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"]
  • Step 2: Run test, verify failure

Run:

python3 -m pytest tests/test_rangacharya_adjudication_guard.py -q
  • Step 3: Add guard functions

Add to scripts/rangacharya.py:

class RangacharyaValidationError(RuntimeError):
    pass


def validation_summary(result: Mapping[str, Any]) -> Dict[str, Any]:
    statuses = []
    for row in _flatten("", dict(result)).items():
        key, value = row
        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"
        )
  • Step 4: Run tests

Run:

python3 -m pytest tests/test_rangacharya_adjudication_guard.py tests/test_rangacharya_variant.py -q
  • Step 5: Commit

Stage only guard files.


Task 5: VedAstro Closure Status Hook

Files:

  • Modify: existing VedAstro gateway/status module found by rg -n "official_closure_state|VedAstro Raw Archive Manifest|gateway_status" scripts tests

  • Modify/Create: focused VedAstro status test

  • Step 1: Locate existing contract

Run:

rg -n "official_closure_state|VedAstro Raw Archive Manifest|gateway_status|official_raw_response" scripts tests

Use the current status function rather than adding a parallel gateway.

  • Step 2: Write failing test for env-only key and blocked raw

In the existing VedAstro status test file, add a test equivalent to:

def test_vedastro_closure_status_never_exposes_secret(monkeypatch):
    monkeypatch.setenv("VEDASTRO_API_KEY", "sk_live_test_secret")
    status = gateway_status()
    text = json.dumps(status, sort_keys=True)
    assert "sk_live_test_secret" not in text
    assert status["official_closure_state"] in {
        "official_verified",
        "official_raw_missing_or_unverified",
        "blocked",
        "not_configured",
    }

Adapt imports to the existing module.

  • Step 3: Implement minimal redaction/status preservation

If status currently includes env values, replace with booleans:

"credential_configured": bool(os.environ.get("VEDASTRO_API_KEY")),

Never include key contents. If no official raw response exists, set:

"official_closure_state": "official_raw_missing_or_unverified"
  • Step 4: Run focused VedAstro tests

Run:

python3 -m pytest tests/test_vedastro_service_adapter_executor.py tests/test_vedastro_official_mcp_bridge.py -q

If these are too broad, run only the specific test nodes touched and record that full files were not run.

  • Step 5: Commit

Stage only VedAstro status/test files touched in this task.


Task 6: Documentation And Planning Sync

Files:

  • Modify: .planning/rangacharya_vedastro_design_20260716/task_plan.md

  • Modify: .planning/rangacharya_vedastro_design_20260716/progress.md

  • Modify: .planning/rangacharya_vedastro_design_20260716/findings.md if new facts appear

  • Step 1: Update planning state

Mark design/planning complete and implementation pending/executing according to actual state.

  • Step 2: Run mandatory pre-work and focused tests

Run:

python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
python3 -m pytest tests/test_rangacharya_source_manifest.py tests/test_rangacharya_variant.py tests/test_rangacharya_adjudication_guard.py tests/test_jaimini_rangacharya_api.py -q
  • Step 3: Final verify no secret leakage

Run:

rg -n "sk_live_|VEDASTRO_API_KEY=.*sk_|api_key.*sk_" references scripts tests docs .planning

Expected: no real user secret. Existing synthetic examples are acceptable only if already present and clearly fake.

  • Step 4: Commit or report uncommitted state

If asked to commit, stage only files from this plan. Otherwise leave changes unstaged and report exact files changed.