feat(rectification): expand dated dasha_boundary probe supply
Distinguish-stage reverse-verify questions were exhausting after 1–3 dated probes. Union boundary windows across representative pairs, keep multiple years per domain, raise the public cap, and allow activation fallback without relaxing MIN_BOUNDARY_DAYS or scoring. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-08-30 — 反推前事探针供给扩容(Skill 10.0.13 不变)
|
||||
|
||||
区分阶段在收窄无年份分盘对比题之后,带年份的 `dasha_boundary` 反推题每个 case 只剩 1–3 条。本轮只加供给,不放宽判定门槛、不改计分口径。
|
||||
|
||||
- 边界窗改为多对代表分钟求并集(时间排序后的相邻对 + 首尾对),仍执行 `MIN_BOUNDARY_DAYS = 45`。
|
||||
- 同领域按 `information_gain` 保留前 N=3 条年份(去重键仍是 `(domain, year, month, source)`)。
|
||||
- 公开上限 `MAX_PROBES` 提到 8:约 4–6 个领域竞争时,可让约 3 个领域各留两年并再加少量 activation,前端出题层仍按信息量全局排序。
|
||||
- 采集上限仍是 `MAX_COLLECTION_PROBES = 3`,避免采集题被连带放宽。
|
||||
- 即使该领域已有边界探针,只要还没到每域 N,也可以再补一条带年份的 `dasha_activation`(仍走 blocked_years / holdout / 契约校验)。
|
||||
- 每域评估预算 K=8:先评均匀抽样的 K 个年月,按信息量保留;若 K 次全部未命中再扫描剩余窗直到第一击,避免漏掉唯一能区分的月份。K 与 N、MAX_PROBES 的关系:N ≤ K,公开列表再按全局信息量截到 MAX_PROBES。
|
||||
- 未改 `_evaluate_contexts` / `LEVEL_P` / `match_level` / information_gain 算法,未放宽 confirmation gate,未新增「已确认唯一分钟」路径。
|
||||
|
||||
同一 fixture(两簇、Moon 100 vs 101、多层分盘差)实测:公开探针 3 → 7 条;三次打分中位 0.2878s → 0.0108s(K 截断减少 `_evaluate_contexts` 次数)。
|
||||
|
||||
## v6.9.14(2026-06-21)—— 发布卫生、CI门禁与包产物校验
|
||||
|
||||
> **验证**:65 techniques registry validate PASS(55 covered / 10 complete / 0 partial / 0 missing);475 pytest PASS;legacy runner 102/102 PASS;frontend Vite build PASS;wheel/sdist build PASS;twine check PASS。
|
||||
|
||||
@@ -42,7 +42,22 @@ from scripts.rectification.probe_question_contract import (
|
||||
)
|
||||
from scripts.rectification.refinement_packet import match_level
|
||||
|
||||
MAX_PROBES = 3
|
||||
# Public discriminator list. Four to six domains typically compete; 8 lets about
|
||||
# three domains keep two years plus a couple of activation fallbacks without
|
||||
# flooding the ask layer, which still ranks globally by information_gain.
|
||||
MAX_PROBES = 8
|
||||
# Collection still asks at most three missing-domain age-band questions.
|
||||
MAX_COLLECTION_PROBES = 3
|
||||
# N: keep the top scored probes per domain (boundary years, plus at most one
|
||||
# activation if the domain is still under this cap).
|
||||
MAX_PROBES_PER_DOMAIN = 3
|
||||
# K: per-domain evaluation budget of unique (year, month) windows.
|
||||
# N <= K. Endpoints and evenly spaced months are tried first (at most K).
|
||||
# If those K miss every discriminator, scan the remainder until the first
|
||||
# hit so a lone month is not dropped; do not keep scanning to fill N.
|
||||
# Shrink K if runtime more than doubles; do not drop multi-pair union.
|
||||
# MAX_PROBES is the published cap after a global information_gain sort.
|
||||
MAX_BOUNDARY_CANDIDATES_PER_DOMAIN = 8
|
||||
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}
|
||||
@@ -284,59 +299,6 @@ def _remaining_contexts(built: dict[str, Any], candidate_times: Sequence[str]) -
|
||||
return remaining
|
||||
|
||||
|
||||
def _pick_representatives(
|
||||
built: dict[str, Any],
|
||||
scan: dict[str, Any],
|
||||
candidate_times: Sequence[str],
|
||||
representative_time: str | None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]] | None:
|
||||
contexts = _static_contexts(built)
|
||||
by_time = {_context_time(item): item for item in contexts}
|
||||
times = [str(_context_time(item)) for item in contexts]
|
||||
remaining: list[str] = []
|
||||
for raw in candidate_times:
|
||||
time = str(raw or "")[:5]
|
||||
if len(time) >= 5 and time in by_time and time not in remaining:
|
||||
remaining.append(time)
|
||||
if len(remaining) >= 2:
|
||||
remaining_set = set(remaining)
|
||||
for transition in scan.get("transitions") or []:
|
||||
if not isinstance(transition, dict):
|
||||
continue
|
||||
layer = transition.get("layer")
|
||||
at = str(transition.get("at") or "")[:5]
|
||||
if layer not in SCORING_LAYERS or at not in by_time or at not in remaining_set:
|
||||
continue
|
||||
left = None
|
||||
for time in remaining:
|
||||
if _clock(time) < _clock(at):
|
||||
left = time
|
||||
if left and left != at:
|
||||
return by_time[left], by_time[at]
|
||||
return by_time[remaining[0]], by_time[remaining[-1]]
|
||||
for transition in scan.get("transitions") or []:
|
||||
if not isinstance(transition, dict):
|
||||
continue
|
||||
layer = transition.get("layer")
|
||||
at = str(transition.get("at") or "")[:5]
|
||||
if layer not in SCORING_LAYERS or at not in by_time:
|
||||
continue
|
||||
index = times.index(at)
|
||||
left = times[index - 1] if index > 0 else at
|
||||
if left != at:
|
||||
return by_time[left], by_time[at]
|
||||
picked: list[str] = []
|
||||
for raw in [*candidate_times, representative_time]:
|
||||
time = str(raw or "")[:5]
|
||||
if len(time) >= 5 and time in by_time and time not in picked:
|
||||
picked.append(time)
|
||||
if len(picked) >= 2:
|
||||
return by_time[picked[0]], by_time[picked[-1]]
|
||||
if len(times) >= 2:
|
||||
return by_time[times[0]], by_time[times[-1]]
|
||||
return None
|
||||
|
||||
|
||||
def _scoreable(context: dict[str, Any]) -> bool:
|
||||
chart = context.get("chart")
|
||||
planets = context.get("planet_longitudes")
|
||||
@@ -480,6 +442,196 @@ def _boundary_years(left: list[int], right: list[int]) -> set[int]:
|
||||
return {item.year for item in _boundary_windows(left, right)}
|
||||
|
||||
|
||||
def _representative_pairs(
|
||||
reps: Sequence[dict[str, Any]],
|
||||
) -> list[tuple[dict[str, Any], dict[str, Any]]]:
|
||||
ordered = sorted(
|
||||
[item for item in reps if _scoreable(item) and _context_time(item)],
|
||||
key=lambda item: _clock(str(_context_time(item))),
|
||||
)
|
||||
if len(ordered) < 2:
|
||||
return []
|
||||
pairs: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
|
||||
def add(left: dict[str, Any], right: dict[str, Any]) -> None:
|
||||
left_time = str(_context_time(left) or "")
|
||||
right_time = str(_context_time(right) or "")
|
||||
if not left_time or not right_time or left_time == right_time:
|
||||
return
|
||||
key = (left_time, right_time) if left_time < right_time else (right_time, left_time)
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
pairs.append((left, right))
|
||||
|
||||
for index in range(len(ordered) - 1):
|
||||
add(ordered[index], ordered[index + 1])
|
||||
if len(ordered) > 2:
|
||||
add(ordered[0], ordered[-1])
|
||||
return pairs
|
||||
|
||||
|
||||
def _vim_cache_key(birth_date: str, moon: float, lo: int, hi: int) -> tuple[Any, ...]:
|
||||
return (birth_date, round(float(moon), 6), lo, hi)
|
||||
|
||||
|
||||
def _narayana_cache_key(
|
||||
ascendant_index: int,
|
||||
planet_longitudes: dict[str, Any],
|
||||
birth_date: str,
|
||||
lo: int,
|
||||
hi: int,
|
||||
) -> tuple[Any, ...]:
|
||||
planet_key = tuple(
|
||||
sorted(
|
||||
(str(name), round(float(lon), 6))
|
||||
for name, lon in planet_longitudes.items()
|
||||
if isinstance(lon, (int, float))
|
||||
)
|
||||
)
|
||||
return (int(ascendant_index), planet_key, birth_date, lo, hi)
|
||||
|
||||
|
||||
def _union_boundary_dates(
|
||||
reps: Sequence[dict[str, Any]],
|
||||
*,
|
||||
birth_date: str,
|
||||
lo: int,
|
||||
hi: int,
|
||||
) -> list[date]:
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
dates_by_key: dict[tuple[int, int], date] = {}
|
||||
for left, right in _representative_pairs(reps):
|
||||
left_moon = float(left["planet_longitudes"]["Moon"])
|
||||
right_moon = float(right["planet_longitudes"]["Moon"])
|
||||
left_vim_key = _vim_cache_key(birth_date, left_moon, lo, hi)
|
||||
right_vim_key = _vim_cache_key(birth_date, right_moon, lo, hi)
|
||||
if left_vim_key not in vim_cache:
|
||||
vim_cache[left_vim_key] = _vim_start_dates(birth_date, left_moon, lo, hi)
|
||||
if right_vim_key not in vim_cache:
|
||||
vim_cache[right_vim_key] = _vim_start_dates(birth_date, right_moon, lo, hi)
|
||||
windows = list(_boundary_windows(vim_cache[left_vim_key], vim_cache[right_vim_key]))
|
||||
left_nara_key = _narayana_cache_key(
|
||||
int(left["ascendant_index"]),
|
||||
left["planet_longitudes"],
|
||||
birth_date,
|
||||
lo,
|
||||
hi,
|
||||
)
|
||||
right_nara_key = _narayana_cache_key(
|
||||
int(right["ascendant_index"]),
|
||||
right["planet_longitudes"],
|
||||
birth_date,
|
||||
lo,
|
||||
hi,
|
||||
)
|
||||
if left_nara_key not in narayana_cache:
|
||||
narayana_cache[left_nara_key] = _narayana_start_dates(
|
||||
int(left["ascendant_index"]),
|
||||
left["planet_longitudes"],
|
||||
birth_date,
|
||||
lo,
|
||||
hi,
|
||||
)
|
||||
if right_nara_key not in narayana_cache:
|
||||
narayana_cache[right_nara_key] = _narayana_start_dates(
|
||||
int(right["ascendant_index"]),
|
||||
right["planet_longitudes"],
|
||||
birth_date,
|
||||
lo,
|
||||
hi,
|
||||
)
|
||||
left_narayana = narayana_cache[left_nara_key]
|
||||
right_narayana = narayana_cache[right_nara_key]
|
||||
if left_narayana is not None and right_narayana is not None:
|
||||
windows.extend(_boundary_windows(left_narayana, right_narayana))
|
||||
for item in windows:
|
||||
dates_by_key.setdefault((item.year, item.month), item)
|
||||
return sorted(dates_by_key.values(), key=lambda item: (item.year, item.month))
|
||||
|
||||
|
||||
def _bounded_candidate_dates(dates: Sequence[date], limit: int) -> list[date]:
|
||||
items = list(dates)
|
||||
if limit <= 0 or len(items) <= limit:
|
||||
return items
|
||||
if limit == 1:
|
||||
return items[:1]
|
||||
picked: list[date] = []
|
||||
seen: set[tuple[int, int]] = set()
|
||||
last_index = len(items) - 1
|
||||
for step in range(limit):
|
||||
index = (step * last_index + (limit - 1) // 2) // (limit - 1)
|
||||
item = items[index]
|
||||
key = (item.year, item.month)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
picked.append(item)
|
||||
if len(picked) < limit:
|
||||
for item in items:
|
||||
key = (item.year, item.month)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
picked.append(item)
|
||||
if len(picked) >= limit:
|
||||
break
|
||||
return picked
|
||||
|
||||
|
||||
def _evaluation_order(dates: Sequence[date], limit: int) -> list[date]:
|
||||
sampled = _bounded_candidate_dates(dates, limit)
|
||||
sampled_keys = {(item.year, item.month) for item in sampled}
|
||||
remainder = [item for item in dates if (item.year, item.month) not in sampled_keys]
|
||||
return sampled + remainder
|
||||
|
||||
|
||||
def _probe_sort_key(row: dict[str, Any]) -> tuple[float, str]:
|
||||
return (-float(row.get("information_gain") or 0), str(row.get("semantic_key") or ""))
|
||||
|
||||
|
||||
def _best_probe_per_year(rows: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
by_year: dict[int, dict[str, Any]] = {}
|
||||
for row in sorted(rows, key=_probe_sort_key):
|
||||
year = int(row["year"])
|
||||
if year not in by_year:
|
||||
by_year[year] = row
|
||||
return list(by_year.values())
|
||||
|
||||
|
||||
def _try_activation_probe(
|
||||
*,
|
||||
reps: Sequence[dict[str, Any]],
|
||||
birth_date: str,
|
||||
birth_year: int,
|
||||
domain: str,
|
||||
now: date,
|
||||
blocked_years: set[int],
|
||||
holdout_keys: set[str],
|
||||
clusters: Sequence[dict[str, Any]],
|
||||
set_version: str,
|
||||
) -> dict[str, Any] | None:
|
||||
midpoint = _age_band_year(birth_year, domain, now)
|
||||
if midpoint is None or midpoint in blocked_years or f"{domain}:{midpoint}" in holdout_keys:
|
||||
return None
|
||||
found = _evaluate_contexts(
|
||||
reps,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
year=midpoint,
|
||||
source="dasha_activation",
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
)
|
||||
if found is None or distinguish_contract_errors(found):
|
||||
return None
|
||||
if not isinstance(found.get("year"), int) or int(found["year"]) <= 0:
|
||||
return None
|
||||
return found
|
||||
|
||||
|
||||
def _score_year(
|
||||
context: dict[str, Any],
|
||||
*,
|
||||
@@ -906,7 +1058,7 @@ def evidence_collection_probes(
|
||||
),
|
||||
event_family=str(DOMAIN_CATALOG[domain]["event_family"]),
|
||||
))
|
||||
if len(rows) >= MAX_PROBES:
|
||||
if len(rows) >= MAX_COLLECTION_PROBES:
|
||||
break
|
||||
return rows
|
||||
|
||||
@@ -984,27 +1136,7 @@ def discriminating_event_probes(
|
||||
if not domains:
|
||||
return []
|
||||
lo, hi = birth_year + 5, min(now.year, birth_year + 80)
|
||||
left, right = reps[0], reps[-1]
|
||||
left_moon = float(left["planet_longitudes"]["Moon"])
|
||||
right_moon = float(right["planet_longitudes"]["Moon"])
|
||||
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_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_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()
|
||||
boundary_dates = _union_boundary_dates(reps, birth_date=birth_date, lo=lo, hi=hi)
|
||||
probes: list[dict[str, Any]] = []
|
||||
for domain in domains:
|
||||
if domain not in DOMAIN_CATALOG:
|
||||
@@ -1012,14 +1144,20 @@ def discriminating_event_probes(
|
||||
known_years = _event_years(events, domain)
|
||||
blocked_years = _existence_blocked_years(domain, known_years)
|
||||
domain_lo = _domain_year_floor(birth_year, domain, lo)
|
||||
boundary = [item for item in boundary_dates if domain_lo <= item.year <= hi]
|
||||
best = None
|
||||
for at in boundary:
|
||||
if at.year in blocked_years:
|
||||
continue
|
||||
if f"{domain}:{at.year}" in holdout_keys:
|
||||
continue
|
||||
found = _evaluate_contexts(
|
||||
eligible = [
|
||||
item
|
||||
for item in boundary_dates
|
||||
if domain_lo <= item.year <= hi
|
||||
and item.year not in blocked_years
|
||||
and f"{domain}:{item.year}" not in holdout_keys
|
||||
]
|
||||
found: list[dict[str, Any]] = []
|
||||
evaluated = 0
|
||||
sample_size = min(MAX_BOUNDARY_CANDIDATES_PER_DOMAIN, len(eligible))
|
||||
for at in _evaluation_order(eligible, MAX_BOUNDARY_CANDIDATES_PER_DOMAIN):
|
||||
if evaluated >= sample_size and found:
|
||||
break
|
||||
row = _evaluate_contexts(
|
||||
reps,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
@@ -1029,25 +1167,42 @@ def discriminating_event_probes(
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
)
|
||||
if found is None:
|
||||
evaluated += 1
|
||||
if row is None or distinguish_contract_errors(row):
|
||||
continue
|
||||
if best is None or float(found["information_gain"]) > float(best["information_gain"]):
|
||||
best = found
|
||||
if best is None:
|
||||
midpoint = _age_band_year(birth_year, domain, now)
|
||||
if midpoint is not None and midpoint not in blocked_years and f"{domain}:{midpoint}" not in holdout_keys:
|
||||
best = _evaluate_contexts(
|
||||
reps,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
year=midpoint,
|
||||
source="dasha_activation",
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
if not isinstance(row.get("year"), int) or int(row["year"]) <= 0:
|
||||
continue
|
||||
found.append(row)
|
||||
if evaluated > sample_size:
|
||||
break
|
||||
kept = _best_probe_per_year(found)[:MAX_PROBES_PER_DOMAIN]
|
||||
if len(kept) < MAX_PROBES_PER_DOMAIN:
|
||||
activation = _try_activation_probe(
|
||||
reps=reps,
|
||||
birth_date=birth_date,
|
||||
birth_year=birth_year,
|
||||
domain=domain,
|
||||
now=now,
|
||||
blocked_years=blocked_years,
|
||||
holdout_keys=set(holdout_keys),
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
)
|
||||
if activation is not None:
|
||||
activation_key = (
|
||||
str(activation["domain"]),
|
||||
int(activation["year"]),
|
||||
int(activation.get("month") or 0),
|
||||
str(activation["source"]),
|
||||
)
|
||||
if best and not distinguish_contract_errors(best):
|
||||
probes.append(best)
|
||||
probes.sort(key=lambda row: (-float(row.get("information_gain") or 0), str(row.get("semantic_key") or "")))
|
||||
existing = {
|
||||
(str(row["domain"]), int(row["year"]), int(row.get("month") or 0), str(row["source"]))
|
||||
for row in kept
|
||||
}
|
||||
if activation_key not in existing:
|
||||
kept.append(activation)
|
||||
probes.extend(kept)
|
||||
probes.sort(key=_probe_sort_key)
|
||||
public: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, int, int, str]] = set()
|
||||
for row in probes:
|
||||
@@ -1055,6 +1210,8 @@ def discriminating_event_probes(
|
||||
continue
|
||||
if distinguish_contract_errors(row):
|
||||
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)
|
||||
if key in seen or "points" in encoded:
|
||||
|
||||
@@ -5,6 +5,10 @@ from datetime import date, datetime
|
||||
|
||||
from scripts.rectification.candidate_contrast import distinguish_contract_errors
|
||||
from scripts.rectification.event_probes import (
|
||||
MAX_BOUNDARY_CANDIDATES_PER_DOMAIN,
|
||||
MAX_COLLECTION_PROBES,
|
||||
MAX_PROBES,
|
||||
MAX_PROBES_PER_DOMAIN,
|
||||
_agent_brief,
|
||||
discriminating_event_probes,
|
||||
event_clarification_probes,
|
||||
@@ -134,6 +138,25 @@ def _probes(request: dict, built: dict, times: list[str], representative: str, *
|
||||
)
|
||||
|
||||
|
||||
def _multi_layer_window_built() -> dict:
|
||||
"""Two signature clusters, moons far enough apart that Vimshottari windows exist.
|
||||
|
||||
Before supply expansion this fixture published exactly three probes
|
||||
(career.2024.02 / relationship.2024.02 / relocation.2015.03).
|
||||
"""
|
||||
early = dict(d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0, d9_asc=1, d10_asc=1, d12_asc=1, d24_asc=1)
|
||||
late = dict(d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0, d9_asc=2, d10_asc=2, d12_asc=2, d24_asc=2)
|
||||
return {
|
||||
"static_contexts": [
|
||||
_context("04:47", **early),
|
||||
_context("04:48", **late),
|
||||
_context("05:00", **early),
|
||||
_context("05:06", **late),
|
||||
_context("05:07", **late),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class EventProbesTest(unittest.TestCase):
|
||||
def test_agent_brief_locks_meaning_without_forcing_template_copy(self) -> None:
|
||||
brief = _agent_brief(
|
||||
@@ -247,6 +270,7 @@ class EventProbesTest(unittest.TestCase):
|
||||
self.assertEqual(probes, [])
|
||||
collection = evidence_collection_probes(request, today=date(2026, 8, 22))
|
||||
self.assertTrue(collection)
|
||||
self.assertEqual(len(collection), MAX_COLLECTION_PROBES)
|
||||
self.assertTrue(all(item["phase"] == "evidence_collection" for item in collection))
|
||||
self.assertTrue(all(item["role"] == "collect" for item in collection))
|
||||
self.assertTrue(all(item["source"] == "age_band" for item in collection))
|
||||
@@ -404,7 +428,7 @@ class EventProbesTest(unittest.TestCase):
|
||||
probes = _probes(request, built, ["05:13", "05:40"], "05:13", precision_current="d5_refine")
|
||||
self.assertFalse(any(item["source"] == "known_event_quality" for item in probes))
|
||||
self.assertTrue(any(item["source"] in {"dasha_activation", "dasha_boundary"} for item in probes))
|
||||
self.assertLessEqual(len(probes), 3)
|
||||
self.assertLessEqual(len(probes), MAX_PROBES)
|
||||
for probe in probes:
|
||||
self.assertEqual(distinguish_contract_errors(probe), [])
|
||||
|
||||
@@ -639,6 +663,239 @@ class EventProbesTest(unittest.TestCase):
|
||||
relationship = [item for item in probes if item["domain"] == "relationship"]
|
||||
self.assertFalse(any(item["year"] == 2023 for item in relationship))
|
||||
|
||||
def test_discriminating_probes_expand_beyond_previous_cap(self) -> None:
|
||||
probes = _probes(_request(), _multi_layer_window_built(), ["05:00", "05:06", "05:07"], "05:00")
|
||||
keys = [item["semantic_key"] for item in probes]
|
||||
self.assertGreater(len(probes), 3)
|
||||
self.assertLessEqual(len(probes), MAX_PROBES)
|
||||
self.assertTrue(keys)
|
||||
for probe in probes:
|
||||
self.assertIsInstance(probe.get("year"), int)
|
||||
self.assertGreaterEqual(int(probe["year"]), 1900)
|
||||
self.assertNotEqual(int(probe["year"]), 0)
|
||||
self.assertEqual(distinguish_contract_errors(probe), [])
|
||||
self.assertIn(probe["source"], {"dasha_boundary", "dasha_activation"})
|
||||
self.assertGreaterEqual(len(probe["style_options"]), 4)
|
||||
self.assertGreaterEqual(len(probe["candidate_ids"]), 2)
|
||||
self.assertGreaterEqual(len(probe["expected_outcomes"]), 2)
|
||||
|
||||
def test_per_domain_boundary_evaluation_respects_k_budget(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
real = probes_mod._evaluate_contexts
|
||||
calls_by_domain: dict[str, int] = {}
|
||||
|
||||
def wrapped(
|
||||
contexts,
|
||||
*,
|
||||
birth_date: str,
|
||||
domain: str,
|
||||
year: int,
|
||||
source: str,
|
||||
month: int | None = None,
|
||||
clusters=None,
|
||||
set_version=None,
|
||||
):
|
||||
if source == "dasha_boundary":
|
||||
calls_by_domain[domain] = calls_by_domain.get(domain, 0) + 1
|
||||
return real(
|
||||
contexts,
|
||||
birth_date=birth_date,
|
||||
domain=domain,
|
||||
year=year,
|
||||
source=source,
|
||||
month=month,
|
||||
clusters=clusters,
|
||||
set_version=set_version,
|
||||
)
|
||||
|
||||
with patch.object(probes_mod, "_evaluate_contexts", side_effect=wrapped):
|
||||
probes = _probes(_request(), _multi_layer_window_built(), ["05:00", "05:06", "05:07"], "05:00")
|
||||
self.assertTrue(probes)
|
||||
self.assertTrue(calls_by_domain)
|
||||
filled: dict[str, set[int]] = {}
|
||||
for probe in probes:
|
||||
if probe["source"] != "dasha_boundary":
|
||||
continue
|
||||
filled.setdefault(str(probe["domain"]), set()).add(int(probe["year"]))
|
||||
for domain, count in calls_by_domain.items():
|
||||
if len(filled.get(domain, set())) >= 2:
|
||||
self.assertLessEqual(
|
||||
count,
|
||||
MAX_BOUNDARY_CANDIDATES_PER_DOMAIN,
|
||||
{domain: count, "filled": filled.get(domain)},
|
||||
)
|
||||
|
||||
def test_adjacent_pair_boundary_is_used_when_first_last_has_none(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("04:50", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0, d9_asc=1),
|
||||
_context("05:10", d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0, d9_asc=1),
|
||||
_context("05:40", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0, d9_asc=2),
|
||||
]
|
||||
}
|
||||
|
||||
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
|
||||
return [date(2018, 3, 15)] if moon <= 100.0 else [date(2018, 9, 20)]
|
||||
|
||||
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
|
||||
moon = float(planets.get("Moon") or 0)
|
||||
return fake_vim(_birth_date, moon, _lo, _hi)
|
||||
|
||||
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
|
||||
del birth_date, domain
|
||||
if year == 2018 and month in {3, 9}:
|
||||
return {
|
||||
"rule_ids": ["vim_md_domain_house"]
|
||||
if probes_mod._context_time(context) == "04:50"
|
||||
else ["no_domain_activation"]
|
||||
}
|
||||
return {"rule_ids": ["no_domain_activation"]}
|
||||
|
||||
with (
|
||||
patch.object(probes_mod, "_vim_start_dates", side_effect=fake_vim),
|
||||
patch.object(probes_mod, "_narayana_start_dates", side_effect=fake_narayana),
|
||||
patch.object(probes_mod, "_score_year", side_effect=fake_score),
|
||||
):
|
||||
probes = _probes(_request(), built, ["04:50", "05:10", "05:40"], "04:50")
|
||||
boundary = [item for item in probes if item["source"] == "dasha_boundary"]
|
||||
self.assertTrue(boundary)
|
||||
self.assertTrue(all(item["year"] == 2018 for item in boundary))
|
||||
self.assertTrue(all(item.get("month") in {3, 9} for item in boundary))
|
||||
self.assertTrue(all(item.get("year") for item in probes))
|
||||
|
||||
def test_same_domain_keeps_multiple_boundary_years(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("05:13", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0),
|
||||
_context("05:40", d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0),
|
||||
]
|
||||
}
|
||||
|
||||
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
|
||||
if moon <= 100.0:
|
||||
return [date(2016, 3, 15), date(2022, 3, 15)]
|
||||
return [date(2016, 9, 20), date(2022, 9, 20)]
|
||||
|
||||
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
|
||||
moon = float(planets.get("Moon") or 0)
|
||||
return fake_vim(_birth_date, moon, _lo, _hi)
|
||||
|
||||
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
|
||||
del birth_date, domain, month
|
||||
early = probes_mod._context_time(context) == "05:13"
|
||||
if year in {2016, 2022}:
|
||||
return {"rule_ids": ["vim_md_domain_house"] if early else ["no_domain_activation"]}
|
||||
return {"rule_ids": ["no_domain_activation"]}
|
||||
|
||||
with (
|
||||
patch.object(probes_mod, "_vim_start_dates", side_effect=fake_vim),
|
||||
patch.object(probes_mod, "_narayana_start_dates", side_effect=fake_narayana),
|
||||
patch.object(probes_mod, "_score_year", side_effect=fake_score),
|
||||
):
|
||||
probes = _probes(_request(), built, ["05:13", "05:40"], "05:13")
|
||||
relocation = [item for item in probes if item["domain"] == "relocation" and item["source"] == "dasha_boundary"]
|
||||
years = {int(item["year"]) for item in relocation}
|
||||
keys = {
|
||||
(item["domain"], int(item["year"]), int(item.get("month") or 0), item["source"])
|
||||
for item in relocation
|
||||
}
|
||||
self.assertGreaterEqual(len(years), 2)
|
||||
self.assertEqual(years, {2016, 2022})
|
||||
self.assertEqual(len(keys), len(relocation))
|
||||
|
||||
def test_activation_supplements_boundary_without_exceeding_domain_cap(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("05:13", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0),
|
||||
_context("05:40", d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0),
|
||||
]
|
||||
}
|
||||
|
||||
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
|
||||
return [date(2020, 3, 15)] if moon <= 100.0 else [date(2020, 9, 20)]
|
||||
|
||||
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
|
||||
moon = float(planets.get("Moon") or 0)
|
||||
return fake_vim(_birth_date, moon, _lo, _hi)
|
||||
|
||||
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
|
||||
del birth_date, domain
|
||||
early = probes_mod._context_time(context) == "05:13"
|
||||
if (year == 2020 and month in {3, 9}) or (year == 2018 and month is None):
|
||||
return {"rule_ids": ["vim_md_domain_house"] if early else ["no_domain_activation"]}
|
||||
return {"rule_ids": ["no_domain_activation"]}
|
||||
|
||||
with (
|
||||
patch.object(probes_mod, "_vim_start_dates", side_effect=fake_vim),
|
||||
patch.object(probes_mod, "_narayana_start_dates", side_effect=fake_narayana),
|
||||
patch.object(probes_mod, "_score_year", side_effect=fake_score),
|
||||
):
|
||||
probes = _probes(_request(), built, ["05:13", "05:40"], "05:13")
|
||||
relocation = [item for item in probes if item["domain"] == "relocation"]
|
||||
sources = {item["source"] for item in relocation}
|
||||
self.assertIn("dasha_boundary", sources)
|
||||
self.assertIn("dasha_activation", sources)
|
||||
self.assertLessEqual(len(relocation), MAX_PROBES_PER_DOMAIN)
|
||||
self.assertLessEqual(len(probes), MAX_PROBES)
|
||||
self.assertTrue(all(isinstance(item.get("year"), int) and item["year"] for item in relocation))
|
||||
activation = next(item for item in relocation if item["source"] == "dasha_activation")
|
||||
self.assertEqual(activation["year"], 2018)
|
||||
self.assertNotIn("month", activation)
|
||||
|
||||
def test_domain_at_n_does_not_add_activation_past_cap(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("05:13", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0),
|
||||
_context("05:40", d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0),
|
||||
]
|
||||
}
|
||||
|
||||
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
|
||||
if moon <= 100.0:
|
||||
return [date(2016, 3, 15), date(2019, 3, 15), date(2022, 3, 15)]
|
||||
return [date(2016, 9, 20), date(2019, 9, 20), date(2022, 9, 20)]
|
||||
|
||||
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
|
||||
moon = float(planets.get("Moon") or 0)
|
||||
return fake_vim(_birth_date, moon, _lo, _hi)
|
||||
|
||||
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
|
||||
del birth_date, domain, month
|
||||
early = probes_mod._context_time(context) == "05:13"
|
||||
if year in {2016, 2019, 2022, 2018}:
|
||||
return {"rule_ids": ["vim_md_domain_house"] if early else ["no_domain_activation"]}
|
||||
return {"rule_ids": ["no_domain_activation"]}
|
||||
|
||||
with (
|
||||
patch.object(probes_mod, "_vim_start_dates", side_effect=fake_vim),
|
||||
patch.object(probes_mod, "_narayana_start_dates", side_effect=fake_narayana),
|
||||
patch.object(probes_mod, "_score_year", side_effect=fake_score),
|
||||
):
|
||||
probes = _probes(_request(), built, ["05:13", "05:40"], "05:13")
|
||||
relocation = [item for item in probes if item["domain"] == "relocation"]
|
||||
self.assertLessEqual(len(relocation), MAX_PROBES_PER_DOMAIN)
|
||||
self.assertFalse(any(item["source"] == "dasha_activation" for item in relocation))
|
||||
self.assertEqual({int(item["year"]) for item in relocation}, {2016, 2019, 2022})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user