fix(rectification): restore classic eight methods and observe KP Placidus cusps
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -198,5 +198,57 @@ def test_family_event_engine_rows_include_d12_and_are_not_skipped() -> None:
|
||||
assert "d2-hora" in finance_layers and "d11-labhamsha" in finance_layers
|
||||
health_layers = public_technique_layers("health_pressure", ["vim_md_domain_house"])
|
||||
assert "d30-trimshamsha" in health_layers
|
||||
occupation_layers = public_technique_layers("occupation", ["vim_md_domain_house"])
|
||||
assert "d1-rashi" in occupation_layers and "d10-dashamsa" in occupation_layers
|
||||
assert all("D10" in context["feature"]["available_layers"] for context in contexts)
|
||||
assert all("D5" in context["feature"]["available_layers"] for context in contexts)
|
||||
|
||||
|
||||
def test_occupation_is_auxiliary_tenth_house_independent_of_career() -> None:
|
||||
prefixes, houses = DOMAIN_CONFIG["occupation"]
|
||||
assert prefixes == ("D10",)
|
||||
assert houses == (10,)
|
||||
assert "occupation" in AUXILIARY_DOMAINS
|
||||
event = {
|
||||
"id": "00000000-0000-4000-8000-000000000005",
|
||||
"domain": "occupation",
|
||||
"event_kind": "occupation_note",
|
||||
"date_start": "2019-01-01",
|
||||
"date_end": "2019-12-31",
|
||||
"precision": "year",
|
||||
"summary": "长期工作",
|
||||
}
|
||||
assert is_scoreable_event(event) is True
|
||||
assert is_primary_scoreable_event(event) is False
|
||||
scored = _score_event(
|
||||
candidate_time="05:13",
|
||||
event={
|
||||
"id": event["id"],
|
||||
"domain": "occupation",
|
||||
"event_kind": "occupation_note",
|
||||
"date": "2019-01-01",
|
||||
"precision": "year",
|
||||
},
|
||||
natal_chart={"ascendant": {"lon": 10.0, "sign": "Aries"}, "planets": {"Sun": {"house": 10, "lon": 12.0}}},
|
||||
varga_charts=[{"Ascendant": {"sign_idx": 0}, "Sun": {"sign_idx": 9}}],
|
||||
vimshottari=("Sun", "Moon", "Mars"),
|
||||
narayana=(None, None),
|
||||
arudha_padas={},
|
||||
)
|
||||
assert "occupation_auxiliary_not_primary" in scored["rule_ids"]
|
||||
career = _score_event(
|
||||
candidate_time="05:13",
|
||||
event={
|
||||
"id": "00000000-0000-4000-8000-000000000006",
|
||||
"domain": "career",
|
||||
"event_kind": "career_entry",
|
||||
"date": "2019-01-01",
|
||||
"precision": "year",
|
||||
},
|
||||
natal_chart={"ascendant": {"lon": 10.0, "sign": "Aries"}, "planets": {"Sun": {"house": 10, "lon": 12.0}}},
|
||||
varga_charts=[{"Ascendant": {"sign_idx": 0}, "Sun": {"sign_idx": 9}}],
|
||||
vimshottari=("Sun", "Moon", "Mars"),
|
||||
narayana=(None, None),
|
||||
arudha_padas={},
|
||||
)
|
||||
assert scored["points"] < career["points"]
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.rectification.horary_observation import build_horary_observation
|
||||
|
||||
|
||||
def _request(**overrides):
|
||||
body = {
|
||||
"birth_date": "1993-04-17",
|
||||
"start_time": "14:29",
|
||||
"end_time": "14:30",
|
||||
"lat": 36.683333,
|
||||
"lon": 114.35,
|
||||
"tz": 8.0,
|
||||
"events": [],
|
||||
}
|
||||
body.update(overrides)
|
||||
return body
|
||||
|
||||
|
||||
def test_horary_without_query_is_skipped() -> None:
|
||||
result = build_horary_observation(_request(events=[{
|
||||
"id": "00000000-0000-4000-8000-000000000001",
|
||||
"domain": "career",
|
||||
"event_kind": "career_entry",
|
||||
"date_start": "2019-07-01",
|
||||
"date_end": "2019-07-01",
|
||||
"precision": "day",
|
||||
}]))
|
||||
assert result["status"] == "skipped"
|
||||
assert result["unique_minute_claim"] is False
|
||||
|
||||
|
||||
def test_dated_horary_query_is_observation_only() -> None:
|
||||
result = build_horary_observation(_request(events=[{
|
||||
"id": "00000000-0000-4000-8000-000000000002",
|
||||
"domain": "horary",
|
||||
"event_kind": "horary_query",
|
||||
"date_start": "2024-01-15",
|
||||
"date_end": "2024-01-15",
|
||||
"precision": "day",
|
||||
"summary": "第一次问起是 14:30",
|
||||
}]))
|
||||
assert result["status"] in {"executed", "blocked"}
|
||||
assert result["unique_minute_claim"] is False
|
||||
assert "不计分" in result["user_meaning"]
|
||||
if result["status"] == "executed":
|
||||
assert result["question_date"] == "2024-01-15"
|
||||
assert result["question_time"] == "14:30"
|
||||
assert result.get("lagna")
|
||||
|
||||
|
||||
def test_horary_without_coordinates_is_blocked_not_a_gate_fail() -> None:
|
||||
result = build_horary_observation({
|
||||
"events": [{
|
||||
"id": "00000000-0000-4000-8000-000000000003",
|
||||
"domain": "horary",
|
||||
"event_kind": "horary_query",
|
||||
"date_start": "2024-01-15",
|
||||
"date_end": "2024-01-15",
|
||||
"precision": "day",
|
||||
}],
|
||||
})
|
||||
assert result["status"] == "blocked"
|
||||
assert result["unique_minute_claim"] is False
|
||||
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from kp_system import KP_LORDS
|
||||
from scripts.active_rectification_event_engine import (
|
||||
build_candidate_static_context,
|
||||
compute_event_candidate_rows,
|
||||
)
|
||||
from scripts.rectification.decision_policy import (
|
||||
build_candidate_decisions,
|
||||
build_decision_receipt,
|
||||
build_technique_audit,
|
||||
)
|
||||
from scripts.rectification.kp_cusp_observation import observe_kp_cusps
|
||||
from scripts.rectification.refinement_packet import window_scan
|
||||
|
||||
|
||||
def _request() -> dict:
|
||||
return {
|
||||
"birth_date": "1993-04-17",
|
||||
"start_time": "14:29",
|
||||
"end_time": "14:30",
|
||||
"lat": 36.683333,
|
||||
"lon": 114.35,
|
||||
"tz": 8.0,
|
||||
"events": [{
|
||||
"id": "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5",
|
||||
"domain": "career",
|
||||
"date": "2019-07-01",
|
||||
"precision": "day",
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
def _score_request() -> dict:
|
||||
return {
|
||||
"events": [
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000001",
|
||||
"domain": "career",
|
||||
"summary": "入职",
|
||||
"event_kind": "career_entry",
|
||||
"precision": "day",
|
||||
"date_start": "2016-09-15",
|
||||
"date_end": "2016-09-15",
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000002",
|
||||
"domain": "relationship",
|
||||
"summary": "开始一段关系",
|
||||
"event_kind": "relationship_start",
|
||||
"precision": "day",
|
||||
"date_start": "2018-03-01",
|
||||
"date_end": "2018-03-01",
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000003",
|
||||
"domain": "education",
|
||||
"summary": "毕业",
|
||||
"event_kind": "education_completion",
|
||||
"precision": "day",
|
||||
"date_start": "2015-06-01",
|
||||
"date_end": "2015-06-01",
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000004",
|
||||
"domain": "family",
|
||||
"summary": "家人变化",
|
||||
"event_kind": "family_event",
|
||||
"precision": "day",
|
||||
"date_start": "2020-01-01",
|
||||
"date_end": "2020-01-01",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def _diagnostics() -> dict:
|
||||
return {
|
||||
"leave_one_event_out_retention_rate": 1,
|
||||
"leave_one_domain_out_retention_rate": 1,
|
||||
"date_sensitivity_retention_rate": 1,
|
||||
"primary_secondary_margin_percent": 50,
|
||||
}
|
||||
|
||||
|
||||
def test_observe_kp_cusps_uses_placidus_not_equal_houses() -> None:
|
||||
context = build_candidate_static_context(_request(), datetime(1993, 4, 17, 14, 29))
|
||||
feature = context["feature"]
|
||||
snapshot = feature["kp_cusps"]
|
||||
natal_houses = context["chart"]["houses"]
|
||||
assert snapshot["status"] == "executed"
|
||||
assert snapshot["house_system"] == "placidus"
|
||||
assert snapshot["ayanamsa"] == "krishnamurti"
|
||||
assert len(snapshot["houses"]) == 12
|
||||
tenth = snapshot["houses"]["10"]
|
||||
equal_tenth = float(natal_houses["house_10"]["cusp_degree"])
|
||||
assert abs(float(tenth["cusp_degree"]) - equal_tenth) > 0.05
|
||||
assert tenth["sub_lord"] in KP_LORDS
|
||||
assert isinstance(feature["kp1_sub_index"], int)
|
||||
assert 0 <= feature["kp1_sub_index"] <= 8
|
||||
assert "KP_cusps" not in feature["blocked_layers"]
|
||||
assert "KP_cusps" in feature["available_layers"]
|
||||
|
||||
|
||||
def test_kp_observation_is_absent_from_scoring_missing_layers() -> None:
|
||||
request = _request()
|
||||
context = build_candidate_static_context(request, datetime(1993, 4, 17, 14, 29))
|
||||
rows = compute_event_candidate_rows(request, static_contexts=[context])
|
||||
assert rows
|
||||
assert "KP_cusps" not in rows[0]["missing_layers"]
|
||||
assert context["feature"]["kp_cusps"]["status"] == "executed"
|
||||
|
||||
|
||||
def test_window_scan_emits_kp_sub_lord_changes_without_confirmation() -> None:
|
||||
scan = window_scan({
|
||||
"static_contexts": [
|
||||
{"feature": {"time": "05:13", "kp1_sub_index": 1, "kp4_sub_index": 2, "kp7_sub_index": 3, "kp10_sub_index": 4}},
|
||||
{"feature": {"time": "05:14", "kp1_sub_index": 2, "kp4_sub_index": 2, "kp7_sub_index": 3, "kp10_sub_index": 4}},
|
||||
]
|
||||
})
|
||||
meanings = [item["user_meaning"] for item in scan["transitions"]]
|
||||
assert "KP 1宫子主 在 05:14 发生变化" in meanings
|
||||
assert "KP 4宫子主 在 05:14 发生变化" not in meanings
|
||||
encoded = str(scan)
|
||||
assert "白羊" not in encoded
|
||||
assert "Aries" not in encoded
|
||||
assert scan["confirmation_allowed"] is False
|
||||
assert scan["unique_minute_claim"] is False
|
||||
|
||||
|
||||
def test_technique_audit_kp_executed_when_snapshot_present() -> None:
|
||||
rows = build_technique_audit(
|
||||
{
|
||||
"matrix": {},
|
||||
"static_contexts": [{
|
||||
"feature": {
|
||||
"time": "05:13",
|
||||
"kp_cusps": {"status": "executed", "house_system": "placidus", "ayanamsa": "krishnamurti"},
|
||||
}
|
||||
}],
|
||||
},
|
||||
house_table={"time": "05:13", "lagna": "金牛座"},
|
||||
)
|
||||
kp = next(row for row in rows if row["technique"] == "KP 宫头")
|
||||
assert kp["status"] == "executed"
|
||||
assert "政策跳过" not in kp["note"]
|
||||
assert "不计分" in kp["note"]
|
||||
|
||||
|
||||
def test_technique_audit_kp_blocked_is_honest_and_does_not_fail_propose() -> None:
|
||||
rows = build_technique_audit({"matrix": {}, "static_contexts": []}, house_table=None)
|
||||
kp = next(row for row in rows if row["technique"] == "KP 宫头")
|
||||
assert kp["status"] == "blocked"
|
||||
assert "政策跳过" not in kp["note"]
|
||||
decisions = build_candidate_decisions(
|
||||
[{"time": "05:13", "score": 20, "evidence": [], "missing_layers": []},
|
||||
{"time": "05:14", "score": 8, "evidence": [], "missing_layers": []}],
|
||||
result_id="00000000-0000-4000-8000-000000000099",
|
||||
)
|
||||
receipt = build_decision_receipt(
|
||||
_score_request(),
|
||||
decisions,
|
||||
{"missing_layers": [], "matrix": {}, "static_contexts": [{"feature": {"time": "05:13", "kp_cusps": {"status": "blocked"}}}]},
|
||||
_diagnostics(),
|
||||
)
|
||||
assert receipt["gates"]["required_layers"]["passed"] is True
|
||||
assert receipt["propose_allowed"] is True
|
||||
audit = next(row for row in receipt["technique_audit_table"] if row["technique"] == "KP 宫头")
|
||||
assert audit["status"] == "blocked"
|
||||
assert "政策跳过" not in audit["note"]
|
||||
|
||||
|
||||
def test_observe_kp_cusps_blocks_without_inventing_whole_sign_proxy() -> None:
|
||||
snapshot = observe_kp_cusps(None, 36.6, 114.3)
|
||||
assert snapshot["status"] == "blocked"
|
||||
assert snapshot["houses"] == {}
|
||||
polar = observe_kp_cusps(2451545.0, 80.0, 0.0)
|
||||
assert polar["status"] == "blocked"
|
||||
assert polar["houses"] == {}
|
||||
@@ -78,6 +78,8 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
"family": ("family_event",),
|
||||
"appearance": ("appearance_note",),
|
||||
"marks": ("birthmark_or_scar",),
|
||||
"occupation": ("occupation_note",),
|
||||
"horary": ("horary_query",),
|
||||
"other": ("other",),
|
||||
}
|
||||
|
||||
@@ -117,6 +119,7 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user