"""Baseline copies for TASK-rect-scoring-perf-20261013. Source commit: 77057a819 git show 77057a819:scripts/rectification/event_probes.py sha256: 9bbadc246e6b8e94bb047522c3e5645613f508b3473ef40b38a207c77fdad58c git show 77057a819:scripts/rectification/dasha_transition_proximity.py sha256: 0afba494aa12899621173c3cc941c6c1d0be9cec0bdf4c4a1c9bcd004ab22de8 The helpers these functions call are unchanged by this task and are imported from the current modules. Only guided_collect_windows and merge_transition_proximity differ from the copies below. """ from __future__ import annotations from datetime import date, datetime from typing import Any, Sequence from scripts.rectification.dasha_transition_proximity import ( _context_time as _proximity_context_time, representative_event_date, score_transition_proximity, ) from scripts.rectification.event_probes import ( GUIDED_ANY_DOMAIN, GUIDED_COLLECT_LIMIT, GUIDED_DOMAIN_ORDER, _GUIDED_TRACK_DOMAIN, _birth_year, _context_time as _probe_context_time, _guided_track_starts, _guided_year_windows, _narayana_start_dates, _remaining_contexts, _representative_pairs, _scoreable, _static_contexts, _vim_start_dates, ) # Both copies call `_context_time`. They are rebound onto separate globals # below so each keeps the helper it had in its source module. _context_time = _probe_context_time def guided_collect_windows( request: dict[str, Any], built: dict[str, Any], *, candidate_times: Sequence[str], today: date | None = None, ) -> list[dict[str, Any]]: """Boundary windows for guided collect. Does not change auto probe generation.""" birth_date = str(request.get("birth_date") or "").strip() birth_year = _birth_year(birth_date) if birth_year is None: return [] try: datetime.strptime(birth_date, "%Y-%m-%d") except ValueError: return [] remaining = _remaining_contexts(built, candidate_times) if len(remaining) < 2: remaining = _static_contexts(built) work = [item for item in remaining if _scoreable(item)] if len(work) < 2: return [] declined = { str(item).strip() for item in (request.get("declined_domains") or []) if str(item).strip() in GUIDED_DOMAIN_ORDER } now = today or date.today() lo, hi = birth_year + 5, min(now.year, birth_year + 80) vim_cache: dict[tuple[Any, ...], list[date]] = {} narayana_cache: dict[tuple[Any, ...], list[date] | None] = {} starts_by_time: dict[str, dict[str, list[date]]] = {} for context in work: time = _context_time(context) if not time: continue starts_by_time[time] = _guided_track_starts( context, birth_date=birth_date, lo=lo, hi=hi, include_pratyantar=True, varga_narayana=True, vim_cache=vim_cache, narayana_cache=narayana_cache, ) rows: list[dict[str, Any]] = [] seen: set[tuple[int, int, int, str]] = set() for left, right in _representative_pairs(work): left_time = str(_context_time(left) or "") right_time = str(_context_time(right) or "") left_tracks = starts_by_time.get(left_time) or {} right_tracks = starts_by_time.get(right_time) or {} for track in ("vim", "nara", "nara:d9", "nara:d10"): left_dates = left_tracks.get(track) or [] right_dates = right_tracks.get(track) or [] if not left_dates or not right_dates: continue for index, (one, two) in enumerate(zip(left_dates, right_dates)): if one == two: continue gathered: list[date] = [] for context in work: time = _context_time(context) dates = (starts_by_time.get(time or "") or {}).get(track) or [] if index < len(dates): gathered.append(dates[index]) if len(gathered) < 2 or min(gathered) == max(gathered): continue ordered = sorted(gathered) cut = ordered[0] + (ordered[-1] - ordered[0]) / 2 left_n = sum(1 for item in gathered if item <= cut) right_n = len(gathered) - left_n if left_n < 1 or right_n < 1: continue # BUG-749: only the D9 / D10 Narayana tracks carry a domain. # Vimshottari and natal Narayana boundaries say nothing about # which area of life moved, so the window is asked open # ("any") instead of being round-robined onto one domain. track_domain = _GUIDED_TRACK_DOMAIN.get(track) domain = ( track_domain if track_domain and track_domain not in declined else GUIDED_ANY_DOMAIN ) for year, month_lo, month_hi in _guided_year_windows(gathered): if year < lo or year > hi: continue key = (year, month_lo, month_hi, domain) if key in seen: continue seen.add(key) rows.append({ "year": year, "month_lo": month_lo, "month_hi": month_hi, "domain": domain, "split": {"left": left_n, "right": right_n}, }) rows.sort(key=lambda row: ( abs(row["split"]["left"] - row["split"]["right"]), abs(now.year - int(row["year"])), -int(row["year"]), int(row["month_lo"]), str(row["domain"]), )) return rows[:GUIDED_COLLECT_LIMIT] 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 # Native contexts retain the local date even when the window crosses midnight. # Legacy time-only contexts still use the request's date. candidate_at = context.get("candidate_at") candidate_date = candidate_at.date().isoformat() if isinstance(candidate_at, datetime) else birth_date vim_key = (candidate_date, round(float(moon), 6), lo, hi) if vim_key not in vim_cache: vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), lo, hi) pd_cache[vim_key] = _vim_start_dates( candidate_date, float(moon), lo, hi, include_pratyantar=True, ) planets = context.get("planet_longitudes") or {} asc = context.get("ascendant_index") narayana_key = ( candidate_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, candidate_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"]) import types as _types def _rebind(function, context_time): globs = dict(function.__globals__) globs["_context_time"] = context_time return _types.FunctionType(function.__code__, globs, function.__name__) guided_collect_windows = _rebind(guided_collect_windows, _probe_context_time) merge_transition_proximity = _rebind(merge_transition_proximity, _proximity_context_time)