fix(rectification): ask reverse-inference probes at engine dasha months
Keep Vimshottari/Narayana start dates instead of truncating to year, so same-year month splits can appear on the choice card. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -202,6 +202,7 @@ def candidate_split_hash(
|
||||
domain: str,
|
||||
year: int,
|
||||
groups: Sequence[Sequence[str]],
|
||||
month: int | None = None,
|
||||
) -> str:
|
||||
version = candidate_set_version_value or candidate_set_version(groups)
|
||||
grouped = "|".join(
|
||||
@@ -209,7 +210,8 @@ def candidate_split_hash(
|
||||
for group in groups
|
||||
if group
|
||||
)
|
||||
payload = f"{version}:{domain}:{year}:{grouped}"
|
||||
window = f"{year}-{int(month):02d}" if isinstance(month, int) and 1 <= month <= 12 else str(year)
|
||||
payload = f"{version}:{domain}:{window}:{grouped}"
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from scripts.rectification.probe_question_contract import complete_style_options
|
||||
from scripts.rectification.refinement_packet import match_level
|
||||
|
||||
MAX_PROBES = 3
|
||||
MIN_BOUNDARY_DAYS = 45
|
||||
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")
|
||||
@@ -160,10 +161,16 @@ def _event_year(event: dict[str, Any]) -> int | None:
|
||||
return event_year(event)
|
||||
|
||||
|
||||
def _year_label(year: int) -> str:
|
||||
def _period_label(year: int, month: int | None = None) -> str:
|
||||
if isinstance(month, int) and 1 <= month <= 12:
|
||||
return f"{year} 年 {month} 月前后"
|
||||
return f"{year} 年前后"
|
||||
|
||||
|
||||
def _year_label(year: int) -> str:
|
||||
return _period_label(year)
|
||||
|
||||
|
||||
def _age_band_year(birth_year: int, domain: str, today: date) -> int | None:
|
||||
catalog = DOMAIN_CATALOG.get(domain)
|
||||
if not catalog:
|
||||
@@ -361,19 +368,53 @@ def _tracks_present(rule_ids: Sequence[str]) -> tuple[bool, bool]:
|
||||
)
|
||||
|
||||
|
||||
def _vim_start_years(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[int]:
|
||||
def _vim_start_dates(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[date]:
|
||||
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(float(moon_longitude))
|
||||
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(birth_date, nakshatra, progress)
|
||||
years: list[int] = []
|
||||
starts: list[date] = []
|
||||
for major in timeline:
|
||||
start = major.get("start")
|
||||
if isinstance(start, datetime) and lo <= start.year <= hi:
|
||||
years.append(start.year)
|
||||
starts.append(start.date())
|
||||
for minor in dasha_analyzer.build_antardasha(major):
|
||||
minor_start = minor.get("start")
|
||||
if isinstance(minor_start, datetime) and lo <= minor_start.year <= hi:
|
||||
years.append(minor_start.year)
|
||||
return years
|
||||
starts.append(minor_start.date())
|
||||
return starts
|
||||
|
||||
|
||||
def _vim_start_years(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[int]:
|
||||
return [item.year for item in _vim_start_dates(birth_date, moon_longitude, lo, hi)]
|
||||
|
||||
|
||||
def _narayana_start_dates(
|
||||
ascendant_index: int,
|
||||
planet_longitudes: dict[str, float],
|
||||
birth_date: str,
|
||||
lo: int,
|
||||
hi: int,
|
||||
) -> list[date] | None:
|
||||
periods = narayana_dasha.calc_narayana_mahadasha(ascendant_index, planet_longitudes)
|
||||
if not periods:
|
||||
return None
|
||||
birth = datetime.strptime(birth_date, "%Y-%m-%d")
|
||||
starts: list[date] = []
|
||||
for major in periods:
|
||||
start_age = major.get("start_age")
|
||||
if not isinstance(start_age, (int, float)):
|
||||
return None
|
||||
at = (birth + timedelta(days=float(start_age) * 365.2425)).date()
|
||||
if lo <= at.year <= hi:
|
||||
starts.append(at)
|
||||
antars = narayana_dasha.calc_narayana_antardasha(periods, int(major["sign_idx"]))
|
||||
for minor in antars:
|
||||
minor_age = minor.get("start_age")
|
||||
if not isinstance(minor_age, (int, float)):
|
||||
continue
|
||||
minor_at = (birth + timedelta(days=float(minor_age) * 365.2425)).date()
|
||||
if lo <= minor_at.year <= hi:
|
||||
starts.append(minor_at)
|
||||
return starts
|
||||
|
||||
|
||||
def _narayana_start_years(
|
||||
@@ -383,36 +424,41 @@ def _narayana_start_years(
|
||||
lo: int,
|
||||
hi: int,
|
||||
) -> list[int] | None:
|
||||
periods = narayana_dasha.calc_narayana_mahadasha(ascendant_index, planet_longitudes)
|
||||
if not periods:
|
||||
return None
|
||||
birth = datetime.strptime(birth_date, "%Y-%m-%d")
|
||||
years: list[int] = []
|
||||
for major in periods:
|
||||
start_age = major.get("start_age")
|
||||
if not isinstance(start_age, (int, float)):
|
||||
return None
|
||||
year = (birth + timedelta(days=float(start_age) * 365.2425)).year
|
||||
if lo <= year <= hi:
|
||||
years.append(year)
|
||||
antars = narayana_dasha.calc_narayana_antardasha(periods, int(major["sign_idx"]))
|
||||
for minor in antars:
|
||||
minor_age = minor.get("start_age")
|
||||
if not isinstance(minor_age, (int, float)):
|
||||
starts = _narayana_start_dates(ascendant_index, planet_longitudes, birth_date, lo, hi)
|
||||
return None if starts is None else [item.year for item in starts]
|
||||
|
||||
|
||||
def _as_start_date(value: date | datetime | int) -> date | None:
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, int) and 1900 <= value <= 2100:
|
||||
return date(value, 7, 1)
|
||||
return None
|
||||
|
||||
|
||||
def _boundary_windows(left: Sequence[date | datetime | int], right: Sequence[date | datetime | int]) -> list[date]:
|
||||
windows: list[date] = []
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for raw_left, raw_right in zip(left, right):
|
||||
one = _as_start_date(raw_left)
|
||||
two = _as_start_date(raw_right)
|
||||
if one is None or two is None:
|
||||
continue
|
||||
if one.year == two.year and abs((one - two).days) < MIN_BOUNDARY_DAYS:
|
||||
continue
|
||||
for item in (one, two):
|
||||
key = (item.year, item.month)
|
||||
if key in seen:
|
||||
continue
|
||||
minor_year = (birth + timedelta(days=float(minor_age) * 365.2425)).year
|
||||
if lo <= minor_year <= hi:
|
||||
years.append(minor_year)
|
||||
return years
|
||||
seen.add(key)
|
||||
windows.append(item)
|
||||
return windows
|
||||
|
||||
|
||||
def _boundary_years(left: list[int], right: list[int]) -> set[int]:
|
||||
years: set[int] = set()
|
||||
for one, two in zip(left, right):
|
||||
if abs(one - two) >= 1:
|
||||
years.add(one)
|
||||
years.add(two)
|
||||
return years
|
||||
return {item.year for item in _boundary_windows(left, right)}
|
||||
|
||||
|
||||
def _score_year(
|
||||
@@ -421,6 +467,7 @@ def _score_year(
|
||||
birth_date: str,
|
||||
domain: str,
|
||||
year: int,
|
||||
month: int | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
catalog = DOMAIN_CATALOG[domain]
|
||||
prefixes, _ = DOMAIN_CONFIG[domain]
|
||||
@@ -432,13 +479,14 @@ def _score_year(
|
||||
moon = (context.get("planet_longitudes") or {}).get("Moon")
|
||||
if candidate_at is None or not isinstance(moon, (int, float)):
|
||||
return None
|
||||
event_at = datetime(year, 7, 1)
|
||||
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
|
||||
event_at = datetime(year, month_value, 15) if month_value else datetime(year, 7, 1)
|
||||
event = {
|
||||
"id": f"probe-{domain}-{year}",
|
||||
"id": f"probe-{domain}-{year}" + (f"-{month_value:02d}" if month_value else ""),
|
||||
"domain": domain,
|
||||
"event_kind": catalog["kind"],
|
||||
"date": f"{year}-07-01",
|
||||
"precision": "year",
|
||||
"date": f"{year}-{month_value:02d}-15" if month_value else f"{year}-07-01",
|
||||
"precision": "month" if month_value else "year",
|
||||
"summary": catalog["event_family"],
|
||||
}
|
||||
try:
|
||||
@@ -532,6 +580,7 @@ def _public_probe(
|
||||
tracks_agree: bool,
|
||||
user_meaning: str,
|
||||
event_family: str,
|
||||
month: int | None = None,
|
||||
**extra: Any,
|
||||
) -> dict[str, Any]:
|
||||
if source == "known_event_quality":
|
||||
@@ -543,9 +592,10 @@ def _public_probe(
|
||||
else:
|
||||
phase = PROBE_PHASE_CANDIDATE_DISCRIMINATOR
|
||||
role = "distinguish"
|
||||
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
|
||||
payload = {
|
||||
"year": year,
|
||||
"year_label": _year_label(year),
|
||||
"year_label": _period_label(year, month_value),
|
||||
"domain": domain,
|
||||
"event_family": event_family,
|
||||
"source": source,
|
||||
@@ -555,13 +605,15 @@ def _public_probe(
|
||||
"user_meaning": user_meaning,
|
||||
"role": role,
|
||||
"phase": phase,
|
||||
"semantic_key": f"{domain}.{year}",
|
||||
"semantic_key": f"{domain}.{year}.{month_value:02d}" if month_value else f"{domain}.{year}",
|
||||
"information_gain": 0.0,
|
||||
"candidate_split_hash": f"{domain}:{year}",
|
||||
"candidate_split_hash": f"{domain}:{year}" + (f"-{month_value:02d}" if month_value else ""),
|
||||
"expected_outcomes": [],
|
||||
"candidate_ids": [],
|
||||
"choice_kind": "event_quality" if source == "known_event_quality" else "existence",
|
||||
}
|
||||
if month_value:
|
||||
payload["month"] = month_value
|
||||
payload.update(extra)
|
||||
if payload["role"] == "distinguish":
|
||||
payload["candidate_ids"] = candidate_ids_from_outcomes(payload.get("expected_outcomes") or [])
|
||||
@@ -661,15 +713,23 @@ def _evaluate_contexts(
|
||||
domain: str,
|
||||
year: int,
|
||||
source: str,
|
||||
month: int | None = None,
|
||||
clusters: Sequence[dict[str, Any]] | None = None,
|
||||
set_version: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
scored_rows: list[tuple[str, list[str]]] = []
|
||||
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
|
||||
for context in contexts:
|
||||
time = _context_time(context)
|
||||
if not time:
|
||||
continue
|
||||
scored = _score_year(context, birth_date=birth_date, domain=domain, year=year)
|
||||
scored = _score_year(
|
||||
context,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
year=year,
|
||||
month=month_value,
|
||||
)
|
||||
if scored is None:
|
||||
continue
|
||||
scored_rows.append((time, list(scored.get("rule_ids") or [])))
|
||||
@@ -729,22 +789,25 @@ def _evaluate_contexts(
|
||||
candidate_set_version_value=version,
|
||||
domain=domain,
|
||||
year=year,
|
||||
month=month_value,
|
||||
groups=[yes_times, no_times],
|
||||
)
|
||||
period = _period_label(year, month_value)
|
||||
probe = _public_probe(
|
||||
year=year,
|
||||
month=month_value,
|
||||
domain=domain,
|
||||
source=source,
|
||||
tracks=("vimshottari", "narayana"),
|
||||
tracks_agree=vim_hit and narayana_hit,
|
||||
user_meaning=_agent_brief(
|
||||
year_label=_year_label(year),
|
||||
year_label=period,
|
||||
domain=domain,
|
||||
family=str(DOMAIN_CATALOG[domain]["event_family"]),
|
||||
),
|
||||
event_family=str(DOMAIN_CATALOG[domain]["event_family"]),
|
||||
information_gain=gain,
|
||||
semantic_key=f"{domain}.{year}.{source}",
|
||||
semantic_key=f"{domain}.{year}.{month_value:02d}.{source}" if month_value else f"{domain}.{year}.{source}",
|
||||
candidate_split_hash=split,
|
||||
candidate_set_version=version,
|
||||
expected_outcomes=outcomes,
|
||||
@@ -904,15 +967,24 @@ def discriminating_event_probes(
|
||||
left, right = reps[0], reps[-1]
|
||||
left_moon = float(left["planet_longitudes"]["Moon"])
|
||||
right_moon = float(right["planet_longitudes"]["Moon"])
|
||||
vim_years = _boundary_years(
|
||||
_vim_start_years(birth_date, left_moon, lo, hi),
|
||||
_vim_start_years(birth_date, right_moon, lo, hi),
|
||||
vim_windows = _boundary_windows(
|
||||
_vim_start_dates(birth_date, left_moon, lo, hi),
|
||||
_vim_start_dates(birth_date, right_moon, lo, hi),
|
||||
)
|
||||
left_narayana = _narayana_start_years(int(left["ascendant_index"]), left["planet_longitudes"], birth_date, lo, hi)
|
||||
right_narayana = _narayana_start_years(int(right["ascendant_index"]), right["planet_longitudes"], birth_date, lo, hi)
|
||||
narayana_years: set[int] = set()
|
||||
left_narayana = _narayana_start_dates(int(left["ascendant_index"]), left["planet_longitudes"], birth_date, lo, hi)
|
||||
right_narayana = _narayana_start_dates(int(right["ascendant_index"]), right["planet_longitudes"], birth_date, lo, hi)
|
||||
narayana_windows: list[date] = []
|
||||
if left_narayana is not None and right_narayana is not None:
|
||||
narayana_years = _boundary_years(left_narayana, right_narayana)
|
||||
narayana_windows = _boundary_windows(left_narayana, right_narayana)
|
||||
boundary_dates: list[date] = []
|
||||
seen_windows: set[tuple[int, int]] = set()
|
||||
for item in [*vim_windows, *narayana_windows]:
|
||||
key = (item.year, item.month)
|
||||
if key in seen_windows:
|
||||
continue
|
||||
seen_windows.add(key)
|
||||
boundary_dates.append(item)
|
||||
boundary_dates.sort()
|
||||
probes: list[dict[str, Any]] = []
|
||||
for domain in domains:
|
||||
if domain not in DOMAIN_CATALOG:
|
||||
@@ -920,18 +992,19 @@ def discriminating_event_probes(
|
||||
known_years = _event_years(events, domain)
|
||||
blocked_years = _existence_blocked_years(domain, known_years)
|
||||
domain_lo = max(lo, birth_year + int(DOMAIN_CATALOG[domain]["age_lo"])) if domain == "relationship" else lo
|
||||
boundary = sorted(year for year in vim_years | narayana_years if domain_lo <= year <= hi)
|
||||
boundary = [item for item in boundary_dates if domain_lo <= item.year <= hi]
|
||||
best = None
|
||||
for year in boundary:
|
||||
if year in blocked_years:
|
||||
for at in boundary:
|
||||
if at.year in blocked_years:
|
||||
continue
|
||||
if f"{domain}:{year}" in holdout_keys:
|
||||
if f"{domain}:{at.year}" in holdout_keys:
|
||||
continue
|
||||
found = _evaluate_contexts(
|
||||
reps,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
year=year,
|
||||
year=at.year,
|
||||
month=at.month,
|
||||
source="dasha_boundary",
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
@@ -956,13 +1029,13 @@ def discriminating_event_probes(
|
||||
probes.append(best)
|
||||
probes.sort(key=lambda row: (-float(row.get("information_gain") or 0), str(row.get("semantic_key") or "")))
|
||||
public: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, int, str]] = set()
|
||||
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:
|
||||
continue
|
||||
if distinguish_contract_errors(row):
|
||||
continue
|
||||
key = (str(row["domain"]), int(row["year"]), str(row["source"]))
|
||||
key = (str(row["domain"]), int(row["year"]), int(row.get("month") or 0), str(row["source"]))
|
||||
encoded = str(row)
|
||||
if key in seen or "points" in encoded:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user