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.
This commit is contained in:
@@ -27,10 +27,24 @@ def _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
|
||||
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-8"
|
||||
assert INPUT_CONTRACT_VERSION == "rectification-calculation-spec-v4"
|
||||
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"])
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Native golden verification plus explicitly synthetic order/gap arithmetic fixtures."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.active_rectification_event_engine import compute_candidate_static_contexts
|
||||
from scripts.rectification.refinement_packet import cluster_scan, lagna_contrast, window_scan
|
||||
|
||||
|
||||
def test_native_lagna_golden_has_explicit_date_labels_and_segments():
|
||||
fixture = json.loads((Path(__file__).resolve().parents[1]
|
||||
/ "frontend/tests/fixtures/rectification-dated-lagna.native.json").read_text(encoding="utf-8"))
|
||||
request = fixture["request"]
|
||||
actual = lagna_contrast({"static_contexts": compute_candidate_static_contexts(request),
|
||||
"candidate_intervals": request["candidate_intervals"]})
|
||||
assert actual == fixture["lagna_contrast"]
|
||||
assert "2000-02-29 23:58" in actual["user_meaning"]
|
||||
assert [row["segment_index"] for row in actual["intervals"]] == [0, 1]
|
||||
|
||||
|
||||
def test_native_scan_golden_matches_real_static_contexts():
|
||||
fixture = json.loads((Path(__file__).resolve().parents[1]
|
||||
/ "frontend/tests/fixtures/rectification-dated-transitions.native.json").read_text(encoding="utf-8"))
|
||||
request = fixture["request"]
|
||||
contexts = compute_candidate_static_contexts(request)
|
||||
assert window_scan({"static_contexts": contexts,
|
||||
"candidate_intervals": request["candidate_intervals"]}) == fixture["scan"]
|
||||
|
||||
|
||||
def context(time, date, index, offset, segment, sign):
|
||||
return {"time": time, "candidate_date": date, "window_index": index,
|
||||
"window_offset_minutes": offset, "segment_index": segment,
|
||||
"feature": {"time": time, "varga_ascendants": {"D9": sign}}}
|
||||
|
||||
|
||||
def test_midnight_transitions_follow_candidate_order_not_clock_order():
|
||||
rows = [context("23:58", "2000-06-14", 0, 0, 0, 0),
|
||||
context("23:59", "2000-06-14", 1, 1, 0, 1),
|
||||
context("00:00", "2000-06-15", 2, 2, 0, 2)]
|
||||
transitions = [row for row in window_scan({"static_contexts": rows, "candidate_intervals": [{"start_at": "2000-06-14T23:58", "end_at": "2000-06-15T00:00"}]})["transitions"] if row["layer"] == "d9"]
|
||||
changes = [row for row in transitions if not row.get("segment_start")]
|
||||
assert [row["at"] for row in changes] == ["23:59", "00:00"]
|
||||
assert [row["window_index"] for row in transitions] == [0, 1, 2]
|
||||
assert transitions[0]["segment_start"] is True
|
||||
assert transitions[-1]["candidate_date"] == "2000-06-15"
|
||||
|
||||
|
||||
def test_disjoint_segments_have_independent_initial_states_not_gap_transitions():
|
||||
rows = [context("00:00", "2000-06-15", 0, 0, 0, 0),
|
||||
context("00:01", "2000-06-15", 1, 1, 0, 1),
|
||||
context("23:00", "2000-06-15", 2, 1380, 1, 5),
|
||||
context("23:01", "2000-06-15", 3, 1381, 1, 6)]
|
||||
transitions = [row for row in window_scan({"static_contexts": rows, "candidate_intervals": [
|
||||
{"start_at": "2000-06-15T00:00", "end_at": "2000-06-15T00:01"},
|
||||
{"start_at": "2000-06-15T23:00", "end_at": "2000-06-15T23:01"},
|
||||
]})["transitions"] if row["layer"] == "d9"]
|
||||
assert [row["at"] for row in transitions if not row.get("segment_start")] == ["00:01", "23:01"]
|
||||
starts = [row for row in transitions if row.get("segment_start")]
|
||||
assert [row["segment_index"] for row in starts] == [0, 1]
|
||||
assert starts[1]["from_sign"] == starts[1]["to_sign"]
|
||||
assert starts[0]["segment_end_at"] == "2000-06-15T00:01"
|
||||
assert starts[1]["segment_start_at"] == "2000-06-15T23:00"
|
||||
|
||||
|
||||
def test_same_day_dated_changes_preserve_legacy_scan_output():
|
||||
rows = [context(f"10:0{i}", "2000-06-15", i, i, 0, i) for i in range(4)]
|
||||
legacy = window_scan({"static_contexts": rows})
|
||||
dated = window_scan({"static_contexts": rows, "candidate_intervals": [{"start_at": "2000-06-15T10:00", "end_at": "2000-06-15T10:03"}]})
|
||||
dated["transitions"] = [{key: value for key, value in row.items()
|
||||
if key not in {"candidate_date", "window_index", "window_offset_minutes", "segment_index", "segment_start_at", "segment_end_at"}}
|
||||
for row in dated["transitions"] if not row.get("segment_start")]
|
||||
assert dated == legacy
|
||||
|
||||
|
||||
def test_cluster_scan_uses_midnight_order_and_never_fills_segment_gaps():
|
||||
rows = [context("23:58", "2000-06-14", 0, 0, 0, 0),
|
||||
context("23:59", "2000-06-14", 1, 1, 0, 1),
|
||||
context("00:00", "2000-06-15", 2, 2, 0, 2),
|
||||
context("00:01", "2000-06-15", 3, 3, 0, 3)]
|
||||
built = {"static_contexts": rows, "candidate_intervals": [{"start_at": "2000-06-14T23:58", "end_at": "2000-06-15T00:01"}]}
|
||||
result = cluster_scan(built, ["23:59", "00:00"], "23:59", 2)
|
||||
assert result["d9_lagna_count"] == 2
|
||||
assert {row["at"] for row in result["transitions"]} == {"23:59", "00:00"}
|
||||
|
||||
|
||||
def test_legacy_transition_shape_has_no_reconstructed_dates():
|
||||
rows = [{"feature": {"time": time, "varga_ascendants": {"D9": sign}}}
|
||||
for time, sign in [("10:00", 0), ("10:01", 1), ("10:02", 2)]]
|
||||
transitions = window_scan({"static_contexts": rows})["transitions"]
|
||||
assert [row["at"] for row in transitions] == ["10:01", "10:02"]
|
||||
assert all("candidate_date" not in row and "segment_start" not in row for row in transitions)
|
||||
@@ -17,6 +17,7 @@ four cached layers is proven in-process instead.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import math
|
||||
@@ -24,6 +25,7 @@ from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -231,10 +233,68 @@ def _contexts_with_layer_cache_cleared(contexts: list[dict[str, Any]]) -> list[d
|
||||
return cleared
|
||||
|
||||
|
||||
def _assert_dated_payload_against_historical_golden(actual: dict[str, Any], expected: dict[str, Any]) -> None:
|
||||
"""Keep the scoring-8 golden immutable; validate exactly the scoring-9 additions."""
|
||||
metadata = {
|
||||
"candidate_window_contract": "dated-v1",
|
||||
"candidate_intervals": [{"start_at": "1990-01-01T12:00", "end_at": "1990-01-01T12:02"}],
|
||||
"candidate_timezone_offset": 8,
|
||||
"candidate_timezone_id": "",
|
||||
}
|
||||
receipt = actual["decision_receipt"]
|
||||
historical_receipt = expected["decision_receipt"]
|
||||
assert not set(metadata).intersection(historical_receipt), "historical fixture must not be relabeled"
|
||||
assert set(receipt) == set(historical_receipt) | set(metadata)
|
||||
for key, value in metadata.items():
|
||||
assert receipt[key] == value, key
|
||||
assert expected["candidate_feature_snapshot"]["algorithm_version"] == "rectification-v5-matrix-scoring-8"
|
||||
assert actual["candidate_feature_snapshot"]["algorithm_version"] == "rectification-v5-matrix-scoring-9"
|
||||
assert historical_receipt["representative_candidate_id"] == "e8c11277-022c-519c-95e9-6a0a04e8bf23"
|
||||
# Independently spell out the result/candidate UUID namespace formula. Do not
|
||||
# call the production identity helper or accept any arbitrary UUID string.
|
||||
identity_request = {
|
||||
key: value for key, value in _normalized_request().items()
|
||||
if key not in {"asked_probe_keys", "dropped_asked_probe_keys", "declined_domains", "column_times", "refresh_probes"}
|
||||
}
|
||||
fingerprint = hashlib.sha256(json.dumps(
|
||||
identity_request, ensure_ascii=True, sort_keys=True, separators=(",", ":"),
|
||||
).encode()).hexdigest()
|
||||
result_id = uuid5(NAMESPACE_URL, f"rectification-v5-matrix-scoring-9:{fingerprint}")
|
||||
representative_time = historical_receipt["representative_time"]
|
||||
candidate_id = uuid5(NAMESPACE_URL, f"rectification-candidate-policy-v3:{result_id}:{representative_time}")
|
||||
assert receipt["representative_candidate_id"] == str(candidate_id)
|
||||
projected = {**actual, "decision_receipt": {
|
||||
**{key: value for key, value in receipt.items() if key not in metadata},
|
||||
"representative_candidate_id": historical_receipt["representative_candidate_id"],
|
||||
}}
|
||||
# BUG-733/985: fingerprints hash unrounded floats; do not turn the
|
||||
# historical snapshot into a cross-process exact-hash contract.
|
||||
_assert_memoization_payloads(projected, expected)
|
||||
|
||||
|
||||
def test_score_candidates_matches_baseline_golden() -> None:
|
||||
expected = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
|
||||
actual = json.loads(json.dumps(_golden_payload(), ensure_ascii=True))
|
||||
_assert_memoization_payloads(actual, expected)
|
||||
_assert_dated_payload_against_historical_golden(actual, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("candidate_window_contract", "dated-v0"),
|
||||
("candidate_intervals", [{"start_at": "1990-01-02T12:00", "end_at": "1990-01-02T12:02"}]),
|
||||
("candidate_timezone_offset", 9),
|
||||
("candidate_timezone_id", "UTC"),
|
||||
("representative_candidate_id", "00000000-0000-4000-8000-000000000001"),
|
||||
("unapproved_metadata", True),
|
||||
("confirmation_allowed", True),
|
||||
])
|
||||
def test_dated_golden_projection_rejects_metadata_identity_and_gate_mutations(field, value) -> None:
|
||||
expected = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
|
||||
actual = json.loads(json.dumps(_golden_payload(), ensure_ascii=True))
|
||||
_assert_dated_payload_against_historical_golden(actual, expected)
|
||||
assert actual["decision_receipt"].get(field) != value
|
||||
actual["decision_receipt"][field] = value
|
||||
with pytest.raises(AssertionError):
|
||||
_assert_dated_payload_against_historical_golden(actual, expected)
|
||||
|
||||
|
||||
def test_golden_float_shift_of_1e_minus_2_fails() -> None:
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""BUG-982/983: explicit civil-date windows; synthetic dates, native computations."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.active_rectification_event_engine import _candidate_datetimes
|
||||
from scripts.rectification.decision_policy import _cluster_span, indistinguishable_width_minutes
|
||||
|
||||
|
||||
def test_declared_minute_stays_on_declared_date():
|
||||
moments = _candidate_datetimes({
|
||||
"birth_date": "2000-06-15", "start_time": "23:55", "end_time": "00:25",
|
||||
"candidate_intervals": [{"start_at": "2000-06-14T23:55", "end_at": "2000-06-15T00:25"}],
|
||||
})
|
||||
assert datetime(2000, 6, 15, 0, 10) in moments
|
||||
assert datetime(2000, 6, 16, 0, 10) not in moments
|
||||
|
||||
|
||||
def test_late_night_unknown_is_two_segments_of_same_day():
|
||||
moments = _candidate_datetimes({
|
||||
"birth_date": "2000-06-15", "start_time": "23:00", "end_time": "03:59",
|
||||
"candidate_intervals": [
|
||||
{"start_at": "2000-06-15T00:00", "end_at": "2000-06-15T03:59"},
|
||||
{"start_at": "2000-06-15T23:00", "end_at": "2000-06-15T23:59"},
|
||||
],
|
||||
})
|
||||
assert len(moments) == 300
|
||||
assert {at.date().isoformat() for at in moments} == {"2000-06-15"}
|
||||
assert not any(4 <= at.hour < 23 for at in moments)
|
||||
|
||||
|
||||
def test_cluster_span_uses_window_order_across_midnight():
|
||||
row = {"time": "23:58", "cluster_times": ["23:58", "23:59", "00:00"],
|
||||
"cluster_positions": [
|
||||
{"time": time, "window_index": i, "window_offset_minutes": i,
|
||||
"segment_index": 0, "candidate_date": "2000-06-14" if i < 2 else "2000-06-15"}
|
||||
for i, time in enumerate(["23:58", "23:59", "00:00"])
|
||||
]}
|
||||
assert _cluster_span(row) == (["23:58", "23:59", "00:00"], "23:58", "00:00")
|
||||
assert indistinguishable_width_minutes([{
|
||||
"rank": 1, "time": "23:58", "tied_minute_count": 3,
|
||||
"cluster_start": "23:58", "cluster_end": "00:00",
|
||||
"cluster_intervals": [{"start_index": 0, "end_index": 2,
|
||||
"start_offset_minutes": 0, "end_offset_minutes": 2,
|
||||
"start_at": "2000-06-14T23:58", "end_at": "2000-06-15T00:00", "segment_index": 0}],
|
||||
}]) == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", [1, 2, 5, 10, 15])
|
||||
def test_same_day_candidate_enumeration_unchanged(step):
|
||||
request = {"birth_date": "2000-06-15", "start_time": "11:45", "end_time": "12:15", "minute_step": step}
|
||||
assert _candidate_datetimes(request) == _candidate_datetimes({**request, "candidate_intervals": [
|
||||
{"start_at": "2000-06-15T11:45", "end_at": "2000-06-15T12:15"},
|
||||
]})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date", ["2000-01-01", "2000-03-01", "2000-02-29", "2001-03-01", "2000-12-31"])
|
||||
@pytest.mark.parametrize("clock", ["00:00", "00:10", "23:59"])
|
||||
def test_calendar_boundaries(date, clock):
|
||||
center = datetime.fromisoformat(f"{date}T{clock}")
|
||||
start, end = center - timedelta(minutes=15), center + timedelta(minutes=15)
|
||||
request = {"birth_date": date, "start_time": start.strftime("%H:%M"), "end_time": end.strftime("%H:%M"),
|
||||
"candidate_intervals": [{"start_at": start.isoformat(timespec="minutes"), "end_at": end.isoformat(timespec="minutes")}]}
|
||||
moments = _candidate_datetimes(request)
|
||||
assert moments == [start + timedelta(minutes=i) for i in range(31)]
|
||||
assert moments[15] == center
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", [2, 15])
|
||||
def test_nondivisible_sampling_preserves_legacy_endpoint_rule(step):
|
||||
request = {"birth_date": "2000-01-01", "start_time": "12:00", "end_time": "12:31", "minute_step": step}
|
||||
moments = _candidate_datetimes(request)
|
||||
assert [at.minute for at in moments] == list(range(0, 32, step))
|
||||
assert moments[-1].minute == 30 # legacy range() never appends an unaligned endpoint
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parts", [
|
||||
[{"start_at": "2000-02-30T00:00", "end_at": "2000-03-01T00:00"}],
|
||||
[{"start_at": "1999-12-30T23:59", "end_at": "1999-12-30T23:59"}],
|
||||
[{"start_at": "2000-01-03T00:00", "end_at": "2000-01-03T00:00"}],
|
||||
[{"start_at": "2000-01-01T01:00", "end_at": "2000-01-01T00:00"}],
|
||||
[{"start_at": "2000-01-01T00:00", "end_at": "2000-01-01T01:00"}, {"start_at": "2000-01-01T01:00", "end_at": "2000-01-01T02:00"}],
|
||||
[{"start_at": "2000-01-01T00:00", "end_at": "2000-01-01T00:01"}, {"start_at": "2000-01-02T00:00", "end_at": "2000-01-02T00:01"}],
|
||||
[{"start_at": "2000-01-01T00:00Z", "end_at": "2000-01-01T00:01Z"}],
|
||||
])
|
||||
def test_invalid_intervals_fail_closed(parts):
|
||||
from scripts.rectification.candidate_window import candidate_intervals
|
||||
with pytest.raises(ValueError):
|
||||
candidate_intervals({"birth_date": "2000-01-01", "start_time": "00:00", "end_time": "23:59", "candidate_intervals": parts})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parts", [None, [], {}, "", [None], [{"start_at": "2000-01-01T00:00"}]])
|
||||
def test_api_normalization_rejects_present_invalid_intervals(parts):
|
||||
from scripts.rectification.contracts import normalize_rectification_request
|
||||
request = {"birth_date": "2000-01-01", "start_time": "23:55", "end_time": "00:25",
|
||||
"lat": 0, "lon": 0, "tz": 0, "events": []}
|
||||
legacy = normalize_rectification_request(request)
|
||||
assert "candidate_intervals" not in legacy
|
||||
with pytest.raises(ValueError):
|
||||
normalize_rectification_request({**request, "candidate_intervals": parts})
|
||||
|
||||
|
||||
def test_report_sampler_recasts_actual_dates_without_gap(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
from scripts import jyotish_engine
|
||||
import flexible_birth_time_profile as profile
|
||||
captured = []
|
||||
def recast(at, **kwargs):
|
||||
captured.append(at)
|
||||
return {"ascendant": {"sign": "Aries"}}
|
||||
monkeypatch.setattr(profile, "_recast_candidate_layers", recast)
|
||||
args = SimpleNamespace(year=2000, month=3, day=1, hour=0, minute=0, lat=0, lon=0, tz=0,
|
||||
birth_time_accuracy="provisional", ayanamsa="raman", candidate_range={"start_time": "23:58", "end_time": "00:02",
|
||||
"candidate_intervals": [{"start_at": "2000-02-29T23:58", "end_at": "2000-03-01T00:02"}]})
|
||||
result = jyotish_engine._build_birth_time_sensitivity(args)
|
||||
assert captured == [datetime(2000, 2, 29, 23, 58)+timedelta(minutes=i) for i in range(5)]
|
||||
assert result["status"] == "candidate_window_only"
|
||||
captured.clear()
|
||||
args.candidate_range = {"start_time": "23:00", "end_time": "03:59", "candidate_intervals": [
|
||||
{"start_at": "2000-03-01T00:00", "end_at": "2000-03-01T03:59"},
|
||||
{"start_at": "2000-03-01T23:00", "end_at": "2000-03-01T23:59"}]}
|
||||
jyotish_engine._build_birth_time_sensitivity(args)
|
||||
assert len(captured) == 31
|
||||
assert all(at.date().isoformat() == "2000-03-01" and (at.hour < 4 or at.hour == 23) for at in captured)
|
||||
|
||||
|
||||
def test_segment_cap_merge_retains_all_minutes_without_daytime():
|
||||
from scripts.rectification.candidate_window import candidate_positions, intervals_from_positions, interval_union_width
|
||||
from scripts.rectification.candidate_contrast import select_signature_representatives
|
||||
request = {"birth_date": "2000-01-01", "start_time": "23:00", "end_time": "03:59", "candidate_intervals": [
|
||||
{"start_at": "2000-01-01T00:00", "end_at": "2000-01-01T03:59"},
|
||||
{"start_at": "2000-01-01T23:00", "end_at": "2000-01-01T23:59"}]}
|
||||
positions = candidate_positions(request, _candidate_datetimes(request))
|
||||
contexts = [{**row, "feature": {"time": row["time"], "ascendant_sign_index": row["window_index"] % 70}} for row in positions]
|
||||
rows = [{"time": row["time"], "score": 1.0} for row in positions]
|
||||
result = select_signature_representatives(rows, contexts)
|
||||
assert len(result) == 64
|
||||
assert {time for row in result for time in row["cluster_times"]} == {row["time"] for row in positions}
|
||||
intervals = [part for row in result for part in intervals_from_positions(row["cluster_positions"])]
|
||||
assert interval_union_width(intervals) == 300
|
||||
assert {part["segment_index"] for part in intervals} == {0, 1}
|
||||
|
||||
|
||||
def test_same_signature_different_segments_and_step_offsets():
|
||||
from scripts.rectification.candidate_window import candidate_positions, intervals_from_positions, interval_union_width
|
||||
from scripts.rectification.candidate_contrast import cluster_contexts_by_signature
|
||||
request = {"birth_date": "2000-01-01", "start_time": "23:00", "end_time": "03:59", "minute_step": 2,
|
||||
"candidate_intervals": [{"start_at": "2000-01-01T00:00", "end_at": "2000-01-01T03:59"},
|
||||
{"start_at": "2000-01-01T23:00", "end_at": "2000-01-01T23:59"}]}
|
||||
rows = candidate_positions(request, _candidate_datetimes(request))
|
||||
contexts = [{**row, "feature": {"time": row["time"]}} for row in rows]
|
||||
assert len(cluster_contexts_by_signature(contexts)) == 2
|
||||
assert rows[120]["window_offset_minutes"] == 1380
|
||||
assert rows[120]["window_index"] == 120
|
||||
assert interval_union_width(intervals_from_positions(rows)) == 298
|
||||
@@ -19,8 +19,8 @@ def _row(time: str, score: float) -> dict:
|
||||
class RelativeSupportScaleTest(unittest.TestCase):
|
||||
def test_holdout_gate_keeps_proportional_default(self) -> None:
|
||||
self.assertEqual(POLICY_VERSION, "rectification-candidate-policy-v3")
|
||||
# Cross-midnight date fix: scoring identity -7 -> -8, not a prior/policy change.
|
||||
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-8")
|
||||
# Explicit date windows: scoring identity -8 -> -9, not a prior/policy change.
|
||||
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-9")
|
||||
self.assertEqual(RELATIVE_SUPPORT_MODE, "proportional")
|
||||
|
||||
def test_offset_top_two_lead_is_at_least_proportional(self) -> None:
|
||||
|
||||
@@ -117,12 +117,15 @@ def test_requires_exactly_two_distinct_candidate_times_before_running_vedastro()
|
||||
handler._compute_rectification_v5_vedastro_validate(request(candidate_times))
|
||||
|
||||
|
||||
def test_passes_only_when_official_layers_discriminate_and_primary_is_strictly_better(monkeypatch):
|
||||
@pytest.mark.parametrize("dated", [False, True])
|
||||
def test_passes_only_when_official_layers_discriminate_and_primary_is_strictly_better(monkeypatch, dated):
|
||||
range_calls = []
|
||||
minute_dates = []
|
||||
|
||||
class Adapter:
|
||||
@staticmethod
|
||||
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
|
||||
minute_dates.append((case["year"], case["month"], case["day"]))
|
||||
return minute_snapshot(case)
|
||||
|
||||
@staticmethod
|
||||
@@ -141,7 +144,13 @@ def test_passes_only_when_official_layers_discriminate_and_primary_is_strictly_b
|
||||
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
|
||||
monkeypatch.setenv("VEDASTRO_API_KEY", "must-not-leak")
|
||||
|
||||
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
|
||||
body = request()
|
||||
if dated:
|
||||
body["birth_date"] = "2000-03-01"
|
||||
body["candidate_intervals"] = [{"start_at": "2000-02-29T05:13", "end_at": "2000-02-29T05:14"}]
|
||||
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(body)
|
||||
if dated:
|
||||
assert minute_dates == [(2000, 2, 29), (2000, 2, 29)]
|
||||
|
||||
assert result["status"] == "pass"
|
||||
assert result["passed"] is True
|
||||
|
||||
@@ -13,12 +13,43 @@ from scripts.research.sealed_holdout_rerun import (
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES = {
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
}
|
||||
IDENTITY_FORBIDDEN_PREFIXES = (
|
||||
"references/rectification_sealed_holdout.v1.json",
|
||||
"docs/research/sealed_holdout_rerun_midnight_anchor_2026_09_21",
|
||||
"docs/research/reported_offset_midnight_anchor_2026_09_21",
|
||||
"docs/research/history/",
|
||||
"artifacts/",
|
||||
)
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_midnight_date_anchor_modules_remain_in_extended_identity():
|
||||
dataset = read(DATASET)
|
||||
identity = implementation_identity()
|
||||
legacy = set(dataset["frozen_scoring"]["files"])
|
||||
assert len(legacy) == 12
|
||||
assert MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES <= set(PRODUCTION_FILES)
|
||||
assert MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES <= set(identity["production_scoring_files"])
|
||||
assert MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES.isdisjoint(legacy)
|
||||
for path in sorted(MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES):
|
||||
assert (ROOT / path).is_file()
|
||||
assert identity["file_sha256"][path] == file_sha256(ROOT / path)
|
||||
assert not any(
|
||||
path == prefix or path.startswith(prefix)
|
||||
for path in identity["file_sha256"]
|
||||
for prefix in IDENTITY_FORBIDDEN_PREFIXES
|
||||
)
|
||||
|
||||
|
||||
def test_contract_tracks_actual_current_scorer_and_dataset_audit():
|
||||
dataset = read(DATASET)
|
||||
contract = read(ROOT / "references/rectification_sealed_holdout.v1.json")
|
||||
@@ -49,6 +80,7 @@ def test_frozen_record_matches_dataset_scorer_and_evaluator_bytes():
|
||||
assert frozen["extended_identity"] == implementation_identity()
|
||||
assert set(PRODUCTION_FILES) <= set(frozen["extended_identity"]["production_scoring_files"])
|
||||
assert {"scripts/rectification/scoring_service.py", "scripts/rectification/dasha_transition_proximity.py"} <= set(PRODUCTION_FILES)
|
||||
assert MIDNIGHT_DATE_ANCHOR_PRODUCTION_FILES <= set(PRODUCTION_FILES)
|
||||
for path, digest in frozen["extended_identity"]["file_sha256"].items():
|
||||
assert digest == file_sha256(ROOT / path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user