feat: import oracle collection and calibration parity

This commit is contained in:
732642856
2026-07-17 22:35:19 +08:00
parent ba9d43e128
commit 8c9b426344
18 changed files with 5344 additions and 24 deletions
@@ -0,0 +1,883 @@
{
"scope": "western_real_case_calibration",
"status": "calculated_not_predictive_validation",
"case_id": "jobs_iphone_2007",
"subject": "Steve Jobs",
"event": {
"date": "2007-01-09",
"type": "career_breakthrough",
"source": {
"url": "https://www.apple.com/newsroom/2007/01/09Apple-Reinvents-the-Phone-with-iPhone/",
"source_grade": "primary"
}
},
"birth_source": {
"url": "https://www.astro.com/adbvip/adbvip_02_24.htm",
"source_grade": "primary",
"time_accuracy_rating": "AA",
"evidence_basis": "BC/BR in hand"
},
"layers": {
"secondary_progressions": {
"technique": "secondary_progressions",
"status": "partial",
"method": "one_ephemeris_day_per_tropical_year",
"target_date": "2007-01-09",
"target_local_time": "2007-01-09T00:00:00-08:00",
"elapsed_tropical_years": 51.8729722,
"progressed_julian_day_ut": 2435215.50838887,
"natal_sun_longitude": 335.74814,
"progressed_planets": {
"sun": {
"longitude": 27.200726,
"sign": "Aries",
"degree_in_sign": 27.200726,
"speed_longitude": 0.97771866,
"retrograde": false
},
"moon": {
"longitude": 327.085209,
"sign": "Aquarius",
"degree_in_sign": 27.085209,
"speed_longitude": 13.22736915,
"retrograde": false
},
"mercury": {
"longitude": 21.467043,
"sign": "Aries",
"degree_in_sign": 21.467043,
"speed_longitude": 2.03176132,
"retrograde": false
},
"venus": {
"longitude": 352.062856,
"sign": "Pisces",
"degree_in_sign": 22.062856,
"speed_longitude": 1.19645701,
"retrograde": false
},
"mars": {
"longitude": 64.755496,
"sign": "Gemini",
"degree_in_sign": 4.755496,
"speed_longitude": 0.67328564,
"retrograde": false
},
"jupiter": {
"longitude": 111.475558,
"sign": "Cancer",
"degree_in_sign": 21.475558,
"speed_longitude": 0.09538143,
"retrograde": false
},
"saturn": {
"longitude": 229.414858,
"sign": "Scorpio",
"degree_in_sign": 19.414858,
"speed_longitude": -0.06655717,
"retrograde": true
},
"uranus": {
"longitude": 113.715663,
"sign": "Cancer",
"degree_in_sign": 23.715663,
"speed_longitude": 0.01464552,
"retrograde": false
},
"neptune": {
"longitude": 206.868278,
"sign": "Libra",
"degree_in_sign": 26.868278,
"speed_longitude": -0.02739973,
"retrograde": true
},
"pluto": {
"longitude": 144.383855,
"sign": "Leo",
"degree_in_sign": 24.383855,
"speed_longitude": -0.00882106,
"retrograde": true
},
"true_node": {
"longitude": 268.467552,
"sign": "Sagittarius",
"degree_in_sign": 28.467552,
"speed_longitude": -0.07938677,
"retrograde": true
}
},
"progressed_angles": {
"status": "used",
"method": "secondary_quotidian_progressed_date_same_location",
"progressed_local_time": "1955-04-17T16:12:04.798161-08:00",
"angles": {
"ascendant": {
"longitude": 176.691966,
"sign": "Virgo",
"degree_in_sign": 26.691966
},
"mc": {
"longitude": 86.278308,
"sign": "Gemini",
"degree_in_sign": 26.278308
},
"descendant": {
"longitude": 356.691966,
"sign": "Pisces",
"degree_in_sign": 26.691966
},
"ic": {
"longitude": 266.278308,
"sign": "Sagittarius",
"degree_in_sign": 26.278308
}
},
"boundary": "Quotidian progressed-date angles; Naibod and solar-arc angle variants are separate methods."
},
"aspects": [
{
"transit_planet": "mercury",
"natal_point": "mc",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 59.85056,
"orb": 0.14944,
"allowed_orb": 5.0
},
{
"transit_planet": "venus",
"natal_point": "ascendant",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 179.768817,
"orb": 0.231183,
"allowed_orb": 5.0
},
{
"transit_planet": "mercury",
"natal_point": "venus",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 90.295161,
"orb": 0.295161,
"allowed_orb": 6.0
},
{
"transit_planet": "jupiter",
"natal_point": "venus",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 179.696324,
"orb": 0.303676,
"allowed_orb": 6.0
},
{
"transit_planet": "jupiter",
"natal_point": "saturn",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 119.687136,
"orb": 0.312864,
"allowed_orb": 6.0
},
{
"transit_planet": "true_node",
"natal_point": "neptune",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 60.416254,
"orb": 0.416254,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "uranus",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 0.419243,
"orb": 0.419243,
"allowed_orb": 6.0
},
{
"transit_planet": "true_node",
"natal_point": "mars",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 120.622924,
"orb": 0.622924,
"allowed_orb": 6.0
},
{
"transit_planet": "venus",
"natal_point": "mc",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 89.254747,
"orb": 0.745253,
"allowed_orb": 5.0
},
{
"transit_planet": "jupiter",
"natal_point": "ascendant",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 60.818481,
"orb": 0.818481,
"allowed_orb": 5.0
},
{
"transit_planet": "sun",
"natal_point": "neptune",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 179.149428,
"orb": 0.850572,
"allowed_orb": 6.0
},
{
"transit_planet": "venus",
"natal_point": "venus",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 60.890974,
"orb": 0.890974,
"allowed_orb": 6.0
},
{
"transit_planet": "venus",
"natal_point": "saturn",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 120.900162,
"orb": 0.900162,
"allowed_orb": 6.0
},
{
"transit_planet": "pluto",
"natal_point": "pluto",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 0.938933,
"orb": 0.938933,
"allowed_orb": 6.0
},
{
"transit_planet": "mercury",
"natal_point": "jupiter",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 89.040846,
"orb": 0.959154,
"allowed_orb": 6.0
},
{
"transit_planet": "moon",
"natal_point": "neptune",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 119.033911,
"orb": 0.966089,
"allowed_orb": 6.0
},
{
"transit_planet": "jupiter",
"natal_point": "jupiter",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 0.967669,
"orb": 0.967669,
"allowed_orb": 6.0
},
{
"transit_planet": "mars",
"natal_point": "sun",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 89.007356,
"orb": 0.992644,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "jupiter",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 118.906969,
"orb": 1.093031,
"allowed_orb": 6.0
},
{
"transit_planet": "neptune",
"natal_point": "neptune",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 1.18302,
"orb": 1.18302,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "ascendant",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 58.578376,
"orb": 1.421624,
"allowed_orb": 5.0
},
{
"transit_planet": "neptune",
"natal_point": "pluto",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 61.54549,
"orb": 1.54549,
"allowed_orb": 6.0
},
{
"transit_planet": "venus",
"natal_point": "jupiter",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 118.445033,
"orb": 1.554967,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "saturn",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 1.747836,
"orb": 1.747836,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "venus",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 61.757024,
"orb": 1.757024,
"allowed_orb": 6.0
},
{
"transit_planet": "moon",
"natal_point": "pluto",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 178.237579,
"orb": 1.762421,
"allowed_orb": 6.0
},
{
"transit_planet": "sun",
"natal_point": "pluto",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 118.122062,
"orb": 1.877938,
"allowed_orb": 6.0
},
{
"transit_planet": "sun",
"natal_point": "mars",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 1.88975,
"orb": 1.88975,
"allowed_orb": 6.0
},
{
"transit_planet": "moon",
"natal_point": "mars",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 62.005267,
"orb": 2.005267,
"allowed_orb": 6.0
},
{
"transit_planet": "venus",
"natal_point": "uranus",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 122.07205,
"orb": 2.07205,
"allowed_orb": 6.0
},
{
"transit_planet": "neptune",
"natal_point": "mars",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 177.777802,
"orb": 2.222198,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "venus",
"aspect": "opposition",
"exact_degrees": 180.0,
"separation": 177.456219,
"orb": 2.543781,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "saturn",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 117.447031,
"orb": 2.552969,
"allowed_orb": 6.0
},
{
"transit_planet": "jupiter",
"natal_point": "uranus",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 2.659348,
"orb": 2.659348,
"allowed_orb": 6.0
},
{
"transit_planet": "mercury",
"natal_point": "uranus",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 92.667863,
"orb": 2.667863,
"allowed_orb": 6.0
},
{
"transit_planet": "neptune",
"natal_point": "uranus",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 92.733372,
"orb": 2.733372,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "ascendant",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 57.120819,
"orb": 2.879181,
"allowed_orb": 5.0
},
{
"transit_planet": "mars",
"natal_point": "moon",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 57.008321,
"orb": 2.991679,
"allowed_orb": 6.0
},
{
"transit_planet": "sun",
"natal_point": "uranus",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 86.93418,
"orb": 3.06582,
"allowed_orb": 6.0
},
{
"transit_planet": "pluto",
"natal_point": "mc",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 63.066252,
"orb": 3.066252,
"allowed_orb": 5.0
},
{
"transit_planet": "true_node",
"natal_point": "pluto",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 123.144764,
"orb": 3.144764,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "jupiter",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 3.207774,
"orb": 3.207774,
"allowed_orb": 6.0
},
{
"transit_planet": "pluto",
"natal_point": "saturn",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 86.778839,
"orb": 3.221161,
"allowed_orb": 6.0
},
{
"transit_planet": "pluto",
"natal_point": "neptune",
"aspect": "sextile",
"exact_degrees": 60.0,
"separation": 63.667443,
"orb": 3.667443,
"allowed_orb": 6.0
},
{
"transit_planet": "mercury",
"natal_point": "pluto",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 123.855745,
"orb": 3.855745,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "neptune",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 94.335635,
"orb": 4.335635,
"allowed_orb": 6.0
},
{
"transit_planet": "pluto",
"natal_point": "mars",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 115.293379,
"orb": 4.706621,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "uranus",
"aspect": "trine",
"exact_degrees": 120.0,
"separation": 115.279952,
"orb": 4.720048,
"allowed_orb": 6.0
},
{
"transit_planet": "true_node",
"natal_point": "true_node",
"aspect": "conjunction",
"exact_degrees": 0.0,
"separation": 4.943849,
"orb": 4.943849,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "mercury",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 84.946868,
"orb": 5.053132,
"allowed_orb": 6.0
},
{
"transit_planet": "uranus",
"natal_point": "mars",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 84.625187,
"orb": 5.374813,
"allowed_orb": 6.0
},
{
"transit_planet": "neptune",
"natal_point": "venus",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 84.303604,
"orb": 5.696396,
"allowed_orb": 6.0
},
{
"transit_planet": "saturn",
"natal_point": "pluto",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 84.09207,
"orb": 5.90793,
"allowed_orb": 6.0
},
{
"transit_planet": "moon",
"natal_point": "saturn",
"aspect": "square",
"exact_degrees": 90.0,
"separation": 95.922515,
"orb": 5.922515,
"allowed_orb": 6.0
}
],
"boundary": "Progressed planets plus explicitly selected quotidian angles. Lunar phases, stations, duration, and interpretation remain separate audited layers."
},
"parans": {
"technique": "parans",
"status": "used",
"target_date": "2007-01-09",
"method": "Swiss Ephemeris rise_trans latitude-aware angular events",
"event_count": 33,
"events": [
{
"planet": "saturn",
"angle": "upper_culmination",
"julian_day_ut": 2454109.94604537,
"local_time": "2007-01-09T02:42:18.320357-08:00"
},
{
"planet": "jupiter",
"angle": "rise",
"julian_day_ut": 2454110.02462397,
"local_time": "2007-01-09T04:35:27.510729-08:00"
},
{
"planet": "moon",
"angle": "upper_culmination",
"julian_day_ut": 2454110.03029454,
"local_time": "2007-01-09T04:43:37.448205-08:00"
},
{
"planet": "pluto",
"angle": "rise",
"julian_day_ut": 2454110.06527459,
"local_time": "2007-01-09T05:33:59.724429-08:00"
},
{
"planet": "mars",
"angle": "rise",
"julian_day_ut": 2454110.07489361,
"local_time": "2007-01-09T05:47:50.807509-08:00"
},
{
"planet": "sun",
"angle": "rise",
"julian_day_ut": 2454110.14259961,
"local_time": "2007-01-09T07:25:20.606518-08:00"
},
{
"planet": "mercury",
"angle": "rise",
"julian_day_ut": 2454110.15358739,
"local_time": "2007-01-09T07:41:09.950433-08:00"
},
{
"planet": "venus",
"angle": "rise",
"julian_day_ut": 2454110.19141445,
"local_time": "2007-01-09T08:35:38.208827-08:00"
},
{
"planet": "neptune",
"angle": "rise",
"julian_day_ut": 2454110.21153608,
"local_time": "2007-01-09T09:04:36.717483-08:00"
},
{
"planet": "jupiter",
"angle": "upper_culmination",
"julian_day_ut": 2454110.22767351,
"local_time": "2007-01-09T09:27:50.991352-08:00"
},
{
"planet": "saturn",
"angle": "set",
"julian_day_ut": 2454110.22986714,
"local_time": "2007-01-09T09:31:00.520680-08:00"
},
{
"planet": "uranus",
"angle": "rise",
"julian_day_ut": 2454110.25682944,
"local_time": "2007-01-09T10:09:50.063788-08:00"
},
{
"planet": "true_node",
"angle": "rise",
"julian_day_ut": 2454110.26490969,
"local_time": "2007-01-09T10:21:28.196996-08:00"
},
{
"planet": "mars",
"angle": "upper_culmination",
"julian_day_ut": 2454110.27172895,
"local_time": "2007-01-09T10:31:17.381368-08:00"
},
{
"planet": "pluto",
"angle": "upper_culmination",
"julian_day_ut": 2454110.27983419,
"local_time": "2007-01-09T10:42:57.673969-08:00"
},
{
"planet": "moon",
"angle": "set",
"julian_day_ut": 2454110.28420722,
"local_time": "2007-01-09T10:49:15.504146-08:00"
},
{
"planet": "sun",
"angle": "upper_culmination",
"julian_day_ut": 2454110.34500033,
"local_time": "2007-01-09T12:16:48.028382-08:00"
},
{
"planet": "mercury",
"angle": "upper_culmination",
"julian_day_ut": 2454110.35064807,
"local_time": "2007-01-09T12:24:55.993106-08:00"
},
{
"planet": "venus",
"angle": "upper_culmination",
"julian_day_ut": 2454110.39828854,
"local_time": "2007-01-09T13:33:32.129903-08:00"
},
{
"planet": "neptune",
"angle": "upper_culmination",
"julian_day_ut": 2454110.42850297,
"local_time": "2007-01-09T14:17:02.656278-08:00"
},
{
"planet": "jupiter",
"angle": "set",
"julian_day_ut": 2454110.43069242,
"local_time": "2007-01-09T14:20:11.825158-08:00"
},
{
"planet": "mars",
"angle": "set",
"julian_day_ut": 2454110.46852186,
"local_time": "2007-01-09T15:14:40.288339-08:00"
},
{
"planet": "uranus",
"angle": "upper_culmination",
"julian_day_ut": 2454110.49125881,
"local_time": "2007-01-09T15:47:24.760886-08:00"
},
{
"planet": "pluto",
"angle": "set",
"julian_day_ut": 2454110.49439241,
"local_time": "2007-01-09T15:51:55.504177-08:00"
},
{
"planet": "true_node",
"angle": "upper_culmination",
"julian_day_ut": 2454110.50587744,
"local_time": "2007-01-09T16:08:27.811086-08:00"
},
{
"planet": "sun",
"angle": "set",
"julian_day_ut": 2454110.5475517,
"local_time": "2007-01-09T17:08:28.467114-08:00"
},
{
"planet": "mercury",
"angle": "set",
"julian_day_ut": 2454110.54793949,
"local_time": "2007-01-09T17:09:01.972344-08:00"
},
{
"planet": "venus",
"angle": "set",
"julian_day_ut": 2454110.6054813,
"local_time": "2007-01-09T18:31:53.584685-08:00"
},
{
"planet": "neptune",
"angle": "set",
"julian_day_ut": 2454110.6454801,
"local_time": "2007-01-09T19:29:29.481048-08:00"
},
{
"planet": "saturn",
"angle": "rise",
"julian_day_ut": 2454110.65931501,
"local_time": "2007-01-09T19:49:24.817192-08:00"
},
{
"planet": "uranus",
"angle": "set",
"julian_day_ut": 2454110.72570392,
"local_time": "2007-01-09T21:25:00.818990-08:00"
},
{
"planet": "true_node",
"angle": "set",
"julian_day_ut": 2454110.74685172,
"local_time": "2007-01-09T21:55:27.988239-08:00"
},
{
"planet": "moon",
"angle": "rise",
"julian_day_ut": 2454110.80992635,
"local_time": "2007-01-09T23:26:17.637054-08:00"
}
],
"paran_pairs_within_4_minutes": [
{
"first": {
"planet": "jupiter",
"angle": "upper_culmination",
"local_time": "2007-01-09T09:27:50.991352-08:00"
},
"second": {
"planet": "saturn",
"angle": "set",
"local_time": "2007-01-09T09:31:00.520680-08:00"
},
"separation_minutes": 3.1588
},
{
"first": {
"planet": "neptune",
"angle": "upper_culmination",
"local_time": "2007-01-09T14:17:02.656278-08:00"
},
"second": {
"planet": "jupiter",
"angle": "set",
"local_time": "2007-01-09T14:20:11.825158-08:00"
},
"separation_minutes": 3.1528
},
{
"first": {
"planet": "sun",
"angle": "set",
"local_time": "2007-01-09T17:08:28.467114-08:00"
},
"second": {
"planet": "mercury",
"angle": "set",
"local_time": "2007-01-09T17:09:01.972344-08:00"
},
"separation_minutes": 0.5584
}
],
"boundary": "Geometric rise/culmination/set simultaneity only; no interpretation or predictive claim is inferred."
}
},
"summary": {
"progressed_aspect_count": 54,
"paran_event_count": 33,
"paran_pair_count": 3
},
"boundary": "Known-event geometry replay only. One positive case cannot establish specificity, false-positive rate, or predictive accuracy."
}
@@ -0,0 +1,26 @@
# Prashna Gulika/Sphuta PyJHora Parity
## Scope
One public synthetic daytime chart: Beijing, 1990-01-01 12:00, UTC+8,
Lahiri, Mean Node. PyJHora 4.8.7 is used only as an AGPL external black-box
benchmark.
## Result
| Point | Local | PyJHora | Absolute delta |
|---|---:|---:|---:|
| Gulika | 22.154273° | 21.867466° | 0.286807° |
| Tri Sphuta | 315.345416° | 315.052227° | 0.293189° |
| Chatur Sphuta | 212.102430° | 211.811742° | 0.290688° |
| Pancha Sphuta | 146.837653° | 146.543676° | 0.293977° |
All rows pass the declared 0.5° tolerance. The local Gulika method now uses
the start of Saturn's share in eight equal day/night parts. The historical
Ghatika-end variant remains available as `legacy_ghatika_end`.
## Boundary
This closes one daytime synthetic comparison only. Night charts, all weekdays,
high-latitude sunrise behavior, and additional locations remain untested. No
Prashna verdict, health claim, or production tuning is unlocked by this packet.
@@ -189,6 +189,24 @@
"path": "references/real_case_calibration/day_level_holdout_v3_preregistration.json",
"sha256": "d7f794630d3def6f9f7b5d94c4365bb508c60a8b44228f35a2d2d4548ea9f360",
"scope": "independent-label day-level holdout preregistration"
},
{
"id": "prashna_sphuta_pyjhora_public_smoke",
"path": "references/oracle/prashna_sphuta_pyjhora_public_smoke.json",
"sha256": "d939a6abf5862bb5ebe600e09149e99042ddc46937fd585cf839f7596d3abaa2",
"scope": "public synthetic Prashna Sphuta PyJHora numeric smoke parity"
},
{
"id": "western_jobs_iphone_calibration",
"path": "docs/benchmark/western_jobs_iphone_2007_calibration.json",
"sha256": "16dda7b6fe4f4df18254edf97b400358a78e44370bc2ca13501084df87fa1a91",
"scope": "public Western progressed-angle and paran geometry replay"
},
{
"id": "month_nearest_negative_controls",
"path": "docs/benchmark/public_real_case_negative_controls_holdout_v2_month_nearest_20260717.json",
"sha256": "cef9e9c9dccb202ce8909d63d8fccf6c2699cf7a2c28e30b58c42e04b5e2ba2c",
"scope": "public month-nearest negative-control evidence, not tuning data"
}
]
}
@@ -0,0 +1,42 @@
{
"schema_version": 1,
"case_id": "synthetic_beijing_1990_noon_lahiri_mean",
"status": "local_parity_verified_single_case",
"birth_data_policy": "public_synthetic_smoke_only",
"engine": {
"name": "PyJHora",
"version": "4.8.7",
"license_boundary": "AGPL external benchmark only; no source code vendored"
},
"settings": {
"ayanamsa": "lahiri",
"node_mode": "mean",
"timezone": 8,
"latitude": 39.9042,
"longitude": 116.4074,
"date": "1990-01-01",
"time": "12:00:00"
},
"outputs": {
"gulika": {"sign_index": 0, "sign": "Aries", "degree_in_sign": 21.86746645438376},
"kunda_lagna": {"sign_index": 3, "sign": "Cancer", "degree_in_sign": 27.444477539207583},
"tri_sphuta": {"sign_index": 10, "sign": "Aquarius", "degree_in_sign": 15.052227332937264},
"chatur_sphuta": {"sign_index": 7, "sign": "Scorpio", "degree_in_sign": 1.8117419385885114},
"pancha_sphuta": {"sign_index": 4, "sign": "Leo", "degree_in_sign": 26.543675779058276}
},
"capture": {
"adapter": "PyJHora black-box calls via drik.gulika_longitude, drik.kunda_lagna and horoscope.chart.sphuta",
"captured_at": "2026-07-17",
"raw_response_available": true
},
"local_comparison": {
"tolerance_degrees": 0.5,
"status": "pass",
"gulika_local_longitude": 22.154273227231883,
"gulika_absolute_delta_degrees": 0.286806772848123,
"tri_sphuta_absolute_delta_degrees": 0.2931885210780365,
"chatur_sphuta_absolute_delta_degrees": 0.290688097340535,
"pancha_sphuta_absolute_delta_degrees": 0.29397742371492086
},
"boundary": "One public synthetic daytime case passes local numeric comparison within 0.5 degrees. Nighttime, weekday, latitude, and additional-location cases remain required before verdict or production tuning can be unlocked."
}
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""Collect bounded, secret-free VedAstro HTTP raw for divisional parity."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
from typing import Any
try:
from scripts import vedastro_service_adapter as adapter
except ModuleNotFoundError: # pragma: no cover - direct script execution
import vedastro_service_adapter as adapter
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
VARGA_FIELDS = {
"D2": "PlanetHoraD2Signs",
"D4": "PlanetChaturthamshaD4Sign",
"D9": "PlanetNavamshaD9Sign",
"D10": "PlanetDashamamshaD10Sign",
}
SHADBALA_COMPONENT_FIELDS = [
"PlanetSthanaBala",
"PlanetDigBala",
"PlanetKalaBala",
"PlanetChestaBala",
"PlanetNaisargikaBala",
"PlanetDrikBala",
]
def _hash(payload: Any) -> str:
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
def collect_chart_core(
case: dict[str, Any],
*,
case_id: str,
planets: list[str],
output_path: Path,
) -> dict[str, Any]:
endpoint = os.environ.get("VEDASTRO_API_ENDPOINT", "").strip()
if not endpoint:
raise RuntimeError("VEDASTRO_API_ENDPOINT is not configured")
manifest = adapter._official_full_snapshot_manifest(case, case_id)
chart_request = next(item for item in manifest["requests"] if item["section"] == "chart_core")
raw_responses: dict[str, Any] = {}
statuses: dict[str, str] = {}
scalar_responses: dict[str, Any] = {}
scalar_statuses: dict[str, str] = {}
component_responses: dict[str, Any] = {}
component_statuses: dict[str, str] = {}
retry_error_codes: list[int] = []
attempt_count = 0
for planet in planets:
request_item = {**chart_request, "fanout_value": planet}
try:
payload, attempts, retries = adapter._post_official_snapshot_section(endpoint, request_item)
raw_responses[planet] = payload
statuses[planet] = adapter._payload_status(payload)
attempt_count += attempts
retry_error_codes.extend(retries)
except Exception as exc: # Preserve partial raw instead of discarding the batch.
statuses[planet] = f"{type(exc).__name__}:{str(exc)[:200]}"
common_body = adapter._official_common_body(case)
scalar_requests = {
"shadbala": {
"section": "shadbala",
"endpoint_path": "/Calculate/AllPlanetStrength",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": common_body,
"calculator_name": "AllPlanetStrength",
},
"ashtakavarga_sav": {
"section": "ashtakavarga_sav",
"endpoint_path": "/Calculate/AshtakvargaLifeMap",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": {"birthTime": common_body["time"], "Ayanamsa": common_body["Ayanamsa"]},
"calculator_name": "AshtakvargaLifeMap",
},
"ashtakavarga_bav": {
"section": "ashtakavarga_bav",
"endpoint_path": "/Calculate/BhinnashtakavargaChart",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": {"birthTime": common_body["time"], "Ayanamsa": common_body["Ayanamsa"]},
"calculator_name": "BhinnashtakavargaChart",
},
"ashtakavarga_sav_chart": {
"section": "ashtakavarga_sav_chart",
"endpoint_path": "/Calculate/SarvashtakavargaChart",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": {"birthTime": common_body["time"], "Ayanamsa": common_body["Ayanamsa"]},
"calculator_name": "SarvashtakavargaChart",
},
}
for section, request_item in scalar_requests.items():
try:
payload, attempts, retries = adapter._post_official_snapshot_section(endpoint, request_item)
scalar_responses[section] = payload
scalar_statuses[section] = adapter._payload_status(payload)
attempt_count += attempts
retry_error_codes.extend(retries)
except Exception as exc:
scalar_statuses[section] = f"{type(exc).__name__}:{str(exc)[:200]}"
for planet in planets:
key = f"{planet}.chesta"
request_item = {
"section": "shadbala_chesta",
"endpoint_path": "/Calculate/PlanetChestaBala",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": {
"planetName": {"Name": planet},
"time": common_body["time"],
"useSpecialSunMoon": False,
"Ayanamsa": common_body["Ayanamsa"],
},
"calculator_name": "PlanetChestaBala",
}
try:
payload, attempts, retries = adapter._post_official_snapshot_section(endpoint, request_item)
component_responses[key] = payload
component_statuses[key] = adapter._payload_status(payload)
attempt_count += attempts
retry_error_codes.extend(retries)
except Exception as exc:
component_statuses[key] = f"{type(exc).__name__}:{str(exc)[:200]}"
settings = {
"case_id": case_id,
"birth": {key: case.get(key) for key in ("year", "month", "day", "hour", "minute", "second", "lat", "lon", "tz")},
"ayanamsa_policy": case.get("ayanamsa_policy") or case.get("ayanamsa") or "lahiri",
"node_policy": case.get("node_policy") or case.get("node_mode") or "mean",
"endpoint_host": adapter._endpoint_host(endpoint),
"calculator": "AllPlanetData",
"planets": planets,
}
packet = {
"scope": "vedastro_official_http_divisional_parity_raw",
"status": "ok"
if statuses and scalar_statuses and component_statuses and all(value == "ok" for value in [*statuses.values(), *scalar_statuses.values(), *component_statuses.values()])
else "partial",
"source": "vedastro_official_http",
"settings": settings,
"coverage": {
"sections": [
*VARGA_FIELDS,
"shadbala_total",
"shadbala_components",
"ashtakavarga_bav",
"ashtakavarga_sav",
],
"field_map": VARGA_FIELDS,
"shadbala_component_fields": SHADBALA_COMPONENT_FIELDS,
"blocked_sections": [],
},
"fanout_statuses": dict(sorted(statuses.items())),
"scalar_statuses": dict(sorted(scalar_statuses.items())),
"component_statuses": dict(sorted(component_statuses.items())),
"attempt_count": attempt_count,
"retry_error_codes": retry_error_codes,
"raw_responses": raw_responses,
"scalar_responses": scalar_responses,
"component_responses": component_responses,
}
packet["response_hash"] = _hash({"chart_core": raw_responses, "scalar": scalar_responses, "components": component_responses})
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(packet, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
return packet
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case", choices=sorted(adapter.PARITY_CASES), default="beijing_first_use_demo")
parser.add_argument("--planets", default=",".join(DEFAULT_PLANETS))
parser.add_argument("--output", type=Path)
args = parser.parse_args()
planets = [item.strip() for item in args.planets.split(",") if item.strip()]
output = args.output or ROOT / "references" / "oracle" / "artifacts" / f"vedastro_{args.case}_divisional_raw.json"
report = collect_chart_core(
dict(adapter.PARITY_CASES[args.case]),
case_id=args.case,
planets=planets,
output_path=output,
)
print(json.dumps({"status": report["status"], "output": str(output), "response_hash": report["response_hash"], "fanout_statuses": report["fanout_statuses"]}, ensure_ascii=False, indent=2, sort_keys=True))
return 0 if report["status"] == "ok" else 1
if __name__ == "__main__":
raise SystemExit(main())
+52 -5
View File
@@ -25,25 +25,72 @@ REQUIRED_PARITY_OUTPUTS = ["D1", "D9", "D10", "D2", "D4", "Vimshottari", "Shadba
def _public_pyjhora_parity_manifest() -> dict:
path = Path("references/oracle/pyjhora_same_chart_parity_public_smoke_manifest.json")
try:
return json.loads(path.read_text(encoding="utf-8"))
baseline = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {"status": "not_available", "tested": False}
supplemental_path = Path("references/oracle/pyjhora_extended_parity_public_smoke_manifest.json")
try:
supplemental = json.loads(supplemental_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
supplemental = None
result = dict(baseline)
supplements = [supplemental] if supplemental and supplemental.get("tested") else []
covered = set(baseline.get("covered_outputs", []))
sample_counts = {name: baseline.get("sample_count", 0) for name in covered}
partial_sample_counts = {}
source_reports = [baseline["source_report"]] if baseline.get("source_report") else []
for item in supplements:
item_count = item.get("sample_count", 0)
for name in item.get("covered_outputs", []):
covered.add(name)
sample_counts[name] = max(sample_counts.get(name, 0), item_count)
for name in item.get("partial_outputs", []):
partial_sample_counts[name] = max(partial_sample_counts.get(name, 0), item_count)
if item.get("source_report"):
source_reports.append(item["source_report"])
result["covered_outputs"] = [name for name in REQUIRED_PARITY_OUTPUTS if name in covered]
result["missing_required_outputs"] = [name for name in REQUIRED_PARITY_OUTPUTS if name not in covered]
result["output_sample_counts"] = sample_counts
result["partial_output_sample_counts"] = partial_sample_counts
result["source_reports"] = source_reports
result["supplemental_verifications"] = supplements
result["boundary"] = (
"Partial PyJHora verification with per-output sample counts. D2/D4/Ashtakavarga use a one-chart "
"supplemental replay; Shadbala absolute values remain mismatched. This is not JHora desktop, "
"VedAstro, jyotishganit, or predictive validation."
)
return result
def _same_chart_parity_contract(engines: dict) -> dict:
pyjhora_public = _public_pyjhora_parity_manifest()
replay_manifest = validate_parity_replay_manifest("references/oracle/three_engine_parity_replay_manifest.json")
replay_covers_all_engines = bool(
replay_manifest.get("tested") and not replay_manifest.get("missing_engines")
)
engine_states = {}
for name, engine in engines.items():
available = engine["status"] == "available"
engine_states[name] = {
"available": available,
"tested": bool(name == "PyJHora/JHora" and pyjhora_public.get("tested")),
"tested": bool(
replay_covers_all_engines
or (name == "PyJHora/JHora" and pyjhora_public.get("tested"))
),
"blocked": not available,
"blocking_reason": "" if available else engine["status"],
}
replay_manifest = validate_parity_replay_manifest("references/oracle/three_engine_parity_replay_manifest.json")
if not all(state["available"] for state in engine_states.values()):
contract_status = "blocked"
elif not all(state["tested"] for state in engine_states.values()):
contract_status = "partial"
else:
contract_status = replay_manifest.get("status", "partial")
return {
"status": "ready" if all(state["available"] for state in engine_states.values()) else "blocked",
"status": contract_status,
"required_outputs": REQUIRED_PARITY_OUTPUTS,
"expected_oracle_fields": {
"VedAstro": [
@@ -71,7 +118,7 @@ def _same_chart_parity_contract(engines: dict) -> dict:
"engine_states": engine_states,
"replay_manifest": replay_manifest,
"partial_verifications": {"PyJHora/JHora": pyjhora_public},
"boundary": "This is a parity contract. Public PyJHora partial verification does not close missing outputs or other engine raw-oracle requirements.",
"boundary": "Availability, executed raw coverage, and numerical parity are separate states; mismatch is not ready.",
}
+28 -3
View File
@@ -25,6 +25,18 @@ GHATIKA_END = {
6: {"day": 26, "night": 10},
}
# Saturn's zero-based share in eight equal day/night parts. Python weekday:
# Monday=0. This is the variant used by PyJHora's public black-box oracle.
SATURN_PART_START = {
0: {"day": 5, "night": 1},
1: {"day": 4, "night": 0},
2: {"day": 3, "night": 6},
3: {"day": 2, "night": 5},
4: {"day": 1, "night": 4},
5: {"day": 0, "night": 3},
6: {"day": 6, "night": 2},
}
def _sidereal_ascendant(jd_ut: float, lat: float, lon: float) -> float:
swe.set_sid_mode(swe.SIDM_LAHIRI)
@@ -38,17 +50,27 @@ def calculate_gulika(
lat: float,
lon: float,
tz: float,
method: str = "saturn_part_start",
) -> dict[str, Any]:
"""Return Gulika from local moment/location using Swiss sunrise and sunset."""
daynight = determine_daytime(moment, lat=lat, lon=lon, tz=tz)
is_day = bool(daynight["is_daytime"])
period = "day" if is_day else "night"
ghatika_end = GHATIKA_END[moment.weekday()][period]
start_jd = daynight["sunrise_jd_ut"] if is_day else daynight["sunset_jd_ut"]
end_jd = daynight["sunset_jd_ut"] if is_day else daynight["sunrise_jd_ut"] + 1.0
if end_jd <= start_jd:
end_jd += 1.0
segment_jd = start_jd + (end_jd - start_jd) * (ghatika_end / 30.0)
if method == "saturn_part_start":
part_index = SATURN_PART_START[moment.weekday()][period]
segment_fraction = part_index / 8.0
ghatika_end = None
elif method == "legacy_ghatika_end":
part_index = None
ghatika_end = GHATIKA_END[moment.weekday()][period]
segment_fraction = ghatika_end / 30.0
else:
raise ValueError("method must be saturn_part_start or legacy_ghatika_end")
segment_jd = start_jd + (end_jd - start_jd) * segment_fraction
longitude = _sidereal_ascendant(segment_jd, float(lat), float(lon))
return {
"scope": "gulika_prasna_marga",
@@ -58,10 +80,13 @@ def calculate_gulika(
"degree_in_sign": round(longitude % 30, 6),
"period": period,
"weekday": moment.weekday(),
"method": method,
"part_index": part_index,
"segment_fraction": segment_fraction,
"ghatika_end": ghatika_end,
"segment_jd_ut": segment_jd,
"daynight_evidence": daynight,
"ayanamsa": "lahiri",
"rule_source": "references/prashna-complete-guide.md#3.5",
"boundary": "Formula is implemented from the local classical guide; external JHora/PyJHora numeric parity remains required before enabling Sphuta or verdict layers.",
"boundary": "PyJHora-aligned Saturn-part-start variant. Numeric parity is evidence only and does not enable Prashna verdict layers.",
}
@@ -12,6 +12,17 @@ from typing import Any
REQUIRED_ENGINES = {"VedAstro", "PyJHora_JHora", "jyotishganit"}
REQUIRED_ROW_FIELDS = {"section", "field", "local_value", "oracle_values", "status"}
REQUIRED_HIGH_RIGOR_SECTIONS = {
"D1",
"D2",
"D4",
"D9",
"D10",
"ashtakavarga_bav",
"ashtakavarga_sav",
"shadbala_total",
"shadbala_components",
}
VALID_ROW_STATUSES = {"match", "mismatch", "blocked", "not_comparable"}
RAW_VERIFIED_STATUSES = {"verified", "official_verified", "imported"}
@@ -79,6 +90,12 @@ def validate_manifest(path: str | Path) -> dict[str, Any]:
for row in rows:
if isinstance(row, dict) and row.get("status") in counts:
counts[row["status"]] += 1
covered_sections = {
str(row.get("section"))
for row in rows
if isinstance(row, dict) and row.get("status") in {"match", "mismatch"}
}
missing_high_rigor_sections = sorted(REQUIRED_HIGH_RIGOR_SECTIONS - covered_sections)
if errors:
status = "invalid"
@@ -92,6 +109,9 @@ def validate_manifest(path: str | Path) -> dict[str, Any]:
elif counts["blocked"]:
status = "partial"
blocked_reason = "some_comparison_rows_blocked"
elif missing_high_rigor_sections:
status = "partial"
blocked_reason = "missing_high_rigor_sections"
else:
status = "pass"
blocked_reason = None
@@ -110,6 +130,8 @@ def validate_manifest(path: str | Path) -> dict[str, Any]:
"mismatch_count": counts["mismatch"],
"blocked_row_count": counts["blocked"],
"not_comparable_count": counts["not_comparable"],
"covered_sections": sorted(covered_sections),
"missing_high_rigor_sections": missing_high_rigor_sections,
"blocked_reason": blocked_reason,
"errors": errors,
"runtime_boundary": manifest.get("runtime_boundary", ""),
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Replay Western timing geometry for an existing public real-case manifest."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
try:
from western_timing_engine import calculate_parans_status, calculate_secondary_progressions
except ImportError:
from scripts.western_timing_engine import calculate_parans_status, calculate_secondary_progressions
DEFAULT_MANIFEST = Path("references/real_case_calibration/replay_manifest.json")
def build_case(case_id: str, manifest_path: Path = DEFAULT_MANIFEST) -> dict:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
case = next((row for row in manifest["cases"] if row["case_id"] == case_id), None)
if case is None:
return {"status": "blocked", "reason": "case_id_not_found", "case_id": case_id}
event = case["event_outcomes"][0]
subject = case["subject"]
birth = {
"year": subject["year"], "month": subject["month"], "day": subject["day"],
"hour": subject["hour"], "minute": subject["minute"], "second": 0,
"latitude": subject["lat"], "longitude": subject["lon"], "timezone": subject["tz"],
}
progressions = calculate_secondary_progressions(target_date=event["event_date"], **birth)
parans = calculate_parans_status(target_date=event["event_date"], **birth)
return {
"scope": "western_real_case_calibration",
"status": "calculated_not_predictive_validation",
"case_id": case_id,
"subject": subject["name"],
"event": {
"date": event["event_date"],
"type": event["event_type"],
"source": event["source"],
},
"birth_source": subject["birth_source"],
"layers": {
"secondary_progressions": progressions,
"parans": parans,
},
"summary": {
"progressed_aspect_count": len(progressions["aspects"]),
"paran_event_count": parans["event_count"],
"paran_pair_count": len(parans["paran_pairs_within_4_minutes"]),
},
"boundary": "Known-event geometry replay only. One positive case cannot establish specificity, false-positive rate, or predictive accuracy.",
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case-id", default="jobs_iphone_2007")
parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
report = build_case(args.case_id, args.manifest)
rendered = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8")
print(rendered, end="")
return 0 if report["status"] != "blocked" else 1
if __name__ == "__main__":
raise SystemExit(main())
+71 -3
View File
@@ -132,6 +132,27 @@ def _progressed_planets(progressed_jd: float) -> dict[str, dict[str, Any]]:
return planets
def _quotidian_progressed_angles(progressed_jd: float, birth: dict[str, Any]) -> dict[str, Any]:
local = _jd_to_local(progressed_jd, birth["timezone"])
progressed_birth = {
**birth,
"year": local.year,
"month": local.month,
"day": local.day,
"hour": local.hour,
"minute": local.minute,
"second": local.second,
}
chart = build_tropical_natal_chart(**progressed_birth)
return {
"status": "used",
"method": "secondary_quotidian_progressed_date_same_location",
"progressed_local_time": local.isoformat(),
"angles": chart["natal"]["angles"],
"boundary": "Quotidian progressed-date angles; Naibod and solar-arc angle variants are separate methods.",
}
def calculate_secondary_progressions(*, target_date: str, **birth: Any) -> dict[str, Any]:
"""Calculate progressed planets using one ephemeris day per tropical year."""
natal_chart = build_tropical_natal_chart(**birth)
@@ -140,6 +161,7 @@ def calculate_secondary_progressions(*, target_date: str, **birth: Any) -> dict[
elapsed_years = (target_jd - birth_jd) / 365.242189
progressed_jd = birth_jd + elapsed_years
planets = _progressed_planets(progressed_jd)
progressed_angles = _quotidian_progressed_angles(progressed_jd, birth)
natal_points = {
**natal_chart["natal"]["planets"],
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
@@ -155,8 +177,9 @@ def calculate_secondary_progressions(*, target_date: str, **birth: Any) -> dict[
"progressed_julian_day_ut": round(progressed_jd, 8),
"natal_sun_longitude": natal_chart["natal"]["planets"]["sun"]["longitude"],
"progressed_planets": planets,
"progressed_angles": progressed_angles,
"aspects": _cross_aspects(planets, natal_points),
"boundary": "Progressed planets only. Progressed angles, lunar phases, stations, duration, and interpretation remain separate audited layers.",
"boundary": "Progressed planets plus explicitly selected quotidian angles. Lunar phases, stations, duration, and interpretation remain separate audited layers.",
}
@@ -391,11 +414,56 @@ def calculate_transit_duration_scan(*, start_date: str, end_date: str, max_days:
def calculate_parans_status(*, target_date: str | None = None, **birth: Any) -> dict[str, Any]:
if not target_date:
return {"technique": "parans", "status": "blocked", "reason": "target_date_required"}
start_jd, target_local = _target_jd(target_date, birth["timezone"])
geopos = (float(birth["longitude"]), float(birth["latitude"]), 0.0)
modes = {
"rise": swe.CALC_RISE,
"upper_culmination": swe.CALC_MTRANSIT,
"set": swe.CALC_SET,
}
events: list[dict[str, Any]] = []
for planet, planet_id in _PLANETS.items():
for angle, mode in modes.items():
try:
status, values = swe.rise_trans(start_jd, planet_id, mode, geopos, 0.0, 15.0, swe.FLG_SWIEPH)
except (swe.Error, TypeError, ValueError):
continue
if status != 0:
continue
event_local = _jd_to_local(float(values[0]), birth["timezone"])
if event_local.date() != target_local.date():
continue
events.append({
"planet": planet,
"angle": angle,
"julian_day_ut": round(float(values[0]), 8),
"local_time": event_local.isoformat(),
})
events.sort(key=lambda row: (row["julian_day_ut"], row["planet"], row["angle"]))
pairs: list[dict[str, Any]] = []
for index, first in enumerate(events):
for second in events[index + 1:]:
delta_minutes = (second["julian_day_ut"] - first["julian_day_ut"]) * 1440.0
if delta_minutes > 4.0:
break
if first["planet"] == second["planet"]:
continue
pairs.append({
"first": {key: first[key] for key in ("planet", "angle", "local_time")},
"second": {key: second[key] for key in ("planet", "angle", "local_time")},
"separation_minutes": round(delta_minutes, 4),
})
return {
"technique": "parans",
"status": "blocked",
"status": "used",
"target_date": target_date,
"reason": "Parans need a dedicated rising/setting/culminating engine and latitude-aware event solver; not yet implemented in this repository.",
"method": "Swiss Ephemeris rise_trans latitude-aware angular events",
"event_count": len(events),
"events": events,
"paran_pairs_within_4_minutes": pairs,
"boundary": "Geometric rise/culmination/set simultaneity only; no interpretation or predictive claim is inferred.",
}
@@ -0,0 +1,59 @@
from __future__ import annotations
import json
from scripts import collect_vedastro_official_parity_raw as collector
def test_collect_chart_core_writes_secret_free_hashed_packet(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://api.vedastro.org/api")
monkeypatch.setenv("VEDASTRO_API_KEY", "must-not-leak")
def fake_post(endpoint, request_item):
if request_item["section"] == "shadbala_chesta":
return {"Status": "Pass", "Payload": {"PlanetChestaBala": "12.5"}}, 1, []
if request_item["section"] == "shadbala":
return {"Status": "Pass", "Payload": {"AllPlanetStrength": "(400, Sun)"}}, 1, []
if request_item["section"] == "ashtakavarga_sav":
return {"Status": "Pass", "Payload": {"AshtakvargaLifeMap": {"TotalBindus": 337}}}, 1, []
if request_item["section"] == "ashtakavarga_bav":
return {"Status": "Pass", "Payload": {"BhinnashtakavargaChart": {"Sun": {"Rows": [1] * 12}}}}, 1, []
if request_item["section"] == "ashtakavarga_sav_chart":
return {"Status": "Pass", "Payload": {"SarvashtakavargaChart": {"Sarvashtakavarga": {"Rows": [28] * 12}}}}, 1, []
planet = request_item["fanout_value"]
return {
"Status": "Pass",
"Payload": {
"AllPlanetData": {
"PlanetHoraD2Signs": {"Name": f"{planet}-D2"},
"PlanetChaturthamshaD4Sign": {"Name": f"{planet}-D4"},
"PlanetNavamshaD9Sign": {"Name": f"{planet}-D9"},
"PlanetDashamamshaD10Sign": {"Name": f"{planet}-D10"},
}
},
}, 1, []
monkeypatch.setattr(collector.adapter, "_post_official_snapshot_section", fake_post)
output = tmp_path / "packet.json"
report = collector.collect_chart_core(
collector.adapter.PARITY_CASES["beijing_first_use_demo"],
case_id="beijing_first_use_demo",
planets=["Sun", "Moon"],
output_path=output,
)
stored = json.loads(output.read_text(encoding="utf-8"))
assert report["status"] == "ok"
assert stored["coverage"]["sections"] == [
"D2", "D4", "D9", "D10", "shadbala_total", "shadbala_components", "ashtakavarga_bav", "ashtakavarga_sav"
]
assert stored["fanout_statuses"] == {"Moon": "ok", "Sun": "ok"}
assert stored["scalar_statuses"] == {
"ashtakavarga_bav": "ok",
"ashtakavarga_sav": "ok",
"ashtakavarga_sav_chart": "ok",
"shadbala": "ok",
}
assert stored["component_statuses"] == {"Moon.chesta": "ok", "Sun.chesta": "ok"}
assert len(stored["response_hash"]) == 64
assert "must-not-leak" not in output.read_text(encoding="utf-8")
@@ -31,15 +31,30 @@ def test_external_engine_adapter_diagnostics_aggregates_three_engines() -> None:
assert report["engines"]["PyJHora/JHora"]["license_boundary"].startswith("AGPL external benchmark")
assert report["engines"]["jyotishganit"]["license"] == "MIT"
contract = report["same_chart_parity_contract"]
assert contract["status"] in {"ready", "blocked"}
expected_contract_status = (
"mismatch"
if all(state["available"] for state in contract["engine_states"].values())
else "blocked"
)
assert contract["status"] == expected_contract_status
assert contract["required_outputs"] == ["D1", "D9", "D10", "D2", "D4", "Vimshottari", "Shadbala", "Ashtakavarga"]
assert "official_raw_response" in contract["expected_oracle_fields"]["VedAstro"]
assert "raw_output_path" in contract["expected_oracle_fields"]["PyJHora/JHora"]
assert contract["engine_states"]["jyotishganit"]["available"] is True
assert contract["engine_states"]["jyotishganit"]["tested"] is True
assert contract["engine_states"]["VedAstro"]["tested"] is True
assert contract["engine_states"]["PyJHora/JHora"]["tested"] is True
pyjhora = contract["partial_verifications"]["PyJHora/JHora"]
assert pyjhora["status"] == "partial_verified"
assert pyjhora["missing_required_outputs"] == ["D2", "D4", "Shadbala", "Ashtakavarga"]
assert contract["replay_manifest"]["tested"] is False
assert contract["replay_manifest"]["blocked_reason"] == "no_same_chart_oracle_rows_imported"
assert pyjhora["missing_required_outputs"] == ["Shadbala"]
assert {"D2", "D4", "Ashtakavarga"} <= set(pyjhora["covered_outputs"])
assert pyjhora["output_sample_counts"]["D2"] == 1
assert pyjhora["output_sample_counts"]["D4"] == 1
assert pyjhora["output_sample_counts"]["Ashtakavarga"] == 1
assert pyjhora["output_sample_counts"]["D1"] == 10
assert pyjhora["supplemental_verifications"][0]["partial_outputs"] == ["Shadbala"]
assert contract["replay_manifest"]["tested"] is True
assert contract["replay_manifest"]["status"] == "mismatch"
assert contract["replay_manifest"]["blocked_reason"] is None
assert contract["replay_manifest"]["missing_high_rigor_sections"] == []
assert report["status"] in {"complete", "partial"}
+19 -2
View File
@@ -1,11 +1,15 @@
from datetime import datetime
from scripts.gulika import GHATIKA_END, calculate_gulika
import json
from pathlib import Path
from scripts.gulika import GHATIKA_END, SATURN_PART_START, calculate_gulika
def test_gulika_uses_prasna_marga_weekday_table() -> None:
assert GHATIKA_END[6] == {"day": 26, "night": 10}
assert GHATIKA_END[0] == {"day": 22, "night": 6}
assert SATURN_PART_START[0] == {"day": 5, "night": 1}
def test_gulika_returns_sidereal_segment_ascendant_with_audit_trace() -> None:
@@ -13,5 +17,18 @@ def test_gulika_returns_sidereal_segment_ascendant_with_audit_trace() -> None:
assert result["status"] == "partial"
assert 0 <= result["longitude"] < 360
assert result["ghatika_end"] in range(0, 31)
assert result["part_index"] in range(0, 8)
assert result["ghatika_end"] is None
assert result["rule_source"].endswith("#3.5")
def test_gulika_matches_public_pyjhora_smoke_oracle() -> None:
packet = json.loads(
Path("references/oracle/prashna_sphuta_pyjhora_public_smoke.json").read_text(encoding="utf-8")
)
expected = packet["outputs"]["gulika"]
result = calculate_gulika(datetime(1990, 1, 1, 12, 0), lat=39.9042, lon=116.4074, tz=8)
assert result["method"] == "saturn_part_start"
assert result["sign_idx"] == expected["sign_index"]
assert abs(result["degree_in_sign"] - expected["degree_in_sign"]) <= 0.5
@@ -0,0 +1,24 @@
import json
from pathlib import Path
def test_public_pyjhora_prashna_sphuta_packet_is_numeric_and_guarded() -> None:
packet = json.loads(
Path("references/oracle/prashna_sphuta_pyjhora_public_smoke.json").read_text(encoding="utf-8")
)
assert packet["status"] == "local_parity_verified_single_case"
assert packet["birth_data_policy"] == "public_synthetic_smoke_only"
assert packet["capture"]["raw_response_available"] is True
assert packet["engine"]["license_boundary"].startswith("AGPL external benchmark")
assert set(packet["outputs"]) == {
"gulika", "kunda_lagna", "tri_sphuta", "chatur_sphuta", "pancha_sphuta"
}
for row in packet["outputs"].values():
assert 0 <= row["sign_index"] < 12
assert 0 <= row["degree_in_sign"] < 30
assert packet["local_comparison"]["status"] == "pass"
assert max(
value for key, value in packet["local_comparison"].items() if key.endswith("delta_degrees")
) <= packet["local_comparison"]["tolerance_degrees"]
assert "remain required" in packet["boundary"]
@@ -12,14 +12,16 @@ from scripts.three_engine_parity_replay_validator import validate_manifest
ROOT = Path(__file__).resolve().parents[1]
def test_three_engine_parity_manifest_passes_d1_with_vedastro_and_pyjhora_raw() -> None:
def test_three_engine_parity_manifest_exposes_raw_coverage_and_formula_mismatches() -> None:
result = validate_manifest(ROOT / "references/oracle/three_engine_parity_replay_manifest.json")
assert result["status"] == "pass"
assert result["comparison_row_count"] == 15
assert result["match_count"] == 14
assert result["status"] == "mismatch"
assert result["comparison_row_count"] == 92
assert result["match_count"] == 32
assert result["mismatch_count"] == 60
assert result["tested"] is True
assert result["blocked_reason"] is None
assert result["missing_high_rigor_sections"] == []
def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path) -> None:
@@ -54,7 +56,8 @@ def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path
result = validate_manifest(path)
assert result["status"] == "pass"
assert result["status"] == "partial"
assert result["blocked_reason"] == "missing_high_rigor_sections"
assert result["tested"] is True
assert result["comparison_row_count"] == 1
assert result["match_count"] == 1
@@ -0,0 +1,17 @@
from scripts.western_real_case_calibration import build_case
def test_jobs_public_case_replays_progressed_angles_and_parans() -> None:
report = build_case("jobs_iphone_2007")
assert report["status"] == "calculated_not_predictive_validation"
assert report["event"]["source"]["source_grade"] == "primary"
assert report["birth_source"]["time_accuracy_rating"] == "AA"
assert report["layers"]["secondary_progressions"]["progressed_angles"]["status"] == "used"
assert report["layers"]["parans"]["status"] == "used"
assert report["summary"]["paran_event_count"] > 0
assert "cannot establish" in report["boundary"]
def test_unknown_case_is_blocked() -> None:
assert build_case("missing")["reason"] == "case_id_not_found"
+11 -2
View File
@@ -97,10 +97,12 @@ def test_transit_duration_scan_groups_daily_windows() -> None:
assert isinstance(scan["windows"], list)
def test_parans_are_explicitly_blocked_until_solver_exists() -> None:
def test_parans_emit_latitude_aware_angular_events() -> None:
parans = calculate_parans_status(**_BIRTH, target_date="2026-07-09")
assert parans["technique"] == "parans"
assert parans["status"] == "blocked"
assert parans["status"] == "used"
assert parans["method"].startswith("Swiss Ephemeris rise_trans")
assert parans["event_count"] > 0
def test_timing_builder_can_emit_advanced_layers() -> None:
@@ -122,3 +124,10 @@ def test_timing_builder_can_emit_advanced_layers() -> None:
"transit_duration_scan",
"parans",
} <= set(timing)
progressed = build_timing_techniques(**_BIRTH, secondary_progression_date="2026-07-09")["secondary_progressions"]
assert progressed["progressed_angles"]["status"] == "used"
assert set(progressed["progressed_angles"]["angles"]) == {"ascendant", "mc", "descendant", "ic"}
parans = timing["parans"]
assert parans["status"] == "used"
assert parans["event_count"] > 0
assert all(row["separation_minutes"] <= 4 for row in parans["paran_pairs_within_4_minutes"])