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:
Jesse_Chen
2026-08-20 11:04:55 +08:00
parent 9a5cf67209
commit 70efb56926
35 changed files with 894 additions and 72 deletions
+58 -4
View File
@@ -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)),
+1
View File
@@ -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 事业分盘", "本轮已对照事业主题,未给类型标签。"),
+31 -28
View File
@@ -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 = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。"
+2 -2
View File
@@ -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":
+1 -1
View File
@@ -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,