"""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