refactor strict evidence and extend parity layers
This commit is contained in:
@@ -105,7 +105,7 @@ def build_pyjhora_sample(sample, *, node_mode='mean'):
|
||||
swe = patch_swisseph()
|
||||
from jhora import utils, const
|
||||
from jhora.panchanga import drik
|
||||
from jhora.horoscope.chart import charts
|
||||
from jhora.horoscope.chart import ashtakavarga, charts, strength
|
||||
from jhora.horoscope.dhasa.graha import vimsottari
|
||||
|
||||
# Align benchmark口径: Lahiri + mean sidereal year. PyJHora default is TRUE_PUSHYA.
|
||||
@@ -126,8 +126,14 @@ def build_pyjhora_sample(sample, *, node_mode='mean'):
|
||||
current_jd = utils.julian_day_number((ty, tm, td), (0, 0, 0))
|
||||
|
||||
rasi = parse_chart_positions(charts.rasi_chart(jd, place))
|
||||
rasi_rows = charts.rasi_chart(jd, place)
|
||||
d2 = parse_chart_positions(charts.hora_chart(rasi_rows, chart_method=2))
|
||||
d4 = parse_chart_positions(charts.chaturthamsa_chart(rasi_rows, chart_method=1))
|
||||
d9 = parse_chart_positions(charts.divisional_chart(jd, place, divisional_chart_factor=9, chart_method=1))
|
||||
d10 = parse_chart_positions(charts.divisional_chart(jd, place, divisional_chart_factor=10, chart_method=1))
|
||||
house_to_planets = utils.get_house_planet_list_from_planet_positions(rasi_rows)
|
||||
bav, sav, _prastara = ashtakavarga.get_ashtaka_varga(house_to_planets)
|
||||
shadbala = strength.shad_bala(jd, place)
|
||||
|
||||
asc = rasi.get('Ascendant') or {}
|
||||
planets = {}
|
||||
@@ -174,6 +180,8 @@ def build_pyjhora_sample(sample, *, node_mode='mean'):
|
||||
'ayanamsa': 'LAHIRI',
|
||||
'd9_method': 'PyJHora divisional_chart chart_method=1',
|
||||
'd10_method': 'PyJHora divisional_chart chart_method=1',
|
||||
'd2_method': 'PyJHora hora_chart chart_method=2 traditional_parasara',
|
||||
'd4_method': 'PyJHora chaturthamsa_chart chart_method=1 traditional_parasara',
|
||||
'dasha_year': 'mean sidereal year',
|
||||
'compat': 'monkeypatch swisseph keyword API + missing constants; dummy timezonefinder only for import',
|
||||
'license_note': 'PyJHora is AGPL-3.0; used only as external benchmark, not vendored into skill.'
|
||||
@@ -183,7 +191,12 @@ def build_pyjhora_sample(sample, *, node_mode='mean'):
|
||||
'degree_in_sign': asc.get('degree_in_sign'),
|
||||
},
|
||||
'planets': planets,
|
||||
'varga': {'D9': d9, 'D10': d10},
|
||||
'varga': {'D2': d2, 'D4': d4, 'D9': d9, 'D10': d10},
|
||||
'ashtakavarga': {
|
||||
'bav': {name: list(bav[index]) for index, name in enumerate(['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Lagna'])},
|
||||
'sav': list(sav),
|
||||
},
|
||||
'shadbala': {name: float(shadbala[6][index]) for index, name in enumerate(['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'])},
|
||||
'dasha': dasha,
|
||||
}
|
||||
|
||||
@@ -234,7 +247,7 @@ def compare_one(sample_id, local, pyjhora):
|
||||
for field in ['sign', 'nakshatra', 'nakshatra_pada']:
|
||||
compare_scalar(rows, sample_id, 'planet', p, field, l.get(field), y.get(field))
|
||||
compare_scalar(rows, sample_id, 'planet', p, 'degree_in_sign', l.get('degree_in_sign'), y.get('degree_in_sign'), tolerance=0.15)
|
||||
for varga_name in ['D9', 'D10']:
|
||||
for varga_name in ['D2', 'D4', 'D9', 'D10']:
|
||||
for body in ['Ascendant'] + PLANETS:
|
||||
l = (local['varga'].get(varga_name) or {}).get(body) or {}
|
||||
y = (pyjhora['varga'].get(varga_name) or {}).get(body) or {}
|
||||
@@ -245,6 +258,24 @@ def compare_one(sample_id, local, pyjhora):
|
||||
pass
|
||||
compare_scalar(rows, sample_id, varga_name, body, 'sign', l.get('sign'), y.get('sign'), boundary_sensitive=boundary_sensitive)
|
||||
compare_scalar(rows, sample_id, varga_name, body, 'degree_in_sign', l.get('degree_in_sign'), y.get('degree_in_sign'), tolerance=0.2, boundary_sensitive=boundary_sensitive)
|
||||
for planet in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Lagna']:
|
||||
for sign_idx, sign in enumerate(SIGNS):
|
||||
compare_scalar(
|
||||
rows, sample_id, 'Ashtakavarga_BAV', planet, sign,
|
||||
(local.get('ashtakavarga', {}).get('bav', {}).get(planet) or [None] * 12)[sign_idx],
|
||||
(pyjhora.get('ashtakavarga', {}).get('bav', {}).get(planet) or [None] * 12)[sign_idx],
|
||||
)
|
||||
for sign_idx, sign in enumerate(SIGNS):
|
||||
compare_scalar(
|
||||
rows, sample_id, 'Ashtakavarga_SAV', 'SAV', sign,
|
||||
(local.get('ashtakavarga', {}).get('sav') or [None] * 12)[sign_idx],
|
||||
(pyjhora.get('ashtakavarga', {}).get('sav') or [None] * 12)[sign_idx],
|
||||
)
|
||||
for planet in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']:
|
||||
compare_scalar(
|
||||
rows, sample_id, 'Shadbala', planet, 'total_virupas',
|
||||
local.get('shadbala', {}).get(planet), pyjhora.get('shadbala', {}).get(planet), tolerance=0.5,
|
||||
)
|
||||
# PyJHora dasha is useful as external signal, but currently has different default starting convention/seed in some cases.
|
||||
# Keep fields in matrix, with generous date tolerance; differences are classified below in report.
|
||||
for field in ['mahadasha_lord', 'antardasha_lord']:
|
||||
@@ -330,6 +361,7 @@ def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description='Compare public benchmark samples against PyJHora.')
|
||||
parser.add_argument('--sample-id', action='append', default=[], help='Run only a named benchmark sample; repeatable.')
|
||||
parser.add_argument('--build-local', action='store_true', help='Explicitly generate missing local canonical baselines.')
|
||||
parser.add_argument('--refresh-local', action='store_true', help='Explicitly rebuild selected local canonical baselines.')
|
||||
parser.add_argument('--node-mode', choices=['mean', 'true'], default='mean', help='Match the node convention before comparing.')
|
||||
parser.add_argument('--output-prefix', default='', help='Optional filename prefix for resumable batch artifacts.')
|
||||
args = parser.parse_args(argv)
|
||||
@@ -344,7 +376,7 @@ def main(argv=None):
|
||||
all_rows = []
|
||||
for sample in samples:
|
||||
local_path = LOCAL_CANON / f"{sample['id']}.canonical.json"
|
||||
if not local_path.exists() and args.build_local:
|
||||
if (not local_path.exists() and args.build_local) or args.refresh_local:
|
||||
from run_skill_baseline import run_sample
|
||||
baseline = run_sample(sample)
|
||||
if not baseline.get('ok'):
|
||||
|
||||
@@ -80,6 +80,8 @@ def canonicalize(sample, data):
|
||||
planets = chart.get('planets', {})
|
||||
d9 = safe_get(modules, 'varga_full', 'D9_Navamsa', default={}) or {}
|
||||
d10 = safe_get(modules, 'varga_full', 'D10_Dasamsa', default={}) or {}
|
||||
d2 = safe_get(modules, 'varga_full', 'D2_Hora', default={}) or {}
|
||||
d4 = safe_get(modules, 'varga_full', 'D4_Turyamsa', default={}) or {}
|
||||
current = safe_get(modules, 'dasha', 'current_dasha', default={}) or {}
|
||||
ad = current.get('antardasha') or {}
|
||||
special = modules.get('special_lagnas', {}) or {}
|
||||
@@ -99,9 +101,16 @@ def canonicalize(sample, data):
|
||||
'ascendant': chart.get('ascendant'),
|
||||
'planets': {},
|
||||
'varga': {
|
||||
'D2': {k: d2.get(k) for k in ['Ascendant'] + PLANETS},
|
||||
'D4': {k: d4.get(k) for k in ['Ascendant'] + PLANETS},
|
||||
'D9': {k: d9.get(k) for k in ['Ascendant'] + PLANETS},
|
||||
'D10': {k: d10.get(k) for k in ['Ascendant'] + PLANETS},
|
||||
},
|
||||
'ashtakavarga': {
|
||||
'bav': {name: safe_get(modules, 'ashtakavarga', 'bav', name, 'bindus', default=[]) for name in PLANETS[:7] + ['Lagna']},
|
||||
'sav': [safe_get(modules, 'ashtakavarga', 'sav', 'scores', sign) for sign in ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']],
|
||||
},
|
||||
'shadbala': {name: safe_get(modules, 'shadbala', 'planets', name, 'total_virupas') for name in PLANETS[:7]},
|
||||
'dasha': {
|
||||
'mahadasha_lord': current.get('lord'),
|
||||
'mahadasha_start': current.get('start'),
|
||||
|
||||
@@ -97,9 +97,10 @@ For large architecture or release work, also read:
|
||||
| ERR-064 | Western cross-system support accepted external JSON but did not calculate a tropical natal chart from standard birth input, inviting agents to treat missing Western data as a completed cross-check. | mitigated 2026-07-14 | `western_chart_engine.py` computes Swiss Ephemeris tropical natal evidence for direct-chart/rectification only. It remains `partial` until separately materialized timing evidence exists; Prashna and external-payload precedence are regression-tested. |
|
||||
| ERR-065 | A newly added release-critical script can be absent from a zip before its first Git commit because the package enumerates tracked files only. | mitigated 2026-07-14 | List `scripts/western_chart_engine.py` in `REQUIRED_CONTRACTS`; the release-package test proves both editions include it before commit. |
|
||||
| ERR-066 | The pre-work fragment sweep invoked a retired `audit-capabilities --mode strict` contract, so governance tests and the mandatory preflight failed before real checks ran. | mitigated 2026-07-14 | Invoke the supported `--mode validate`; `tests/test_preflight_fragment_scan.py` and `pre_work_check.py` must remain green before substantive work. |
|
||||
| ERR-067 | A generic “Western timing” label can imply techniques that have not been computed. | mitigated 2026-07-15 | Native timing requires explicit transit, solar-return, secondary-progression, or solar-arc fields; boundaries still name unavailable progressed angles, converse, parans, midpoints, duration, and interpretation. |
|
||||
| ERR-067 | A generic “Western timing” label can imply techniques that have not been computed. | mitigated 2026-07-15 | Native timing requires explicit fields for each layer. Current native layers cover transit, solar-return, secondary-progression, solar-arc, converse, midpoint, lunar-return and daily duration scan; progressed angles, parans and interpretation remain explicitly blocked/partial. |
|
||||
| ERR-068 | PyJHora comparison reports hard-coded `2026-06-03` as generation time, making fresh external benchmark artifacts appear stale and weakening audit traceability. | mitigated 2026-07-15 | `write_report()` records an injected-or-current UTC ISO timestamp; keep the deterministic timestamp regression. |
|
||||
| ERR-069 | Yoga validation tests and helper runner still imported rules from a `.workbuddy` mirror, so full pytest could fail or silently validate a divergent checkout. | mitigated 2026-07-15 | Resolve repo root from each file location; retain runtime-boundary and focused Yoga regressions. |
|
||||
| ERR-070 | PyJHora parity for D2/D4/BAV/SAV can pass while Shadbala total virupas still mismatch, so a row-filled Shadbala oracle packet can be mistaken for absolute-value parity. | active external formula blocker | Keep `docs/research/pyjhora_d2_d4_ashtakavarga_shadbala_parity_2026_07_15.md`; do not claim Shadbala external absolute closure until component-level formulas reconcile with PyJHora/JHora raw values. |
|
||||
|
||||
## Fragment Sweep Command Set
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# PyJHora D2/D4/Ashtakavarga/Shadbala Parity Note 2026-07-15
|
||||
|
||||
## Scope
|
||||
|
||||
Same-chart replay for `smoke_beijing_1990_noon` after extending `run_pyjhora_compare.py`
|
||||
and local canonical generation to include D2, D4, BAV/SAV, and Shadbala totals.
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/jyotish/scripts/run_pyjhora_compare.py --sample-id smoke_beijing_1990_noon --refresh-local --output-prefix pyjhora_d4_fix
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
| Section | Rows | Status |
|
||||
|---|---:|---|
|
||||
| D2 | 20 | match |
|
||||
| D4 | 20 | match |
|
||||
| D9 | 20 | match |
|
||||
| D10 | 20 | match |
|
||||
| Ashtakavarga BAV | 96 | match |
|
||||
| Ashtakavarga SAV | 12 | match |
|
||||
| Shadbala total virupas | 7 | mismatch |
|
||||
|
||||
Total matrix: `232 match / 7 mismatch / 239 fields`.
|
||||
|
||||
The D4 mismatch was fixed by aligning local BPHS/Parashara Chaturthamsa to the
|
||||
PyJHora traditional mapping:
|
||||
|
||||
```text
|
||||
D4 target sign = natal sign + 3 * quarter_index
|
||||
```
|
||||
|
||||
## Shadbala Boundary
|
||||
|
||||
The remaining mismatch is not a missing row problem. It is an absolute-value
|
||||
formula mismatch between local `modules.shadbala.planets.*.total_virupas` and
|
||||
PyJHora `strength.shad_bala(...)[6]`.
|
||||
|
||||
Current boundary:
|
||||
|
||||
- D2/D4/D9/D10: externally replayed against PyJHora for the smoke chart.
|
||||
- Ashtakavarga BAV/SAV: externally replayed against PyJHora for the smoke chart.
|
||||
- Shadbala totals: available locally, but not PyJHora absolute-value parity.
|
||||
|
||||
Do not claim Shadbala external absolute closure from this replay until component
|
||||
level formula reconciliation is completed.
|
||||
+7
-9
@@ -716,12 +716,10 @@ def _execute_mcp_consultation_workflow(
|
||||
western_evidence_packet: dict[str, Any] | None = None,
|
||||
western_oracle_payload: dict[str, Any] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow
|
||||
from consultation_workflow_service import execute_consultation_workflow
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
result = execute_consultation_workflow(
|
||||
handler,
|
||||
body={
|
||||
{
|
||||
"question": question,
|
||||
"year": year,
|
||||
"month": month,
|
||||
@@ -4264,12 +4262,12 @@ def strict_workflow(
|
||||
result["chart"] = chart
|
||||
result["strict_workflow"] = _collect_strict_evidence(route, chart)
|
||||
try:
|
||||
from jyotish_api_server import JyotishAPIHandler
|
||||
from consultation_workflow_service import build_runtime_evidence_helpers
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
vedastro_official = handler._high_rigor_vedastro_official_summary(chart)
|
||||
vedastro_archive_manifest = handler._compute_vedastro_gateway_archives()
|
||||
interpretation_coverage = handler._interpretation_source_runtime_coverage(chart)
|
||||
runtime_helpers = build_runtime_evidence_helpers(chart)
|
||||
vedastro_official = runtime_helpers["vedastro_official"]
|
||||
vedastro_archive_manifest = runtime_helpers["vedastro_archive_manifest"]
|
||||
interpretation_coverage = runtime_helpers["interpretation_coverage"]
|
||||
machine_evidence_packet = _UNIFIED_CONSULTATION_ORCHESTRATOR.machine_evidence_packet(
|
||||
chart=chart,
|
||||
route_packet=result.get("routing") if isinstance(result.get("routing"), dict) else route_packet,
|
||||
|
||||
@@ -34,7 +34,14 @@ To add only requested native time evidence, pass:
|
||||
"transit_date": "2026-07-09",
|
||||
"solar_return_year": 2026,
|
||||
"secondary_progression_date": "2026-07-09",
|
||||
"solar_arc_date": "2026-07-09"
|
||||
"solar_arc_date": "2026-07-09",
|
||||
"converse_secondary_progression_date": "2026-07-09",
|
||||
"converse_solar_arc_date": "2026-07-09",
|
||||
"midpoint_date": "2026-07-09",
|
||||
"lunar_return_start_date": "2026-07-01",
|
||||
"duration_scan_start_date": "2026-07-01",
|
||||
"duration_scan_end_date": "2026-07-31",
|
||||
"parans_date": "2026-07-09"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -44,9 +51,16 @@ To add only requested native time evidence, pass:
|
||||
return chart at the supplied birthplace/location. Both are calculation data,
|
||||
not event verdicts. `secondary_progression_date` uses one ephemeris day per
|
||||
tropical year for progressed planets. `solar_arc_date` applies the true
|
||||
secondary-progressed-Sun arc to natal planets/ASC/MC. Both remain `partial`:
|
||||
progressed angles, converses, parans, midpoints, duration, and interpretation
|
||||
are not asserted.
|
||||
secondary-progressed-Sun arc to natal planets/ASC/MC.
|
||||
`converse_secondary_progression_date` and `converse_solar_arc_date` add the
|
||||
matching backward-progressed layers. `midpoint_date` emits natal midpoint
|
||||
geometry and transit midpoint conjunction/opposition hits. `lunar_return_start_date`
|
||||
finds the next exact tropical lunar return. `duration_scan_start_date` plus
|
||||
`duration_scan_end_date` groups daily transit-to-natal aspect windows.
|
||||
These remain evidence layers, not event verdicts. Progressed angles are marked
|
||||
blocked until a method is selected; `parans_date` currently returns a structured
|
||||
`blocked` state because a latitude-aware rising/setting/culminating solver is
|
||||
not yet implemented.
|
||||
|
||||
## Accepted Input
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared consultation workflow boundary for API and MCP callers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS_DIR = ROOT / "scripts"
|
||||
for path in (ROOT, SCRIPTS_DIR):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
|
||||
def execute_consultation_workflow(body: dict[str, Any], *, surface: str = "api") -> dict[str, Any]:
|
||||
from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow as _execute
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
return _execute(handler, body=body, surface=surface)
|
||||
|
||||
|
||||
def build_runtime_evidence_helpers(chart: dict[str, Any]) -> dict[str, Any]:
|
||||
from jyotish_api_server import JyotishAPIHandler
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
return {
|
||||
"vedastro_official": handler._high_rigor_vedastro_official_summary(chart),
|
||||
"vedastro_archive_manifest": handler._compute_vedastro_gateway_archives(),
|
||||
"interpretation_coverage": handler._interpretation_source_runtime_coverage(chart),
|
||||
}
|
||||
@@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from mcp_server import _existing_interpretation_source_pack # noqa: E402
|
||||
from scripts.strict_evidence_service import existing_interpretation_source_pack # noqa: E402
|
||||
|
||||
|
||||
REQUIRED_LAYERS = [
|
||||
@@ -100,7 +100,7 @@ CANDIDATE_KEYWORDS = [
|
||||
|
||||
|
||||
def build_report() -> dict[str, Any]:
|
||||
source_pack = _existing_interpretation_source_pack()
|
||||
source_pack = existing_interpretation_source_pack()
|
||||
inventory = source_pack.get("interpretation_source_inventory") if isinstance(source_pack, dict) else {}
|
||||
if not isinstance(inventory, dict):
|
||||
inventory = {}
|
||||
|
||||
@@ -204,6 +204,13 @@ def _western_evidence_packet_from_body(
|
||||
solar_return_year=timing_request.get('solar_return_year'),
|
||||
secondary_progression_date=timing_request.get('secondary_progression_date'),
|
||||
solar_arc_date=timing_request.get('solar_arc_date'),
|
||||
converse_secondary_progression_date=timing_request.get('converse_secondary_progression_date'),
|
||||
converse_solar_arc_date=timing_request.get('converse_solar_arc_date'),
|
||||
midpoint_date=timing_request.get('midpoint_date'),
|
||||
lunar_return_start_date=timing_request.get('lunar_return_start_date'),
|
||||
duration_scan_start_date=timing_request.get('duration_scan_start_date'),
|
||||
duration_scan_end_date=timing_request.get('duration_scan_end_date'),
|
||||
parans_date=timing_request.get('parans_date'),
|
||||
)
|
||||
if timing:
|
||||
packet['timing_techniques'] = timing
|
||||
@@ -211,7 +218,8 @@ def _western_evidence_packet_from_body(
|
||||
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
|
||||
packet['boundary'] = (
|
||||
'Native calculations include only explicitly requested transit, solar-return, secondary-progression, '
|
||||
'and solar-arc layers; they do not infer duration, outcomes, or interpretation.'
|
||||
'solar-arc, midpoint, lunar-return and daily duration-scan layers; parans remain blocked until a '
|
||||
'dedicated latitude-aware event solver is implemented. Outputs do not infer outcomes or interpretation.'
|
||||
)
|
||||
return packet
|
||||
except Exception as exc: # pragma: no cover - defensive boundary
|
||||
|
||||
@@ -1200,9 +1200,13 @@ def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, st
|
||||
monthly_frame = strict.get('monthly_adjudication_summary') if isinstance(strict, dict) else {}
|
||||
monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {}
|
||||
event_judgement = strict.get('event_judgement') if isinstance(strict, dict) else {}
|
||||
event_judgement = event_judgement if isinstance(event_judgement, dict) else {}
|
||||
adjudication = strict.get('adjudication_stages') if isinstance(strict, dict) else {}
|
||||
adjudication = adjudication if isinstance(adjudication, dict) else {}
|
||||
boundary_contract = strict.get('prediction_boundary_contract') if isinstance(strict, dict) else {}
|
||||
boundary_contract = boundary_contract if isinstance(boundary_contract, dict) else {}
|
||||
confidence_boundary = boundary_contract.get('confidence_boundary') if isinstance(boundary_contract, dict) else {}
|
||||
confidence_boundary = confidence_boundary if isinstance(confidence_boundary, dict) else {}
|
||||
confidence_cap = strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown'
|
||||
dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None
|
||||
|
||||
@@ -1657,11 +1661,21 @@ def _build_ai_prompt_pack(report):
|
||||
if isinstance(primary_strict_contract, dict)
|
||||
else {}
|
||||
)
|
||||
try:
|
||||
from strict_evidence_service import existing_interpretation_source_pack
|
||||
fallback_source_pack = existing_interpretation_source_pack()
|
||||
except Exception:
|
||||
fallback_source_pack = {}
|
||||
interpretation_source_audit = (
|
||||
primary_audit.get('interpretation_source_pack')
|
||||
if isinstance(primary_audit, dict) and isinstance(primary_audit.get('interpretation_source_pack'), dict)
|
||||
else {}
|
||||
)
|
||||
fallback_domain_layers = (
|
||||
fallback_source_pack.get('domain_invocation_layers')
|
||||
if isinstance(fallback_source_pack, dict) and isinstance(fallback_source_pack.get('domain_invocation_layers'), dict)
|
||||
else {}
|
||||
)
|
||||
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
|
||||
capability_evidence_pool = build_capability_evidence_pool_summary()
|
||||
|
||||
@@ -1743,7 +1757,7 @@ def _build_ai_prompt_pack(report):
|
||||
'missing_refs': interpretation_source_audit.get('missing_refs') or [],
|
||||
},
|
||||
'prediction_boundary_contract': primary_prediction_boundary_contract or {},
|
||||
'domain_invocation_layers': primary_domain_invocation_layers or {},
|
||||
'domain_invocation_layers': primary_domain_invocation_layers or fallback_domain_layers or {},
|
||||
'output_template_contract': primary_output_template_contract or {},
|
||||
'mevg_collection_queue': primary_mevg_collection_queue or {},
|
||||
'real_case_calibration_layer': primary_real_case_calibration_layer or {},
|
||||
@@ -2038,20 +2052,20 @@ def _attach_vedastro_official_full_snapshot(report, args):
|
||||
|
||||
def _load_strict_evidence_collector():
|
||||
try:
|
||||
from mcp_server import _collect_strict_evidence as collector
|
||||
from strict_evidence_service import collect_strict_evidence as collector
|
||||
return collector
|
||||
except Exception:
|
||||
mcp_path = os.path.join(ROOT_DIR, 'mcp_server.py')
|
||||
if not os.path.exists(mcp_path):
|
||||
service_path = os.path.join(SCRIPT_DIR, 'strict_evidence_service.py')
|
||||
if not os.path.exists(service_path):
|
||||
raise
|
||||
spec = importlib.util.spec_from_file_location("jyotish_root_mcp_server", mcp_path)
|
||||
spec = importlib.util.spec_from_file_location("jyotish_strict_evidence_service", service_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Unable to load mcp_server from {mcp_path}")
|
||||
raise ImportError(f"Unable to load strict_evidence_service from {service_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
collector = getattr(module, "_collect_strict_evidence", None)
|
||||
collector = getattr(module, "collect_strict_evidence", None)
|
||||
if collector is None:
|
||||
raise ImportError("mcp_server._collect_strict_evidence not found")
|
||||
raise ImportError("strict_evidence_service.collect_strict_evidence not found")
|
||||
return collector
|
||||
|
||||
|
||||
|
||||
@@ -200,19 +200,23 @@ def build_status(oracle_file: str) -> dict[str, Any]:
|
||||
"summary": {
|
||||
"shadbala_task_count": len(shadbala_tasks),
|
||||
"external_verified_shadbala_tasks": len(external_verified),
|
||||
"can_claim_shadbala_absolute_closure": True,
|
||||
"external_packet_fields_complete": True,
|
||||
"same_chart_parity_status": "blocked",
|
||||
"same_chart_parity_reason": "PyJHora same-chart replay still mismatches local Shadbala total virupas.",
|
||||
"can_claim_shadbala_absolute_closure": False,
|
||||
"production_tuning_allowed": False,
|
||||
"required_planets": REQUIRED_PLANETS,
|
||||
"required_components": REQUIRED_COMPONENTS,
|
||||
},
|
||||
"first_priority": None,
|
||||
"next_actions": [
|
||||
"Shadbala external absolute-value closure is complete for the current target set.",
|
||||
"Reconcile Shadbala component-level formulas against PyJHora/JHora same-chart raw values.",
|
||||
"Keep global calibration blocked until Tajika/Sahams and other oracle fronts pass validation.",
|
||||
],
|
||||
"boundary": (
|
||||
"This board isolates Shadbala absolute values. Dasha boundary dates are a separate closure task. "
|
||||
"Production tuning remains forbidden until external component-level evidence is complete."
|
||||
"Packet fields are complete for the current target set, but absolute closure remains blocked "
|
||||
"until same-chart parity passes."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -283,15 +287,17 @@ def render_markdown(report: dict[str, Any]) -> str:
|
||||
"",
|
||||
f"- shadbala_task_count: `{summary['shadbala_task_count']}`",
|
||||
f"- external_verified_shadbala_tasks: `{summary['external_verified_shadbala_tasks']}`",
|
||||
f"- external_packet_fields_complete: `{str(summary.get('external_packet_fields_complete', False)).lower()}`",
|
||||
f"- same_chart_parity_status: `{summary.get('same_chart_parity_status', 'not_checked')}`",
|
||||
f"- can_claim_shadbala_absolute_closure: `{str(summary['can_claim_shadbala_absolute_closure']).lower()}`",
|
||||
f"- production_tuning_allowed: `{str(summary['production_tuning_allowed']).lower()}`",
|
||||
"",
|
||||
]
|
||||
if first is None:
|
||||
lines.extend([
|
||||
"## Closure Complete",
|
||||
"## Packet Complete; Parity Blocked",
|
||||
"",
|
||||
"Shadbala external absolute-value closure is complete for the current target set.",
|
||||
"Shadbala external packet fields are complete for the current target set, but same-chart parity is still blocked.",
|
||||
"",
|
||||
"## Next Actions",
|
||||
"",
|
||||
|
||||
@@ -48,7 +48,7 @@ Do not add private birth data, API keys, or desktop oracle screenshots to this p
|
||||
如果没有 VedAstro official_raw_response,请标记 official_blocked 或 local_fallback。
|
||||
如果我提供西方占星导出,请作为 western_oracle_payload 进入统一主链,不要把单边西占信号说成双系统互证。
|
||||
如果我没有西占导出,请自动计算热带本命证据包(ASC/MC、宫位、主要相位、容许度),并明确它只完成本命层;流年、次限、太阳弧、日返仍须单独计算或导入。
|
||||
如需西占时间技术,请传 western_timing:`{"transit_date":"YYYY-MM-DD","solar_return_year":YYYY,"secondary_progression_date":"YYYY-MM-DD","solar_arc_date":"YYYY-MM-DD"}`;当前支持指定日 transit、精确太阳回归、次限行星与真实太阳弧;未实现的角度推进、converse、paran/midpoint 不得标成已用。
|
||||
如需西占时间技术,请传 western_timing:`{"transit_date":"YYYY-MM-DD","solar_return_year":YYYY,"secondary_progression_date":"YYYY-MM-DD","solar_arc_date":"YYYY-MM-DD","converse_secondary_progression_date":"YYYY-MM-DD","converse_solar_arc_date":"YYYY-MM-DD","midpoint_date":"YYYY-MM-DD","lunar_return_start_date":"YYYY-MM-DD","duration_scan_start_date":"YYYY-MM-DD","duration_scan_end_date":"YYYY-MM-DD","parans_date":"YYYY-MM-DD"}`;当前支持指定日 transit、精确太阳回归、次限行星、真实太阳弧、converse 次限/太阳弧、midpoints、月返和每日过境持续窗口;parans 与高级次限角度仍返回 blocked,不得标成已用。
|
||||
|
||||
## Highest Quality Mode
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stable strict-evidence service boundary.
|
||||
|
||||
This module is the import target for engine/API code. The current implementation
|
||||
delegates to the legacy MCP implementation while the large helper stack is being
|
||||
extracted out of `mcp_server.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
def collect_strict_evidence(route: str, result: dict[str, Any]) -> dict[str, Any]:
|
||||
from mcp_server import _collect_strict_evidence
|
||||
|
||||
return _collect_strict_evidence(route, result)
|
||||
|
||||
|
||||
def existing_interpretation_source_pack() -> dict[str, Any]:
|
||||
from mcp_server import _existing_interpretation_source_pack
|
||||
|
||||
return _existing_interpretation_source_pack()
|
||||
+1
-1
@@ -62,7 +62,7 @@ def varga_map(si, pi, div):
|
||||
o = _odd(si)
|
||||
if div==2: return (4 if o else 3) if pi==0 else (3 if o else 4)
|
||||
if div==3: return (si+pi*4)%12 # Drekkana: same → +4 → +8, no odd/even distinction
|
||||
if div==4: return (si+pi)%12 if o else (si+8+pi)%12
|
||||
if div==4: return (si+pi*3)%12
|
||||
if div==7: return (si+pi)%12 if o else (si+6+pi)%12
|
||||
if div==9:
|
||||
# BPHS Navamsa: movable=same, fixed=9th from sign (+8), dual=5th from sign (+4)
|
||||
|
||||
@@ -187,12 +187,231 @@ def calculate_solar_arc_directions(*, target_date: str, **birth: Any) -> dict[st
|
||||
}
|
||||
|
||||
|
||||
def calculate_converse_secondary_progressions(*, target_date: str, **birth: Any) -> dict[str, Any]:
|
||||
"""Calculate converse progressed planets using one ephemeris day per tropical year backward."""
|
||||
natal_chart = build_tropical_natal_chart(**birth)
|
||||
target_jd, local = _target_jd(target_date, birth["timezone"])
|
||||
birth_jd = _birth_jd(**birth)
|
||||
elapsed_years = (target_jd - birth_jd) / 365.242189
|
||||
progressed_jd = birth_jd - elapsed_years
|
||||
planets = _progressed_planets(progressed_jd)
|
||||
natal_points = {
|
||||
**natal_chart["natal"]["planets"],
|
||||
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
|
||||
"mc": natal_chart["natal"]["angles"]["mc"],
|
||||
}
|
||||
return {
|
||||
"technique": "converse_secondary_progressions",
|
||||
"status": "partial",
|
||||
"method": "one_ephemeris_day_per_tropical_year_backward",
|
||||
"target_date": target_date,
|
||||
"target_local_time": local.isoformat(),
|
||||
"elapsed_tropical_years": round(elapsed_years, 8),
|
||||
"progressed_julian_day_ut": round(progressed_jd, 8),
|
||||
"progressed_planets": planets,
|
||||
"aspects": _cross_aspects(planets, natal_points),
|
||||
"progressed_angles": {
|
||||
"status": "blocked",
|
||||
"reason": "Progressed angle method is not selected; quotidian/solar-arc/Naibod variants are not interchangeable.",
|
||||
},
|
||||
"boundary": "Converse progressed planets only; progressed angles and interpretation remain blocked until a method is selected.",
|
||||
}
|
||||
|
||||
|
||||
def calculate_converse_solar_arc_directions(*, target_date: str, **birth: Any) -> dict[str, Any]:
|
||||
"""Direct natal points backward by the converse secondary-progressed Sun arc."""
|
||||
natal_chart = build_tropical_natal_chart(**birth)
|
||||
progressions = calculate_converse_secondary_progressions(target_date=target_date, **birth)
|
||||
natal_sun = natal_chart["natal"]["planets"]["sun"]["longitude"]
|
||||
progressed_sun = progressions["progressed_planets"]["sun"]["longitude"]
|
||||
arc = _longitude(natal_sun - progressed_sun)
|
||||
natal_points = {
|
||||
**natal_chart["natal"]["planets"],
|
||||
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
|
||||
"mc": natal_chart["natal"]["angles"]["mc"],
|
||||
}
|
||||
directed = {name: _point(point["longitude"] - arc) for name, point in natal_points.items()}
|
||||
return {
|
||||
"technique": "converse_solar_arc_directions",
|
||||
"status": "partial",
|
||||
"method": "converse_secondary_progressed_sun_arc",
|
||||
"target_date": target_date,
|
||||
"natal_sun_longitude": natal_sun,
|
||||
"converse_progressed_sun_longitude": progressed_sun,
|
||||
"converse_solar_arc_degrees": round(arc, 8),
|
||||
"directed_points": directed,
|
||||
"aspects": _cross_aspects(directed, natal_points),
|
||||
"boundary": "Backward solar arc applied to natal planets/ASC/MC. Interpretation and parans remain separate audited layers.",
|
||||
}
|
||||
|
||||
|
||||
def _midpoint_longitude(first: float, second: float) -> float:
|
||||
diff = _longitude(second - first)
|
||||
if diff > 180.0:
|
||||
diff -= 360.0
|
||||
return _longitude(first + diff / 2.0)
|
||||
|
||||
|
||||
def calculate_midpoints(*, target_date: str | None = None, orb: float = 1.5, **birth: Any) -> dict[str, Any]:
|
||||
"""Calculate natal midpoint tree and optional transit conjunction/opposition hits."""
|
||||
natal_chart = build_tropical_natal_chart(**birth)
|
||||
natal_points = {
|
||||
**natal_chart["natal"]["planets"],
|
||||
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
|
||||
"mc": natal_chart["natal"]["angles"]["mc"],
|
||||
}
|
||||
names = [name for name in [*_PLANETS.keys(), "ascendant", "mc"] if name in natal_points]
|
||||
midpoints: dict[str, dict[str, Any]] = {}
|
||||
for index, first_name in enumerate(names):
|
||||
for second_name in names[index + 1:]:
|
||||
key = f"{first_name}/{second_name}"
|
||||
lon = _midpoint_longitude(natal_points[first_name]["longitude"], natal_points[second_name]["longitude"])
|
||||
midpoints[key] = _point(lon)
|
||||
result: dict[str, Any] = {
|
||||
"technique": "midpoints",
|
||||
"status": "used",
|
||||
"method": "shortest_arc_direct_midpoints",
|
||||
"orb_degrees": float(orb),
|
||||
"natal_midpoints": midpoints,
|
||||
"boundary": "Midpoint geometry only; hits are conjunction/opposition contacts, not interpretations.",
|
||||
}
|
||||
if target_date:
|
||||
transit = calculate_transit_to_natal(target_date=target_date, **birth)
|
||||
hits: list[dict[str, Any]] = []
|
||||
for transit_name, transit_point in transit["transit_planets"].items():
|
||||
for midpoint_name, midpoint in midpoints.items():
|
||||
separation = abs(transit_point["longitude"] - midpoint["longitude"])
|
||||
separation = min(separation, 360.0 - separation)
|
||||
for aspect, exact in {"conjunction": 0.0, "opposition": 180.0}.items():
|
||||
hit_orb = abs(separation - exact)
|
||||
if hit_orb <= orb:
|
||||
hits.append({
|
||||
"transit_planet": transit_name,
|
||||
"midpoint": midpoint_name,
|
||||
"aspect": aspect,
|
||||
"orb": round(hit_orb, 6),
|
||||
"separation": round(separation, 6),
|
||||
})
|
||||
result["target_date"] = target_date
|
||||
result["transit_midpoint_hits"] = sorted(hits, key=lambda row: (row["orb"], row["transit_planet"], row["midpoint"]))
|
||||
return result
|
||||
|
||||
|
||||
def calculate_lunar_return(*, start_date: str, **birth: Any) -> dict[str, Any]:
|
||||
"""Find the next exact tropical lunar return after a local start date."""
|
||||
natal_chart = build_tropical_natal_chart(**birth)
|
||||
natal_moon = natal_chart["natal"]["planets"]["moon"]["longitude"]
|
||||
start_jd, _ = _target_jd(start_date, birth["timezone"])
|
||||
return_jd = swe.mooncross_ut(natal_moon, start_jd, swe.FLG_SWIEPH)
|
||||
return_local = _jd_to_local(return_jd, birth["timezone"])
|
||||
return_birth = {
|
||||
**birth,
|
||||
"year": return_local.year,
|
||||
"month": return_local.month,
|
||||
"day": return_local.day,
|
||||
"hour": return_local.hour,
|
||||
"minute": return_local.minute,
|
||||
"second": return_local.second,
|
||||
}
|
||||
return_chart = build_tropical_natal_chart(**return_birth)
|
||||
returned_moon = return_chart["natal"]["planets"]["moon"]["longitude"]
|
||||
delta = abs(_longitude(returned_moon - natal_moon))
|
||||
delta = min(delta, 360.0 - delta)
|
||||
return {
|
||||
"technique": "lunar_return",
|
||||
"status": "used",
|
||||
"method": "Swiss Ephemeris mooncross_ut tropical longitude",
|
||||
"start_date": start_date,
|
||||
"return_julian_day_ut": round(return_jd, 8),
|
||||
"return_local_time": return_local.isoformat(),
|
||||
"natal_moon_longitude": natal_moon,
|
||||
"return_moon_longitude": returned_moon,
|
||||
"moon_longitude_delta": round(delta, 8),
|
||||
"return_chart": return_chart,
|
||||
"boundary": "Exact lunar return time and chart only; monthly topics require separate audited interpretation.",
|
||||
}
|
||||
|
||||
|
||||
def calculate_transit_duration_scan(*, start_date: str, end_date: str, max_days: int = 370, **birth: Any) -> dict[str, Any]:
|
||||
"""Scan daily transit-to-natal aspect activity and group consecutive windows."""
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
if end < start:
|
||||
raise ValueError("end_date must be on or after start_date")
|
||||
days = (end.date() - start.date()).days + 1
|
||||
if days > max_days:
|
||||
raise ValueError(f"duration scan range exceeds max_days={max_days}")
|
||||
daily_hits: list[dict[str, Any]] = []
|
||||
active: dict[tuple[str, str, str], dict[str, Any]] = {}
|
||||
windows: list[dict[str, Any]] = []
|
||||
for offset in range(days):
|
||||
current = (start + timedelta(days=offset)).date().isoformat()
|
||||
transit = calculate_transit_to_natal(target_date=current, **birth)
|
||||
keys = set()
|
||||
for aspect in transit["aspects"]:
|
||||
key = (aspect["transit_planet"], aspect["natal_point"], aspect["aspect"])
|
||||
keys.add(key)
|
||||
if key not in active:
|
||||
active[key] = {"start_date": current, "min_orb": aspect["orb"]}
|
||||
else:
|
||||
active[key]["min_orb"] = min(active[key]["min_orb"], aspect["orb"])
|
||||
for key in list(active):
|
||||
if key not in keys:
|
||||
row = active.pop(key)
|
||||
windows.append({
|
||||
"transit_planet": key[0],
|
||||
"natal_point": key[1],
|
||||
"aspect": key[2],
|
||||
"start_date": row["start_date"],
|
||||
"end_date": (start + timedelta(days=offset - 1)).date().isoformat(),
|
||||
"min_orb": round(row["min_orb"], 6),
|
||||
})
|
||||
daily_hits.append({"date": current, "hit_count": len(transit["aspects"]), "aspects": transit["aspects"]})
|
||||
final_date = end.date().isoformat()
|
||||
for key, row in active.items():
|
||||
windows.append({
|
||||
"transit_planet": key[0],
|
||||
"natal_point": key[1],
|
||||
"aspect": key[2],
|
||||
"start_date": row["start_date"],
|
||||
"end_date": final_date,
|
||||
"min_orb": round(row["min_orb"], 6),
|
||||
})
|
||||
return {
|
||||
"technique": "transit_duration_scan",
|
||||
"status": "used",
|
||||
"method": "daily local-midnight transit snapshots grouped into consecutive aspect windows",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"days_scanned": days,
|
||||
"daily_hits": daily_hits,
|
||||
"windows": sorted(windows, key=lambda row: (row["start_date"], row["min_orb"], row["transit_planet"])),
|
||||
"boundary": "Daily scan only; exact ingress/egress times require sub-daily root finding.",
|
||||
}
|
||||
|
||||
|
||||
def calculate_parans_status(*, target_date: str | None = None, **birth: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"technique": "parans",
|
||||
"status": "blocked",
|
||||
"target_date": target_date,
|
||||
"reason": "Parans need a dedicated rising/setting/culminating engine and latitude-aware event solver; not yet implemented in this repository.",
|
||||
}
|
||||
|
||||
|
||||
def build_timing_techniques(
|
||||
*,
|
||||
transit_date: str | None = None,
|
||||
solar_return_year: int | None = None,
|
||||
secondary_progression_date: str | None = None,
|
||||
solar_arc_date: str | None = None,
|
||||
converse_secondary_progression_date: str | None = None,
|
||||
converse_solar_arc_date: str | None = None,
|
||||
midpoint_date: str | None = None,
|
||||
lunar_return_start_date: str | None = None,
|
||||
duration_scan_start_date: str | None = None,
|
||||
duration_scan_end_date: str | None = None,
|
||||
parans_date: str | None = None,
|
||||
**birth: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Materialize only the requested, independently auditable timing layers."""
|
||||
@@ -207,4 +426,24 @@ def build_timing_techniques(
|
||||
)
|
||||
if solar_arc_date:
|
||||
techniques["solar_arc_directions"] = calculate_solar_arc_directions(target_date=solar_arc_date, **birth)
|
||||
if converse_secondary_progression_date:
|
||||
techniques["converse_secondary_progressions"] = calculate_converse_secondary_progressions(
|
||||
target_date=converse_secondary_progression_date, **birth
|
||||
)
|
||||
if converse_solar_arc_date:
|
||||
techniques["converse_solar_arc_directions"] = calculate_converse_solar_arc_directions(
|
||||
target_date=converse_solar_arc_date, **birth
|
||||
)
|
||||
if midpoint_date:
|
||||
techniques["midpoints"] = calculate_midpoints(target_date=midpoint_date, **birth)
|
||||
if lunar_return_start_date:
|
||||
techniques["lunar_return"] = calculate_lunar_return(start_date=lunar_return_start_date, **birth)
|
||||
if duration_scan_start_date and duration_scan_end_date:
|
||||
techniques["transit_duration_scan"] = calculate_transit_duration_scan(
|
||||
start_date=duration_scan_start_date,
|
||||
end_date=duration_scan_end_date,
|
||||
**birth,
|
||||
)
|
||||
if parans_date:
|
||||
techniques["parans"] = calculate_parans_status(target_date=parans_date, **birth)
|
||||
return techniques
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from benchmarks.jyotish.scripts.run_pyjhora_compare import write_report
|
||||
from benchmarks.jyotish.scripts.run_pyjhora_compare import compare_one
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -17,6 +18,7 @@ def test_pyjhora_compare_help_is_non_executing():
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "--build-local" in result.stdout
|
||||
assert "--refresh-local" in result.stdout
|
||||
assert "--output-prefix" in result.stdout
|
||||
assert "FileNotFoundError" not in result.stderr
|
||||
|
||||
@@ -30,3 +32,26 @@ def test_pyjhora_report_uses_supplied_utc_generation_timestamp():
|
||||
|
||||
assert "生成时间:2026-07-15T04:30:00+00:00" in report
|
||||
assert "生成时间:2026-06-03" not in report
|
||||
|
||||
|
||||
def test_pyjhora_comparison_includes_d2_d4_bav_sav_and_shadbala_rows():
|
||||
bodies = ["Ascendant", "Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
|
||||
chart = {body: {"sign": "Aries", "degree_in_sign": 1.0} for body in bodies}
|
||||
planet = {body: {"sign": "Aries", "degree_in_sign": 1.0, "nakshatra": "Ashwini", "nakshatra_pada": 1} for body in bodies[1:]}
|
||||
local = {
|
||||
"ascendant": chart["Ascendant"], "planets": planet,
|
||||
"varga": {"D2": chart, "D4": chart, "D9": chart, "D10": chart},
|
||||
"dasha": {},
|
||||
"ashtakavarga": {"bav": {name: [1] * 12 for name in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Lagna"]}, "sav": [7] * 12},
|
||||
"shadbala": {name: 100.0 for name in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]},
|
||||
}
|
||||
pyjhora = {
|
||||
"ascendant": chart["Ascendant"], "planets": planet,
|
||||
"varga": {"D2": chart, "D4": chart, "D9": chart, "D10": chart},
|
||||
"dasha": {},
|
||||
"ashtakavarga": local["ashtakavarga"], "shadbala": local["shadbala"],
|
||||
}
|
||||
|
||||
sections = {row["section"] for row in compare_one("fixture", local, pyjhora)}
|
||||
|
||||
assert {"D2", "D4", "Ashtakavarga_BAV", "Ashtakavarga_SAV", "Shadbala"} <= sections
|
||||
|
||||
@@ -43,3 +43,17 @@ def test_mcp_docstring_marks_workbuddy_as_distribution_mirror_not_runtime_source
|
||||
assert "/.workbuddy/skills/jyotish-vedic-astrology/mcp_server.py" not in text
|
||||
assert "distribution mirror" in text
|
||||
assert "reference only" in text
|
||||
|
||||
|
||||
def test_engine_does_not_import_mcp_server_for_strict_evidence() -> None:
|
||||
text = (ROOT / "scripts" / "jyotish_engine.py").read_text(encoding="utf-8", errors="ignore")
|
||||
assert "from mcp_server import" not in text
|
||||
assert "mcp_server.py" not in text
|
||||
assert "strict_evidence_service" in text
|
||||
|
||||
|
||||
def test_mcp_server_does_not_instantiate_api_handler_directly() -> None:
|
||||
text = (ROOT / "mcp_server.py").read_text(encoding="utf-8", errors="ignore")
|
||||
assert "from jyotish_api_server import" not in text
|
||||
assert "JyotishAPIHandler" not in text
|
||||
assert "consultation_workflow_service" in text
|
||||
|
||||
@@ -36,13 +36,15 @@ def test_shadbala_oracle_closure_status_identifies_first_absolute_value_packet()
|
||||
report = json.loads(completed.stdout)
|
||||
assert report["scope"] == "shadbala_external_absolute_value_closure_status"
|
||||
assert report["schema_version"] == 1
|
||||
assert report["summary"]["shadbala_task_count"] == 4
|
||||
assert report["summary"]["external_verified_shadbala_tasks"] == 4
|
||||
assert report["summary"]["can_claim_shadbala_absolute_closure"] is True
|
||||
assert report["summary"]["shadbala_task_count"] == 2
|
||||
assert report["summary"]["external_verified_shadbala_tasks"] == 2
|
||||
assert report["summary"]["external_packet_fields_complete"] is True
|
||||
assert report["summary"]["same_chart_parity_status"] == "blocked"
|
||||
assert report["summary"]["can_claim_shadbala_absolute_closure"] is False
|
||||
assert report["summary"]["required_planets"] == ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
|
||||
assert report["summary"]["required_components"] == ["sthana", "dig", "kala", "chesta", "naisargika", "drik", "total_rupa"]
|
||||
assert report["first_priority"] is None
|
||||
assert report["next_actions"][0] == "Shadbala external absolute-value closure is complete for the current target set."
|
||||
assert report["next_actions"][0] == "Reconcile Shadbala component-level formulas against PyJHora/JHora same-chart raw values."
|
||||
|
||||
|
||||
def test_shadbala_oracle_closure_status_markdown_can_be_written(tmp_path: Path) -> None:
|
||||
@@ -53,5 +55,6 @@ def test_shadbala_oracle_closure_status_markdown_can_be_written(tmp_path: Path)
|
||||
assert output.exists()
|
||||
markdown = output.read_text(encoding="utf-8")
|
||||
assert "# Shadbala External Absolute-Value Closure Status" in markdown
|
||||
assert "can_claim_shadbala_absolute_closure: `true`" in markdown
|
||||
assert "closure is complete for the current target set" in markdown
|
||||
assert "same_chart_parity_status: `blocked`" in markdown
|
||||
assert "can_claim_shadbala_absolute_closure: `false`" in markdown
|
||||
assert "same-chart parity is still blocked" in markdown
|
||||
|
||||
@@ -44,6 +44,13 @@ def drekkana_ref(lon: float) -> int:
|
||||
return (sign_index + part_index * 4) % 12
|
||||
|
||||
|
||||
def chaturthamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / (30 / 4))
|
||||
return (sign_index + part_index * 3) % 12
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_navamsa_matches_bphs_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 9)
|
||||
@@ -66,6 +73,20 @@ def test_drekkana_uses_same_plus_four_plus_eight(lon: float) -> None:
|
||||
assert result["sign_idx"] == drekkana_ref(lon)
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_chaturthamsa_matches_pyjhora_parashara_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 4)
|
||||
assert result["sign_idx"] == chaturthamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[chaturthamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
def test_chaturthamsa_reference_boundary_examples() -> None:
|
||||
assert [varga_map(0, part, 4) for part in range(4)] == [0, 3, 6, 9]
|
||||
assert [varga_map(8, part, 4) for part in range(4)] == [8, 11, 2, 5]
|
||||
assert [varga_map(11, part, 4) for part in range(4)] == [11, 2, 5, 8]
|
||||
|
||||
|
||||
def test_varga_map_boundary_examples() -> None:
|
||||
assert varga_map(0, 0, 9) == 0 # Aries Navamsa starts Aries
|
||||
assert varga_map(1, 0, 9) == 9 # Taurus Navamsa starts Capricorn (9th from sign)
|
||||
|
||||
@@ -4,9 +4,15 @@ from __future__ import annotations
|
||||
|
||||
from scripts.western_timing_engine import (
|
||||
build_timing_techniques,
|
||||
calculate_converse_secondary_progressions,
|
||||
calculate_converse_solar_arc_directions,
|
||||
calculate_lunar_return,
|
||||
calculate_midpoints,
|
||||
calculate_parans_status,
|
||||
calculate_secondary_progressions,
|
||||
calculate_solar_arc_directions,
|
||||
calculate_solar_return,
|
||||
calculate_transit_duration_scan,
|
||||
calculate_transit_to_natal,
|
||||
)
|
||||
|
||||
@@ -57,3 +63,62 @@ def test_solar_arc_uses_secondary_progressed_sun_arc() -> None:
|
||||
assert directions["method"] == "secondary_progressed_sun_arc"
|
||||
assert 0 < directions["solar_arc_degrees"] < 40
|
||||
assert directions["directed_points"]["sun"]["longitude"] != directions["natal_sun_longitude"]
|
||||
|
||||
|
||||
def test_converse_progressions_and_solar_arc_are_auditable() -> None:
|
||||
progressions = calculate_converse_secondary_progressions(**_BIRTH, target_date="2026-07-09")
|
||||
assert progressions["technique"] == "converse_secondary_progressions"
|
||||
assert progressions["progressed_angles"]["status"] == "blocked"
|
||||
directions = calculate_converse_solar_arc_directions(**_BIRTH, target_date="2026-07-09")
|
||||
assert directions["technique"] == "converse_solar_arc_directions"
|
||||
assert 0 < directions["converse_solar_arc_degrees"] < 40
|
||||
assert directions["directed_points"]["sun"]["longitude"] != directions["natal_sun_longitude"]
|
||||
|
||||
|
||||
def test_midpoints_emit_geometry_and_optional_transit_hits() -> None:
|
||||
midpoints = calculate_midpoints(**_BIRTH, target_date="2026-07-09")
|
||||
assert midpoints["technique"] == "midpoints"
|
||||
assert "sun/moon" in midpoints["natal_midpoints"]
|
||||
assert isinstance(midpoints["transit_midpoint_hits"], list)
|
||||
|
||||
|
||||
def test_lunar_return_calculates_next_exact_return_chart() -> None:
|
||||
lunar_return = calculate_lunar_return(**_BIRTH, start_date="2026-07-01")
|
||||
assert lunar_return["technique"] == "lunar_return"
|
||||
assert lunar_return["moon_longitude_delta"] < 0.01
|
||||
assert lunar_return["return_chart"]["natal"]["planets"]["moon"]["sign"]
|
||||
|
||||
|
||||
def test_transit_duration_scan_groups_daily_windows() -> None:
|
||||
scan = calculate_transit_duration_scan(**_BIRTH, start_date="2026-07-01", end_date="2026-07-03")
|
||||
assert scan["technique"] == "transit_duration_scan"
|
||||
assert scan["days_scanned"] == 3
|
||||
assert len(scan["daily_hits"]) == 3
|
||||
assert isinstance(scan["windows"], list)
|
||||
|
||||
|
||||
def test_parans_are_explicitly_blocked_until_solver_exists() -> None:
|
||||
parans = calculate_parans_status(**_BIRTH, target_date="2026-07-09")
|
||||
assert parans["technique"] == "parans"
|
||||
assert parans["status"] == "blocked"
|
||||
|
||||
|
||||
def test_timing_builder_can_emit_advanced_layers() -> None:
|
||||
timing = build_timing_techniques(
|
||||
**_BIRTH,
|
||||
converse_secondary_progression_date="2026-07-09",
|
||||
converse_solar_arc_date="2026-07-09",
|
||||
midpoint_date="2026-07-09",
|
||||
lunar_return_start_date="2026-07-01",
|
||||
duration_scan_start_date="2026-07-01",
|
||||
duration_scan_end_date="2026-07-02",
|
||||
parans_date="2026-07-09",
|
||||
)
|
||||
assert {
|
||||
"converse_secondary_progressions",
|
||||
"converse_solar_arc_directions",
|
||||
"midpoints",
|
||||
"lunar_return",
|
||||
"transit_duration_scan",
|
||||
"parans",
|
||||
} <= set(timing)
|
||||
|
||||
Reference in New Issue
Block a user