Add date-isolated caches and regression coverage, align scoring identity, and freeze full research reruns while preserving historical artifacts. Record unresolved cache/receipt identity and end-to-end acceptance gaps for branch review only. Co-Authored-By: Claude Code <noreply@anthropic.com>
157 lines
7.8 KiB
Python
157 lines
7.8 KiB
Python
"""Candidate-local dasha dates; public AA replay and synthetic cache boundaries."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
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
|
|
|
|
assert engine_scoring_versions()["algorithm_version"] == ALGORITHM_VERSION == "rectification-v5-matrix-scoring-8"
|
|
assert INPUT_CONTRACT_VERSION == "rectification-calculation-spec-v4"
|
|
|
|
|
|
@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,score_sha256", [
|
|
(1, "2aabdda6bb56baf6a9d0b119964ee1ab8023fede9ea8703f6d265207c490bec2"),
|
|
(2, "6585d895aeb79e26257b1002696c16b5e252f9a702f2a5702f929fd80486abb9"),
|
|
(3, "246296903d915e4886229530fa554428d7f65fb686f70cc369711347ebaf2610"),
|
|
])
|
|
def test_same_day_public_aa_scores_keep_pre_fix_bytes(ordinal, score_sha256):
|
|
# Golden hashes captured from the unmodified production path, 121 minutes each.
|
|
# Only ordinal and score bytes are retained; no birth data or coordinates.
|
|
request, moments = shifted_window(_cases()[ordinal - 1], 0, 60)
|
|
assert len({moment.date() for moment in moments}) == 1
|
|
built = build_event_contribution_matrix(request)
|
|
scores = [row["score"] for row in score_from_matrix(request, built)]
|
|
assert len(scores) == 121
|
|
assert hashlib.sha256(_canonical(scores)).hexdigest() == score_sha256
|
|
|
|
|
|
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
|