fix(rectification): separate adjacent minutes with transition proximity
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user