fix(rectification): split D4/D5 stages and recast after adopt
Independent Staging Quality Gate / validate (push) Successful in 9m19s
Independent Staging Quality Gate / publish (push) Failing after 7m45s

Keep family on D12+D7 instead of mixing it into D4, score D5 on education, and show a natal recast plus technique audit after adopt without unique-minute claims.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-20 09:11:54 +08:00
parent b1b4f5fac9
commit a93a3cc1a9
40 changed files with 1195 additions and 66 deletions
+110 -1
View File
@@ -40,6 +40,105 @@ def _decimal(value: Any, default: str = "0") -> Decimal:
return Decimal(default)
_AUDIT_LABELS = {
"d1-rashi": ("D1 本命盘", "本轮已按该分钟重算本命宫位。"),
"d2-hora": ("D2 财帛分盘", "本轮已对照财帛主题。"),
"d4-chaturthamsha": ("D4 迁移分盘", "本轮已对照居所或迁移。"),
"d5-panchamsha": ("D5 成就分盘", "本轮已对照学业或被委以责任的变化。"),
"d7-saptamsha": ("D7 子女分盘", "本轮已对照子女或伴侣细节。"),
"d9-navamsa": ("D9 婚姻分盘", "本轮已对照关系主题,未给类型标签。"),
"d10-dashamsa": ("D10 事业分盘", "本轮已对照事业主题,未给类型标签。"),
"d11-labhamsha": ("D11 收益分盘", "本轮已对照收益主题。"),
"d12-dwadashamsha": ("D12 父母分盘", "本轮已对照家人主题。"),
"d24-chaturvimshamsha": ("D24 教育分盘", "本轮已对照学业主题。"),
"d30-trimshamsha": ("D30 健康压力分盘", "本轮已对照健康压力主题。"),
"vimshottari-dasha": ("Vimshottari", "本轮已对照主限。"),
"narayana-dasha": ("Narayana", "本轮已对照分盘大运。"),
"gochara": ("Gochara", "本轮已做受控行运辅助对照。"),
"ashtakavarga": ("Ashtakavarga", "本轮已做 Ashtakavarga 辅助对照。"),
"shadbala": ("Shadbala", "本轮已做已核验的 Shadbala 分量辅助对照。"),
"arudha-pada": ("Arudha Pada", "本轮已做 Arudha 辅助对照。"),
"functional-benefic-malefic": ("功能吉凶星", "本轮已叠加本命功能吉凶星。"),
}
def natal_recast_copy(time: str, lagna: str) -> dict[str, Any]:
return {
"time": time[:5],
"lagna": lagna,
"user_meaning": (
f"本命宫位已按 {time[:5]} 重算(上升 {lagna})。"
"下面是本轮实际执行的技法,不能当作唯一分钟确认。"
),
"unique_minute_claim": False,
"confirmation_allowed": False,
}
def _executed_public_methods(built: dict[str, Any]) -> list[str]:
methods: set[str] = set()
for contributions in (built.get("matrix") or {}).values():
if not isinstance(contributions, dict):
continue
for cell in contributions.values():
if not isinstance(cell, dict):
continue
for layer in cell.get("technique_layers") or []:
if layer in _AUDIT_LABELS:
methods.add(str(layer))
for rule in cell.get("rule_ids") or []:
text = str(rule)
if text.startswith("vim_"):
methods.add("vimshottari-dasha")
elif text.startswith("narayana_"):
methods.add("narayana-dasha")
elif "functional_benefic" in text or "functional_malefic" in text:
methods.add("functional-benefic-malefic")
elif text.startswith("gochara") or "controlled_transit" in text:
methods.add("gochara")
elif "ashtakavarga" in text:
methods.add("ashtakavarga")
elif "shadbala" in text:
methods.add("shadbala")
elif "arudha" in text:
methods.add("arudha-pada")
return [key for key in _AUDIT_LABELS if key in methods]
def build_technique_audit(
built: dict[str, Any],
*,
house_table: dict[str, Any] | None,
) -> list[dict[str, str]]:
executed = set(_executed_public_methods(built))
if house_table:
executed.add("d1-rashi")
rows: list[dict[str, str]] = []
for method in _AUDIT_LABELS:
if method not in executed:
continue
label, note = _AUDIT_LABELS[method]
rows.append({"technique": label, "status": "executed", "note": note})
rows.append({
"technique": "KP 宫头",
"status": "blocked",
"note": "KP 宫头本轮未计算。",
})
rows.extend((
{
"technique": "VedAstro 分钟级校验",
"status": "blocked",
"note": "官方分钟级校验尚未评估。",
},
{
"technique": "唯一分钟确认",
"status": "blocked",
"note": "采用不等于确认唯一分钟。",
},
))
return rows
def _quantized_score(row: CandidateScoreRow) -> Decimal:
return _decimal(row.get("score")).quantize(SCORE_QUANTUM, rounding=ROUND_HALF_UP)
@@ -263,12 +362,22 @@ def build_decision_receipt(
"exact_confirmation": exact_confirmation,
},
}
house_table = compact_house_table_from_contexts(
house_tables_by_time: dict[str, dict[str, Any]] = {}
for decision in candidate_decisions:
table = compact_house_table_from_contexts(built.get("static_contexts"), decision.get("time"))
if table:
house_tables_by_time[table["time"]] = table
house_table = house_tables_by_time.get(representative["time"] if representative else "") or compact_house_table_from_contexts(
built.get("static_contexts"),
representative["time"] if representative else None,
)
if house_table:
receipt["house_table"] = house_table
recast = natal_recast_copy(house_table["time"], house_table["lagna"])
receipt["natal_recast"] = recast
if house_tables_by_time:
receipt["house_tables_by_time"] = house_tables_by_time
receipt["technique_audit_table"] = build_technique_audit(built, house_table=house_table)
receipt.update({
"window_scan": packet["window_scan"],
"event_dasha_ledger": packet["event_dasha_ledger"],
+22 -5
View File
@@ -113,11 +113,19 @@ def _split_track_points(rule_ids: Sequence[str], points: float) -> tuple[float,
return points * vim / total, points * narayana / total
_LAYER_LABEL = {"d1": "本命上升", "d9": "D9", "d10": "D10", "d4": "D4", "d12": "D12"}
_LAYER_LABEL = {
"d1": "本命上升",
"d9": "D9",
"d10": "D10",
"d4": "D4",
"d5": "D5",
"d7": "D7",
"d12": "D12",
}
def window_scan(built: dict[str, Any]) -> dict[str, Any]:
"""D1/D9/D10/D4 diversity plus change minutes. Indices only; never sign names."""
"""D1/D9/D10/D4/D5/D7/D12 diversity plus change minutes. 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
@@ -128,6 +136,8 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]:
"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,
}
for layer, bucket in counts.items():
@@ -154,11 +164,15 @@ def window_scan(built: dict[str, Any]) -> dict[str, Any]:
"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,
}
@@ -360,14 +374,17 @@ def precision_stage(scan: dict[str, Any], event_count: int) -> dict[str, Any]:
current = "d10_refine"
meaning = "关系盘已较稳,事业盘仍会换升。可再补一件记得时间的工作变化。"
elif scan.get("d4_candidates_differ"):
current = "theme_refine"
meaning = "核心分盘已较稳。若还想收窄,可再补一件记得时间的家或住处变化;也可以先采用代表性时间"
current = "d4_refine"
meaning = "事业盘已较稳,居所盘仍会换升。可再补一件记得时间的家或住处变化。"
elif scan.get("d5_candidates_differ"):
current = "d5_refine"
meaning = "居所盘已较稳,成就盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化;不要贴类型标签。"
else:
current = "ready_to_adopt"
meaning = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。"
return {
"current": current,
"can_stop": current in {"d9_refine", "d10_refine", "theme_refine", "ready_to_adopt"},
"can_stop": current in {"d9_refine", "d10_refine", "d4_refine", "d5_refine", "theme_refine", "ready_to_adopt"},
"user_meaning": meaning,
"unique_minute_claim": False,
}
+6 -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-3"
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-4"
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
PRECISION_WEIGHTS = {
"day": 1.0,
@@ -184,7 +184,11 @@ 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"})
layers.update({"d1-rashi", "d12-dwadashamsha", "d7-saptamsha"})
elif domain == "education":
layers.update({"d1-rashi", "d24-chaturvimshamsha", "d5-panchamsha"})
elif domain == "relocation":
layers.update({"d1-rashi", "d4-chaturthamsha"})
elif domain in {"appearance", "marks"}:
layers.add("d1-rashi")
return sorted(layers)