fix(rectification): separate adjacent minutes with transition proximity
Independent Staging Quality Gate / validate (push) Successful in 11m56s
Independent Staging Quality Gate / publish (push) Has been cancelled

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>
This commit is contained in:
Jesse_Chen
2026-09-01 17:41:06 +08:00
parent 20df8c6022
commit 8743dcb105
18 changed files with 1402 additions and 55 deletions
@@ -0,0 +1,198 @@
"""Deterministic dasha-transition proximity scoring for day/month events.
Birth-time drift of about 1 minute moves Vimshottari/Narayana transition
dates by a few days. A dated event near a candidate's AD/PD change is a
bounded auxiliary signal, never larger than one day-level event body.
"""
from __future__ import annotations
from collections.abc import Callable, Sequence
from datetime import date
from typing import Any
from scripts.rectification.event_probes import _narayana_start_dates, _vim_start_dates
PROXIMITY_WINDOW_DAYS = 45
DAY_KERNEL_DAYS = 15
MONTH_KERNEL_DAYS = 45
DAY_MAX_POINTS = 1.0
MONTH_MAX_POINTS = 0.35
VIM_SHARE = 0.6
NARAYANA_SHARE = 0.4
def representative_event_date(event: dict[str, Any]) -> date | None:
precision = str(event.get("precision") or "")
if precision not in {"day", "month"}:
return None
raw_start = event.get("date_start") or event.get("date")
raw_end = event.get("date_end") or raw_start
try:
start = date.fromisoformat(str(raw_start)[:10])
end = date.fromisoformat(str(raw_end)[:10])
except ValueError:
return None
if precision == "day" or start == end:
return start
mid_day = min(15, end.day)
try:
return start.replace(day=mid_day)
except ValueError:
return start
def _nearest_delta(starts: Sequence[date], event_date: date) -> tuple[date | None, int | None]:
eligible = [
item for item in starts
if abs((item - event_date).days) <= PROXIMITY_WINDOW_DAYS
]
if not eligible:
return None, None
nearest = min(eligible, key=lambda item: (abs((item - event_date).days), item.toordinal()))
return nearest, abs((nearest - event_date).days)
def _kernel(delta_days: int | None, width: float) -> float:
if delta_days is None or width <= 0:
return 0.0
return max(0.0, 1.0 - (delta_days / width))
def score_transition_proximity(
*,
event_date: date,
precision: str,
vim_starts: Sequence[date],
narayana_starts: Sequence[date] | None = None,
vim_pd_starts: Sequence[date] | None = None,
) -> dict[str, Any]:
if precision not in {"day", "month"}:
return {
"points": 0.0,
"rule_ids": [],
"nearest_vim_delta_days": None,
"nearest_narayana_delta_days": None,
}
kernel_width = float(DAY_KERNEL_DAYS if precision == "day" else MONTH_KERNEL_DAYS)
cap = DAY_MAX_POINTS if precision == "day" else MONTH_MAX_POINTS
ad_starts = list(vim_starts)
pd_starts = list(vim_pd_starts or ())
ad_date, ad_delta = _nearest_delta(ad_starts, event_date)
pd_date, pd_delta = _nearest_delta(pd_starts, event_date)
if pd_delta is not None and (ad_delta is None or pd_delta < ad_delta):
vim_delta = pd_delta
vim_kind = "pd"
vim_date = pd_date
else:
vim_delta = ad_delta
vim_kind = "ad"
vim_date = ad_date
_, narayana_delta = _nearest_delta(list(narayana_starts or ()), event_date)
vim_kernel = _kernel(vim_delta, kernel_width)
narayana_kernel = _kernel(narayana_delta, kernel_width)
points = round(cap * (VIM_SHARE * vim_kernel + NARAYANA_SHARE * narayana_kernel), 4)
rules: list[str] = []
if vim_kernel > 0:
rules.append(f"vim_transition_proximity_{vim_kind}")
if narayana_kernel > 0:
rules.append("narayana_transition_proximity_ad")
return {
"points": points,
"rule_ids": rules,
"nearest_vim_delta_days": vim_delta,
"nearest_narayana_delta_days": narayana_delta,
"nearest_vim_date": vim_date,
}
def _context_time(context: dict[str, Any]) -> str | None:
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
raw = feature.get("time")
if isinstance(raw, str) and len(raw) >= 5:
return raw[:5]
at = context.get("candidate_at")
if hasattr(at, "strftime"):
return at.strftime("%H:%M")
return None
def merge_transition_proximity(
matrix: dict[str, dict[str, dict[str, Any]]],
events: Sequence[dict[str, Any]],
static_contexts: Sequence[dict[str, Any]],
birth_date: str,
*,
public_technique_layers: Callable[[str, Sequence[str]], list[str]],
) -> None:
by_time = {
time: context
for context in static_contexts
if isinstance(context, dict) and (time := _context_time(context))
}
vim_cache: dict[tuple[Any, ...], list[date]] = {}
pd_cache: dict[tuple[Any, ...], list[date]] = {}
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
for event in events:
if not isinstance(event, dict):
continue
event_id = str(event.get("id") or "")
cells = matrix.get(event_id)
if not event_id or not isinstance(cells, dict):
continue
event_date = representative_event_date(event)
if event_date is None:
continue
precision = str(event.get("precision") or "")
lo, hi = event_date.year - 1, event_date.year + 1
for time, cell in cells.items():
context = by_time.get(str(time)[:5])
if not isinstance(cell, dict) or not isinstance(context, dict):
continue
moon = (context.get("planet_longitudes") or {}).get("Moon")
if not isinstance(moon, (int, float)):
continue
vim_key = (birth_date, round(float(moon), 6), lo, hi)
if vim_key not in vim_cache:
vim_cache[vim_key] = _vim_start_dates(birth_date, float(moon), lo, hi)
pd_cache[vim_key] = _vim_start_dates(
birth_date,
float(moon),
lo,
hi,
include_pratyantar=True,
)
planets = context.get("planet_longitudes") or {}
asc = context.get("ascendant_index")
narayana_key = (
birth_date,
int(asc) if isinstance(asc, int) else None,
lo,
hi,
round(float(moon), 6),
)
if narayana_key not in narayana_cache:
narayana_cache[narayana_key] = (
_narayana_start_dates(int(asc), planets, birth_date, lo, hi)
if isinstance(asc, int) and isinstance(planets, dict)
else None
)
ad_starts = vim_cache[vim_key]
ad_set = set(ad_starts)
pd_only = [item for item in pd_cache[vim_key] if item not in ad_set]
scored = score_transition_proximity(
event_date=event_date,
precision=precision,
vim_starts=ad_starts,
vim_pd_starts=pd_only,
narayana_starts=narayana_cache[narayana_key] or [],
)
if scored["points"] <= 0 and not scored["rule_ids"]:
continue
cell["points"] = round(float(cell.get("points") or 0) + float(scored["points"]), 4)
cell["rule_ids"] = sorted({
*list(cell.get("rule_ids") or []),
*scored["rule_ids"],
})
domain = str(event.get("domain") or cell.get("domain") or "")
cell["technique_layers"] = public_technique_layers(domain, cell["rule_ids"])
+7 -2
View File
@@ -26,7 +26,7 @@ from scripts.rectification_policy import (
MIN_CONFIRMATION_MARGIN_PERCENT,
)
POLICY_VERSION = "rectification-candidate-policy-v2"
POLICY_VERSION = "rectification-candidate-policy-v3"
RECEIPT_VERSION = "candidate-decision-receipt-v2"
EXECUTION_LEDGER_VERSION = "rectification-execution-ledger-v2"
SCORE_QUANTUM = Decimal("0.0001")
@@ -71,6 +71,7 @@ _AUDIT_LABELS = {
"shadbala": ("Shadbala", "本轮已做已核验的 Shadbala 分量辅助对照。"),
"arudha-pada": ("Arudha Pada", "本轮已做 Arudha 辅助对照。"),
"functional-benefic-malefic": ("功能吉凶星", "本轮已叠加本命功能吉凶星。"),
"dasha-transition-proximity": ("换运贴近度", "本轮已对照日级事件与候选换运日期的贴近程度。"),
}
@@ -100,7 +101,9 @@ def _executed_public_methods(built: dict[str, Any]) -> list[str]:
methods.add(str(layer))
for rule in cell.get("rule_ids") or []:
text = str(rule)
if text.startswith("vim_"):
if "transition_proximity" in text:
methods.add("dasha-transition-proximity")
elif text.startswith("vim_"):
methods.add("vimshottari-dasha")
elif text.startswith("narayana_"):
methods.add("narayana-dasha")
@@ -652,6 +655,8 @@ def build_decision_receipt(
"evidence_collection_probes": packet.get("evidence_collection_probes") or [],
"candidate_contrast_opportunities": packet.get("candidate_contrast_opportunities") or [],
"holdout_validation_probes": packet.get("holdout_validation_probes") or [],
"dropped_probes": packet.get("dropped_probes") or [],
"prospective_probes": packet.get("prospective_probes") or [],
"horary_observation": build_horary_observation(request),
"unique_minute_claim": False,
})
+404 -20
View File
@@ -1,7 +1,9 @@
"""Public-safe biographical probes from candidate dasha / varga differences.
Discriminators are feature-signature clusters over the full birth window.
known_event_quality is clarification only and never a distinguish probe.
known_event_quality may distinguish when signature groups disagree on the
event's theme varga type, the probe is anchored to confirmed evidence, and
the case cap is respected. Unanchored quality stays clarification-only.
"""
from __future__ import annotations
@@ -35,7 +37,7 @@ from scripts.rectification.candidate_contrast import (
missing_collection_domains,
opportunity_from_probe,
)
from scripts.rectification.case_holdout import holdout_domain_years
from scripts.rectification.case_holdout import holdout_domain_years, holdout_event_ids
from scripts.rectification.probe_question_contract import (
QUESTION_CONTRACT_VERSION,
completed_style_options,
@@ -59,6 +61,66 @@ MAX_PROBES_PER_DOMAIN = 3
# MAX_PROBES is the published cap after a global information_gain sort.
MAX_BOUNDARY_CANDIDATES_PER_DOMAIN = 8
MIN_BOUNDARY_DAYS = 45
MAX_QUALITY_DISTINGUISH_PROBES = 2
ANSWER_PRIOR_TABLE_VERSION = "rectification-answer-priors-v1"
DOMINANT_ANSWER_PRIOR = 0.8
# Conservative population rates, not fitted from product users.
# Broad family existence (yearless / 3-year window / age band) is near-certain
# in adult life. Specified-year long-distance move is uncommon. Quality answers
# are closer to even because they condition on a known event.
_DEFAULT_EXISTENCE_PRIORS = {"yes": 0.35, "weak_yes": 0.15, "no": 0.40, "unsure": 0.10}
_DEFAULT_QUALITY_PRIORS = {"yes": 0.30, "weak_yes": 0.20, "no": 0.40, "unsure": 0.10}
ANSWER_PRIORS: dict[tuple[str, str], dict[str, float]] = {
("family", "existence"): {"yes": 0.85, "weak_yes": 0.05, "no": 0.05, "unsure": 0.05},
("relocation", "existence"): {"yes": 0.20, "weak_yes": 0.10, "no": 0.60, "unsure": 0.10},
("education", "existence"): {"yes": 0.45, "weak_yes": 0.15, "no": 0.30, "unsure": 0.10},
("relationship", "existence"): {"yes": 0.40, "weak_yes": 0.15, "no": 0.35, "unsure": 0.10},
("career", "existence"): {"yes": 0.40, "weak_yes": 0.15, "no": 0.35, "unsure": 0.10},
("education", "event_quality"): dict(_DEFAULT_QUALITY_PRIORS),
("relationship", "event_quality"): dict(_DEFAULT_QUALITY_PRIORS),
("career", "event_quality"): dict(_DEFAULT_QUALITY_PRIORS),
("relocation", "event_quality"): dict(_DEFAULT_QUALITY_PRIORS),
("family", "event_quality"): dict(_DEFAULT_QUALITY_PRIORS),
}
DOMAIN_QUALITY_LAYER = {
"education": "d24",
"relationship": "d9",
"career": "d10",
"relocation": "d4",
"family": "d12",
}
QUALITY_DISTINGUISH_OPTIONS: dict[str, tuple[dict[str, str], ...]] = {
"education": (
{"label": "发挥明显失常", "answer_class": "yes"},
{"label": "只是将就调剂", "answer_class": "weak_yes"},
{"label": "基本如愿录取", "answer_class": "no"},
{"label": "当时说不清楚", "answer_class": "unsure"},
),
"relationship": (
{"label": "明显受挫变难", "answer_class": "yes"},
{"label": "只是将就相处", "answer_class": "weak_yes"},
{"label": "整体比较顺利", "answer_class": "no"},
{"label": "当时说不清楚", "answer_class": "unsure"},
),
"career": (
{"label": "明显受挫受压", "answer_class": "yes"},
{"label": "只是将就应付", "answer_class": "weak_yes"},
{"label": "整体比较顺利", "answer_class": "no"},
{"label": "当时说不清楚", "answer_class": "unsure"},
),
"relocation": (
{"label": "搬迁特别折腾", "answer_class": "yes"},
{"label": "只是将就安顿", "answer_class": "weak_yes"},
{"label": "整体比较顺利", "answer_class": "no"},
{"label": "当时说不清楚", "answer_class": "unsure"},
),
"family": (
{"label": "家里特别操心", "answer_class": "yes"},
{"label": "只是普通操心", "answer_class": "weak_yes"},
{"label": "整体比较顺利", "answer_class": "no"},
{"label": "当时说不清楚", "answer_class": "unsure"},
),
}
LEVEL_RANK = {"none": 0, "weak": 1, "medium": 2, "strong": 3}
LEVEL_P = {"none": 0.15, "weak": 0.35, "medium": 0.62, "strong": 0.82}
SCORING_LAYERS = ("d1", "d9", "d10", "d4", "d5", "d24", "d7", "d12", "d2", "d11", "d30")
@@ -349,7 +411,14 @@ def _tracks_present(rule_ids: Sequence[str]) -> tuple[bool, bool]:
)
def _vim_start_dates(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[date]:
def _vim_start_dates(
birth_date: str,
moon_longitude: float,
lo: int,
hi: int,
*,
include_pratyantar: bool = False,
) -> list[date]:
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(float(moon_longitude))
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(birth_date, nakshatra, progress)
starts: list[date] = []
@@ -361,6 +430,12 @@ def _vim_start_dates(birth_date: str, moon_longitude: float, lo: int, hi: int) -
minor_start = minor.get("start")
if isinstance(minor_start, datetime) and lo <= minor_start.year <= hi:
starts.append(minor_start.date())
if not include_pratyantar:
continue
for prat in dasha_analyzer.build_antardasha(minor):
prat_start = prat.get("start")
if isinstance(prat_start, datetime) and lo <= prat_start.year <= hi:
starts.append(prat_start.date())
return starts
@@ -738,6 +813,187 @@ def _information_gain(left_level: str, right_level: str) -> float:
return round(max(0.0, 1.0 - after), 4)
def _broad_existence_window(probe: dict[str, Any]) -> bool:
if str(probe.get("source") or "") == "age_band":
return True
year = probe.get("year")
if not isinstance(year, int) or year <= 0:
return True
span = probe.get("window_span_years")
return isinstance(span, int) and span >= 3
def _answer_priors_for(probe: dict[str, Any]) -> dict[str, float]:
kind = str(probe.get("choice_kind") or "existence")
if kind not in {"existence", "event_quality"}:
kind = "existence"
domain = str(probe.get("domain") or "")
if domain == "family" and kind == "existence" and not _broad_existence_window(probe):
return dict(_DEFAULT_EXISTENCE_PRIORS)
priors = ANSWER_PRIORS.get((domain, kind))
if priors:
return dict(priors)
return dict(_DEFAULT_QUALITY_PRIORS if kind == "event_quality" else _DEFAULT_EXISTENCE_PRIORS)
def _expected_information_gain(raw_split_gain: float, priors: dict[str, float]) -> float:
weight = (
float(priors.get("yes") or 0)
+ float(priors.get("no") or 0)
+ 0.5 * float(priors.get("weak_yes") or 0)
)
return round(max(0.0, raw_split_gain * weight), 4)
def _apply_prior_ranking(probe: dict[str, Any]) -> dict[str, Any]:
raw = float(probe.get("information_gain") or 0)
priors = _answer_priors_for(probe)
probe["raw_split_gain"] = raw
probe["answer_priors"] = priors
probe["information_gain"] = _expected_information_gain(raw, priors)
return probe
def _dominant_existence_prior(probe: dict[str, Any], priors: dict[str, float]) -> bool:
if str(probe.get("choice_kind") or "existence") != "existence":
return False
if str(probe.get("source") or "") == "known_event_quality":
return False
return max(priors.values()) > DOMINANT_ANSWER_PRIOR
def _event_month(event: dict[str, Any]) -> int | None:
raw = str(event.get("date") or event.get("date_start") or "")
if len(raw) >= 7 and raw[4] == "-":
try:
month = int(raw[5:7])
except ValueError:
return None
if 1 <= month <= 12:
return month
return None
def _display_date_label(event: dict[str, Any]) -> str:
year = _event_year(event)
month = _event_month(event)
if year is None:
return "那次"
if month:
return f"{year}{month}"
return f"{year}"
def _quality_user_meaning(event: dict[str, Any], domain: str) -> str:
label = _display_date_label(event)
if domain == "education":
return (
f"{label}那次上大学,更接近如愿、将就调剂、发挥失常还是说不清。"
"只问那次经历的实际体验,不得改时间范围。"
)
family = str(DOMAIN_CATALOG[domain]["event_family"])
return (
f"{label}那次{family},当时更接近顺利、将就、明显受挫还是说不清。"
"只问那次经历的实际体验,不得改时间范围。"
)
def _quality_distinguish_probes(
events: Sequence[dict[str, Any]],
clusters: Sequence[dict[str, Any]],
*,
set_version: str,
holdout_ids: set[str],
holdout_keys: set[str],
) -> list[dict[str, Any]]:
if len(clusters) < 2:
return []
rows: list[dict[str, Any]] = []
for event in events:
if not isinstance(event, dict):
continue
event_id = str(event.get("id") or "")
domain = str(event.get("domain") or "")
year = _event_year(event)
layer = DOMAIN_QUALITY_LAYER.get(domain)
if not event_id or year is None or layer is None or domain not in DOMAIN_CATALOG:
continue
if event_id in holdout_ids or f"{domain}:{year}" in holdout_keys:
continue
if _quality_encoded(event, domain):
continue
groups: dict[int, list[str]] = {}
for cluster in clusters:
representative = cluster.get("representative") if isinstance(cluster, dict) else None
if not isinstance(representative, dict):
continue
sign = _layer_value(representative, layer)
if not isinstance(sign, int):
continue
bucket = groups.setdefault(sign, [])
for time in cluster.get("times") or []:
clock = str(time)[:5]
if len(clock) == 5 and clock not in bucket:
bucket.append(clock)
if len(groups) < 2:
continue
signs = sorted(groups)
yes_times = sorted(groups[signs[-1]], key=_clock)
no_times = sorted(groups[signs[0]], key=_clock)
overlap = set(yes_times) & set(no_times)
yes_times = [time for time in yes_times if time not in overlap]
no_times = [time for time in no_times if time not in overlap]
if len(yes_times) < 1 or len(no_times) < 1:
continue
month = _event_month(event)
outcomes = [
{"answer_class": "yes", "supports": yes_times, "conflicts": no_times},
{"answer_class": "weak_yes", "supports": yes_times, "conflicts": no_times},
{"answer_class": "no", "supports": no_times, "conflicts": yes_times},
{"answer_class": "unsure", "supports": [], "conflicts": []},
]
split = candidate_split_hash(
candidate_set_version_value=set_version,
domain=domain,
year=year,
month=month,
groups=[yes_times, no_times],
)
gain = round(_group_entropy([len(yes_times), len(no_times)]), 4)
if gain <= 0:
continue
label = _display_date_label(event)
probe = _public_probe(
year=year,
month=month,
domain=domain,
source="known_event_quality",
tracks=("vimshottari", "narayana"),
tracks_agree=True,
user_meaning=_quality_user_meaning(event, domain),
event_family=str(DOMAIN_CATALOG[domain]["quality_family"]),
information_gain=gain,
semantic_key=f"{domain}.{year}.known_event_quality",
candidate_split_hash=split,
candidate_set_version=set_version,
expected_outcomes=outcomes,
candidate_ids=candidate_ids_from_outcomes(outcomes),
left_time=yes_times[0],
right_time=no_times[0],
target_evidence_id=event_id,
display_date_label=label,
role="distinguish",
phase=PROBE_PHASE_CANDIDATE_DISCRIMINATOR,
style_options=list(QUALITY_DISTINGUISH_OPTIONS.get(domain) or ()),
)
if distinguish_contract_errors(probe):
continue
rows.append(_apply_prior_ranking(probe))
if len(rows) >= MAX_QUALITY_DISTINGUISH_PROBES:
break
return rows
def _year_activated(rule_ids: Sequence[str]) -> bool:
return _has_domain_activation(rule_ids) or LEVEL_RANK.get(match_level(rule_ids), 0) >= 2
@@ -1083,7 +1339,7 @@ def candidate_contrast_opportunities(
return [opportunity_from_probe(probe) for probe in probes]
def discriminating_event_probes(
def _discriminating_event_probe_lists(
request: dict[str, Any],
built: dict[str, Any],
*,
@@ -1092,35 +1348,36 @@ def discriminating_event_probes(
representative_time: str | None,
precision_current: str | None = None,
today: date | None = None,
) -> list[dict[str, Any]]:
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
del precision_current, representative_time
birth_date = str(request.get("birth_date") or "").strip()
birth_year = _birth_year(birth_date)
empty: tuple[list[dict[str, Any]], list[dict[str, Any]]] = ([], [])
if birth_year is None:
return []
return empty
try:
datetime.strptime(birth_date, "%Y-%m-%d")
except ValueError:
return []
return empty
events = [item for item in (request.get("events") or []) if isinstance(item, dict)]
if not discriminator_gate_open(events):
return []
return empty
holdout_keys = holdout_domain_years(events)
now = today or date.today()
full = _static_contexts(built)
if len(full) < 2:
return []
return empty
clusters = cluster_contexts_by_signature(full)
if len(clusters) < 2:
remaining = _remaining_contexts(built, candidate_times) or full
clusters = cluster_contexts_by_signature(remaining)
if len(clusters) < 2:
return []
return empty
reps = [cluster["representative"] for cluster in clusters if _scoreable(cluster["representative"])]
if len(reps) < 2:
reps = [item for item in full if _scoreable(item)]
if len(reps) < 2:
return []
return empty
set_version = candidate_set_version([cluster["times"] for cluster in clusters])
remaining_layers = _differing_layers(full)
if not remaining_layers:
@@ -1133,10 +1390,8 @@ def discriminating_event_probes(
events,
d1_differs="d1" in remaining_layers or bool(scan.get("d1_candidates_differ")),
)
if not domains:
return []
lo, hi = birth_year + 5, min(now.year, birth_year + 80)
boundary_dates = _union_boundary_dates(reps, birth_date=birth_date, lo=lo, hi=hi)
boundary_dates = _union_boundary_dates(reps, birth_date=birth_date, lo=lo, hi=hi) if domains else []
probes: list[dict[str, Any]] = []
for domain in domains:
if domain not in DOMAIN_CATALOG:
@@ -1202,23 +1457,152 @@ def discriminating_event_probes(
if activation_key not in existing:
kept.append(activation)
probes.extend(kept)
probes.extend(_quality_distinguish_probes(
events,
clusters,
set_version=set_version,
holdout_ids=set(holdout_event_ids(events)),
holdout_keys=set(holdout_keys),
))
probes.sort(key=_probe_sort_key)
public, dropped = _partition_ranked_probes(probes)
assert_distinguish_contract(public)
return public, dropped
def _partition_ranked_probes(
probes: Sequence[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
public: list[dict[str, Any]] = []
dropped: list[dict[str, Any]] = []
seen: set[tuple[str, int, int, str]] = set()
for row in probes:
if row.get("source") == "known_event_quality" or row.get("phase") != PROBE_PHASE_CANDIDATE_DISCRIMINATOR:
anchored_quality = (
row.get("source") == "known_event_quality"
and row.get("role") == "distinguish"
and row.get("target_evidence_id")
)
if row.get("source") == "known_event_quality" and not anchored_quality:
continue
if row.get("phase") != PROBE_PHASE_CANDIDATE_DISCRIMINATOR:
continue
if distinguish_contract_errors(row):
continue
ranked = row if "raw_split_gain" in row else _apply_prior_ranking(dict(row))
priors = ranked.get("answer_priors") or _answer_priors_for(ranked)
if _dominant_existence_prior(ranked, priors):
dropped.append({
"semantic_key": ranked.get("semantic_key"),
"reason": "dominant_answer_prior",
"domain": ranked.get("domain"),
"year": ranked.get("year"),
"source": ranked.get("source"),
"answer_priors": priors,
})
continue
if not isinstance(row.get("year"), int) or int(row["year"]) <= 0:
continue
key = (str(row["domain"]), int(row["year"]), int(row.get("month") or 0), str(row["source"]))
encoded = str(row)
key = (str(ranked["domain"]), int(ranked["year"]), int(ranked.get("month") or 0), str(ranked["source"]))
encoded = str(ranked)
if key in seen or "points" in encoded:
continue
seen.add(key)
public.append(row)
public.append(ranked)
if len(public) >= MAX_PROBES:
break
assert_distinguish_contract(public)
return public
public.sort(key=_probe_sort_key)
return public, dropped
def discriminating_event_probe_set(
request: dict[str, Any],
built: dict[str, Any],
*,
scan: dict[str, Any],
candidate_times: Sequence[str],
representative_time: str | None,
precision_current: str | None = None,
today: date | None = None,
) -> dict[str, list[dict[str, Any]]]:
probes, dropped = _discriminating_event_probe_lists(
request,
built,
scan=scan,
candidate_times=candidate_times,
representative_time=representative_time,
precision_current=precision_current,
today=today,
)
return {"probes": probes, "dropped": dropped}
def discriminating_event_probes(
request: dict[str, Any],
built: dict[str, Any],
*,
scan: dict[str, Any],
candidate_times: Sequence[str],
representative_time: str | None,
precision_current: str | None = None,
today: date | None = None,
) -> list[dict[str, Any]]:
probes, _dropped = _discriminating_event_probe_lists(
request,
built,
scan=scan,
candidate_times=candidate_times,
representative_time=representative_time,
precision_current=precision_current,
today=today,
)
return probes
def prospective_event_windows(
request: dict[str, Any],
built: dict[str, Any],
*,
candidate_times: Sequence[str],
today: date | None = None,
) -> list[dict[str, Any]]:
birth_date = str(request.get("birth_date") or "").strip()
if not birth_date:
return []
now = today or date.today()
lo, hi = now.year + 1, now.year + 3
full = _static_contexts(built)
if len(full) < 2:
return []
clusters = cluster_contexts_by_signature(full)
if len(clusters) < 2:
remaining = _remaining_contexts(built, candidate_times) or full
clusters = cluster_contexts_by_signature(remaining)
if len(clusters) < 2:
return []
labels = ("A", "B", "C")
rows: list[dict[str, Any]] = []
for index, cluster in enumerate(clusters[:3]):
context = cluster.get("representative") if isinstance(cluster, dict) else None
if not isinstance(context, dict):
continue
moon = (context.get("planet_longitudes") or {}).get("Moon")
if not isinstance(moon, (int, float)):
continue
starts = _vim_start_dates(birth_date, float(moon), lo, hi, include_pratyantar=True)
future = [item for item in starts if item.year >= lo]
if not future:
continue
start = min(future)
label = labels[index]
window = f"{start.year}{start.month} 月附近"
rows.append({
"candidate_label": label,
"domain": "career",
"window_label": window,
"user_meaning": (
f"候选 {label} 预测下一次事业变动更可能在 {window}"
"这是预测窗口,不是承诺;下次发生时回来补一条,可进一步分辨。"
),
"used_for_scoring": False,
})
return rows
+12 -2
View File
@@ -558,12 +558,13 @@ def build_refinement_packet(
from scripts.rectification.case_holdout import reserved_holdout_events
from scripts.rectification.event_probes import (
candidate_contrast_opportunities,
discriminating_event_probes,
discriminating_event_probe_set,
event_clarification_probes,
evidence_collection_probes,
prospective_event_windows,
)
grid_times = list(built.get("candidate_times") or candidate_times)
probes = discriminating_event_probes(
bundle = discriminating_event_probe_set(
request,
built,
scan=scan,
@@ -571,6 +572,8 @@ def build_refinement_packet(
representative_time=representative_time,
precision_current=str(stage.get("current") or "") or None,
)
probes = bundle["probes"]
dropped = list(bundle["dropped"])
clarification = event_clarification_probes(request)
collection = evidence_collection_probes(request)
opportunities = candidate_contrast_opportunities(
@@ -602,6 +605,11 @@ def build_refinement_packet(
}
for prompt in oos_blind_prompts(request)
)
prospective = prospective_event_windows(
request,
built,
candidate_times=grid_times,
) if not probes else []
return {
"window_scan": scan,
"event_dasha_ledger": ledger,
@@ -616,6 +624,8 @@ def build_refinement_packet(
"evidence_collection_probes": collection,
"candidate_contrast_opportunities": opportunities,
"holdout_validation_probes": holdout,
"dropped_probes": dropped,
"prospective_probes": prospective,
"unique_minute_claim": False,
"confirmation_allowed": False,
}
+23 -8
View File
@@ -10,10 +10,11 @@ from typing import Any
from scripts.active_rectification_event_engine import compute_candidate_static_contexts, compute_event_candidate_rows
from scripts.active_rectification_events import CandidateScoreRow
from scripts.rectification.dasha_transition_proximity import merge_transition_proximity
from scripts.rectification.contracts import LifeEvent, RectificationRequest, is_scoreable_event
from scripts.rectification.case_holdout import holdout_event_ids
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-6"
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-7"
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
PRECISION_WEIGHTS = {
"day": 1.0,
@@ -179,11 +180,14 @@ def precision_weight(precision: str) -> float:
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:"))
}
layers: set[str] = set()
for rule in rule_ids:
if rule.startswith(("event_kind:", "event_kind_profile:")):
continue
if "transition_proximity" in rule:
layers.add("dasha-transition-proximity")
continue
layers.add(rule.split(":", 1)[0])
if domain == "career":
layers.update({"d1-rashi", "d10-dashamsa"})
elif domain == "family":
@@ -235,6 +239,7 @@ def scoreable_request(request: RectificationRequest) -> RectificationRequest:
def build_event_contribution_matrix(
request: RectificationRequest,
row_provider: Callable[[dict[str, Any]], Sequence[CandidateScoreRow]] | None = None,
static_contexts: Sequence[dict[str, Any]] | None = None,
) -> dict[str, Any]:
scoring_request = scoreable_request(request)
if not scoring_request["events"]:
@@ -242,7 +247,8 @@ def build_event_contribution_matrix(
"candidate_times": [], "matrix": {}, "date_sensitivity": [],
"missing_layers": [], "static_contexts": None,
}
static_contexts = None if row_provider is not None else compute_candidate_static_contexts(scoring_request)
if static_contexts is None and row_provider is None:
static_contexts = compute_candidate_static_contexts(scoring_request)
provider = row_provider or (lambda value: compute_event_candidate_rows(value, static_contexts=static_contexts))
matrix: dict[str, dict[str, dict[str, Any]]] = defaultdict(dict)
missing_layers: set[str] = set()
@@ -288,9 +294,18 @@ def build_event_contribution_matrix(
"score_variance": round(variance, 6),
"sample_winners": winners,
})
matrix_payload = dict(matrix)
if static_contexts:
merge_transition_proximity(
matrix_payload,
scoring_request["events"],
static_contexts,
scoring_request["birth_date"],
public_technique_layers=public_technique_layers,
)
return {
"candidate_times": candidate_grid or [],
"matrix": dict(matrix),
"matrix": matrix_payload,
"date_sensitivity": date_sensitivity,
"missing_layers": sorted(missing_layers),
"static_contexts": static_contexts,