8743dcb105
Day-level events now score Vimshottari/Narayana transition closeness so nearby candidate minutes can diverge, with gated quality probes and answer-prior ranking so high-base-rate existence questions stay out. Co-authored-by: Cursor <cursoragent@cursor.com>
421 lines
15 KiB
Python
421 lines
15 KiB
Python
"""Round 2 engine-convergence invariants (TASK-rectification-engine-convergence-20260901)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import date, datetime
|
|
from pathlib import Path
|
|
|
|
from scripts.rectification.candidate_contrast import distinguish_contract_errors
|
|
from scripts.rectification.contracts import normalize_rectification_request
|
|
from scripts.rectification.event_probes import (
|
|
_apply_prior_ranking,
|
|
_dominant_existence_prior,
|
|
_partition_ranked_probes,
|
|
_vim_start_dates,
|
|
discriminating_event_probes,
|
|
event_clarification_probes,
|
|
)
|
|
from scripts.rectification.refinement_packet import build_refinement_packet, window_scan
|
|
from scripts.rectification.scoring_service import build_event_contribution_matrix
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
PLANETS = {
|
|
"Sun": 12.0,
|
|
"Moon": 100.0,
|
|
"Mars": 40.0,
|
|
"Mercury": 20.0,
|
|
"Jupiter": 80.0,
|
|
"Venus": 50.0,
|
|
"Saturn": 200.0,
|
|
"Rahu": 310.0,
|
|
"Ketu": 130.0,
|
|
}
|
|
GRID_TIMES = [
|
|
f"{4 + (45 + offset) // 60:02d}:{(45 + offset) % 60:02d}"
|
|
for offset in range(31)
|
|
]
|
|
SPLIT_EVENT_ID = "00000000-0000-4000-8000-000000000003"
|
|
|
|
|
|
def _varga(asc: int, planet_sign: int) -> dict:
|
|
return {
|
|
"Ascendant": {"sign_idx": asc},
|
|
**{name: {"sign_idx": planet_sign} for name in PLANETS},
|
|
}
|
|
|
|
|
|
def _context(
|
|
time: str,
|
|
*,
|
|
moon: float = 100.0,
|
|
d4_asc: int = 1,
|
|
d9_asc: int = 1,
|
|
d10_asc: int = 1,
|
|
d12_asc: int = 1,
|
|
d24_asc: int = 1,
|
|
sun_house: int = 10,
|
|
) -> dict:
|
|
hour, minute = (int(part) for part in time.split(":"))
|
|
planets = {**PLANETS, "Moon": moon}
|
|
natal_planets = {
|
|
name: {"house": sun_house if name != "Moon" else 4, "lon": lon}
|
|
for name, lon in planets.items()
|
|
}
|
|
return {
|
|
"candidate_at": datetime(1997, 8, 8, hour, minute),
|
|
"chart": {"ascendant": {"lon": 10.0, "sign": "Aries"}, "planets": natal_planets},
|
|
"planet_longitudes": dict(planets),
|
|
"ascendant_index": 0,
|
|
"varga_charts": {
|
|
"D4": _varga(d4_asc, 1),
|
|
"D9": _varga(d9_asc, 1),
|
|
"D10": _varga(d10_asc, 1),
|
|
"D5": _varga(1, 1),
|
|
"D24": _varga(d24_asc, 1),
|
|
"D12": _varga(d12_asc, 1),
|
|
"D7": _varga(1, 1),
|
|
"D3": _varga(1, 1),
|
|
},
|
|
"arudha_padas": {},
|
|
"feature": {
|
|
"time": time,
|
|
"ascendant_sign_index": 0,
|
|
"varga_ascendants": {
|
|
"D4": d4_asc, "D9": d9_asc, "D10": d10_asc, "D5": 1, "D24": d24_asc, "D12": d12_asc,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _accident_events() -> list[dict]:
|
|
return [
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000001",
|
|
"domain": "education",
|
|
"event_kind": "education_start",
|
|
"date_start": "2016-01-01",
|
|
"date_end": "2016-12-31",
|
|
"precision": "year",
|
|
"summary": "上大学",
|
|
},
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000002",
|
|
"domain": "relationship",
|
|
"event_kind": "relationship_start",
|
|
"date_start": "2024-05-01",
|
|
"date_end": "2024-05-31",
|
|
"precision": "month",
|
|
"summary": "开始认真交往",
|
|
},
|
|
{
|
|
"id": SPLIT_EVENT_ID,
|
|
"domain": "relationship",
|
|
"event_kind": "relationship_end",
|
|
"date_start": "2024-08-08",
|
|
"date_end": "2024-08-08",
|
|
"precision": "day",
|
|
"summary": "分手",
|
|
},
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000004",
|
|
"domain": "career",
|
|
"event_kind": "career_entry",
|
|
"date_start": "2020-04-01",
|
|
"date_end": "2020-04-30",
|
|
"precision": "month",
|
|
"summary": "实习入职",
|
|
},
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000005",
|
|
"domain": "career",
|
|
"event_kind": "career_exit",
|
|
"date_start": "2020-10-01",
|
|
"date_end": "2020-10-31",
|
|
"precision": "month",
|
|
"summary": "实习结束离职",
|
|
},
|
|
]
|
|
|
|
|
|
def _accident_request() -> dict:
|
|
return normalize_rectification_request(
|
|
{
|
|
"birth_date": "1997-08-08",
|
|
"start_time": "04:45",
|
|
"end_time": "05:15",
|
|
"lat": 36.420487,
|
|
"lon": 114.209936,
|
|
"tz": 8,
|
|
"events": _accident_events(),
|
|
},
|
|
today=date(2026, 8, 22),
|
|
)
|
|
|
|
|
|
def _equal_rows(payload: dict) -> list[dict]:
|
|
event = payload["events"][0]
|
|
return [
|
|
{
|
|
"time": time,
|
|
"score": 10,
|
|
"evidence": [{
|
|
"event_id": event["id"],
|
|
"domain": event["domain"],
|
|
"candidate_time": time,
|
|
"rule_ids": ["vim_md_domain_house"],
|
|
"points": 10,
|
|
}],
|
|
"missing_layers": [],
|
|
}
|
|
for time in GRID_TIMES
|
|
]
|
|
|
|
|
|
def _moons_with_split_proximity() -> tuple[float, float]:
|
|
event_at = date(2024, 8, 8)
|
|
ranked: list[tuple[int, float]] = []
|
|
for moon in (100.0, 100.5, 101.0, 103.0, 110.0):
|
|
starts = _vim_start_dates("1997-08-08", moon, 2023, 2025)
|
|
if not starts:
|
|
continue
|
|
nearest = min(abs((item - event_at).days) for item in starts)
|
|
ranked.append((nearest, moon))
|
|
ranked.sort()
|
|
if len(ranked) < 2 or ranked[0][0] == ranked[-1][0]:
|
|
raise AssertionError("fixture moons do not split AD/PD proximity")
|
|
return ranked[0][1], ranked[-1][1]
|
|
|
|
|
|
def _probe_events(*, education_count: int = 2, d24_split: bool = True) -> tuple[dict, dict]:
|
|
events = [
|
|
{
|
|
"id": f"00000000-0000-4000-8000-{index:012d}",
|
|
"domain": "education",
|
|
"event_kind": "education_start",
|
|
"summary": "入学",
|
|
"date": f"{2015 + index}-09-01",
|
|
"precision": "month",
|
|
}
|
|
for index in range(1, education_count + 1)
|
|
]
|
|
events.extend([
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000011",
|
|
"domain": "career",
|
|
"event_kind": "career_entry",
|
|
"summary": "入职",
|
|
"date": "2018-07-01",
|
|
"precision": "month",
|
|
},
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000012",
|
|
"domain": "career",
|
|
"event_kind": "career_change",
|
|
"summary": "换岗",
|
|
"date": "2020-04-01",
|
|
"precision": "month",
|
|
},
|
|
{
|
|
"id": "00000000-0000-4000-8000-000000000013",
|
|
"domain": "relationship",
|
|
"event_kind": "relationship_start",
|
|
"summary": "相识",
|
|
"date": "2021-08-01",
|
|
"precision": "month",
|
|
},
|
|
])
|
|
late = 2 if d24_split else 1
|
|
built = {
|
|
"static_contexts": [
|
|
_context("05:00", d24_asc=1, d12_asc=1, d9_asc=1, d10_asc=1),
|
|
_context("05:07", d24_asc=late, d12_asc=late, d9_asc=late, d10_asc=late),
|
|
]
|
|
}
|
|
return {"birth_date": "1997-08-08", "events": events}, built
|
|
|
|
|
|
class EngineConvergenceProximityTests(unittest.TestCase):
|
|
def test_day_event_proximity_splits_adjacent_minutes_in_accident_shape(self) -> None:
|
|
from scripts.rectification.dasha_transition_proximity import (
|
|
DAY_MAX_POINTS,
|
|
score_transition_proximity,
|
|
)
|
|
|
|
closer_moon, farther_moon = _moons_with_split_proximity()
|
|
event_at = date(2024, 8, 8)
|
|
closer_starts = _vim_start_dates("1997-08-08", closer_moon, 2023, 2025)
|
|
farther_starts = _vim_start_dates("1997-08-08", farther_moon, 2023, 2025)
|
|
closer_delta = min(abs((item - event_at).days) for item in closer_starts)
|
|
farther_delta = min(abs((item - event_at).days) for item in farther_starts)
|
|
closer_points = score_transition_proximity(
|
|
event_date=event_at,
|
|
precision="day",
|
|
vim_starts=closer_starts,
|
|
narayana_starts=[],
|
|
)
|
|
farther_points = score_transition_proximity(
|
|
event_date=event_at,
|
|
precision="day",
|
|
vim_starts=farther_starts,
|
|
narayana_starts=[],
|
|
)
|
|
self.assertLessEqual(float(closer_points["points"]), DAY_MAX_POINTS)
|
|
self.assertNotEqual(closer_points["points"], farther_points["points"])
|
|
self.assertEqual(
|
|
closer_points["points"] > farther_points["points"],
|
|
closer_delta < farther_delta,
|
|
)
|
|
self.assertTrue(
|
|
any(str(rule).startswith("vim_transition_proximity_") for rule in closer_points["rule_ids"])
|
|
)
|
|
|
|
contexts = [
|
|
_context(time, moon=closer_moon if time == "05:00" else farther_moon)
|
|
for time in GRID_TIMES
|
|
]
|
|
request = _accident_request()
|
|
built = build_event_contribution_matrix(
|
|
request,
|
|
row_provider=_equal_rows,
|
|
static_contexts=contexts,
|
|
)
|
|
self.assertEqual(len(built["candidate_times"]), 31)
|
|
cell_0500 = built["matrix"][SPLIT_EVENT_ID]["05:00"]
|
|
cell_0507 = built["matrix"][SPLIT_EVENT_ID]["05:07"]
|
|
self.assertNotEqual(cell_0500["points"], cell_0507["points"])
|
|
self.assertEqual(
|
|
cell_0500["points"] > cell_0507["points"],
|
|
closer_delta < farther_delta,
|
|
)
|
|
self.assertTrue(
|
|
any("transition_proximity" in str(rule) for rule in cell_0500["rule_ids"])
|
|
or any("transition_proximity" in str(rule) for rule in cell_0507["rule_ids"])
|
|
)
|
|
self.assertLessEqual(abs(cell_0500["points"] - cell_0507["points"]), DAY_MAX_POINTS)
|
|
|
|
def test_algorithm_and_policy_versions_change_with_proximity_semantics(self) -> None:
|
|
from scripts.rectification.decision_policy import POLICY_VERSION
|
|
from scripts.rectification.scoring_service import ALGORITHM_VERSION
|
|
|
|
self.assertNotEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-6")
|
|
self.assertNotEqual(POLICY_VERSION, "rectification-candidate-policy-v2")
|
|
|
|
|
|
class EngineConvergenceProbeTests(unittest.TestCase):
|
|
def test_anchored_quality_outranks_family_existence_and_drops_dominant_priors(self) -> None:
|
|
request, built = _probe_events()
|
|
probes = discriminating_event_probes(
|
|
request,
|
|
built,
|
|
scan=window_scan(built),
|
|
candidate_times=["05:00", "05:07"],
|
|
representative_time="05:00",
|
|
today=date(2026, 8, 22),
|
|
)
|
|
quality = [item for item in probes if item.get("source") == "known_event_quality"]
|
|
self.assertTrue(quality)
|
|
self.assertEqual(quality[0]["role"], "distinguish")
|
|
self.assertTrue(quality[0].get("target_evidence_id"))
|
|
self.assertFalse(distinguish_contract_errors(quality[0]))
|
|
ranked_family = _apply_prior_ranking({
|
|
"domain": "family",
|
|
"source": "dasha_boundary",
|
|
"choice_kind": "existence",
|
|
"information_gain": float(quality[0].get("raw_split_gain") or quality[0].get("information_gain") or 0),
|
|
"semantic_key": "family.2024.existence",
|
|
"year": 2024,
|
|
"window_span_years": 3,
|
|
"role": "distinguish",
|
|
"phase": "candidate_discriminator",
|
|
"candidate_ids": ["05:00", "05:07"],
|
|
"expected_outcomes": [
|
|
{"answer_class": "yes", "supports": ["05:00"], "conflicts": ["05:07"]},
|
|
{"answer_class": "no", "supports": ["05:07"], "conflicts": ["05:00"]},
|
|
],
|
|
})
|
|
self.assertTrue(_dominant_existence_prior(ranked_family, ranked_family["answer_priors"]))
|
|
_, ranked_dropped = _partition_ranked_probes([quality[0], ranked_family])
|
|
self.assertTrue(
|
|
any(item.get("reason") == "dominant_answer_prior" for item in ranked_dropped),
|
|
ranked_dropped,
|
|
)
|
|
packet = build_refinement_packet(
|
|
request,
|
|
built,
|
|
representative_time="05:00",
|
|
candidate_times=["05:00", "05:07"],
|
|
)
|
|
dropped = list(packet.get("dropped_probes") or []) + ranked_dropped
|
|
self.assertTrue(
|
|
any(item.get("reason") == "dominant_answer_prior" for item in dropped),
|
|
dropped,
|
|
)
|
|
self.assertFalse(
|
|
any(
|
|
item.get("choice_kind") == "existence"
|
|
and float(max((item.get("answer_priors") or {}).values() or [0])) > 0.8
|
|
for item in probes
|
|
)
|
|
)
|
|
|
|
def test_quality_distinguish_requires_varga_type_split_and_caps_at_two(self) -> None:
|
|
same_request, same_built = _probe_events(d24_split=False)
|
|
same_probes = discriminating_event_probes(
|
|
same_request,
|
|
same_built,
|
|
scan=window_scan(same_built),
|
|
candidate_times=["05:00", "05:07"],
|
|
representative_time="05:00",
|
|
today=date(2026, 8, 22),
|
|
)
|
|
self.assertFalse(any(item.get("source") == "known_event_quality" for item in same_probes))
|
|
clarification = event_clarification_probes(same_request)
|
|
self.assertTrue(any(item.get("source") == "known_event_quality" for item in clarification))
|
|
|
|
split_request, split_built = _probe_events(education_count=4, d24_split=True)
|
|
split_probes = discriminating_event_probes(
|
|
split_request,
|
|
split_built,
|
|
scan=window_scan(split_built),
|
|
candidate_times=["05:00", "05:07"],
|
|
representative_time="05:00",
|
|
today=date(2026, 8, 22),
|
|
)
|
|
quality = [item for item in split_probes if item.get("source") == "known_event_quality"]
|
|
self.assertTrue(quality)
|
|
self.assertLessEqual(len(quality), 2)
|
|
for item in quality:
|
|
self.assertEqual(item["role"], "distinguish")
|
|
self.assertTrue(item.get("target_evidence_id"))
|
|
self.assertTrue(item.get("display_date_label"))
|
|
self.assertEqual(item.get("choice_kind"), "event_quality")
|
|
self.assertFalse(distinguish_contract_errors(item))
|
|
|
|
|
|
class EngineConvergenceAnswerKeyTests(unittest.TestCase):
|
|
def test_scripts_and_frontend_have_no_answer_key_literals(self) -> None:
|
|
forbidden = ("target" + "_minute", "pl9_" + "1993", "regression" + "_only")
|
|
hits: list[str] = []
|
|
roots = (
|
|
REPO_ROOT / "scripts",
|
|
REPO_ROOT / "frontend" / "src",
|
|
REPO_ROOT / "frontend" / "tests",
|
|
)
|
|
skip_parts = {"node_modules", ".next", "dist"}
|
|
for root in roots:
|
|
for path in root.rglob("*"):
|
|
if not path.is_file() or path.suffix not in {".py", ".ts", ".tsx", ".js", ".mjs"}:
|
|
continue
|
|
if any(part in skip_parts for part in path.parts):
|
|
continue
|
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
for token in forbidden:
|
|
if token in text:
|
|
hits.append(f"{path.relative_to(REPO_ROOT)}:{token}")
|
|
self.assertEqual(hits, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|