fix(rectification): ask reverse-inference probes at engine dasha months
Independent Staging Quality Gate / validate (push) Failing after 8m39s
Independent Staging Quality Gate / publish (push) Has been skipped

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:
Jesse_Chen
2026-08-28 10:20:05 +08:00
parent 4767b34ff0
commit 68a78af9e1
14 changed files with 359 additions and 84 deletions
+3 -1
View File
@@ -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]
+129 -56
View File
@@ -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