Files
Jyotisha/tests/test_rectification_v5_services.py
T
Jesse_Chen 8e31680b45 fix(rectification): honor declared birth-time uncertainty and split windows over two hours (BUG-571–573)
Intake stores how sure the user is; rectification now searches that range, offers a one-click widen when event fit is low at the edge, and trisects windows longer than two hours before the minute grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 12:18:50 +08:00

1020 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import unittest
from datetime import date
from unittest.mock import patch
from uuid import UUID
from scripts.active_rectification_event_engine import _candidate_datetimes
from scripts.jyotish_api_server import (
API_COMMAND_MAP,
TECHNIQUE_EXAMPLE_ENDPOINTS,
BadRequest,
JyotishAPIHandler,
)
from scripts.rectification.api_service import diagnostics, score_candidates
from scripts.rectification.contracts import normalize_rectification_request
from scripts.rectification.scoring_service import (
build_event_contribution_matrix,
calculation_spec,
sample_event_dates,
score_from_matrix,
sha256,
)
EVENT_ID = "00000000-0000-4000-8000-000000000001"
def event(index: int, domain: str, event_kind: str, *, precision: str = "day"):
return {
"id": f"00000000-0000-4000-8000-{index:012d}",
"domain": domain,
"event_kind": event_kind,
"date_start": "2016-09-15",
"date_end": "2016-09-15",
"precision": precision,
"summary": event_kind,
}
def request(
*,
precision: str = "month",
event_kind: str = "education_milestone",
domain: str = "education",
start_time: str = "05:13",
end_time: str = "05:15",
date_start: str = "2016-09-01",
date_end: str = "2016-09-30",
):
return {
"birth_date": "1997-08-08",
"start_time": start_time,
"end_time": end_time,
"lat": 36.419,
"lon": 114.213,
"tz": 8,
"events": [{
"id": EVENT_ID,
"domain": domain,
"event_kind": event_kind,
"date_start": date_start,
"date_end": date_end,
"precision": precision,
"summary": "大学入学",
}],
}
class RectificationV5ServicesTest(unittest.TestCase):
def test_event_contract_v2_accepts_native_kinds_and_retains_background_events(self):
supported = {
"education": ("education_start", "education_completion", "education_interruption", "education_change"),
"career": ("career_entry", "career_change", "promotion", "career_pressure", "career_exit", "business_start"),
"relationship": ("relationship_start", "relationship_commitment", "relationship_separation", "relationship_end"),
"relocation": ("relocation", "foreign_move", "return", "home_change"),
"finance": ("finance_gain", "finance_loss", "income_change", "asset_change"),
"health": ("self_health_event", "pressure_period"),
"family": ("family_event",),
"appearance": ("appearance_note",),
"marks": ("birthmark_or_scar",),
"occupation": ("occupation_note",),
"horary": ("horary_query",),
"other": ("other",),
}
for domain, kinds in supported.items():
for event_kind in kinds:
normalized = normalize_rectification_request(
request(domain=domain, event_kind=event_kind),
today=date(2026, 7, 28),
)
self.assertEqual(normalized["events"][0]["domain"], domain)
self.assertEqual(normalized["events"][0]["event_kind"], event_kind)
def test_event_contract_v2_preserves_server_provenance_and_enforces_self_scoring(self):
body = request(domain="career", event_kind="career_entry")
body["events"][0].update({
"source_turn_id": "33333333-3333-4333-8333-333333333333",
"subject": "self",
"date_source": "user_quote",
"date_reliability": "month_exact",
"date_corroboration": "劳动合同",
"date_conflict_status": "none",
})
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
event_value = normalized["events"][0]
self.assertEqual(event_value["source_turn_id"], "33333333-3333-4333-8333-333333333333")
self.assertEqual(event_value["subject"], "self")
self.assertEqual(event_value["date_source"], "user_quote")
self.assertEqual(event_value["date_reliability"], "month_exact")
self.assertEqual(event_value["date_corroboration"], "劳动合同")
self.assertEqual(event_value["date_conflict_status"], "none")
body["events"][0]["subject"] = "family"
with self.assertRaisesRegex(ValueError, "subject must be self for scoreable events"):
normalize_rectification_request(body, today=date(2026, 7, 28))
def test_background_only_events_are_retained_without_invoking_the_scoring_engine(self):
body = request()
body["events"] = [
event(2, "other", "other", precision="year"),
event(3, "horary", "horary_query", precision="day"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
def fail_if_called(_request):
self.fail("background events must not reach the scoring engine")
built = build_event_contribution_matrix(normalized, row_provider=fail_if_called)
self.assertEqual(built["candidate_times"], [])
self.assertEqual(built["matrix"], {})
self.assertEqual(built["date_sensitivity"], [])
def test_family_events_are_scoreable_and_reach_the_engine(self):
body = request()
body["events"] = [event(1, "family", "family_event")]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
seen = []
def rows(value):
seen.append(value["events"][0]["domain"])
event_value = value["events"][0]
return [{
"time": "05:13",
"score": 4,
"evidence": [{
"event_id": event_value["id"],
"domain": event_value["domain"],
"candidate_time": "05:13",
"rule_ids": ["vim_md_domain_house", "d1-rashi", "d12-dwadashamsha"],
"points": 4,
}],
"missing_layers": [],
}]
built = build_event_contribution_matrix(normalized, row_provider=rows)
self.assertEqual(seen, ["family"])
self.assertEqual(built["matrix"][normalized["events"][0]["id"]]["05:13"]["technique_layers"], [
"d1-rashi", "d12-dwadashamsha", "d3-drekkana", "d7-saptamsha", "vim_md_domain_house",
])
def test_declared_date_precision_changes_real_contribution_weight(self):
def rows(value):
event = value["events"][0]
return [{
"time": "05:13",
"score": 10,
"evidence": [{
"event_id": event["id"],
"domain": event["domain"],
"candidate_time": "05:13",
"rule_ids": ["vim_md_domain_house"],
"points": 10,
}],
"missing_layers": [],
}]
cases = {
"day": request(precision="day", event_kind="education_start", date_start="2016-09-15", date_end="2016-09-15"),
"month": request(precision="month", event_kind="education_start"),
"year": request(
precision="year",
event_kind="education_start",
date_start="2016-01-01",
date_end="2016-12-31",
),
}
points = {}
for precision, body in cases.items():
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
built = build_event_contribution_matrix(normalized, row_provider=rows)
points[precision] = built["matrix"][EVENT_ID]["05:13"]["points"]
self.assertEqual(points, {"day": 10, "month": 8, "year": 5})
def test_calculation_spec_hash_matches_typescript_for_integral_timezone(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
self.assertEqual(
sha256(calculation_spec(normalized)),
"d68a59ae345a87bcdf7ad45fee913d3aca02c553f896ce6c173a8bd4ab327af3",
)
def test_cross_midnight_range_preserves_next_day_datetimes_and_typescript_hash(self):
normalized = normalize_rectification_request(
request(start_time="23:00", end_time="03:59"),
today=date(2026, 7, 28),
)
candidates = _candidate_datetimes(normalized)
self.assertEqual(len(candidates), 300)
self.assertEqual(candidates[0].isoformat(), "1997-08-08T23:00:00")
self.assertEqual(candidates[60].isoformat(), "1997-08-09T00:00:00")
self.assertEqual(candidates[-1].isoformat(), "1997-08-09T03:59:00")
self.assertEqual(
sha256(calculation_spec(normalized)),
"1923a1ee2fa873f1a26c97b23d05cfee38ec38071b60076a014ad17b2844cedd",
)
def test_candidate_range_boundaries_stay_bounded_and_equal_is_one_minute(self):
full_day = normalize_rectification_request(
request(start_time="00:00", end_time="23:59"),
today=date(2026, 7, 28),
)
equal = normalize_rectification_request(
request(start_time="05:13", end_time="05:13"),
today=date(2026, 7, 28),
)
self.assertEqual(len(_candidate_datetimes(full_day)), 1_440)
self.assertEqual(len(_candidate_datetimes(equal)), 1)
with self.assertRaisesRegex(ValueError, "end_time must be HH:MM"):
normalize_rectification_request(
request(start_time="00:00", end_time="24:00"),
today=date(2026, 7, 28),
)
def test_daytime_candidate_range_remains_on_birth_date(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
candidates = _candidate_datetimes(normalized)
self.assertEqual([value.isoformat() for value in candidates], [
"1997-08-08T05:13:00",
"1997-08-08T05:14:00",
"1997-08-08T05:15:00",
])
def test_shared_validator_retains_background_events_and_rejects_non_self_health(self):
background = normalize_rectification_request(
request(domain="family", event_kind="family_event"), today=date(2026, 7, 28)
)
self.assertEqual(background["events"][0]["event_kind"], "family_event")
with self.assertRaisesRegex(ValueError, "event_kind does not match domain"):
normalize_rectification_request(
request(domain="family", event_kind="family_bereavement"), today=date(2026, 7, 28)
)
with self.assertRaisesRegex(ValueError, "event_kind does not match domain"):
normalize_rectification_request(request(domain="health_pressure", event_kind="family_health_event"), today=date(2026, 7, 28))
normalized = normalize_rectification_request(request(domain="health_pressure", event_kind="self_health_event"), today=date(2026, 7, 28))
self.assertEqual(normalized["events"][0]["event_kind"], "self_health_event")
def test_relationship_end_is_native_and_uses_pressure_semantics(self):
normalized = normalize_rectification_request(
request(domain="relationship", event_kind="relationship_end", precision="day"),
today=date(2026, 7, 28),
)
def rows(value):
event_value = value["events"][0]
return [{
"time": "05:13", "score": 10,
"evidence": [{
"event_id": EVENT_ID, "domain": "relationship", "candidate_time": "05:13",
"rule_ids": ["controlled_transit_saturn_domain_house", f"event_kind:{event_value['event_kind']}"],
"points": 10,
}],
"missing_layers": [],
}]
built = build_event_contribution_matrix(normalized, row_provider=rows)
contribution = built["matrix"][EVENT_ID]["05:13"]
self.assertIn("event_kind_profile:relationship_end:pressure", contribution["rule_ids"])
self.assertGreater(contribution["points"], 10)
def test_relationship_start_and_change_use_distinct_rule_conditioned_profiles(self):
def relationship_rows(_value):
return [
{
"time": "05:13", "score": 10,
"evidence": [{
"event_id": EVENT_ID, "domain": "relationship", "candidate_time": "05:13",
"rule_ids": ["vim_md_domain_house", "vim_ad_functional_benefic_auxiliary"],
"points": 10,
}],
"missing_layers": [],
},
{
"time": "05:14", "score": 9,
"evidence": [{
"event_id": EVENT_ID, "domain": "relationship", "candidate_time": "05:14",
"rule_ids": ["controlled_transit_saturn_domain_house", "vim_ad_functional_malefic_auxiliary"],
"points": 9,
}],
"missing_layers": [],
},
]
start = normalize_rectification_request(
request(domain="relationship", event_kind="relationship_start"), today=date(2026, 7, 28)
)
change = normalize_rectification_request(
request(domain="relationship", event_kind="relationship_change"), today=date(2026, 7, 28)
)
start_matrix = build_event_contribution_matrix(start, row_provider=relationship_rows)
change_matrix = build_event_contribution_matrix(change, row_provider=relationship_rows)
self.assertGreater(start_matrix["matrix"][EVENT_ID]["05:13"]["points"], start_matrix["matrix"][EVENT_ID]["05:14"]["points"] )
self.assertGreater(change_matrix["matrix"][EVENT_ID]["05:14"]["points"], change_matrix["matrix"][EVENT_ID]["05:13"]["points"] )
self.assertEqual(change_matrix["date_sensitivity"][0]["sample_winners"], ["05:14", "05:14", "05:14"])
self.assertNotIn("event_kind_profile", change_matrix["matrix"][EVENT_ID]["05:14"]["technique_layers"])
def test_relationship_kind_profile_does_not_create_points_without_activation(self):
normalized = normalize_rectification_request(
request(domain="relationship", event_kind="relationship_change", precision="day"),
today=date(2026, 7, 28),
)
def rows(_value):
return [{
"time": "05:13", "score": 0,
"evidence": [{
"event_id": EVENT_ID, "domain": "relationship", "candidate_time": "05:13",
"rule_ids": ["no_domain_activation", "event_kind:relationship_change"],
"points": 0,
}],
"missing_layers": [],
}]
built = build_event_contribution_matrix(normalized, row_provider=rows)
self.assertEqual(built["matrix"][EVENT_ID]["05:13"]["points"], 0)
def test_date_sampling_preserves_declared_range_and_uses_bounded_samples(self):
base = request()["events"][0]
self.assertEqual(sample_event_dates({**base, "precision": "month"}), ["2016-09-01", "2016-09-15", "2016-09-30"])
year = {**base, "precision": "year", "date_start": "2016-01-01", "date_end": "2016-12-31"}
self.assertEqual(len(sample_event_dates(year)), 12)
ranged = {**base, "precision": "range", "date_start": "2015-01-01", "date_end": "2016-12-31"}
self.assertLessEqual(len(sample_event_dates(ranged)), 12)
def test_contribution_matrix_and_leave_out_diagnostics_use_matrix_math(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
def rows(value):
sampled = value["events"][0]["date"]
shift = {"2016-09-01": 0, "2016-09-15": 1, "2016-09-30": 2}[sampled]
return [{
"time": candidate,
"score": points + shift,
"evidence": [{
"event_id": EVENT_ID,
"domain": "education",
"candidate_time": candidate,
"rule_ids": ["D24:test"],
"points": points + shift,
}],
"missing_layers": ["KP_cusps"],
} for candidate, points in [("05:13", 9), ("05:14", 10), ("05:15", 8)]]
built = build_event_contribution_matrix(normalized, row_provider=rows)
scored = score_from_matrix(normalized, built)
self.assertEqual(built["matrix"][EVENT_ID]["05:14"]["points"], 8.8)
self.assertEqual(scored[1]["score"], 8.8)
self.assertEqual(built["missing_layers"], ["KP_cusps"])
def test_formal_score_and_diagnostics_endpoints_share_the_service_bundle(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
built = {
"candidate_times": ["05:13", "05:14"],
"matrix": {EVENT_ID: {
"05:13": {"points": 10, "rule_ids": ["D24:a"], "technique_layers": ["D24"]},
"05:14": {"points": 8, "rule_ids": ["D24:b"], "technique_layers": ["D24"]},
}},
"date_sensitivity": [{
"event_id": EVENT_ID,
"declared_date_range": {"start": "2016-09-01", "end": "2016-09-30", "precision": "month"},
"sample_dates": ["2016-09-01", "2016-09-15", "2016-09-30"],
"winner_retention_rate": 1,
"score_variance": 1,
"sample_winners": ["05:13", "05:13", "05:13"],
}],
"missing_layers": ["KP_cusps"],
"static_contexts": [{"feature": {"time": "05:13"}}, {"feature": {"time": "05:14"}}],
}
feature = {
"calculation_spec_hash": "0" * 64,
"algorithm_version": "rectification-v5-matrix-scoring-2",
"candidate_count": 2,
"feature_hash": "1" * 64,
"features": [{"time": "05:13"}, {"time": "05:14"}],
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value=feature
):
scored = score_candidates(normalized)
diagnostic_result = diagnostics(normalized)
self.assertFalse(scored["can_confirm_exact_minute"])
self.assertIn("event_contribution_matrix", scored)
self.assertEqual(diagnostic_result["diagnostics"]["leave_one_event_out_retention_rate"], 1)
self.assertFalse(diagnostic_result["can_confirm_exact_minute"])
def test_score_endpoint_returns_candidate_decision_receipt_v2_and_real_execution_ledger(self):
body = request()
body["events"] = [
event(1, "education", "education_start"),
event(2, "career", "promotion", precision="month"),
event(3, "finance", "finance_gain"),
event(5, "family", "family_event"),
event(4, "other", "other", precision="year"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
scored_ids = [item["id"] for item in normalized["events"] if item["domain"] != "other"]
built = {
"candidate_times": ["05:13", "05:14", "05:15"],
"matrix": {
event_id: {
"05:13": {"points": 4, "rule_ids": ["vim_md_domain_house"], "technique_layers": ["vim_md_domain_house"]},
"05:14": {"points": 2, "rule_ids": ["vim_md_domain_house"], "technique_layers": ["vim_md_domain_house"]},
"05:15": {"points": 1, "rule_ids": ["vim_md_domain_house"], "technique_layers": ["vim_md_domain_house"]},
}
for event_id in scored_ids
},
"date_sensitivity": [
{
"event_id": event_id,
"declared_date_range": {"start": "2016-09-15", "end": "2016-09-15", "precision": "day"},
"sample_dates": ["2016-09-15"],
"winner_retention_rate": 1,
"score_variance": 0,
"sample_winners": ["05:13"],
}
for event_id in scored_ids
],
"missing_layers": [],
"static_contexts": [],
}
diagnostic_values = {
"primary_cluster_retention_rate": 1,
"leave_one_event_out_retention_rate": .9,
"leave_one_domain_out_retention_rate": .9,
"date_sensitivity_retention_rate": .9,
"neighbor_support_minutes": 1,
"primary_secondary_margin_percent": 30,
"event_date_sensitivity": built["date_sensitivity"],
}
feature = {
"calculation_spec_hash": "0" * 64,
"algorithm_version": "rectification-v5-matrix-scoring-2",
"candidate_count": 3,
"feature_hash": "1" * 64,
"features": [],
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value=feature
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
first = score_candidates(normalized)
second = score_candidates(normalized)
self.assertEqual(first["event_contract_version"], "rectification-event-contract-v2")
self.assertEqual(first["candidate_decisions"], second["candidate_decisions"])
self.assertEqual(sum(item["relative_support"] for item in first["candidate_decisions"]), 100)
for rank, candidate in enumerate(first["candidate_decisions"], start=1):
self.assertEqual(set(candidate), {"candidate_id", "rank", "time", "relative_support", "tied_minute_count"})
self.assertEqual(candidate["rank"], rank)
UUID(candidate["candidate_id"])
receipt = first["candidate_decision_receipt"]
self.assertEqual(receipt["receipt_version"], "candidate-decision-receipt-v2")
self.assertTrue(receipt["selection_allowed"])
self.assertTrue(receipt["acceptance_allowed"])
self.assertFalse(receipt["confirmation_allowed"])
self.assertTrue(receipt["gates"]["domain_diversity"]["passed"])
self.assertTrue(receipt["gates"]["date_quality"]["passed"])
self.assertTrue(receipt["gates"]["exact_confirmation"]["fail_closed"])
self.assertEqual(receipt["gates"]["exact_confirmation"]["external_validation_status"], "not_evaluated")
self.assertNotEqual(receipt["gates"]["exact_confirmation"]["external_validation_status"], "fail")
self.assertEqual(first["decision_receipt"], receipt)
# C2 holdout 校准:offset/softmax 覆盖下降,默认仍用 proportionalpolicy 不 bump
self.assertEqual(first["decision_policy_version"], "rectification-candidate-policy-v3")
self.assertTrue(receipt["display_allowed"])
self.assertTrue(receipt["accept_allowed"])
self.assertFalse(receipt["confirm_allowed"])
self.assertIsNotNone(receipt["representative_candidate_id"])
self.assertEqual(receipt["representative_time"], "05:13")
entries = first["execution_ledger"]
background = next(item for item in entries if item.get("event_id") == normalized["events"][4]["id"])
self.assertEqual(background["status"], "retained_not_scored")
executed = next(item for item in entries if item.get("event_id") == normalized["events"][0]["id"])
self.assertEqual(executed["technique_layers"], ["vim_md_domain_house"])
def test_diagnostics_endpoint_mirrors_candidate_decision_policy_fields(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
scored = {
"result_id": "00000000-0000-4000-8000-000000000099",
"algorithm_version": "rectification-v5-matrix-scoring-2",
"event_contract_version": "rectification-event-contract-v2",
"decision_policy_version": "rectification-candidate-policy-v2",
"calculation_spec_hash": "0" * 64,
"candidate_decisions": [],
"candidate_decision_receipt": {},
"decision_receipt": {},
"execution_ledger_version": "rectification-execution-ledger-v2",
"execution_ledger": [],
"diagnostics": {},
"missing_layers": [],
"display_allowed": False,
"selection_allowed": False,
"acceptance_allowed": False,
"propose_allowed": False,
"confirmation_allowed": False,
"representative_candidate_id": None,
"representative_time": None,
"overall_confidence": "low",
"margin_percent": 0.0,
}
with patch("scripts.rectification.api_service.score_candidates", return_value=scored):
result = diagnostics(normalized)
for field in (
"display_allowed",
"selection_allowed",
"acceptance_allowed",
"propose_allowed",
"confirmation_allowed",
"representative_candidate_id",
"representative_time",
"overall_confidence",
"margin_percent",
):
self.assertEqual(result[field], scored[field])
self.assertFalse(result["can_confirm_exact_minute"])
def test_single_domain_or_quantized_top_tie_blocks_candidate_acceptance(self):
body = request()
body["events"] = [
event(1, "career", "career_entry"),
event(2, "career", "promotion"),
event(3, "career", "career_change"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
built = {
"candidate_times": ["05:13", "05:14"],
"matrix": {
item["id"]: {
"05:13": {"points": 3.333346, "rule_ids": ["D10:test"], "technique_layers": ["D10"]},
"05:14": {"points": 3.333333, "rule_ids": ["D10:test"], "technique_layers": ["D10"]},
}
for item in normalized["events"]
},
"date_sensitivity": [],
"missing_layers": [],
"static_contexts": [],
}
diagnostic_values = {
"primary_cluster_retention_rate": 1,
"leave_one_event_out_retention_rate": 1,
"leave_one_domain_out_retention_rate": 1,
"date_sensitivity_retention_rate": 1,
"neighbor_support_minutes": 1,
"primary_secondary_margin_percent": 50,
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value={}
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
result = score_candidates(normalized)
receipt = result["candidate_decision_receipt"]
self.assertFalse(receipt["acceptance_allowed"])
self.assertIn("insufficient_domain_diversity", receipt["reasons"])
self.assertIn("tied_top_score", receipt["reasons"])
self.assertIn("tied_top_score", receipt["confirmation_reasons"])
self.assertNotIn("tied_top_score", receipt["acceptance_reasons"])
self.assertEqual(result["candidate_decisions"][0]["tied_minute_count"], 2)
self.assertEqual(receipt["tie_policy"]["score_quantum"], .0001)
self.assertEqual(receipt["tie_policy"]["absolute_tolerance"], .0001)
def test_two_domain_quantized_top_tie_allows_adoption_but_blocks_confirmation(self):
body = request()
body["events"] = [
event(1, "career", "career_entry"),
event(2, "career", "promotion"),
event(3, "education", "education_start"),
event(4, "family", "family_event"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
built = {
"candidate_times": ["05:13", "05:14"],
"matrix": {
item["id"]: {
"05:13": {"points": 3.333346, "rule_ids": ["D10:test"], "technique_layers": ["D10"]},
"05:14": {"points": 3.333333, "rule_ids": ["D10:test"], "technique_layers": ["D10"]},
}
for item in normalized["events"]
},
"date_sensitivity": [],
"missing_layers": [],
"static_contexts": [],
}
diagnostic_values = {
"primary_cluster_retention_rate": 1,
"leave_one_event_out_retention_rate": 1,
"leave_one_domain_out_retention_rate": 1,
"date_sensitivity_retention_rate": 1,
"neighbor_support_minutes": 1,
"primary_secondary_margin_percent": 50,
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value={}
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
result = score_candidates(normalized)
receipt = result["candidate_decision_receipt"]
self.assertTrue(receipt["acceptance_allowed"])
self.assertTrue(receipt["selection_allowed"])
self.assertFalse(receipt["confirmation_allowed"])
self.assertIn("tied_top_score", receipt["confirmation_reasons"])
self.assertNotIn("tied_top_score", receipt["acceptance_reasons"])
self.assertEqual(result["candidate_decisions"][0]["tied_minute_count"], 2)
self.assertFalse(result["can_confirm_exact_minute"])
def test_low_date_quality_blocks_acceptance_even_with_multiple_domains_and_a_unique_candidate(self):
body = request()
body["events"] = [
event(1, "education", "education_start", precision="year"),
event(2, "career", "promotion", precision="year"),
event(3, "finance", "finance_gain", precision="year"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
built = {
"candidate_times": ["05:13", "05:14"],
"matrix": {
item["id"]: {
"05:13": {"points": 4, "rule_ids": ["test"], "technique_layers": ["test"]},
"05:14": {"points": 1, "rule_ids": ["test"], "technique_layers": ["test"]},
}
for item in normalized["events"]
},
"date_sensitivity": [],
"missing_layers": [],
"static_contexts": [],
}
diagnostic_values = {
"leave_one_event_out_retention_rate": 1,
"leave_one_domain_out_retention_rate": 1,
"date_sensitivity_retention_rate": 1,
"primary_secondary_margin_percent": 50,
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value={}
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
result = score_candidates(normalized)
receipt = result["candidate_decision_receipt"]
self.assertTrue(result["candidate_decisions"])
self.assertFalse(receipt["acceptance_allowed"])
self.assertFalse(receipt["gates"]["date_quality"]["passed"])
self.assertIn("low_date_quality", receipt["reasons"])
self.assertFalse(result["can_confirm_exact_minute"])
def test_http_registry_exposes_all_v5_endpoints(self):
expected = {
"rectification-v5-candidate-features": "/api/rectification/v5/candidate-features",
"rectification-v5-score": "/api/rectification/v5/score",
"rectification-v5-diagnostics": "/api/rectification/v5/diagnostics",
"rectification-v5-versions": "/api/rectification/v5/versions",
}
for command, endpoint in expected.items():
self.assertEqual(API_COMMAND_MAP[command], endpoint)
self.assertIn(endpoint, TECHNIQUE_EXAMPLE_ENDPOINTS)
def test_engine_scoring_versions_match_score_identity(self):
from scripts.rectification.api_service import engine_scoring_versions
from scripts.rectification.decision_policy import POLICY_VERSION
from scripts.rectification.scoring_service import ALGORITHM_VERSION
versions = engine_scoring_versions()
self.assertEqual(versions["algorithm_version"], ALGORITHM_VERSION)
self.assertEqual(versions["decision_policy_version"], POLICY_VERSION)
handler = object.__new__(JyotishAPIHandler)
payload = handler._compute_rectification_v5_versions()
self.assertEqual(payload["algorithm_version"], ALGORITHM_VERSION)
self.assertEqual(payload["decision_policy_version"], POLICY_VERSION)
self.assertEqual(payload["endpoint"], "rectification_v5_versions")
def test_zero_evidence_returns_declared_window_report(self):
body = request(start_time="23:58", end_time="00:02")
body["events"] = []
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
result = score_candidates(normalized)
report = result["rectification_report"]
self.assertEqual(report["candidate_range"]["start_time"], "23:58")
self.assertEqual(report["candidate_range"]["end_time"], "00:02")
self.assertEqual(report["candidate_range"]["width_minutes"], 5)
self.assertIsNone(report["representative_time"])
self.assertEqual(report["evidence"], [])
self.assertFalse(report["candidate_range"]["representative_is_unique"])
self.assertIn("do_not_apply_as_birth_time_truth", result["next_step_codes"])
def test_report_exposes_evidence_excluded_candidates_and_diagnostics_consistently(self):
body = request()
body["events"] = [
event(1, "education", "education_start"),
event(2, "career", "promotion"),
]
normalized = normalize_rectification_request(body, today=date(2026, 7, 28))
built = {
"candidate_times": ["05:13", "05:14", "05:15"],
"matrix": {
item["id"]: {
"05:13": {"points": 4, "rule_ids": ["D10:test"], "technique_layers": ["D10"]},
"05:14": {"points": 4, "rule_ids": ["D9:test"], "technique_layers": ["D9"]},
"05:15": {"points": -1, "rule_ids": ["D1:test"], "technique_layers": ["D1"]},
}
for item in normalized["events"]
},
"date_sensitivity": [],
"missing_layers": [],
"static_contexts": [
{"feature": {"time": "05:13", "ascendant_sign_index": 1}},
{"feature": {"time": "05:14", "ascendant_sign_index": 2}},
{"feature": {"time": "05:15", "ascendant_sign_index": 3}},
],
}
diagnostic_values = {
"primary_cluster_retention_rate": 1,
"leave_one_event_out_retention_rate": 1,
"leave_one_domain_out_retention_rate": 1,
"date_sensitivity_retention_rate": 1,
"neighbor_support_minutes": 1,
"primary_secondary_margin_percent": 30,
}
with patch("scripts.rectification.api_service.build_event_contribution_matrix", return_value=built), patch(
"scripts.rectification.api_service.build_candidate_feature_snapshot", return_value={}
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
scored = score_candidates(normalized)
diagnostic_result = diagnostics(normalized)
report = scored["rectification_report"]
self.assertEqual(report["candidate_range"]["start_time"], "05:13")
self.assertEqual(report["candidate_range"]["end_time"], "05:14")
self.assertEqual(report["representative_time"], "05:13")
self.assertEqual([row["status"] for row in report["evidence"]], ["supporting", "supporting"])
self.assertEqual(report["evidence"][0]["methods"], ["D10"])
self.assertGreaterEqual(len(report["excluded_candidates"]), 1)
self.assertEqual(scored["candidate_summary"]["stability"]["label"], scored["overall_confidence"])
self.assertEqual(diagnostic_result["rectification_report"], report)
self.assertEqual(diagnostic_result["next_step_codes"], scored["next_step_codes"])
def test_http_handler_enforces_subject_and_event_kind_boundaries(self):
handler = object.__new__(JyotishAPIHandler)
retained = handler._rectification_v5_request(request(domain="family", event_kind="family_event"))
self.assertEqual(retained["events"][0]["event_kind"], "family_event")
with self.assertRaisesRegex(BadRequest, "event_kind does not match domain"):
handler._rectification_v5_request(request(domain="family", event_kind="family_bereavement"))
with self.assertRaisesRegex(BadRequest, "event_kind does not match domain"):
handler._rectification_v5_request(request(domain="health_pressure", event_kind="family_health_event"))
normalized = handler._rectification_v5_request(
request(domain="health_pressure", event_kind="self_health_event")
)
self.assertEqual(normalized["events"][0]["event_kind"], "self_health_event")
def test_v4_compatibility_and_v5_score_handlers_share_the_v5_service(self):
handler = object.__new__(JyotishAPIHandler)
result = {"result_id": "00000000-0000-4000-8000-000000000099", "can_confirm_exact_minute": False}
with patch("scripts.rectification.api_service.score_candidates", return_value=result) as scorer:
v5 = handler._compute_rectification_v5_score(request())
v4 = handler._compute_active_rectification_events_v4(request())
self.assertEqual(scorer.call_count, 2)
self.assertEqual(v5["endpoint"], "rectification_v5_score")
self.assertEqual(v4["endpoint"], "active_rectification_events_v4")
self.assertEqual(v5["result_id"], v4["result_id"])
self.assertFalse(v5["can_confirm_exact_minute"])
self.assertFalse(v4["can_confirm_exact_minute"])
def test_minute_step_default_keeps_calculation_spec_hash_and_full_grid(self):
normalized = normalize_rectification_request(request(), today=date(2026, 7, 28))
self.assertNotIn("minute_step", normalized)
self.assertEqual(
sha256(calculation_spec(normalized)),
"d68a59ae345a87bcdf7ad45fee913d3aca02c553f896ce6c173a8bd4ab327af3",
)
candidates = _candidate_datetimes(normalized)
self.assertEqual(len(candidates), 3)
with_step = normalize_rectification_request({**request(), "minute_step": 1}, today=date(2026, 7, 28))
self.assertNotIn("minute_step", with_step)
self.assertEqual(
[value.isoformat() for value in candidates],
[value.isoformat() for value in _candidate_datetimes(with_step)],
)
def test_minute_step_ten_yields_one_hundred_forty_four_full_day_candidates(self):
normalized = normalize_rectification_request(
{**request(start_time="00:00", end_time="23:59"), "minute_step": 10},
today=date(2026, 7, 28),
)
self.assertEqual(normalized["minute_step"], 10)
self.assertEqual(calculation_spec(normalized)["minuteStep"], 10)
candidates = _candidate_datetimes(normalized)
self.assertEqual(len(candidates), 144)
self.assertEqual(candidates[0].isoformat(), "1997-08-08T00:00:00")
self.assertEqual(candidates[1].isoformat(), "1997-08-08T00:10:00")
self.assertEqual(candidates[-1].isoformat(), "1997-08-08T23:50:00")
def test_block_scan_five_periods_sum_to_one_hundred(self):
from scripts.rectification.api_service import BLOCK_SCAN_PERIODS, block_scan
from uuid import uuid4
events = [
{
"id": str(uuid4()),
"domain": domain,
"event_kind": kind,
"date_start": start,
"date_end": end,
"precision": precision,
"summary": kind,
}
for domain, kind, start, end, precision in (
("education", "education_start", "2016-09-01", "2016-09-30", "month"),
("relationship", "relationship_end", "2024-08-08", "2024-08-08", "day"),
("relocation", "relocation", "2023-07-01", "2023-07-31", "month"),
)
]
normalized = normalize_rectification_request(
{
"birth_date": "1998-03-15",
"start_time": "00:00",
"end_time": "23:59",
"lat": 39.9042,
"lon": 116.4074,
"tz": 8.0,
"minute_step": 10,
"events": events,
},
today=date(2026, 7, 28),
)
result = block_scan(normalized)
self.assertEqual(result["candidate_count"], 144)
self.assertEqual(len(result["blocks"]), 5)
self.assertEqual(
[(row["period"], row["start_time"], row["end_time"]) for row in result["blocks"]],
list(BLOCK_SCAN_PERIODS),
)
self.assertEqual(round(sum(float(row["relative_support"]) for row in result["blocks"]), 1), 100.0)
self.assertFalse(result["acceptance_allowed"])
self.assertFalse(result["selection_allowed"])
self.assertEqual(result["precision_stage"]["current"], "block_scan")
self.assertEqual(result["discriminating_event_probes"], [])
self.assertEqual(result["decision_receipt"]["precision_stage"]["current"], "block_scan")
def test_block_scan_seven_events_finishes_within_fifteen_seconds(self):
import time
from uuid import uuid4
from scripts.rectification.api_service import block_scan
events = [
{
"id": str(uuid4()),
"domain": domain,
"event_kind": kind,
"date_start": start,
"date_end": end,
"precision": precision,
"summary": kind,
}
for domain, kind, start, end, precision in (
("education", "education_start", "2016-09-01", "2016-09-30", "month"),
("relationship", "relationship_end", "2024-08-08", "2024-08-08", "day"),
("relocation", "relocation", "2023-07-01", "2023-07-31", "month"),
("career", "career_entry", "2018-07-01", "2018-07-31", "month"),
("finance", "income_change", "2021-03-01", "2021-03-31", "month"),
("health", "self_health_event", "2020-11-01", "2020-11-30", "month"),
("family", "family_event", "2019-05-01", "2019-05-31", "month"),
)
]
normalized = normalize_rectification_request(
{
"birth_date": "1998-03-15",
"start_time": "00:00",
"end_time": "23:59",
"lat": 39.9042,
"lon": 116.4074,
"tz": 8.0,
"minute_step": 10,
"events": events,
},
today=date(2026, 7, 28),
)
started = time.time()
result = block_scan(normalized)
elapsed = time.time() - started
self.assertEqual(result["candidate_count"], 144)
self.assertLessEqual(elapsed, 15, f"block_scan took {elapsed:.1f}s")
def test_block_scan_equal_scores_share_evenly_and_length_does_not_win(self):
from scripts.rectification.api_service import block_scan
def grid(score_for):
return [
{"time": f"{hour:02d}:{minute:02d}", "score": score_for(hour), "supporting_event_ids": []}
for hour in range(24)
for minute in range(0, 60, 10)
]
equal_rows = grid(lambda _hour: 12.0)
morning_rows = grid(lambda hour: 20.0 if 4 <= hour <= 7 else 10.0)
dummy = {
"result_id": "00000000-0000-4000-8000-000000000099",
"algorithm_version": "test",
"calculation_spec": {},
"calculation_spec_hash": "abc",
"decision_receipt": {},
}
request_body = {
"birth_date": "1998-03-15",
"start_time": "00:00",
"end_time": "23:59",
"lat": 39.9042,
"lon": 116.4074,
"tz": 8.0,
"minute_step": 10,
"events": [],
}
with patch("scripts.rectification.api_service.score_candidates", return_value={**dummy, "candidate_scores": equal_rows}):
equal = block_scan(request_body)
self.assertEqual([row["relative_support"] for row in equal["blocks"]], [20.0, 20.0, 20.0, 20.0, 20.0])
self.assertEqual([row["candidate_count"] for row in equal["blocks"]], [24, 24, 36, 30, 30])
with patch("scripts.rectification.api_service.score_candidates", return_value={**dummy, "candidate_scores": morning_rows}):
morning = block_scan(request_body)
shares = {row["period"]: row["relative_support"] for row in morning["blocks"]}
self.assertEqual(max(shares, key=shares.get), "early_morning")
self.assertGreater(shares["early_morning"], shares["afternoon"])
self.assertEqual(shares["afternoon"], 0.0)
def test_block_scan_custom_three_blocks_sum_to_one_hundred(self):
from scripts.rectification.api_service import block_scan
dummy = {
"result_id": "00000000-0000-4000-8000-000000000099",
"algorithm_version": "test",
"calculation_spec": {},
"calculation_spec_hash": "abc",
"decision_receipt": {},
}
rows = [
{"time": f"{hour:02d}:{minute:02d}", "score": 12.0, "supporting_event_ids": []}
for hour in range(12, 18)
for minute in range(0, 60, 10)
if not (hour == 17 and minute > 50)
]
request_body = {
"birth_date": "1998-03-15",
"start_time": "12:00",
"end_time": "17:59",
"lat": 39.9042,
"lon": 116.4074,
"tz": 8.0,
"minute_step": 10,
"events": [],
"blocks": [
{"label": "sub_1", "start_time": "12:00", "end_time": "13:59"},
{"label": "sub_2", "start_time": "14:00", "end_time": "15:59"},
{"label": "sub_3", "start_time": "16:00", "end_time": "17:59"},
],
}
with patch("scripts.rectification.api_service.score_candidates", return_value={**dummy, "candidate_scores": rows}):
result = block_scan(request_body)
self.assertEqual(len(result["blocks"]), 3)
self.assertEqual(round(sum(float(row["relative_support"]) for row in result["blocks"]), 1), 100.0)
shares = sorted(float(row["relative_support"]) for row in result["blocks"])
self.assertEqual(shares, [33.3, 33.3, 33.4])
def test_block_scan_rejects_overlapping_or_out_of_window_blocks(self):
from scripts.rectification.contracts import normalize_rectification_request
base = {
"birth_date": "1998-03-15",
"start_time": "12:00",
"end_time": "17:59",
"lat": 39.9042,
"lon": 116.4074,
"tz": 8.0,
"events": [],
}
with self.assertRaises(ValueError):
normalize_rectification_request({
**base,
"blocks": [
{"label": "sub_1", "start_time": "12:00", "end_time": "14:00"},
{"label": "sub_2", "start_time": "13:30", "end_time": "15:00"},
],
}, today=date(2026, 7, 28))
with self.assertRaises(ValueError):
normalize_rectification_request({
**base,
"blocks": [
{"label": "sub_1", "start_time": "03:00", "end_time": "04:00"},
],
}, today=date(2026, 7, 28))
if __name__ == "__main__":
unittest.main()