Files
Jyotisha/tests/test_dasha_transition_proximity_cross_midnight.py
T
jesse-ux b85c4a686a
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s
fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
2026-09-21 02:55:00 +08:00

189 lines
9.5 KiB
Python

"""Candidate-local dasha dates; public AA replay and synthetic cache boundaries."""
from __future__ import annotations
import json
from datetime import date, datetime, timedelta
import pytest
from scripts.active_rectification_event_engine import compute_candidate_static_contexts
from scripts.rectification import dasha_transition_proximity as proximity
from scripts.rectification.scoring_service import (
build_event_contribution_matrix,
public_technique_layers,
score_from_matrix,
)
from scripts.research.reported_offset_sweep import shifted_window
from scripts.research.sealed_holdout_rerun import DATASET
def _canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def _cases():
return json.loads(DATASET.read_text(encoding="utf-8"))["cases"]
def test_fixed_scoring_identity_is_exposed_without_changing_input_contract():
from scripts.rectification.api_service import engine_scoring_versions
from scripts.rectification.scoring_service import (
ALGORITHM_VERSION, INPUT_CONTRACT_VERSION, calculation_spec, sha256,
)
assert engine_scoring_versions()["algorithm_version"] == ALGORITHM_VERSION == "rectification-v5-matrix-scoring-9"
assert INPUT_CONTRACT_VERSION == "rectification-calculation-spec-v5"
request = {"birth_date": "2000-03-01", "start_time": "00:00", "end_time": "00:02",
"lat": 0.0, "lon": 0.0, "tz": 0.0, "events": []}
legacy_spec = calculation_spec(request)
assert legacy_spec["version"] == "rectification-calculation-spec-v4"
# Computed from unchanged baseline 3be740f8, not from the helper under test.
assert sha256(legacy_spec) == "bfdee46405a38f2508a2071929b60d79d15a78ca0e1e81f6c7ffd1652e301a08"
dated_spec = calculation_spec({**request, "candidate_intervals": [
{"start_at": "2000-03-01T00:00", "end_at": "2000-03-01T00:02"},
]})
assert dated_spec == {**legacy_spec, "version": "rectification-calculation-spec-v5",
"candidateIntervals": [{"start_at": "2000-03-01T00:00", "end_at": "2000-03-01T00:02"}]}
assert sha256(dated_spec) != sha256(legacy_spec)
@pytest.mark.parametrize("start", ["2000-01-01", "2000-02-29", "2000-12-31"])
def test_every_candidate_uses_own_date_and_caches_do_not_cross_dates(monkeypatch, start):
# Intentionally identical synthetic chart values: only date distinguishes caches.
anchor = date.fromisoformat(start)
moments = [datetime.combine(anchor, datetime.min.time()) + timedelta(hours=23, minutes=50+i)
for i in range(21)]
contexts = [{"candidate_at": at, "feature": {"time": at.strftime("%H:%M")},
"planet_longitudes": {"Moon": 42.0}, "ascendant_index": 1}
for at in moments]
event_date = date(2020, 1, 10)
calls = {"ad": [], "pd": [], "narayana": []}
def vim(birth_date, moon, lo, hi, *, include_pratyantar=False):
calls["pd" if include_pratyantar else "ad"].append(birth_date)
delta = (date.fromisoformat(birth_date) - anchor).days
return [event_date + timedelta(days=delta + (3 if include_pratyantar else 2))]
def narayana(asc, planets, birth_date, lo, hi):
calls["narayana"].append(birth_date)
delta = (date.fromisoformat(birth_date) - anchor).days
return [event_date + timedelta(days=delta + 4)]
monkeypatch.setattr(proximity, "_vim_start_dates", vim)
monkeypatch.setattr(proximity, "_narayana_start_dates", narayana)
# Two events sharing the year band exercise cache reuse, not merely a new call.
events = [{"id": precision, "domain": "career", "precision": precision,
"date_start": event_date.isoformat(), "date_end": event_date.isoformat()}
for precision in ("day", "month")]
matrix = {event["id"]: {at.strftime("%H:%M"): {"points": 2.0, "rule_ids": []}
for at in moments} for event in events}
proximity.merge_transition_proximity(matrix, events, contexts, start,
public_technique_layers=public_technique_layers)
for at in moments:
delta = (at.date() - anchor).days
for precision in ("day", "month"):
expected = proximity.score_transition_proximity(
event_date=event_date, precision=precision,
vim_starts=[event_date + timedelta(days=delta + 2)],
vim_pd_starts=[event_date + timedelta(days=delta + 3)],
narayana_starts=[event_date + timedelta(days=delta + 4)],
)
actual = matrix[precision][at.strftime("%H:%M")]
assert actual["points"] == round(2.0 + expected["points"], 4)
assert actual["rule_ids"] == sorted(expected["rule_ids"])
expected_dates = [anchor.isoformat(), (anchor + timedelta(days=1)).isoformat()]
assert calls == {kind: expected_dates for kind in calls}
def test_legacy_context_without_candidate_at_retains_request_date(monkeypatch):
calls = []
def vim(birth_date, moon, lo, hi, **kwargs):
calls.append(birth_date)
return []
monkeypatch.setattr(proximity, "_vim_start_dates", vim)
monkeypatch.setattr(proximity, "_narayana_start_dates", lambda *args: [])
proximity.merge_transition_proximity(
{"event": {"12:00": {"points": 2.0}}},
[{"id": "event", "precision": "day", "date": "2020-01-10"}],
[{"feature": {"time": "12:00"}, "planet_longitudes": {"Moon": 42.0}}],
"2000-01-01", public_technique_layers=public_technique_layers,
)
assert calls == ["2000-01-01", "2000-01-01"]
@pytest.mark.parametrize("ordinal", [1, 2, 3])
def test_same_day_public_aa_scores_keep_pre_fix_bytes(monkeypatch, ordinal):
from scripts.rectification import scoring_service
# BUG-985 / BUG-733: equivalence is same-process, not a cross-machine float golden.
request, moments = shifted_window(_cases()[ordinal - 1], 0, 60)
assert {moment.date().isoformat() for moment in moments} == {request["birth_date"]}
contexts = compute_candidate_static_contexts(request)
assert [context["candidate_at"] for context in contexts] == moments
built = build_event_contribution_matrix(request, static_contexts=contexts)
scores = [row["score"] for row in score_from_matrix(request, built)]
legacy_calls = []
def legacy_merge(matrix, events, static_contexts, birth_date, **kwargs):
# Strip only at the helper boundary; the main event engine needs candidate_at.
assert static_contexts is contexts
legacy_contexts = [
{key: value for key, value in context.items() if key != "candidate_at"}
for context in static_contexts
]
legacy_calls.append(len(legacy_contexts))
return proximity.merge_transition_proximity(
matrix, events, legacy_contexts, birth_date, **kwargs,
)
with monkeypatch.context() as legacy:
legacy.setattr(scoring_service, "merge_transition_proximity", legacy_merge)
legacy_built = build_event_contribution_matrix(request, static_contexts=contexts)
legacy_scores = [row["score"] for row in score_from_matrix(request, legacy_built)]
assert legacy_calls == [121]
assert len(scores) == len(legacy_scores) == 121
assert _canonical(scores) == _canonical(legacy_scores)
assert _canonical(built["matrix"]) == _canonical(legacy_built["matrix"])
def test_real_cross_midnight_all_candidates_match_independent_dated_calculation(monkeypatch):
# Existing public AA case naturally crosses midnight at radius 60; no birth mutation.
request, moments = shifted_window(_cases()[5], 0, 60)
contexts = compute_candidate_static_contexts(request)
assert [context["candidate_at"] for context in contexts] == moments
assert len({moment.date() for moment in moments}) == 2
original_vim, original_narayana = proximity._vim_start_dates, proximity._narayana_start_dates
seen_vim, seen_narayana = set(), set()
def vim(birth_date, moon, lo, hi, *, include_pratyantar=False):
seen_vim.add((birth_date, round(moon, 6), include_pratyantar))
return original_vim(birth_date, moon, lo, hi, include_pratyantar=include_pratyantar)
def narayana(asc, planets, birth_date, lo, hi):
seen_narayana.add((birth_date, asc, round(planets["Moon"], 6)))
return original_narayana(asc, planets, birth_date, lo, hi)
with monkeypatch.context() as capture:
capture.setattr(proximity, "_vim_start_dates", vim)
capture.setattr(proximity, "_narayana_start_dates", narayana)
actual = build_event_contribution_matrix(request, static_contexts=contexts)
expected_rows, expected_matrix = [], {}
for context in contexts:
candidate_date = context["candidate_at"].date().isoformat()
dated_request = {**request, "birth_date": candidate_date}
# One correctly dated candidate per independent matrix, with fresh local caches.
built = build_event_contribution_matrix(dated_request, static_contexts=[context])
expected_rows.extend(score_from_matrix(dated_request, built))
for event_id, cells in built["matrix"].items():
expected_matrix.setdefault(event_id, {}).update(cells)
assert _canonical(actual["matrix"]) == _canonical(expected_matrix)
assert _canonical(score_from_matrix(request, actual)) == _canonical(expected_rows)
for context in contexts:
candidate_date = context["candidate_at"].date().isoformat()
moon = round(context["planet_longitudes"]["Moon"], 6)
assert (candidate_date, moon, False) in seen_vim
assert (candidate_date, moon, True) in seen_vim
assert (candidate_date, context["ascendant_index"], moon) in seen_narayana