fix(rectification): fold D24 into education refine and score family D3
Window scan now drives d5_refine when D24 changes, family events use D3, and pada/Hora/Ghati are display-only. New cases bind Skill 10.0.7 without unique-minute confirmation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Final, assert_never
|
||||
@@ -39,7 +40,10 @@ import functional_benefics # noqa: E402
|
||||
import jaimini # noqa: E402
|
||||
import shadbala # noqa: E402
|
||||
import narayana_dasha # noqa: E402
|
||||
import saham_daynight # noqa: E402
|
||||
import special_lagnas # noqa: E402
|
||||
import varga # noqa: E402
|
||||
from scripts.rectification.refinement_packet import NAKSHATRA_SPAN # noqa: E402
|
||||
|
||||
AYANAMSA: Final = "lahiri"
|
||||
NODE_MODE: Final = "mean"
|
||||
@@ -53,8 +57,8 @@ DOMAIN_CONFIG: Final[dict[EventDomain, DomainConfig]] = {
|
||||
"career": (("D10",), (10,)),
|
||||
"finance": (("D2", "D11"), (2, 11)),
|
||||
"health_pressure": (("D30",), (6, 8, 12)),
|
||||
# 六亲: D12 parents, D7 children/spouse detail, plus D1 houses 3/4/5/9.
|
||||
"family": (("D12", "D7"), (3, 4, 5, 9)),
|
||||
# 六亲: D12 parents, D7 children/spouse detail, D3 siblings, plus D1 houses 3/4/5/9.
|
||||
"family": (("D12", "D7", "D3"), (3, 4, 5, 9)),
|
||||
# Dated appearance/marks: D1 lagna / 1st house only. Not a primary formula.
|
||||
"appearance": ((), (1,)),
|
||||
}
|
||||
@@ -340,6 +344,49 @@ def _arudha_sign(arudha_padas: dict, key: str) -> int | None:
|
||||
return int(sign_index) if isinstance(sign_index, int) and 0 <= sign_index <= 11 else None
|
||||
|
||||
|
||||
def _pada_index(longitude: float) -> int:
|
||||
return int((float(longitude) % 360.0) / (NAKSHATRA_SPAN / 4.0)) % 108
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def _sunrise_local_naive(date_iso: str, lat: float, lon: float, tz: float) -> datetime | None:
|
||||
"""Real sunrise only. Polar or unavailable locations omit Hora/Ghati; never invent 06:00."""
|
||||
try:
|
||||
import swisseph as swe
|
||||
noon = datetime.fromisoformat(f"{date_iso}T12:00:00")
|
||||
row = saham_daynight.determine_daytime(noon, lat=lat, lon=lon, tz=tz)
|
||||
year, month, day, ut_hours = swe.revjul(float(row["sunrise_jd_ut"]))
|
||||
return datetime(int(year), int(month), int(day)) + timedelta(hours=float(ut_hours) + float(tz))
|
||||
except (saham_daynight.SahamDayNightError, TypeError, ValueError, OverflowError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _fine_minute_indices(
|
||||
*,
|
||||
ascendant_longitude: float,
|
||||
candidate_at: datetime,
|
||||
lat: float,
|
||||
lon: float,
|
||||
tz: float,
|
||||
) -> dict[str, int]:
|
||||
indices: dict[str, int] = {"pada_index": _pada_index(ascendant_longitude)}
|
||||
sunrise = _sunrise_local_naive(candidate_at.date().isoformat(), lat, lon, tz)
|
||||
if sunrise is None:
|
||||
return indices
|
||||
calculator = special_lagnas.SpecialLagnasCalculator()
|
||||
wrapped = float(ascendant_longitude) % 360.0
|
||||
try:
|
||||
hora = calculator.calculate_hora_lagna(wrapped, 0.0, candidate_at, sunrise)
|
||||
ghati = calculator.calculate_ghati_lagna(wrapped, candidate_at, sunrise)
|
||||
hora_degree = float(hora["degree"])
|
||||
ghati_degree = float(ghati["degree"])
|
||||
except (TypeError, ValueError, KeyError, OverflowError):
|
||||
return indices
|
||||
indices["hora_sign_index"] = int(hora_degree // 30.0) % 12
|
||||
indices["ghati_sign_index"] = int(ghati_degree // 30.0) % 12
|
||||
return indices
|
||||
|
||||
|
||||
def build_candidate_static_context(
|
||||
request: RectificationEventRequest,
|
||||
candidate_at: datetime,
|
||||
@@ -369,12 +416,12 @@ def build_candidate_static_context(
|
||||
charts = varga.calc_all_vargas(
|
||||
planet_longitudes,
|
||||
ascendant_longitude,
|
||||
divisions=[2, 4, 5, 7, 9, 10, 12, 24, 30],
|
||||
divisions=[2, 3, 4, 5, 7, 9, 10, 12, 24, 30],
|
||||
)
|
||||
d11_chart = _d11_chart(planet_longitudes, ascendant_longitude)
|
||||
varga_charts = {
|
||||
prefix: d11_chart if prefix == "D11" else _varga_chart(charts, prefix)
|
||||
for prefix in ("D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30")
|
||||
for prefix in ("D2", "D3", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30")
|
||||
}
|
||||
available_layers = ["D1"]
|
||||
blocked_layers = ["KP_cusps"]
|
||||
@@ -417,6 +464,13 @@ def build_candidate_static_context(
|
||||
"time": candidate_at.strftime("%H:%M"),
|
||||
"ascendant_degree": ascendant_longitude,
|
||||
"ascendant_sign_index": ascendant_index,
|
||||
**_fine_minute_indices(
|
||||
ascendant_longitude=ascendant_longitude,
|
||||
candidate_at=candidate_at,
|
||||
lat=float(request["lat"]),
|
||||
lon=float(request["lon"]),
|
||||
tz=float(request["tz"]),
|
||||
),
|
||||
"varga_ascendants": varga_ascendants,
|
||||
"arudha_signs": arudha_signs,
|
||||
"available_layers": sorted(set(available_layers)),
|
||||
|
||||
@@ -52,6 +52,7 @@ _AUDIT_LABELS = {
|
||||
"d2-hora": ("D2 财帛分盘", "本轮已对照财帛主题。"),
|
||||
"d4-chaturthamsha": ("D4 迁移分盘", "本轮已对照居所或迁移。"),
|
||||
"d5-panchamsha": ("D5 成就分盘", "本轮已对照学业或被委以责任的变化。"),
|
||||
"d3-drekkana": ("D3 兄弟分盘", "本轮已对照兄弟姐妹主题。"),
|
||||
"d7-saptamsha": ("D7 子女分盘", "本轮已对照子女或伴侣细节。"),
|
||||
"d9-navamsa": ("D9 婚姻分盘", "本轮已对照关系主题,未给类型标签。"),
|
||||
"d10-dashamsa": ("D10 事业分盘", "本轮已对照事业主题,未给类型标签。"),
|
||||
|
||||
@@ -121,25 +121,38 @@ _LAYER_LABEL = {
|
||||
"d5": "D5",
|
||||
"d7": "D7",
|
||||
"d12": "D12",
|
||||
"d24": "D24",
|
||||
"pada": "Nakshatra pada",
|
||||
"hora": "Hora Lagna",
|
||||
"ghati": "Ghati Lagna",
|
||||
}
|
||||
|
||||
|
||||
def _scan_layer_value(feature: dict[str, Any], layer: str) -> int | None:
|
||||
vargas = feature.get("varga_ascendants") if isinstance(feature.get("varga_ascendants"), dict) else {}
|
||||
raw = {
|
||||
"d1": feature.get("ascendant_sign_index"),
|
||||
"d9": vargas.get("D9"),
|
||||
"d10": vargas.get("D10"),
|
||||
"d4": vargas.get("D4"),
|
||||
"d5": vargas.get("D5"),
|
||||
"d7": vargas.get("D7"),
|
||||
"d12": vargas.get("D12"),
|
||||
"d24": vargas.get("D24"),
|
||||
"pada": feature.get("pada_index"),
|
||||
"hora": feature.get("hora_sign_index"),
|
||||
"ghati": feature.get("ghati_sign_index"),
|
||||
}.get(layer)
|
||||
return raw if isinstance(raw, int) else None
|
||||
|
||||
|
||||
def window_scan(built: dict[str, Any]) -> dict[str, Any]:
|
||||
"""D1/D9/D10/D4/D5/D7/D12 diversity plus change minutes. Indices only; never sign names."""
|
||||
"""D1/D9/D10/D4/D5/D7/D12/D24 plus display-only pada/Hora/Ghati. Indices only; never sign names."""
|
||||
counts: dict[str, set[int]] = {layer: set() for layer in _LAYER_LABEL}
|
||||
transitions: list[dict[str, Any]] = []
|
||||
previous: dict[str, int | None] | None = None
|
||||
for feature in _features(built):
|
||||
vargas = feature.get("varga_ascendants") if isinstance(feature.get("varga_ascendants"), dict) else {}
|
||||
current = {
|
||||
"d1": feature.get("ascendant_sign_index") if isinstance(feature.get("ascendant_sign_index"), int) else None,
|
||||
"d9": vargas.get("D9") if isinstance(vargas.get("D9"), int) else None,
|
||||
"d10": vargas.get("D10") if isinstance(vargas.get("D10"), int) else None,
|
||||
"d4": vargas.get("D4") if isinstance(vargas.get("D4"), int) else None,
|
||||
"d5": vargas.get("D5") if isinstance(vargas.get("D5"), int) else None,
|
||||
"d7": vargas.get("D7") if isinstance(vargas.get("D7"), int) else None,
|
||||
"d12": vargas.get("D12") if isinstance(vargas.get("D12"), int) else None,
|
||||
}
|
||||
current = {layer: _scan_layer_value(feature, layer) for layer in _LAYER_LABEL}
|
||||
for layer, bucket in counts.items():
|
||||
value = current[layer]
|
||||
if isinstance(value, int):
|
||||
@@ -156,26 +169,16 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]:
|
||||
"user_meaning": f"{label} 在 {time} 发生变化",
|
||||
})
|
||||
previous = current
|
||||
return {
|
||||
payload: dict[str, Any] = {
|
||||
"scanned": True,
|
||||
"confirmation_allowed": False,
|
||||
"unique_minute_claim": False,
|
||||
"d1_lagna_count": len(counts["d1"]),
|
||||
"d9_lagna_count": len(counts["d9"]),
|
||||
"d10_lagna_count": len(counts["d10"]),
|
||||
"d4_lagna_count": len(counts["d4"]),
|
||||
"d5_lagna_count": len(counts["d5"]),
|
||||
"d7_lagna_count": len(counts["d7"]),
|
||||
"d12_lagna_count": len(counts["d12"]),
|
||||
"d1_candidates_differ": len(counts["d1"]) > 1,
|
||||
"d9_candidates_differ": len(counts["d9"]) > 1,
|
||||
"d10_candidates_differ": len(counts["d10"]) > 1,
|
||||
"d4_candidates_differ": len(counts["d4"]) > 1,
|
||||
"d5_candidates_differ": len(counts["d5"]) > 1,
|
||||
"d7_candidates_differ": len(counts["d7"]) > 1,
|
||||
"d12_candidates_differ": len(counts["d12"]) > 1,
|
||||
"transitions": transitions,
|
||||
}
|
||||
for layer in _LAYER_LABEL:
|
||||
payload[f"{layer}_lagna_count" if layer.startswith("d") else f"{layer}_count"] = len(counts[layer])
|
||||
payload[f"{layer}_candidates_differ"] = len(counts[layer]) > 1
|
||||
return payload
|
||||
|
||||
|
||||
def event_dasha_ledger(
|
||||
@@ -376,9 +379,9 @@ def precision_stage(scan: dict[str, Any], event_count: int) -> dict[str, Any]:
|
||||
elif scan.get("d4_candidates_differ"):
|
||||
current = "d4_refine"
|
||||
meaning = "事业盘已较稳,居所盘仍会换升。可再补一件记得时间的搬家或住处变化。"
|
||||
elif scan.get("d5_candidates_differ"):
|
||||
elif scan.get("d5_candidates_differ") or scan.get("d24_candidates_differ"):
|
||||
current = "d5_refine"
|
||||
meaning = "居所盘已较稳,成就盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化;不要贴类型标签。"
|
||||
meaning = "居所盘已较稳,成就盘或学业盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化;不要贴类型标签。"
|
||||
else:
|
||||
current = "ready_to_adopt"
|
||||
meaning = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。"
|
||||
|
||||
@@ -12,7 +12,7 @@ from scripts.active_rectification_event_engine import compute_candidate_static_c
|
||||
from scripts.active_rectification_events import CandidateScoreRow
|
||||
from scripts.rectification.contracts import LifeEvent, RectificationRequest, is_scoreable_event
|
||||
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-4"
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-5"
|
||||
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
|
||||
PRECISION_WEIGHTS = {
|
||||
"day": 1.0,
|
||||
@@ -184,7 +184,7 @@ def public_technique_layers(domain: str, rule_ids: Sequence[str]) -> list[str]:
|
||||
if domain == "career":
|
||||
layers.update({"d1-rashi", "d10-dashamsa"})
|
||||
elif domain == "family":
|
||||
layers.update({"d1-rashi", "d12-dwadashamsha", "d7-saptamsha"})
|
||||
layers.update({"d1-rashi", "d12-dwadashamsha", "d7-saptamsha", "d3-drekkana"})
|
||||
elif domain == "education":
|
||||
layers.update({"d1-rashi", "d24-chaturvimshamsha", "d5-panchamsha"})
|
||||
elif domain == "relocation":
|
||||
|
||||
@@ -80,7 +80,7 @@ def build_rectification_technique_contract(
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"calculation_status": "not_started" if event_count == 0 else "evaluated",
|
||||
"used_divisional_charts": ["D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"],
|
||||
"used_divisional_charts": ["D2", "D3", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"],
|
||||
"used_arudha": ["A7", "UL", "A10"],
|
||||
"dasha_tracks": ["vimshottari_md_ad_pd", "narayana_md_ad"],
|
||||
"missing_layers": reported_missing_layers,
|
||||
|
||||
Reference in New Issue
Block a user