feat: complete minute birth-time rectification flow
This commit is contained in:
@@ -9,6 +9,7 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import jyotish_api_server as api_server # noqa: E402
|
||||
from jyotish_api_server import BadRequest, JyotishAPIHandler # noqa: E402
|
||||
|
||||
|
||||
@@ -406,19 +407,115 @@ def test_gochara_conflict_downgrades_without_verified_timing_claim_red() -> None
|
||||
assert scored["timing_claim_status"] == "exploratory_unvalidated"
|
||||
|
||||
|
||||
def test_high_rigor_event_rectification_queues_vedastro_packet(monkeypatch) -> None:
|
||||
def test_high_rigor_event_rectification_requires_real_vedastro_candidate_discrimination(monkeypatch) -> None:
|
||||
original_loader = api_server._load_local_module
|
||||
|
||||
class LocalScorer:
|
||||
@staticmethod
|
||||
def score_life_events(request):
|
||||
return {
|
||||
"result_id": "local-result",
|
||||
"confidence": "high",
|
||||
"can_apply": True,
|
||||
"winning_segment": {
|
||||
"start_time": "14:30",
|
||||
"end_time": "14:30",
|
||||
"representative_time": "14:30",
|
||||
"width_minutes": 1,
|
||||
},
|
||||
"event_count": len(request["events"]),
|
||||
"domain_count": len({event["domain"] for event in request["events"]}),
|
||||
"top_score": 30,
|
||||
"second_score": 20,
|
||||
"margin_percent": 33.33,
|
||||
"reasons": [],
|
||||
"evidence": [],
|
||||
"algorithm_version": "fixture",
|
||||
"canonical_input_hash": "canonical-fixture",
|
||||
"calculation_contract": {"events": request["events"]},
|
||||
"stability_diagnostics": {
|
||||
"neighbor_stability": {"all_required_passed": True},
|
||||
"leave_one_event_out": {"status": "pass"},
|
||||
},
|
||||
"missing_layers": [],
|
||||
"candidate_ranking_summary": [
|
||||
{"rank": 1, "time": "14:30", "score": 30, "tied_minute_count": 1},
|
||||
{"rank": 2, "time": "14:31", "score": 20, "tied_minute_count": 1},
|
||||
],
|
||||
}
|
||||
|
||||
class VedAstroAdapter:
|
||||
@staticmethod
|
||||
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
|
||||
minute = case["minute"]
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"source": "vedastro_official",
|
||||
"layers": {
|
||||
"ascendant_house_boundaries": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"asc-{minute}",
|
||||
"ascendant": {"sign": "Leo", "degree_in_sign": minute / 10},
|
||||
"houses": {"House1": {}},
|
||||
},
|
||||
"D9": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"d9-{minute}",
|
||||
"houses": {"House1": {}},
|
||||
"planets": {},
|
||||
},
|
||||
"D10": {
|
||||
"status": "ok",
|
||||
"fingerprint": "d10-same",
|
||||
"houses": {"House1": {}},
|
||||
"planets": {},
|
||||
},
|
||||
"dasha_boundaries": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"dasha-{minute}",
|
||||
"boundary_count": 3,
|
||||
},
|
||||
"kp_cusp_sub_lord": {
|
||||
"status": "unsupported_by_verified_official_interface",
|
||||
"reason": "not supported by verified official interface",
|
||||
},
|
||||
},
|
||||
"raw_response": {"must_not": "leak"},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def run_range_scan_for_case(case, _domain, _start, _end, case_id="user_chart"):
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"event_count": 1,
|
||||
"top_event": {"event_id": f"event-{case_id}"},
|
||||
"evidence_ledger": [{"signal_lift": 1}],
|
||||
"raw_response": {"must_not": "leak"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"scripts.rectification_three_engine_packet._enqueue_vedastro_gateway_job",
|
||||
api_server,
|
||||
"_load_local_module",
|
||||
lambda name: LocalScorer if name == "active_rectification_events" else VedAstroAdapter if name == "vedastro_service_adapter" else original_loader(name),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"scripts.rectification_three_engine_packet.build_packet",
|
||||
lambda _case: {
|
||||
"engine_status": {"local": "ok", "pyjhora": "ok", "jyotishganit": "ok"},
|
||||
"match_count": 3,
|
||||
"mismatch_count": 0,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
JyotishAPIHandler,
|
||||
"_compute_vedastro_gateway_run",
|
||||
lambda *_args, **_kwargs: {
|
||||
"scope": "vedastro_gateway_job_receipt",
|
||||
"status": "queued",
|
||||
"job_id": "vgw_rectification",
|
||||
"poll_path": "/api/vedastro_gateway/jobs/vgw_rectification",
|
||||
"raw_response_archive": {
|
||||
"status": "pending",
|
||||
"official_raw_response_available": False,
|
||||
},
|
||||
"boundary": "VedAstro raw response remains server-side; this receipt never returns request data or raw evidence.",
|
||||
"status": "ok",
|
||||
"official_closure_state": "official_verified",
|
||||
"official_closure_reason": "official_raw_response_present",
|
||||
"official_raw_response": {"must_not": "leak"},
|
||||
},
|
||||
)
|
||||
result = _handler()._compute_active_rectification_events(
|
||||
@@ -444,30 +541,290 @@ def test_high_rigor_event_rectification_queues_vedastro_packet(monkeypatch) -> N
|
||||
}
|
||||
)
|
||||
receipt = result["three_engine_packet"]["vedastro"]
|
||||
assert receipt["status"] == "queued"
|
||||
assert receipt["job_id"] == "vgw_rectification"
|
||||
assert "1993" not in str(receipt)
|
||||
assert receipt["status"] == "official_verified"
|
||||
contract = result["technique_contract"]
|
||||
assert result["can_apply"] is False
|
||||
assert contract["confirmation_allowed"] is False
|
||||
assert contract["decision"] == "continue_rectification"
|
||||
validation = contract["external_engines"]["validation"]
|
||||
assert result["can_apply"] is True
|
||||
assert contract["confirmation_allowed"] is True
|
||||
assert contract["decision"] == "confirm_minute"
|
||||
assert contract["canonical_input_hash"]
|
||||
assert contract["gates"]["public_holdout_release"]["status"] == "blocked"
|
||||
assert contract["gates"]["vedastro_minute_sensitive_validation"]["status"] == "pass"
|
||||
assert validation["minute_sensitive_validation"]["discriminated"] is True
|
||||
assert validation["minute_sensitive_validation"]["discriminated_layers"]
|
||||
assert validation["event_background_validation"]["used_for_decision"] is False
|
||||
assert validation["event_background_validation"]["candidates"][0]["metric"] == validation["event_background_validation"]["candidates"][1]["metric"]
|
||||
assert "must_not" not in str(validation)
|
||||
assert result["calculation_contract"]["events"][0]["summary"] == "2011 年 9 月离开家乡开始大学生活"
|
||||
|
||||
|
||||
def test_active_rectification_events_accepts_product_limit_of_eight() -> None:
|
||||
events = [
|
||||
def test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow(monkeypatch) -> None:
|
||||
original_loader = api_server._load_local_module
|
||||
vedastro_calls: list[tuple[str, str, str, str]] = []
|
||||
|
||||
class VedAstroAdapter:
|
||||
@staticmethod
|
||||
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
|
||||
candidate_time = f'{case["hour"]:02d}:{case["minute"]:02d}'
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"source": "vedastro_official",
|
||||
"layers": {
|
||||
"ascendant_house_boundaries": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"asc-{candidate_time}",
|
||||
"ascendant": {"sign": "Leo", "degree_in_sign": case["minute"] / 10},
|
||||
"houses": {"House1": {}},
|
||||
},
|
||||
"D9": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"d9-{candidate_time}",
|
||||
"houses": {"House1": {}},
|
||||
"planets": {},
|
||||
},
|
||||
"D10": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"d10-{candidate_time}",
|
||||
"houses": {"House1": {}},
|
||||
"planets": {},
|
||||
},
|
||||
"dasha_boundaries": {
|
||||
"status": "ok",
|
||||
"fingerprint": f"dasha-{candidate_time}",
|
||||
"boundary_count": 3,
|
||||
},
|
||||
"kp_cusp_sub_lord": {
|
||||
"status": "unsupported_by_verified_official_interface",
|
||||
"reason": "not supported by verified official interface",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
|
||||
candidate_time = f'{case["hour"]:02d}:{case["minute"]:02d}'
|
||||
vedastro_calls.append((candidate_time, domain, start, end))
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"event_count": 1,
|
||||
"top_event": {"event_id": f"event-{case_id}"},
|
||||
"evidence_ledger": [{"signal_lift": 1}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
api_server,
|
||||
"_load_local_module",
|
||||
lambda name: VedAstroAdapter if name == "vedastro_service_adapter" else original_loader(name),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"scripts.rectification_three_engine_packet.build_packet",
|
||||
lambda _case: {
|
||||
"engine_status": {"local": "ok", "pyjhora": "ok", "jyotishganit": "ok"},
|
||||
"match_count": 3,
|
||||
"mismatch_count": 0,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
JyotishAPIHandler,
|
||||
"_compute_vedastro_gateway_run",
|
||||
lambda *_args, **_kwargs: {
|
||||
"status": "ok",
|
||||
"official_closure_state": "official_verified",
|
||||
"official_closure_reason": "official_raw_response_present",
|
||||
"official_raw_response": {"status": "ok"},
|
||||
},
|
||||
)
|
||||
|
||||
result = _handler()._compute_active_rectification_events(
|
||||
{
|
||||
"id": f"00000000-0000-4000-a000-{index:012d}",
|
||||
"domain": "education" if index % 2 == 0 else "career",
|
||||
"date": f"{2011 + index}",
|
||||
"precision": "year",
|
||||
"summary": f"event {index}",
|
||||
"birth_date": "1997-08-08",
|
||||
"start_time": "04:00",
|
||||
"end_time": "07:59",
|
||||
"lat": 36.420487,
|
||||
"lon": 114.209936,
|
||||
"tz": 8,
|
||||
"high_rigor": True,
|
||||
"events": [
|
||||
{"id": "00000000-0000-4000-8000-000000000001", "domain": "education", "date": "2016-09", "precision": "month", "summary": "离家去外地上大学"},
|
||||
{"id": "00000000-0000-4000-8000-000000000002", "domain": "career", "date": "2020-04", "precision": "month", "summary": "去石油化工研究院实习做研究员"},
|
||||
{"id": "00000000-0000-4000-8000-000000000003", "domain": "career", "date": "2020-10", "precision": "month", "summary": "从研究院辞职"},
|
||||
{"id": "00000000-0000-4000-8000-000000000004", "domain": "education", "date": "2020-12", "precision": "month", "summary": "参加研究生考试结果不理想"},
|
||||
{"id": "00000000-0000-4000-8000-000000000005", "domain": "relocation", "date": "2021-01", "precision": "month", "summary": "回家备考并长期在家"},
|
||||
{"id": "00000000-0000-4000-8000-000000000006", "domain": "education", "date": "2022-12", "precision": "month", "summary": "考研结束后转向自学前端"},
|
||||
{"id": "00000000-0000-4000-8000-000000000007", "domain": "career", "date": "2023-04", "precision": "month", "summary": "去北京入职医疗器械公司"},
|
||||
{"id": "00000000-0000-4000-8000-000000000008", "domain": "relationship", "date": "2024-08-08", "precision": "day", "summary": "恋爱关系发生重大转折"},
|
||||
{"id": "00000000-0000-4000-8000-000000000009", "domain": "relationship", "date": "2024-10", "precision": "month", "summary": "短暂复联后主动断联"},
|
||||
{"id": "00000000-0000-4000-8000-000000000010", "domain": "finance", "date": "2026-01", "precision": "month", "summary": "公司无法正常发放工资"},
|
||||
{"id": "00000000-0000-4000-8000-000000000011", "domain": "career", "date": "2026-07-10", "precision": "day", "summary": "与朋友正式决定创业"},
|
||||
{"id": "00000000-0000-4000-8000-000000000012", "domain": "career", "date": "2026-07-21", "precision": "day", "summary": "提交公司注册材料"},
|
||||
],
|
||||
}
|
||||
for index in range(8)
|
||||
)
|
||||
|
||||
assert result["winning_segment"] == {
|
||||
"start_time": "05:07",
|
||||
"end_time": "05:08",
|
||||
"representative_time": "05:07",
|
||||
"width_minutes": 2,
|
||||
}
|
||||
assert result["stability_diagnostics"]["neighbor_stability"]["all_required_passed"] is False
|
||||
assert result["stability_diagnostics"]["leave_one_event_out"]["status"] != "pass"
|
||||
assert len(vedastro_calls) == 6
|
||||
assert {call[0] for call in vedastro_calls} == {"05:07", "05:21"}
|
||||
assert {call[1] for call in vedastro_calls} == {"career", "wealth", "marriage"}
|
||||
assert all(start == end for _, _, start, end in vedastro_calls)
|
||||
assert {
|
||||
(domain, start)
|
||||
for candidate, domain, start, _ in vedastro_calls
|
||||
if candidate == "05:07"
|
||||
} == {
|
||||
("career", "2026-07-21"),
|
||||
("wealth", "2026-01-16"),
|
||||
("marriage", "2024-08-08"),
|
||||
}
|
||||
assert {
|
||||
candidate
|
||||
for candidate, _, _, _ in vedastro_calls
|
||||
} == {
|
||||
item["time"]
|
||||
for item in result["candidate_ranking_summary"][:2]
|
||||
}
|
||||
validation = result["technique_contract"]["external_engines"]["validation"]
|
||||
event_validation = validation["event_background_validation"]
|
||||
assert event_validation["eligible_event_count"] == 12
|
||||
assert event_validation["supported_event_count"] == 3
|
||||
assert event_validation["used_for_decision"] is False
|
||||
assert event_validation["candidates"][0]["metric"] == event_validation["candidates"][1]["metric"]
|
||||
assert "one_strongest_event_per_native_adapter_domain" in event_validation["selection_policy"]
|
||||
assert result["three_engine_packet"]["vedastro"]["status"] == "official_verified"
|
||||
assert result["three_engine_packet"]["vedastro"]["search_events_role"] == "background_only"
|
||||
assert result["technique_contract"]["gates"]["vedastro_minute_sensitive_validation"]["status"] == "pass"
|
||||
assert validation["minute_sensitive_validation"]["discriminated"] is True
|
||||
assert result["technique_contract"]["confirmation_allowed"] is True
|
||||
assert result["technique_contract"]["decision"] == "confirm_minute"
|
||||
assert result["can_apply"] is True
|
||||
assert "neighbor_stability_not_passed" not in result["technique_contract"]["hard_blockers"]
|
||||
assert "leave_one_event_out_not_passed" not in result["technique_contract"]["hard_blockers"]
|
||||
|
||||
|
||||
def test_identical_vedastro_minute_sensitive_snapshots_do_not_discriminate_candidates() -> None:
|
||||
layers = {
|
||||
name: {"status": "ok", "fingerprint": f"same-{name}"}
|
||||
for name in api_server._VEDASTRO_MINUTE_SENSITIVE_LAYERS
|
||||
}
|
||||
snapshots = [
|
||||
{"candidate_time": "05:07", "available": True, "layers": layers},
|
||||
{"candidate_time": "05:21", "available": True, "layers": layers},
|
||||
]
|
||||
|
||||
comparison = api_server._compare_vedastro_minute_snapshots(snapshots)
|
||||
|
||||
assert comparison["comparison_ready"] is True
|
||||
assert comparison["discriminated"] is False
|
||||
assert comparison["discriminated_layers"] == []
|
||||
assert all(item["status"] == "same" for item in comparison["differences"].values())
|
||||
|
||||
|
||||
def test_search_events_difference_cannot_override_identical_minute_snapshots(monkeypatch) -> None:
|
||||
original_loader = api_server._load_local_module
|
||||
|
||||
class LocalScorer:
|
||||
@staticmethod
|
||||
def score_life_events(request):
|
||||
return {
|
||||
"result_id": "local-result",
|
||||
"confidence": "high",
|
||||
"can_apply": True,
|
||||
"winning_segment": {
|
||||
"start_time": "14:30",
|
||||
"end_time": "14:30",
|
||||
"representative_time": "14:30",
|
||||
"width_minutes": 1,
|
||||
},
|
||||
"event_count": len(request["events"]),
|
||||
"domain_count": len({event["domain"] for event in request["events"]}),
|
||||
"top_score": 30,
|
||||
"second_score": 20,
|
||||
"margin_percent": 33.33,
|
||||
"reasons": [],
|
||||
"evidence": [],
|
||||
"algorithm_version": "fixture",
|
||||
"canonical_input_hash": "canonical-fixture",
|
||||
"calculation_contract": {"events": request["events"]},
|
||||
"stability_diagnostics": {
|
||||
"neighbor_stability": {"all_required_passed": True},
|
||||
"leave_one_event_out": {"status": "pass"},
|
||||
},
|
||||
"missing_layers": [],
|
||||
"candidate_ranking_summary": [
|
||||
{"rank": 1, "time": "14:30", "score": 30, "tied_minute_count": 1},
|
||||
{"rank": 2, "time": "14:31", "score": 20, "tied_minute_count": 1},
|
||||
],
|
||||
}
|
||||
|
||||
class VedAstroAdapter:
|
||||
@staticmethod
|
||||
def run_rectification_minute_snapshot_for_case(_case, case_id="user_chart"):
|
||||
layers = {
|
||||
name: {
|
||||
"status": "ok",
|
||||
"fingerprint": f"same-{name}",
|
||||
"houses": {"House1": {}},
|
||||
"planets": {},
|
||||
"boundary_count": 3,
|
||||
}
|
||||
for name in api_server._VEDASTRO_MINUTE_SENSITIVE_LAYERS
|
||||
}
|
||||
layers["ascendant_house_boundaries"]["ascendant"] = {
|
||||
"sign": "Leo",
|
||||
"degree_in_sign": 12.5,
|
||||
}
|
||||
layers["kp_cusp_sub_lord"] = {
|
||||
"status": "unsupported_by_verified_official_interface",
|
||||
"reason": "not supported by verified official interface",
|
||||
}
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"source": "vedastro_official",
|
||||
"layers": layers,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def run_range_scan_for_case(case, _domain, _start, _end, case_id="user_chart"):
|
||||
event_count = 10 if case["minute"] == 30 else 1
|
||||
return {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"event_count": event_count,
|
||||
"top_event": {"event_id": f"event-{case_id}"},
|
||||
"evidence_ledger": [{"signal_lift": event_count}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
api_server,
|
||||
"_load_local_module",
|
||||
lambda name: LocalScorer if name == "active_rectification_events" else VedAstroAdapter if name == "vedastro_service_adapter" else original_loader(name),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"scripts.rectification_three_engine_packet.build_packet",
|
||||
lambda _case: {
|
||||
"engine_status": {"local": "ok", "pyjhora": "ok", "jyotishganit": "ok"},
|
||||
"match_count": 3,
|
||||
"mismatch_count": 0,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
JyotishAPIHandler,
|
||||
"_compute_vedastro_gateway_run",
|
||||
lambda *_args, **_kwargs: {
|
||||
"status": "ok",
|
||||
"official_closure_state": "official_verified",
|
||||
"official_closure_reason": "official_raw_response_present",
|
||||
},
|
||||
)
|
||||
|
||||
result = _handler()._compute_active_rectification_events(
|
||||
{
|
||||
"birth_date": "1993-04-17",
|
||||
@@ -476,9 +833,21 @@ def test_active_rectification_events_accepts_product_limit_of_eight() -> None:
|
||||
"lat": 36.683333,
|
||||
"lon": 114.35,
|
||||
"tz": 8,
|
||||
"events": events,
|
||||
"high_rigor": True,
|
||||
"events": [
|
||||
{"id": "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5", "domain": "education", "date": "2011-09", "precision": "month"},
|
||||
{"id": "0790866c-ad5e-4a45-b2b4-a5c73f6be6ea", "domain": "career", "date": "2019-07-01", "precision": "day"},
|
||||
{"id": "0ef52e51-ab5f-453b-81e5-adb44a929224", "domain": "relationship", "date": "2021", "precision": "year"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["event_count"] == 8
|
||||
validation = result["technique_contract"]["external_engines"]["validation"]
|
||||
background_candidates = validation["event_background_validation"]["candidates"]
|
||||
assert background_candidates[0]["metric"] != background_candidates[1]["metric"]
|
||||
assert validation["event_background_validation"]["used_for_decision"] is False
|
||||
assert validation["minute_sensitive_validation"]["discriminated"] is False
|
||||
assert result["can_apply"] is False
|
||||
assert result["technique_contract"]["confirmation_allowed"] is False
|
||||
assert "vedastro_minute_sensitive_layers_not_discriminated" in result["reasons"]
|
||||
assert "vedastro_candidate_not_discriminated" not in result["reasons"]
|
||||
|
||||
@@ -20,32 +20,29 @@ def _row(time: str, score: float) -> CandidateScoreRow:
|
||||
}
|
||||
|
||||
|
||||
def test_high_evidence_candidate_remains_blocked_until_public_holdout_release() -> None:
|
||||
def test_locally_stable_high_evidence_candidate_can_enter_external_validation() -> None:
|
||||
result = adjudicate_candidate_rows(
|
||||
[
|
||||
_row("14:22", 16),
|
||||
_row("14:23", 16),
|
||||
_row("14:24", 16),
|
||||
_row("14:25", 16),
|
||||
_row("14:26", 16),
|
||||
_row("14:27", 10),
|
||||
],
|
||||
[_row(f"14:{minute:02d}", 20 if minute == 25 else 10) for minute in range(20, 31)],
|
||||
event_count=4,
|
||||
domain_count=3,
|
||||
request_fingerprint="high-fixture",
|
||||
leave_one_event_out={"status": "pass", "runs": []},
|
||||
)
|
||||
|
||||
assert result["confidence"] == "high"
|
||||
assert result["can_apply"] is False
|
||||
assert "minute_holdout_not_ready" in result["reasons"]
|
||||
assert result["can_apply"] is True
|
||||
assert result["winning_segment"] == {
|
||||
"start_time": "14:22",
|
||||
"end_time": "14:26",
|
||||
"representative_time": "14:24",
|
||||
"width_minutes": 5,
|
||||
"start_time": "14:25",
|
||||
"end_time": "14:25",
|
||||
"representative_time": "14:25",
|
||||
"width_minutes": 1,
|
||||
}
|
||||
assert result["margin_percent"] == 37.5
|
||||
assert result["stability_diagnostics"]["neighbor_stability"]["all_required_passed"] is False
|
||||
assert result["margin_percent"] == 50.0
|
||||
assert result["stability_diagnostics"]["neighbor_stability"]["all_required_passed"] is True
|
||||
assert result["candidate_ranking_summary"][:2] == [
|
||||
{"rank": 1, "time": "14:25", "score": 20, "tied_minute_count": 1},
|
||||
{"rank": 2, "time": "14:24", "score": 10, "tied_minute_count": 10},
|
||||
]
|
||||
|
||||
|
||||
def test_tied_disjoint_candidates_abstain() -> None:
|
||||
@@ -213,7 +210,7 @@ def test_event_summary_is_fingerprinted_without_unlocking_minute_application() -
|
||||
assert changed_hash != input_hash
|
||||
assert result["canonical_input_hash"] == input_hash
|
||||
assert result["can_apply"] is False
|
||||
assert "minute_holdout_not_ready" in result["reasons"]
|
||||
assert "insufficient_events" in result["reasons"]
|
||||
|
||||
|
||||
def test_finance_events_use_d2_d11_and_recompute_both_dashas_per_minute(monkeypatch) -> None:
|
||||
|
||||
@@ -36,6 +36,65 @@ def test_domain_chart_exposes_effective_parameters_and_result_hash() -> None:
|
||||
assert result["calculation_contract"]["effective"]["ayanamsa"] == "lahiri"
|
||||
assert result["result_hash"]
|
||||
|
||||
|
||||
def test_historical_timezone_resolution_distinguishes_new_york_dst() -> None:
|
||||
winter = calculation_service.resolve_timezone_context(
|
||||
lat=40.7128,
|
||||
lon=-74.006,
|
||||
local_datetime=datetime(1990, 1, 15, 12, 0),
|
||||
)
|
||||
summer = calculation_service.resolve_timezone_context(
|
||||
lat=40.7128,
|
||||
lon=-74.006,
|
||||
local_datetime=datetime(1990, 7, 15, 12, 0),
|
||||
)
|
||||
|
||||
assert winter == {
|
||||
"timezone_id": "America/New_York",
|
||||
"timezone_offset": -5.0,
|
||||
"local_time_status": "resolved",
|
||||
}
|
||||
assert summer["timezone_id"] == "America/New_York"
|
||||
assert summer["timezone_offset"] == -4.0
|
||||
|
||||
|
||||
def test_timezone_resolution_refuses_dst_fold_and_gap_offsets() -> None:
|
||||
folded = calculation_service.resolve_timezone_context(
|
||||
lat=40.7128,
|
||||
lon=-74.006,
|
||||
local_datetime=datetime(2020, 11, 1, 1, 30),
|
||||
)
|
||||
gap = calculation_service.resolve_timezone_context(
|
||||
lat=40.7128,
|
||||
lon=-74.006,
|
||||
local_datetime=datetime(2020, 3, 8, 2, 30),
|
||||
)
|
||||
|
||||
assert folded["timezone_offset"] is None
|
||||
assert folded["local_time_status"] == "ambiguous"
|
||||
assert gap["timezone_offset"] is None
|
||||
assert gap["local_time_status"] == "nonexistent"
|
||||
|
||||
|
||||
def test_domain_chart_exposes_inferred_timezone_id() -> None:
|
||||
payload = {key: value for key, value in BIRTH.items() if key != "tz"}
|
||||
result = calculation_service.compute_chart(payload)
|
||||
|
||||
assert result["calculation_contract"]["effective"]["timezone_id"] == "Asia/Kolkata"
|
||||
assert result["calculation_contract"]["effective"]["timezone_source"] == "iana_inferred"
|
||||
|
||||
|
||||
def test_location_timezone_api_contract_preserves_unknown_local_time() -> None:
|
||||
result = jyotish_api_server.resolve_location_timezone_payload({
|
||||
"latitude": 51.5074,
|
||||
"longitude": -0.1278,
|
||||
"birthDate": "1990-01-01",
|
||||
})
|
||||
|
||||
assert result["timezoneId"] == "Europe/London"
|
||||
assert result["timezoneOffset"] is None
|
||||
assert result["localTimeStatus"] == "not_provided"
|
||||
|
||||
def test_api_chart_uses_same_domain_contract_and_preserves_shape(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -159,4 +218,3 @@ def test_api_dasha_boundary_comes_from_domain_service(monkeypatch: pytest.Monkey
|
||||
)
|
||||
assert rest["dasha"]["start_date"] == expected["periods"][0]["start"]
|
||||
assert rest["dasha"]["result_hash"] == expected["result_hash"]
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ def test_authenticated_dynamic_questionnaire_cannot_open_minute_confirmation(mon
|
||||
|
||||
assert result["confidence"] == "high"
|
||||
assert result["can_apply"] is False
|
||||
assert "minute_holdout_not_ready" in result["reasons"]
|
||||
assert "vedastro_validation_required" in result["reasons"]
|
||||
|
||||
|
||||
def test_opportunity_route_defaults_legacy_missing_events_to_empty(monkeypatch) -> None:
|
||||
|
||||
@@ -29,7 +29,9 @@ def test_pyjhora_adapter_diagnostics_reports_current_status() -> None:
|
||||
|
||||
assert report["scope"] == "pyjhora_adapter_diagnostics"
|
||||
assert report["adapter_command"] == "python3 benchmarks/jyotish/scripts/run_pyjhora_compare.py"
|
||||
assert report["status"] in {"available", "missing_dependency"}
|
||||
assert report["status"] in {"available", "missing_dependency", "dependency_import_failed"}
|
||||
if report["status"] == "available":
|
||||
assert report["dependency_import_error"] is None
|
||||
|
||||
|
||||
def test_pyjhora_adapter_diagnostics_reports_missing_dependency() -> None:
|
||||
@@ -43,6 +45,6 @@ def test_pyjhora_adapter_diagnostics_reports_actionable_external_boundary() -> N
|
||||
report = _run_diag({"PYJHORA_MODULE_NAME": "__definitely_missing_pyjhora_module__"})
|
||||
|
||||
assert report["install_hint"]["package"] == "PyJHora"
|
||||
assert "pip install PyJHora" in report["install_hint"]["commands"]
|
||||
assert "requirements-reference-engines.txt" in report["install_hint"]["commands"][0]
|
||||
assert report["license_boundary"] == "AGPL external benchmark only; do not vendor or make it a runtime dependency."
|
||||
assert report["ephemeris_data_note"]
|
||||
|
||||
@@ -21,20 +21,21 @@ def test_contract_discloses_used_and_missing_layers() -> None:
|
||||
assert "ashtakavarga" in contract["auxiliary_layers"]
|
||||
assert "shadbala_verified_components" in contract["auxiliary_layers"]
|
||||
assert "shadbala_sthana_drik_naisargika" in contract["partial_layers"]
|
||||
assert contract["external_engines"]["status"] == "not_run"
|
||||
assert contract["external_engines"]["status"] == "not_evaluated"
|
||||
|
||||
|
||||
def test_high_rigor_requires_real_three_engine_evidence() -> None:
|
||||
contract = build_rectification_technique_contract(event_count=4, domain_count=3, high_rigor=True)
|
||||
assert contract["external_engines"]["status"] == "required_not_run"
|
||||
assert "three_engine_parity_not_passed" in contract["hard_blockers"]
|
||||
assert contract["external_engines"]["status"] == "not_evaluated"
|
||||
assert "vedastro_validation_not_passed" in contract["hard_blockers"]
|
||||
assert contract["can_narrow_to_minute"] is False
|
||||
|
||||
|
||||
def test_public_holdout_blocks_confirmation_even_when_other_gates_pass() -> None:
|
||||
def test_vedastro_minute_sensitive_validation_is_required_after_local_gates_pass() -> None:
|
||||
contract = build_rectification_technique_contract(
|
||||
event_count=4,
|
||||
domain_count=3,
|
||||
local_candidate_ready=True,
|
||||
required_layers_complete=True,
|
||||
canonical_input_hash="canonical-fixture",
|
||||
stability_diagnostics={
|
||||
@@ -46,11 +47,64 @@ def test_public_holdout_blocks_confirmation_even_when_other_gates_pass() -> None
|
||||
assert contract["canonical_input_hash"] == "canonical-fixture"
|
||||
assert contract["gates"]["neighbor_stability"]["status"] == "pass"
|
||||
assert contract["gates"]["leave_one_event_out"]["status"] == "pass"
|
||||
assert contract["gates"]["public_holdout_release"]["status"] == "blocked"
|
||||
assert contract["gates"]["vedastro_minute_sensitive_validation"]["status"] == "not_evaluated"
|
||||
assert contract["confirmation_allowed"] is False
|
||||
assert contract["decision"] == "continue_rectification"
|
||||
|
||||
|
||||
def test_semantic_vedastro_validation_allows_minute_confirmation() -> None:
|
||||
contract = build_rectification_technique_contract(
|
||||
event_count=5,
|
||||
domain_count=3,
|
||||
high_rigor=True,
|
||||
local_candidate_ready=True,
|
||||
required_layers_complete=True,
|
||||
stability_diagnostics={
|
||||
"neighbor_stability": {"all_required_passed": True},
|
||||
"leave_one_event_out": {"status": "pass"},
|
||||
},
|
||||
external_validation={
|
||||
"status": "pass",
|
||||
"vedastro_status": "official_verified",
|
||||
"mismatch_count": 0,
|
||||
"engine_status": {"local": "ok", "pyjhora": "ok", "jyotishganit": "ok"},
|
||||
"minute_sensitive_validation": {"status": "pass"},
|
||||
},
|
||||
)
|
||||
|
||||
assert contract["gates"]["vedastro_official_response"]["status"] == "pass"
|
||||
assert contract["gates"]["vedastro_minute_sensitive_validation"]["status"] == "pass"
|
||||
assert contract["confirmation_allowed"] is True
|
||||
assert contract["decision"] == "confirm_minute"
|
||||
|
||||
|
||||
def test_neighbor_and_leave_one_out_failures_remain_diagnostic_after_external_validation() -> None:
|
||||
contract = build_rectification_technique_contract(
|
||||
event_count=12,
|
||||
domain_count=5,
|
||||
high_rigor=True,
|
||||
local_candidate_ready=True,
|
||||
required_layers_complete=True,
|
||||
stability_diagnostics={
|
||||
"neighbor_stability": {"all_required_passed": False},
|
||||
"leave_one_event_out": {"status": "fail"},
|
||||
},
|
||||
external_validation={
|
||||
"status": "pass",
|
||||
"vedastro_status": "official_verified",
|
||||
"mismatch_count": 0,
|
||||
"engine_status": {"local": "ok", "pyjhora": "ok", "jyotishganit": "ok"},
|
||||
"minute_sensitive_validation": {"status": "pass"},
|
||||
},
|
||||
)
|
||||
|
||||
assert contract["gates"]["neighbor_stability"]["status"] == "diagnostic_fail"
|
||||
assert contract["gates"]["leave_one_event_out"]["status"] == "diagnostic_fail"
|
||||
assert "neighbor_stability_not_passed" not in contract["hard_blockers"]
|
||||
assert "leave_one_event_out_not_passed" not in contract["hard_blockers"]
|
||||
assert contract["confirmation_allowed"] is True
|
||||
|
||||
|
||||
def test_contract_reports_the_actual_missing_dasha_layer() -> None:
|
||||
contract = build_rectification_technique_contract(
|
||||
event_count=3,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from scripts.rectification_three_engine_packet import build_packet, case_hash
|
||||
from scripts.rectification_three_engine_packet import (
|
||||
JYOTISHGANIT_DATA_DIR,
|
||||
_ensure_jyotishganit_data_dir,
|
||||
build_packet,
|
||||
case_hash,
|
||||
)
|
||||
|
||||
CASE = {"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "lat": 0.0, "lon": 0.0, "tz": 0.0}
|
||||
|
||||
@@ -28,6 +33,13 @@ def test_case_hash_uses_only_normalized_calculation_input() -> None:
|
||||
assert case_hash(equivalent) == case_hash({**CASE, "ayanamsa": "lahiri", "node_mode": "mean"})
|
||||
|
||||
|
||||
def test_jyotishganit_uses_project_writable_cache(monkeypatch) -> None:
|
||||
monkeypatch.delenv("JYOTISHGANIT_DATA_DIR", raising=False)
|
||||
|
||||
assert _ensure_jyotishganit_data_dir() == str(JYOTISHGANIT_DATA_DIR)
|
||||
assert __import__("os").environ["JYOTISHGANIT_DATA_DIR"] == str(JYOTISHGANIT_DATA_DIR)
|
||||
|
||||
|
||||
def test_high_rigor_packet_queues_safe_vedastro_receipt_without_raw(monkeypatch) -> None:
|
||||
monkeypatch.setattr("scripts.rectification_three_engine_packet._local_d1", lambda _: {"Sun": "Aries"})
|
||||
monkeypatch.setattr("scripts.rectification_three_engine_packet._pyjhora_d1", lambda _: {"Sun": "Aries"})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _birth_case() -> dict[str, object]:
|
||||
@@ -59,24 +63,35 @@ def test_fast_snapshot_executes_only_five_scalar_methods(monkeypatch) -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_disabled_range_scan_never_calls_network(monkeypatch) -> None:
|
||||
def test_rectification_range_scan_cannot_be_disabled_separately_from_official_network(monkeypatch) -> None:
|
||||
from scripts import vedastro_service_adapter
|
||||
|
||||
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://api.vedastro.org/api")
|
||||
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "1")
|
||||
monkeypatch.setenv("VEDASTRO_RANGE_SCAN_NETWORK_ENABLED", "0")
|
||||
calls: list[dict[str, object]] = []
|
||||
|
||||
def fail_if_called(*_args, **_kwargs):
|
||||
raise AssertionError("range scan network request should not run in chat mode")
|
||||
def fake_post(_endpoint: str, request_preview: dict[str, object]):
|
||||
calls.append(request_preview)
|
||||
return {"Status": "Pass", "Payload": []}, 1, []
|
||||
|
||||
monkeypatch.setattr(vedastro_service_adapter, "_post_json_with_retry", fail_if_called)
|
||||
monkeypatch.setattr(vedastro_service_adapter, "_post_json_with_retry", fake_post)
|
||||
|
||||
result = vedastro_service_adapter.run_range_scan_for_case(
|
||||
_birth_case(),
|
||||
"career",
|
||||
"2026-07-14",
|
||||
"2026-08-14",
|
||||
"2026-07-14",
|
||||
)
|
||||
|
||||
assert result["status"] == "network_execution_disabled"
|
||||
assert "interactive chat path" in result["reason"]
|
||||
assert len(calls) == 1
|
||||
assert result["status"] == "ok"
|
||||
|
||||
|
||||
def test_official_env_example_enables_official_gateway_and_range_scan() -> None:
|
||||
example = (ROOT / ".env.official.example").read_text(encoding="utf-8")
|
||||
|
||||
assert "VEDASTRO_GATEWAY_MODE=official_first" in example
|
||||
assert "VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api" in example
|
||||
assert "VEDASTRO_ENABLE_NETWORK=1" in example
|
||||
assert "VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=1" in example
|
||||
|
||||
@@ -196,6 +196,22 @@ def test_official_full_snapshot_marks_semantic_rate_limit_payloads(monkeypatch)
|
||||
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://example.invalid/api")
|
||||
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "1")
|
||||
monkeypatch.setenv("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", "0")
|
||||
monkeypatch.setenv("VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED", "1")
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_capability_runner_snapshot_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "snapshot_sections": {}, "section_statuses": {}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_python_bridge_snapshot_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "snapshot_sections": {}, "section_statuses": {}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_full_capability_catalog_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "summary": {}, "coverage": {}},
|
||||
)
|
||||
monkeypatch.setattr(adapter, "_post_official_snapshot_section", fake_post)
|
||||
|
||||
result = adapter.run_official_full_snapshot_for_case(
|
||||
@@ -522,6 +538,23 @@ def test_official_full_snapshot_extracts_official_chart_and_varga_from_pass_payl
|
||||
|
||||
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://example.invalid/api")
|
||||
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "1")
|
||||
monkeypatch.setenv("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", "0")
|
||||
monkeypatch.setenv("VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED", "1")
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_capability_runner_snapshot_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "snapshot_sections": {}, "section_statuses": {}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_python_bridge_snapshot_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "snapshot_sections": {}, "section_statuses": {}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_full_capability_catalog_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "summary": {}, "coverage": {}},
|
||||
)
|
||||
monkeypatch.setattr(adapter, "_post_official_snapshot_section", fake_post)
|
||||
|
||||
result = adapter.run_official_full_snapshot_for_case(
|
||||
@@ -663,6 +696,77 @@ def test_official_full_snapshot_can_use_python_bridge_bundle_without_rest_endpoi
|
||||
assert result["snapshot_sections"]["ashtakavarga"]["Payload"]["AshtakvargaLifeMap"]["TotalBindus"] == 337
|
||||
|
||||
|
||||
def test_rectification_minute_snapshot_exposes_safe_official_layer_fingerprints(monkeypatch) -> None:
|
||||
from scripts import vedastro_service_adapter as adapter
|
||||
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"run_official_full_snapshot_for_case",
|
||||
lambda _case, case_id="user_chart": {
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"official_chart": {
|
||||
"ascendant": {"sign": "Leo", "degree_in_sign": 13.0, "raw_response": "secret"},
|
||||
"houses": {
|
||||
"House1": {
|
||||
"sign": "Leo",
|
||||
"degree_in_sign": 13.0,
|
||||
"vargas": {
|
||||
"D9": {"sign": "Cancer", "degree_in_sign": 27.7},
|
||||
"D10": {"sign": "Sagittarius", "degree_in_sign": 10.8},
|
||||
},
|
||||
}
|
||||
},
|
||||
"planets": {
|
||||
"Sun": {
|
||||
"sign": "Aries",
|
||||
"degree_in_sign": 3.5,
|
||||
"vargas": {
|
||||
"D9": {"sign": "Taurus", "degree_in_sign": 1.5},
|
||||
"D10": {"sign": "Taurus", "degree_in_sign": 5.0},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"snapshot_sections": {
|
||||
"dasha_all": {"Payload": {"DasaAtRange": [{"start": "2020-01-01"}, {"start": "2021-01-01"}]}},
|
||||
"raw_response": {"must_not": "leak"},
|
||||
},
|
||||
"section_statuses": {"dasha_all": "ok"},
|
||||
"source_metadata": {"response_hash": "official-response-hash", "api_key": "must-not-leak"},
|
||||
},
|
||||
)
|
||||
|
||||
result = adapter.run_rectification_minute_snapshot_for_case(
|
||||
{
|
||||
"year": 1955,
|
||||
"month": 2,
|
||||
"day": 24,
|
||||
"hour": 19,
|
||||
"minute": 15,
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194,
|
||||
"tz": -8,
|
||||
},
|
||||
case_id="minute_snapshot_unit",
|
||||
)
|
||||
|
||||
assert result["available"] is True
|
||||
assert result["status"] == "ok"
|
||||
assert result["candidate_time"] == "19:15"
|
||||
assert all(
|
||||
result["layers"][layer]["status"] == "ok"
|
||||
for layer in ("ascendant_house_boundaries", "D9", "D10", "dasha_boundaries")
|
||||
)
|
||||
assert all(
|
||||
result["layers"][layer]["fingerprint"]
|
||||
for layer in ("ascendant_house_boundaries", "D9", "D10", "dasha_boundaries")
|
||||
)
|
||||
assert result["layers"]["kp_cusp_sub_lord"]["status"] == "unsupported_by_verified_official_interface"
|
||||
assert "raw_response" not in str(result)
|
||||
assert "must-not-leak" not in str(result)
|
||||
|
||||
|
||||
def test_official_full_snapshot_prefers_official_capability_runner_bundle(monkeypatch) -> None:
|
||||
from scripts import vedastro_service_adapter as adapter
|
||||
|
||||
@@ -1015,6 +1119,12 @@ def test_official_full_snapshot_skips_rest_sections_already_filled_by_python_bun
|
||||
|
||||
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://example.invalid/api")
|
||||
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "1")
|
||||
monkeypatch.setenv("VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED", "1")
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_try_official_full_capability_catalog_bundle",
|
||||
lambda _case: {"available": False, "status": "blocked", "summary": {}, "coverage": {}},
|
||||
)
|
||||
monkeypatch.setattr(adapter, "_try_official_python_bridge_snapshot_bundle", fake_bundle)
|
||||
monkeypatch.setattr(adapter, "_post_official_snapshot_section", fake_post)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user