fix(rectification): score family events and allow appearance follow-up
Dated family evidence now moves candidates via D12 and kin houses, career receipts expose both D1-10 and D10, and appearance/marks may be asked as auxiliary first-house scores. New cases bind Skill 10.0.4. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -53,7 +53,15 @@ DOMAIN_CONFIG: Final[dict[EventDomain, DomainConfig]] = {
|
||||
"career": (("D10",), (10,)),
|
||||
"finance": (("D2", "D11"), (2, 11)),
|
||||
"health_pressure": (("D30",), (6, 8, 12)),
|
||||
# 六亲: D12 parents plus D1 houses 3/4/5/9 (siblings, mother/home, children, father).
|
||||
"family": (("D12",), (3, 4, 5, 9)),
|
||||
# Dated appearance/marks: D1 lagna / 1st house only. Not a primary formula.
|
||||
"appearance": ((), (1,)),
|
||||
}
|
||||
AUXILIARY_DOMAINS: Final[frozenset[str]] = frozenset({"appearance"})
|
||||
AUXILIARY_SCORE_FACTOR: Final = 0.4
|
||||
|
||||
|
||||
class RectificationEventCalculationError(RuntimeError):
|
||||
"""Raised when stored rectification evidence cannot be calculated safely."""
|
||||
|
||||
@@ -235,6 +243,9 @@ def _score_event(
|
||||
points += 0.35
|
||||
|
||||
event_kind = event.get("event_kind", event["domain"])
|
||||
if event["domain"] in AUXILIARY_DOMAINS:
|
||||
points *= AUXILIARY_SCORE_FACTOR
|
||||
rules.append("appearance_auxiliary_not_primary")
|
||||
if not rules:
|
||||
rules.append("no_domain_activation")
|
||||
rules.append(f"event_kind:{event_kind}")
|
||||
@@ -358,12 +369,12 @@ def build_candidate_static_context(
|
||||
charts = varga.calc_all_vargas(
|
||||
planet_longitudes,
|
||||
ascendant_longitude,
|
||||
divisions=[2, 4, 9, 10, 24, 30],
|
||||
divisions=[2, 4, 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", "D9", "D10", "D11", "D24", "D30")
|
||||
for prefix in ("D2", "D4", "D9", "D10", "D11", "D12", "D24", "D30")
|
||||
}
|
||||
available_layers = ["D1"]
|
||||
blocked_layers = ["KP_cusps"]
|
||||
|
||||
@@ -35,6 +35,8 @@ EventDomain = Literal[
|
||||
"career",
|
||||
"finance",
|
||||
"health_pressure",
|
||||
"family",
|
||||
"appearance",
|
||||
]
|
||||
Confidence = Literal["low", "medium", "high"]
|
||||
|
||||
|
||||
@@ -27,9 +27,12 @@ EVENT_KINDS: dict[str, frozenset[str]] = {
|
||||
"health": frozenset({"self_health_event", "pressure_period"}),
|
||||
"health_pressure": frozenset({"self_health_event", "pressure_period"}), # v1 domain compatibility
|
||||
"family": frozenset({"family_event"}),
|
||||
"appearance": frozenset({"appearance_note"}),
|
||||
"marks": frozenset({"birthmark_or_scar"}),
|
||||
"other": frozenset({"other"}),
|
||||
}
|
||||
BACKGROUND_EVENT_KINDS = frozenset({"family_event", "other"})
|
||||
BACKGROUND_EVENT_KINDS = frozenset({"other"})
|
||||
AUXILIARY_EVENT_KINDS = frozenset({"appearance_note", "birthmark_or_scar"})
|
||||
SCOREABLE_EVENT_KINDS: dict[str, frozenset[str]] = {
|
||||
domain: frozenset(kind for kind in kinds if kind not in BACKGROUND_EVENT_KINDS)
|
||||
for domain, kinds in EVENT_KINDS.items()
|
||||
@@ -85,6 +88,21 @@ def is_scoreable_event(event: LifeEvent) -> bool:
|
||||
return event["event_kind"] not in BACKGROUND_EVENT_KINDS
|
||||
|
||||
|
||||
def is_primary_scoreable_event(event: LifeEvent) -> bool:
|
||||
"""Dated events that may move the candidate ranking as a primary formula."""
|
||||
return is_scoreable_event(event) and event["event_kind"] not in AUXILIARY_EVENT_KINDS
|
||||
|
||||
|
||||
def subject_for_event_domain(domain: str, subject: str | None = None) -> Literal["self", "family", "other"]:
|
||||
if domain == "family":
|
||||
return "family"
|
||||
if domain == "other":
|
||||
return "other"
|
||||
if subject in {"self", "family", "other"}:
|
||||
return cast(Literal["self", "family", "other"], subject)
|
||||
return "self"
|
||||
|
||||
|
||||
def _bounded_number(body: dict[str, Any], name: str, minimum: float, maximum: float) -> float:
|
||||
value = body.get(name)
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
||||
@@ -167,12 +185,11 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) ->
|
||||
summary = raw_event.get("summary", "")
|
||||
if not isinstance(summary, str) or len(summary) > 1_000:
|
||||
raise ValueError(f"events[{index}].summary must be a string up to 1000 characters")
|
||||
subject = raw_event.get("subject")
|
||||
if subject is None:
|
||||
subject = "family" if domain == "family" else "other" if domain == "other" else "self"
|
||||
if subject not in {"self", "family", "other"}:
|
||||
raw_subject = raw_event.get("subject")
|
||||
if raw_subject is not None and raw_subject not in {"self", "family", "other"}:
|
||||
raise ValueError(f"events[{index}].subject is invalid")
|
||||
if event_kind not in BACKGROUND_EVENT_KINDS and subject != "self":
|
||||
subject = subject_for_event_domain(cast(str, domain), None if raw_subject is None else str(raw_subject))
|
||||
if event_kind not in BACKGROUND_EVENT_KINDS and domain != "family" and subject != "self":
|
||||
raise ValueError(f"events[{index}].subject must be self for scoreable events")
|
||||
cleaned_event: dict[str, Any] = {
|
||||
"id": event_id,
|
||||
|
||||
@@ -9,6 +9,7 @@ from scripts.active_rectification_events import CandidateScoreRow
|
||||
from scripts.rectification.contracts import (
|
||||
EVENT_CONTRACT_VERSION,
|
||||
RectificationRequest,
|
||||
is_primary_scoreable_event,
|
||||
is_scoreable_event,
|
||||
)
|
||||
from scripts.rectification.house_table import compact_house_table_from_contexts
|
||||
@@ -146,7 +147,7 @@ def build_decision_receipt(
|
||||
built: dict[str, Any],
|
||||
diagnostics: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
scoreable_events = [event for event in request["events"] if is_scoreable_event(event)]
|
||||
scoreable_events = [event for event in request["events"] if is_primary_scoreable_event(event)]
|
||||
domains = sorted({event["domain"] for event in scoreable_events})
|
||||
candidate_presence = _gate(bool(candidate_decisions), candidate_count=len(candidate_decisions))
|
||||
event_quality = _gate(
|
||||
|
||||
@@ -113,7 +113,7 @@ 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"}
|
||||
_LAYER_LABEL = {"d1": "本命上升", "d9": "D9", "d10": "D10", "d4": "D4", "d12": "D12"}
|
||||
|
||||
|
||||
def window_scan(built: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -128,6 +128,7 @@ 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,
|
||||
"d12": vargas.get("D12") if isinstance(vargas.get("D12"), int) else None,
|
||||
}
|
||||
for layer, bucket in counts.items():
|
||||
value = current[layer]
|
||||
@@ -153,10 +154,12 @@ 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"]),
|
||||
"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,
|
||||
"d12_candidates_differ": len(counts["d12"]) > 1,
|
||||
"transitions": transitions,
|
||||
}
|
||||
|
||||
|
||||
@@ -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-2"
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-3"
|
||||
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
|
||||
PRECISION_WEIGHTS = {
|
||||
"day": 1.0,
|
||||
@@ -50,6 +50,9 @@ _ENGINE_KIND_BY_NATIVE_KIND: dict[str, tuple[str, str]] = {
|
||||
"finance_change": ("finance", "finance_change"),
|
||||
"self_health_event": ("health_pressure", "self_health_event"),
|
||||
"pressure_period": ("health_pressure", "self_health_event"),
|
||||
"family_event": ("family", "family_event"),
|
||||
"appearance_note": ("appearance", "appearance_note"),
|
||||
"birthmark_or_scar": ("appearance", "birthmark_or_scar"),
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +164,9 @@ _KIND_SEMANTICS: dict[str, tuple[int, float]] = {
|
||||
"asset_change": (0, 1.0),
|
||||
"self_health_event": (-1, 1.0),
|
||||
"pressure_period": (-1, 1.2),
|
||||
"family_event": (0, 1.0),
|
||||
"appearance_note": (0, 0.8),
|
||||
"birthmark_or_scar": (-1, 0.8),
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +174,22 @@ def precision_weight(precision: str) -> float:
|
||||
return PRECISION_WEIGHTS[precision]
|
||||
|
||||
|
||||
def public_technique_layers(domain: str, rule_ids: Sequence[str]) -> list[str]:
|
||||
"""Public methods actually computed for this event. Career always lists D1-10 and D10."""
|
||||
layers = {
|
||||
rule.split(":", 1)[0]
|
||||
for rule in rule_ids
|
||||
if not rule.startswith(("event_kind:", "event_kind_profile:"))
|
||||
}
|
||||
if domain == "career":
|
||||
layers.update({"d1-rashi", "d10-dashamsa"})
|
||||
elif domain == "family":
|
||||
layers.update({"d1-rashi", "d12-dwadashamsha"})
|
||||
elif domain in {"appearance", "marks"}:
|
||||
layers.add("d1-rashi")
|
||||
return sorted(layers)
|
||||
|
||||
|
||||
def _event_kind_factor(event_kind: str, rule_ids: Sequence[str]) -> float:
|
||||
direction, intensity = _KIND_SEMANTICS.get(event_kind, (0, 1.0))
|
||||
support = sum(any(rule.endswith(marker) for marker in _SUPPORT_RULES) for rule in rule_ids)
|
||||
@@ -237,12 +259,10 @@ def build_event_contribution_matrix(
|
||||
matrix[event["id"]][candidate_time] = {
|
||||
"points": round(sum(points) / len(points), 4),
|
||||
"rule_ids": sorted({rule for item in evidences for rule in item["rule_ids"]}),
|
||||
"technique_layers": sorted({
|
||||
rule.split(":", 1)[0]
|
||||
for item in evidences
|
||||
for rule in item["rule_ids"]
|
||||
if not rule.startswith(("event_kind:", "event_kind_profile:"))
|
||||
}),
|
||||
"technique_layers": public_technique_layers(
|
||||
event["domain"],
|
||||
[rule for item in evidences for rule in item["rule_ids"]],
|
||||
),
|
||||
}
|
||||
winner = max(set(winners), key=winners.count)
|
||||
mean = sum(matrix[event["id"]][time]["points"] for time in candidate_grid) / len(candidate_grid)
|
||||
|
||||
@@ -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", "D9", "D10", "D11", "D24", "D30"],
|
||||
"used_divisional_charts": ["D2", "D4", "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