Files
Jyotisha/tests/test_rectification_v5_services.py
T
Jesse_Chen 62afa521f4 fix(rectification): keep proportional prior after holdout calibration (BUG-560)
Implement offset and softmax relative-support scales and a public holdout calibration script, but leave the default proportional because no scheme passed the coverage-and-width gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 23:05:42 +08:00

794 lines
38 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"])
if __name__ == "__main__":
unittest.main()