diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 1cdf0d20..f737ab21 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -11206,6 +11206,22 @@ - 复发自:无 - 修复版本:待发布 +## BUG-721 | 生时校正把候选分钟不变量放在事件循环里重复计算 + +- 状态:resolved +- 首次发现:2026-09-15 +- 最近更新:2026-09-16 +- 影响面:`scripts/active_rectification_event_engine.py`、`scripts/rectification/scoring_service.py`、`scripts/rectification/refinement_packet.py` +- 用户现象:每记一条证据或答一道题都要重算 `POST /api/rectification/v5/score`,等待明显偏长。不是回归,也不是结果算错。 +- 触发条件:一次请求里有多个候选分钟,并且事件带 year/month 采样(采样日把候选×事件再放大)。 +- 根因:`build_candidate_static_context` 引入后,排盘/分盘按候选分钟只算一次,但 Shadbala、Ashtakavarga、Vimshottari 时间轴、Narayana 周期表仍留在 `_candidate_row` 的事件循环里,被「候选 × 事件 × 采样日」三重放大。过境盘只依赖事件日期,却按候选分钟在最内层重算。`build_refinement_packet` 在 `probe_times == grid_times` 时对 `_discriminating_event_probe_lists` 算两遍。`scoring_service._cached_rows` 是加错层的死代码,生产入口从不走。 +- 修复:把候选分钟不变量挂进 static context,过境盘在 `compute_event_candidate_rows` 调用栈内用局部字典按日期缓存 chart(规则判定仍按候选算),探针在时间网格相同时复用一次结果;删掉 `_cached_rows`。不改算法、权重、阈值、采样规则,也不修 static context 里 Shadbala 的 `birth_minute` 双算。 +- 验证:基线 `a8d29d1b` 真实跑出 golden(公开 1990-01-01 北京烟测盘 + 虚构事件);改后 `candidate_scores` 与剔除 `column_compare_ms` 的 `decision_receipt` 逐字相同。计数断言:`calc_shadbala` / `calc_ashtakavarga` / `build_dasha_timeline` / `calc_narayana_mahadasha` 在引擎打分路径上各等于候选分钟数;过境 `compute_chart` 等于去重事件日期数;year 精度过境仍早退 `[]`;默认探针路径 1 次、`refresh_probes` 且 refresh 列存在时 2 次。 +- 防复发:新增的候选分钟不变量必须进 static context,不得留在 `_candidate_row` 的事件循环里;新增的事件不变量不得按候选迭代。记忆化只允许请求内显式传递的 context / 局部字典,禁止模块级 `lru_cache` 跨请求持有出生资料派生数据。 +- 相关记录:无 +- 复发自:无 +- 修复版本:待发布 + ## BUG-722 | 意图分类器两次异常被说成用户没说清,经历被丢掉 - 状态:resolved @@ -11253,4 +11269,3 @@ - 相关记录:BUG-059、BUG-388 - 复发自:BUG-059(总预算约束);BUG-388 的防复发只写了单次尝试,因此没拦住 - 修复版本:待发布 - diff --git a/docs/tasks/PROGRESS-rectification-engine-memoization-20260915.md b/docs/tasks/PROGRESS-rectification-engine-memoization-20260915.md new file mode 100644 index 00000000..d8a2210e --- /dev/null +++ b/docs/tasks/PROGRESS-rectification-engine-memoization-20260915.md @@ -0,0 +1,61 @@ +# PROGRESS · 重算记忆化(2026-09-15 / 执行 2026-09-16) + +- 执行分支:`codex/rectification-engine-memoization-20260915` +- 工作树:`.worktrees/rectification-engine-memoization-20260915` +- 任务书基线:`6b3248bf`(已被后续合入)。本机开工 `HEAD` = `origin/staging` = `a8d29d1b` +- BUG:`BUG-721`(落库时最大号仍是 720,未顺延) + +## 开工前置 + +| 项 | 结果 | +| --- | --- | +| `git status -sb` 第一行 | `## codex/rectification-engine-memoization-20260915...origin/staging` | +| `HEAD` | `a8d29d1b` | +| BUG_HISTORY 检索 | `重复计算` / `shadbala` / `scoring_service` / `_cached_rows`:无同模块分层遗漏记录。最大号 BUG-720 | +| 本机 Python | 主仓 `.venv` 是 25 字节指针。本 worktree 用 Anaconda 3.11.7 `--system-site-packages` 建真实 venv,复用已有 `pyswisseph 2.10.3.2` / swisseph `20230604`,未升级该包 | +| 基线 golden | 改代码前用 `a8d29d1b` 真实跑出 `tests/golden/rectification_engine_memoization_v1.json`。golden 测试改前绿 | + +未改 `docs/tasks/README.md` 状态列。未推 staging。未改 `jyotish_api_server.py`、前端、`sample_event_dates`、`_SWISSEPH_LOCK`。 + +## 做了什么 + +6.1–6.6 全做,未按 §8 让步砍项。 + +| 条 | 做法 | +| --- | --- | +| 6.1 | `build_candidate_static_context` 把已算的 `ashtakavarga_result` / `shadbala_result` 挂进 context。辅助函数只读结果做判定。Shadbala 只复用 `sthana_bala.total` / `drik_bala` / `naisargika_bala`;测试证明带/不带 `birth_minute` 这三项相等。`birth_minute` 双算不修(§4.3) | +| 6.2 | Vimshottari 时间轴、Narayana 周期表进 context;`_active_vimshottari` / `_active_narayana` 只留 `find_current` / `get_current_narayana_dasha` | +| 6.3 | `compute_event_candidate_rows` 内局部字典按事件日期缓存过境 **chart**;规则仍按候选算。`precision == "year"` 仍早退 `[]` | +| 6.4 | `probe_times == grid_times` 时 `discriminating_event_probe_set` 结果复用给 contrast;`refresh_probes` 且 refresh 列存在时仍两次 | +| 6.5 | 删除 `scoring_service._cached_rows`。`_canonical` 仍被 `sha256` 使用,保留 | +| 6.6 | 公开 1990-01-01 北京烟测盘 + 虚构事件。golden 由基线代码跑出,禁止手造 | + +记忆化只在请求内:static context dict,以及 `compute_event_candidate_rows` / `build_refinement_packet` 栈内局部字典。无模块级跨请求缓存。 + +## 测试 + +定向(任务书 §9 + 本单新文件): + +| 套件 | 结果 | +| --- | --- | +| `tests/test_rectification_engine_memoization.py` | **10 passed** | +| `test_rectification_v5_services.py` 等 7 个任务书名单 | 与本单文件合计 **129 passed** | +| quality-gate quick 的 pytest(含 `tests/test_rectification_*.py`) | **776 passed, 1 skipped**(575.78s) | + +本机 `run_quality_gate.py --profile quick` 在 pytest 之后的 `npm test` 失败:`tsx` 不在 PATH(Windows 前端环境缺口)。本单未改前端,不把这条写成通过。 + +未改任何既有断言的期望值。测试总数高于开工时(新增 10 条)。 + +## CHANGELOG + +未改。纯内部记忆化,用户可见打分与决策回执逐字不变(计时字段除外)。 + +## 偏离 + +- 任务书写 `.venv/bin/python`,本机换成 `.\.venv\Scripts\python.exe`。 +- golden 测试只锁 `candidate_scores` 与剔除 `column_compare_ms` 的 `decision_receipt`(6.6 原文)。`candidate_feature_snapshot` 写进 golden 文件备查,不作为 pytest 硬断言:KP/Hora 指纹在 pytest 全量收集下会出现第二条稳定哈希,与打分无关。 +- 未 `git push origin HEAD:staging`。 + +## 观察项(不修) + +`build_candidate_static_context` 的 Shadbala `birth_hour = hour + minute/60` 同时又传 `birth_minute`,只流进 fingerprints,本单照搬。 diff --git a/scripts/active_rectification_event_engine.py b/scripts/active_rectification_event_engine.py index ba094418..7ccb2268 100644 --- a/scripts/active_rectification_event_engine.py +++ b/scripts/active_rectification_event_engine.py @@ -107,13 +107,15 @@ def _active_vimshottari( birth_date: str, moon_longitude: float, event_at: datetime, + timeline: list[dict[str, Any]] | None = None, ) -> tuple[str, str, str]: - nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude) - timeline, _, _, _ = dasha_analyzer.build_dasha_timeline( - birth_date, - nakshatra, - progress, - ) + if timeline is None: + nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude) + timeline, _, _, _ = dasha_analyzer.build_dasha_timeline( + birth_date, + nakshatra, + progress, + ) _, major = dasha_analyzer.find_current(timeline, event_at) minor = dasha_analyzer.find_current_sub( dasha_analyzer.build_antardasha(major), @@ -131,11 +133,13 @@ def _active_narayana( planet_longitudes: dict[str, float], birth_at: datetime, event_at: datetime, + periods: list[dict[str, Any]] | None = None, ) -> tuple[int | None, int | None]: - periods = narayana_dasha.calc_narayana_mahadasha( - ascendant_index, - planet_longitudes, - ) + if periods is None: + periods = narayana_dasha.calc_narayana_mahadasha( + ascendant_index, + planet_longitudes, + ) age = max((event_at - birth_at).total_seconds() / (365.2425 * 86_400), 0.0) active = narayana_dasha.get_current_narayana_dasha(periods, age) major = active.get("md") or {} @@ -280,23 +284,45 @@ def _score_event( } +def _transit_chart_cache_key( + request: RectificationEventRequest, + event_at: datetime, +) -> tuple[Any, ...]: + return ( + event_at.date().isoformat(), + float(request["lat"]), + float(request["lon"]), + float(request["tz"]), + request.get("ayanamsa", AYANAMSA), + request.get("node_mode", NODE_MODE), + ) + + def _controlled_transit_rules( request: RectificationEventRequest, event: LifeEvent, natal_ascendant_index: int, target_houses: tuple[int, ...], + transit_chart_cache: dict[tuple[Any, ...], dict[str, Any]] | None = None, ) -> list[str]: """Use only Jupiter/Saturn and only day/month dated events as a weak check.""" if event["precision"] == "year": return [] event_at = _event_datetime(event) - transit_chart = domain_calculation_service.compute_chart({ + payload = { "year": event_at.year, "month": event_at.month, "day": event_at.day, "hour": 12, "minute": 0, "lat": request["lat"], "lon": request["lon"], "tz": request["tz"], "ayanamsa": request.get("ayanamsa", AYANAMSA), "node_mode": request.get("node_mode", NODE_MODE), - }) + } + cache_key = _transit_chart_cache_key(request, event_at) + if transit_chart_cache is not None and cache_key in transit_chart_cache: + transit_chart = transit_chart_cache[cache_key] + else: + transit_chart = domain_calculation_service.compute_chart(payload) + if transit_chart_cache is not None: + transit_chart_cache[cache_key] = transit_chart rules: list[str] = [] for planet in ("Jupiter", "Saturn"): item = transit_chart.get("planets", {}).get(planet) or {} @@ -305,9 +331,16 @@ def _controlled_transit_rules( return rules -def _ashtakavarga_auxiliary(natal_chart: dict, ascendant_index: int, target_houses: tuple[int, ...]) -> tuple[list[str], float]: +def _ashtakavarga_auxiliary( + natal_chart: dict, + ascendant_index: int, + target_houses: tuple[int, ...], + ashtakavarga_result: dict[str, Any] | None = None, +) -> tuple[list[str], float]: """Return a bounded SAV consistency adjustment, never a standalone trigger.""" - result = ashtakavarga.calc_ashtakavarga(natal_chart.get("planets", {}), ascendant_index) + result = ashtakavarga_result + if result is None: + result = ashtakavarga.calc_ashtakavarga(natal_chart.get("planets", {}), ascendant_index) if not result.get("all_bav_valid") or not (result.get("sav") or {}).get("valid"): return [], 0.0 house_scores = result.get("house_scores_full") or {} @@ -323,17 +356,24 @@ def _ashtakavarga_auxiliary(natal_chart: dict, ascendant_index: int, target_hous return [], 0.0 -def _shadbala_verified_components_auxiliary(natal_chart: dict, birth_hour: float, dasha_lords: tuple[str, str, str]) -> tuple[list[str], float]: +def _shadbala_verified_components_auxiliary( + natal_chart: dict, + birth_hour: float, + dasha_lords: tuple[str, str, str], + shadbala_result: dict[str, Any] | None = None, +) -> tuple[list[str], float]: """Use only Sthana/Drik/Naisargika, whose oracle comparison is already matched.""" planets = natal_chart.get("planets", {}) sun = planets.get("Sun") or {} moon = planets.get("Moon") or {} if not isinstance(sun.get("lon"), (int, float)) or not isinstance(moon.get("lon"), (int, float)): return [], 0.0 - result = shadbala.calc_shadbala( - planets, str(natal_chart["ascendant"].get("sign") or "Aries"), birth_hour, - float(sun["lon"]), float(moon["lon"]), - ) + result = shadbala_result + if result is None: + result = shadbala.calc_shadbala( + planets, str(natal_chart["ascendant"].get("sign") or "Aries"), birth_hour, + float(sun["lon"]), float(moon["lon"]), + ) values = { planet: float((row.get("sthana_bala") or {}).get("total", 0)) + float(row.get("drik_bala", 0)) + float(row.get("naisargika_bala", 0)) for planet, row in (result.get("planets") or {}).items() @@ -490,6 +530,26 @@ def build_candidate_static_context( except (KeyError, TypeError, ValueError): blocked_layers.append("Shadbala") + vimshottari_timeline = None + try: + nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(planet_longitudes["Moon"]) + vimshottari_timeline, _, _, _ = dasha_analyzer.build_dasha_timeline( + candidate_at.date().isoformat(), + nakshatra, + progress, + ) + except (KeyError, TypeError, ValueError): + vimshottari_timeline = None + + narayana_periods = None + try: + narayana_periods = narayana_dasha.calc_narayana_mahadasha( + ascendant_index, + planet_longitudes, + ) + except (KeyError, TypeError, ValueError): + narayana_periods = None + birth_info = chart.get("birth_info") if isinstance(chart.get("birth_info"), dict) else {} kp_snapshot = observe_kp_cusps( birth_info.get("julian_day"), @@ -538,6 +598,10 @@ def build_candidate_static_context( "arudha_padas": arudha_padas, "varga_charts": varga_charts, "feature": feature_payload, + "ashtakavarga_result": ashtakavarga_result, + "shadbala_result": shadbala_result, + "vimshottari_timeline": vimshottari_timeline, + "narayana_periods": narayana_periods, } @@ -572,7 +636,12 @@ def _candidate_row( missing_layers.extend(prefixes) continue try: - vimshottari = _active_vimshottari(candidate_at.date().isoformat(), moon_longitude, event_at) + vimshottari = _active_vimshottari( + candidate_at.date().isoformat(), + moon_longitude, + event_at, + context.get("vimshottari_timeline"), + ) except (KeyError, TypeError, ValueError): missing_layers.append("Vimshottari_MD_AD_PD") continue @@ -582,6 +651,7 @@ def _candidate_row( planet_longitudes, candidate_at, event_at, + context.get("narayana_periods"), ) except (KeyError, TypeError, ValueError): missing_layers.append("Narayana_MD_AD") @@ -598,16 +668,30 @@ def _candidate_row( narayana=narayana, arudha_padas=arudha_padas, )) - transit_rules = _controlled_transit_rules(request, event, ascendant_index, DOMAIN_CONFIG[event["domain"]][1]) + transit_rules = _controlled_transit_rules( + request, + event, + ascendant_index, + DOMAIN_CONFIG[event["domain"]][1], + context.get("_transit_chart_cache"), + ) if transit_rules: evidence[-1]["rule_ids"].extend(transit_rules) evidence[-1]["points"] = round(evidence[-1]["points"] + 0.25 * len(transit_rules) * precision_weight(event["precision"]), 4) - av_rules, av_points = _ashtakavarga_auxiliary(chart, ascendant_index, DOMAIN_CONFIG[event["domain"]][1]) + av_rules, av_points = _ashtakavarga_auxiliary( + chart, + ascendant_index, + DOMAIN_CONFIG[event["domain"]][1], + context.get("ashtakavarga_result"), + ) if av_rules: evidence[-1]["rule_ids"].extend(av_rules) evidence[-1]["points"] = round(evidence[-1]["points"] + av_points * precision_weight(event["precision"]), 4) shadbala_rules, shadbala_points = _shadbala_verified_components_auxiliary( - chart, candidate_at.hour + candidate_at.minute / 60, vimshottari, + chart, + candidate_at.hour + candidate_at.minute / 60, + vimshottari, + context.get("shadbala_result"), ) if shadbala_rules: evidence[-1]["rule_ids"].extend(shadbala_rules) @@ -719,4 +803,8 @@ def compute_event_candidate_rows( ) -> list[CandidateScoreRow]: """Return every computed minute row while reusing one static chart scan per candidate.""" contexts = list(static_contexts) if static_contexts is not None else compute_candidate_static_contexts(request, candidates=candidates) - return [_candidate_row(request, context) for context in contexts] + transit_chart_cache: dict[tuple[Any, ...], dict[str, Any]] = {} + return [ + _candidate_row(request, {**context, "_transit_chart_cache": transit_chart_cache}) + for context in contexts + ] diff --git a/scripts/rectification/refinement_packet.py b/scripts/rectification/refinement_packet.py index afde0756..eb7e4d70 100644 --- a/scripts/rectification/refinement_packet.py +++ b/scripts/rectification/refinement_packet.py @@ -697,7 +697,11 @@ def build_refinement_packet( "unique_minute_claim": False, "confirmation_allowed": False, } - from scripts.rectification.candidate_contrast import PROBE_PHASE_HOLDOUT_VALIDATION, event_year + from scripts.rectification.candidate_contrast import ( + PROBE_PHASE_HOLDOUT_VALIDATION, + event_year, + opportunity_from_probe, + ) from scripts.rectification.case_holdout import reserved_holdout_events from scripts.rectification.event_probes import ( candidate_contrast_opportunities, @@ -729,13 +733,16 @@ def build_refinement_packet( dropped = list(bundle["dropped"]) clarification = event_clarification_probes(request) collection = evidence_collection_probes(request) - opportunities = candidate_contrast_opportunities( - request, - built, - scan=scan, - candidate_times=grid_times, - representative_time=representative_time, - ) + if probe_times == grid_times: + opportunities = [opportunity_from_probe(probe) for probe in probes] + else: + opportunities = candidate_contrast_opportunities( + request, + built, + scan=scan, + candidate_times=grid_times, + representative_time=representative_time, + ) reserved = reserved_holdout_events(request.get("events") or []) holdout = [ { diff --git a/scripts/rectification/scoring_service.py b/scripts/rectification/scoring_service.py index 6052d78b..055067a3 100644 --- a/scripts/rectification/scoring_service.py +++ b/scripts/rectification/scoring_service.py @@ -5,7 +5,6 @@ import json from collections import defaultdict from collections.abc import Callable, Sequence from datetime import date, timedelta -from functools import lru_cache from typing import Any from scripts.active_rectification_event_engine import compute_candidate_static_contexts, compute_event_candidate_rows @@ -126,11 +125,6 @@ def _canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")) -@lru_cache(maxsize=4096) -def _cached_rows(serialized: str) -> tuple[CandidateScoreRow, ...]: - return tuple(compute_event_candidate_rows(json.loads(serialized))) - - _SUPPORT_RULES = ( "functional_benefic_auxiliary", "arudha_auxiliary", diff --git a/tests/golden/rectification_engine_memoization_v1.json b/tests/golden/rectification_engine_memoization_v1.json new file mode 100644 index 00000000..d406c3a8 --- /dev/null +++ b/tests/golden/rectification_engine_memoization_v1.json @@ -0,0 +1,1718 @@ +{ + "source_commit": "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53", + "candidate_scores": [ + { + "time": "12:00", + "score": 8.6274, + "supporting_event_ids": [ + "00000000-0000-4000-8000-000000000001", + "00000000-0000-4000-8000-000000000002" + ], + "conflicting_event_ids": [] + }, + { + "time": "12:01", + "score": 8.6227, + "supporting_event_ids": [ + "00000000-0000-4000-8000-000000000001", + "00000000-0000-4000-8000-000000000002" + ], + "conflicting_event_ids": [] + }, + { + "time": "12:02", + "score": 8.1701, + "supporting_event_ids": [ + "00000000-0000-4000-8000-000000000001", + "00000000-0000-4000-8000-000000000002" + ], + "conflicting_event_ids": [] + } + ], + "decision_receipt": { + "receipt_version": "candidate-decision-receipt-v2", + "contract_version": "v2", + "event_contract_version": "rectification-event-contract-v2", + "policy_version": "rectification-candidate-policy-v3", + "decision_policy_version": "rectification-candidate-policy-v3", + "display_allowed": true, + "selection_allowed": false, + "acceptance_allowed": false, + "propose_allowed": false, + "confirmation_allowed": false, + "accept_allowed": false, + "confirm_allowed": false, + "representative_candidate_id": "85ca5490-07ab-54b7-acf0-59385f705015", + "representative_time": "12:00", + "overall_confidence": "low", + "margin_percent": 5.3006, + "reasons": [ + "insufficient_events", + "insufficient_diagnostic_stability", + "insufficient_confirmation_events", + "insufficient_confirmation_domains", + "insufficient_confirmation_margin", + "engine_exact_confirmation_not_granted", + "external_validation_not_passed", + "public_aa_holdout_not_ready" + ], + "acceptance_reasons": [ + "insufficient_events" + ], + "confirmation_reasons": [ + "insufficient_diagnostic_stability", + "insufficient_confirmation_events", + "insufficient_confirmation_domains", + "insufficient_confirmation_margin", + "engine_exact_confirmation_not_granted", + "external_validation_not_passed", + "public_aa_holdout_not_ready" + ], + "tie_policy": { + "score_quantum": 0.0001, + "absolute_tolerance": 0.0001, + "rounding": "ROUND_HALF_UP" + }, + "gates": { + "candidate_presence": { + "passed": true, + "candidate_count": 2 + }, + "event_quality": { + "passed": false, + "scoreable_event_count": 2, + "minimum": 3 + }, + "domain_diversity": { + "passed": true, + "scoreable_domain_count": 2, + "minimum": 2, + "domains": [ + "career", + "education" + ] + }, + "date_quality": { + "passed": true, + "precision_weight_total": 1.3, + "precision_weight_mean": 0.65, + "minimum_precision_weight_mean": 0.65, + "low_reliability_event_ids": [], + "unresolved_conflict_event_ids": [] + }, + "unique_top": { + "passed": true, + "tied_minute_count": 1 + }, + "diagnostic_quality": { + "passed": false, + "minimum_retention": 0.75, + "minimum_margin_percent": 10.0, + "margin_percent": 5.3006, + "leave_one_event_out_retention_rate": 1.0, + "leave_one_domain_out_retention_rate": 1.0, + "date_sensitivity_retention_rate": 0.9583333333333333 + }, + "required_layers": { + "passed": true, + "missing_layers": [], + "skipped_by_policy": [] + }, + "exact_confirmation": { + "passed": false, + "fail_closed": true, + "engine_granted": false, + "adjacent_passed": true, + "indistinguishable_width_minutes": 3, + "max_confirmation_width_minutes": 5, + "external_validation_status": "not_evaluated", + "unique_minute_path": "closed_at_representative", + "required_scoreable_events": 4, + "required_scoreable_domains": 3, + "reason": "engine_and_external_validation_must_explicitly_pass", + "holdout": { + "sealed_benchmark_id": "minute_rectification_fact_ranker_v4_holdout_v3", + "status": "not_ready", + "valid_public_aa_cases": 20, + "required_cases": 20, + "top_1_rate": 0.15, + "confirmation_coverage_rate": 0.0 + } + } + }, + "house_table": { + "time": "12:00", + "lagna": "\u53cc\u9c7c\u5ea7", + "houses": [ + { + "house": 1, + "sign": "\u53cc\u9c7c\u5ea7", + "occupants": [] + }, + { + "house": 2, + "sign": "\u767d\u7f8a\u5ea7", + "occupants": [] + }, + { + "house": 3, + "sign": "\u91d1\u725b\u5ea7", + "occupants": [] + }, + { + "house": 4, + "sign": "\u53cc\u5b50\u5ea7", + "occupants": [ + "\u6728\u661f" + ] + }, + { + "house": 5, + "sign": "\u5de8\u87f9\u5ea7", + "occupants": [ + "\u8ba1\u90fd" + ] + }, + { + "house": 6, + "sign": "\u72ee\u5b50\u5ea7", + "occupants": [] + }, + { + "house": 7, + "sign": "\u5904\u5973\u5ea7", + "occupants": [] + }, + { + "house": 8, + "sign": "\u5929\u79e4\u5ea7", + "occupants": [] + }, + { + "house": 9, + "sign": "\u5929\u874e\u5ea7", + "occupants": [ + "\u706b\u661f" + ] + }, + { + "house": 10, + "sign": "\u5c04\u624b\u5ea7", + "occupants": [ + "\u592a\u9633", + "\u571f\u661f" + ] + }, + { + "house": 11, + "sign": "\u6469\u7faf\u5ea7", + "occupants": [ + "\u6c34\u661f", + "\u91d1\u661f", + "\u7f57\u777a" + ] + }, + { + "house": 12, + "sign": "\u6c34\u74f6\u5ea7", + "occupants": [ + "\u6708\u4eae" + ] + } + ] + }, + "natal_recast": { + "time": "12:00", + "lagna": "\u53cc\u9c7c\u5ea7", + "user_meaning": "\u672c\u547d\u5bab\u4f4d\u5df2\u6309 12:00 \u91cd\u7b97\uff08\u4e0a\u5347 \u53cc\u9c7c\u5ea7\uff09\u3002\u4e0b\u9762\u662f\u672c\u8f6e\u5b9e\u9645\u6267\u884c\u7684\u6280\u6cd5\uff0c\u4e0d\u80fd\u5f53\u4f5c\u552f\u4e00\u5206\u949f\u786e\u8ba4\u3002", + "unique_minute_claim": false, + "confirmation_allowed": false + }, + "house_tables_by_time": { + "12:00": { + "time": "12:00", + "lagna": "\u53cc\u9c7c\u5ea7", + "houses": [ + { + "house": 1, + "sign": "\u53cc\u9c7c\u5ea7", + "occupants": [] + }, + { + "house": 2, + "sign": "\u767d\u7f8a\u5ea7", + "occupants": [] + }, + { + "house": 3, + "sign": "\u91d1\u725b\u5ea7", + "occupants": [] + }, + { + "house": 4, + "sign": "\u53cc\u5b50\u5ea7", + "occupants": [ + "\u6728\u661f" + ] + }, + { + "house": 5, + "sign": "\u5de8\u87f9\u5ea7", + "occupants": [ + "\u8ba1\u90fd" + ] + }, + { + "house": 6, + "sign": "\u72ee\u5b50\u5ea7", + "occupants": [] + }, + { + "house": 7, + "sign": "\u5904\u5973\u5ea7", + "occupants": [] + }, + { + "house": 8, + "sign": "\u5929\u79e4\u5ea7", + "occupants": [] + }, + { + "house": 9, + "sign": "\u5929\u874e\u5ea7", + "occupants": [ + "\u706b\u661f" + ] + }, + { + "house": 10, + "sign": "\u5c04\u624b\u5ea7", + "occupants": [ + "\u592a\u9633", + "\u571f\u661f" + ] + }, + { + "house": 11, + "sign": "\u6469\u7faf\u5ea7", + "occupants": [ + "\u6c34\u661f", + "\u91d1\u661f", + "\u7f57\u777a" + ] + }, + { + "house": 12, + "sign": "\u6c34\u74f6\u5ea7", + "occupants": [ + "\u6708\u4eae" + ] + } + ] + }, + "12:01": { + "time": "12:01", + "lagna": "\u53cc\u9c7c\u5ea7", + "houses": [ + { + "house": 1, + "sign": "\u53cc\u9c7c\u5ea7", + "occupants": [] + }, + { + "house": 2, + "sign": "\u767d\u7f8a\u5ea7", + "occupants": [] + }, + { + "house": 3, + "sign": "\u91d1\u725b\u5ea7", + "occupants": [] + }, + { + "house": 4, + "sign": "\u53cc\u5b50\u5ea7", + "occupants": [ + "\u6728\u661f" + ] + }, + { + "house": 5, + "sign": "\u5de8\u87f9\u5ea7", + "occupants": [ + "\u8ba1\u90fd" + ] + }, + { + "house": 6, + "sign": "\u72ee\u5b50\u5ea7", + "occupants": [] + }, + { + "house": 7, + "sign": "\u5904\u5973\u5ea7", + "occupants": [] + }, + { + "house": 8, + "sign": "\u5929\u79e4\u5ea7", + "occupants": [] + }, + { + "house": 9, + "sign": "\u5929\u874e\u5ea7", + "occupants": [ + "\u706b\u661f" + ] + }, + { + "house": 10, + "sign": "\u5c04\u624b\u5ea7", + "occupants": [ + "\u592a\u9633", + "\u571f\u661f" + ] + }, + { + "house": 11, + "sign": "\u6469\u7faf\u5ea7", + "occupants": [ + "\u6c34\u661f", + "\u91d1\u661f", + "\u7f57\u777a" + ] + }, + { + "house": 12, + "sign": "\u6c34\u74f6\u5ea7", + "occupants": [ + "\u6708\u4eae" + ] + } + ] + }, + "12:02": { + "time": "12:02", + "lagna": "\u53cc\u9c7c\u5ea7", + "houses": [ + { + "house": 1, + "sign": "\u53cc\u9c7c\u5ea7", + "occupants": [] + }, + { + "house": 2, + "sign": "\u767d\u7f8a\u5ea7", + "occupants": [] + }, + { + "house": 3, + "sign": "\u91d1\u725b\u5ea7", + "occupants": [] + }, + { + "house": 4, + "sign": "\u53cc\u5b50\u5ea7", + "occupants": [ + "\u6728\u661f" + ] + }, + { + "house": 5, + "sign": "\u5de8\u87f9\u5ea7", + "occupants": [ + "\u8ba1\u90fd" + ] + }, + { + "house": 6, + "sign": "\u72ee\u5b50\u5ea7", + "occupants": [] + }, + { + "house": 7, + "sign": "\u5904\u5973\u5ea7", + "occupants": [] + }, + { + "house": 8, + "sign": "\u5929\u79e4\u5ea7", + "occupants": [] + }, + { + "house": 9, + "sign": "\u5929\u874e\u5ea7", + "occupants": [ + "\u706b\u661f" + ] + }, + { + "house": 10, + "sign": "\u5c04\u624b\u5ea7", + "occupants": [ + "\u592a\u9633", + "\u571f\u661f" + ] + }, + { + "house": 11, + "sign": "\u6469\u7faf\u5ea7", + "occupants": [ + "\u6c34\u661f", + "\u91d1\u661f", + "\u7f57\u777a" + ] + }, + { + "house": 12, + "sign": "\u6c34\u74f6\u5ea7", + "occupants": [ + "\u6708\u4eae" + ] + } + ] + } + }, + "technique_audit_table": [ + { + "technique": "D1 \u672c\u547d\u76d8", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u6309\u8be5\u5206\u949f\u91cd\u7b97\u672c\u547d\u5bab\u4f4d\u3002" + }, + { + "technique": "D5 \u6210\u5c31\u5206\u76d8", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5b66\u4e1a\u6216\u88ab\u59d4\u4ee5\u8d23\u4efb\u7684\u53d8\u5316\u3002" + }, + { + "technique": "D10 \u4e8b\u4e1a\u5206\u76d8", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u4e8b\u4e1a\u4e3b\u9898\uff0c\u53ef\u7528 D10 \u4e0a\u5347\u7c7b\u578b\u8868\u4f5c\u6821\u65f6\u65b9\u6cd5\uff0c\u4e0d\u662f\u547d\u8fd0\u627f\u8bfa\u3002" + }, + { + "technique": "D24 \u6559\u80b2\u5206\u76d8", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5b66\u4e1a\u4e3b\u9898\u3002" + }, + { + "technique": "Vimshottari", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u4e3b\u9650\u3002" + }, + { + "technique": "Narayana", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5206\u76d8\u5927\u8fd0\u3002" + }, + { + "technique": "Ashtakavarga", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u505a Ashtakavarga \u8f85\u52a9\u5bf9\u7167\u3002" + }, + { + "technique": "Shadbala", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u505a\u5df2\u6838\u9a8c\u7684 Shadbala \u5206\u91cf\u8f85\u52a9\u5bf9\u7167\u3002" + }, + { + "technique": "\u6362\u8fd0\u8d34\u8fd1\u5ea6", + "status": "executed", + "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u65e5\u7ea7\u4e8b\u4ef6\u4e0e\u5019\u9009\u6362\u8fd0\u65e5\u671f\u7684\u8d34\u8fd1\u7a0b\u5ea6\u3002" + }, + { + "technique": "KP \u5bab\u5934", + "status": "executed", + "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002" + }, + { + "technique": "VedAstro \u5206\u949f\u7ea7\u6821\u9a8c", + "status": "blocked", + "note": "\u5b98\u65b9\u5206\u949f\u654f\u611f\u6821\u9a8c\u5c1a\u672a\u8dd1\u901a\u3002\u672a\u8c03\u7528\u4e0d\u7b49\u4e8e\u5931\u8d25\uff0c\u4f46\u7f3a\u8fd9\u4e00\u5c42\u4e0d\u80fd\u5199\u786e\u8ba4\u3002" + }, + { + "technique": "\u552f\u4e00\u5206\u949f\u786e\u8ba4", + "status": "blocked", + "note": "\u672c\u4f1a\u8bdd\u4ee5\u4ee3\u8868\u6027\u65f6\u95f4\u6536\u53e3\uff0c\u4e0d\u786e\u8ba4\u552f\u4e00\u5206\u949f\u3002" + } + ], + "window_scan": { + "scanned": true, + "confirmation_allowed": false, + "unique_minute_claim": false, + "transitions": [ + { + "layer": "kp1", + "at": "12:01", + "user_meaning": "KP 1\u5bab\u5b50\u4e3b \u5728 12:01 \u53d1\u751f\u53d8\u5316", + "from_sign": "\u5c04\u624b\u5ea7", + "to_sign": "\u767d\u7f8a\u5ea7" + }, + { + "layer": "d9", + "at": "12:02", + "user_meaning": "D9 \u5728 12:02 \u53d1\u751f\u53d8\u5316", + "from_sign": "\u5c04\u624b\u5ea7", + "to_sign": "\u6469\u7faf\u5ea7" + }, + { + "layer": "d12", + "at": "12:02", + "user_meaning": "D12 \u5728 12:02 \u53d1\u751f\u53d8\u5316", + "from_sign": "\u5929\u79e4\u5ea7", + "to_sign": "\u5929\u874e\u5ea7" + }, + { + "layer": "d24", + "at": "12:02", + "user_meaning": "D24 \u5728 12:02 \u53d1\u751f\u53d8\u5316", + "from_sign": "\u5929\u79e4\u5ea7", + "to_sign": "\u5929\u874e\u5ea7" + }, + { + "layer": "d30", + "at": "12:02", + "user_meaning": "D30 \u5728 12:02 \u53d1\u751f\u53d8\u5316", + "from_sign": "\u6469\u7faf\u5ea7", + "to_sign": "\u5929\u874e\u5ea7" + }, + { + "layer": "pada", + "at": "12:02", + "user_meaning": "Nakshatra pada \u5728 12:02 \u53d1\u751f\u53d8\u5316" + } + ], + "d1_lagna_count": 1, + "d1_candidates_differ": false, + "d9_lagna_count": 2, + "d9_candidates_differ": true, + "d10_lagna_count": 1, + "d10_candidates_differ": false, + "d4_lagna_count": 1, + "d4_candidates_differ": false, + "d5_lagna_count": 1, + "d5_candidates_differ": false, + "d7_lagna_count": 1, + "d7_candidates_differ": false, + "d12_lagna_count": 2, + "d12_candidates_differ": true, + "d24_lagna_count": 2, + "d24_candidates_differ": true, + "d2_lagna_count": 1, + "d2_candidates_differ": false, + "d11_lagna_count": 1, + "d11_candidates_differ": false, + "d30_lagna_count": 2, + "d30_candidates_differ": true, + "pada_count": 2, + "pada_candidates_differ": true, + "hora_count": 1, + "hora_candidates_differ": false, + "ghati_count": 1, + "ghati_candidates_differ": false, + "bhava_count": 1, + "bhava_candidates_differ": false, + "pranapada_count": 1, + "pranapada_candidates_differ": false, + "kp1_count": 2, + "kp1_candidates_differ": true, + "kp4_count": 1, + "kp4_candidates_differ": false, + "kp7_count": 1, + "kp7_candidates_differ": false, + "kp10_count": 1, + "kp10_candidates_differ": false, + "d9_sign_names": [ + "\u5c04\u624b\u5ea7", + "\u6469\u7faf\u5ea7" + ], + "d10_sign_names": [ + "\u91d1\u725b\u5ea7" + ] + }, + "event_dasha_ledger": [ + { + "summary": "\u5165\u5b66", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari", + "narayana" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "education", + "year_month": "2008-01" + }, + { + "summary": "\u5165\u804c", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "career", + "year_month": "2012-06" + } + ], + "event_dasha_ledger_by_time": { + "12:00": [ + { + "summary": "\u5165\u5b66", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari", + "narayana" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "education", + "year_month": "2008-01" + }, + { + "summary": "\u5165\u804c", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "career", + "year_month": "2012-06" + } + ], + "12:02": [ + { + "summary": "\u5165\u5b66", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari", + "narayana" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "education", + "year_month": "2008-01" + }, + { + "summary": "\u5165\u804c", + "match": "strong", + "match_label": "\u5f3a\u76f8\u5173", + "tracks": [ + "vimshottari" + ], + "gochara": "not_seen", + "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09", + "domain": "career", + "year_month": "2012-06" + } + ] + }, + "prospective_windows_by_time": { + "12:00": [ + { + "domain": "career", + "from": "2027-02", + "to": "2027-02" + }, + { + "domain": "career", + "from": "2027-06", + "to": "2027-06" + } + ], + "12:02": [ + { + "domain": "career", + "from": "2027-01", + "to": "2027-01" + }, + { + "domain": "career", + "from": "2027-06", + "to": "2027-06" + } + ] + }, + "event_fit_rate": { + "matched": 2, + "total": 2, + "percent": 100, + "band": "high", + "label": "\u9ad8\u5ea6\u543b\u5408\uff08\u4e8b\u4ef6\u543b\u5408\u7387 \u226580%\uff09", + "unique_minute_claim": false, + "user_meaning": "\u5f53\u524d\u7a97 2/2 \u4ef6\u5df2\u786e\u8ba4\u4e8b\u4ef6\u4e0e Dasha/Gochara \u543b\u5408\uff0c\u9ad8\u5ea6\u543b\u5408\uff08\u4e8b\u4ef6\u543b\u5408\u7387 \u226580%\uff09\u3002\u8fd9\u662f\u76f8\u5bf9\u62df\u5408\uff0c\u4e0d\u662f\u5df2\u786e\u8ba4\u552f\u4e00\u51fa\u751f\u5206\u949f\u3002" + }, + "dasha_agreement": { + "status": "agree", + "vimshottari_top": "12:00", + "narayana_top": "12:00", + "user_meaning": "\u4e3b\u9650\u548c\u5206\u76d8\u5927\u8fd0\u90fd\u66f4\u652f\u6301\u540c\u4e00\u6bb5\u4ee3\u8868\u6027\u65f6\u95f4\u3002\u8fd9\u4ecd\u4e0d\u662f\u552f\u4e00\u5206\u949f\u786e\u8ba4\u3002" + }, + "lagna_contrast": null, + "nakshatra_boundary": { + "near_boundary": false, + "distance_degrees": 2.886, + "user_meaning": null, + "options": [] + }, + "precision_stage": { + "current": "d9_refine", + "can_stop": true, + "user_meaning": "\u672c\u547d\u4e0a\u5347\u5df2\u8f83\u7a33\uff0c\u5173\u7cfb\u76d8\u4ecd\u4f1a\u6362\u5347\u3002\u53ef\u518d\u8865\u4e00\u4ef6\u8bb0\u5f97\u65f6\u95f4\u7684\u611f\u60c5\u6216\u5173\u7cfb\u53d8\u5316\u3002", + "unique_minute_claim": false + }, + "oos_blind_prompts": [ + { + "domain": "relationship", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u611f\u60c5\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5173\u7cfb\u53d8\u5316\uff1f", + "used_for_scoring": false + }, + { + "domain": "family", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u5bb6\u4eba\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5bb6\u4eba\u53d8\u5316\uff1f", + "used_for_scoring": false + }, + { + "domain": "finance", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u8d22\u52a1\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u6536\u5165\u6216\u8d44\u4ea7\u53d8\u5316\uff1f", + "used_for_scoring": false + } + ], + "discriminating_event_probes": [], + "event_clarification_probes": [ + { + "year": 2008, + "year_label": "2008 \u5e74\u524d\u540e", + "domain": "education", + "event_family": "\u5b66\u4e1a\u6216\u8003\u8bd5\u53d1\u6325\u5931\u5e38\u3001\u538b\u529b\u7279\u522b\u5927", + "source": "known_event_quality", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": true, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2008 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a education\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5b66\u4e1a\u6216\u8003\u8bd5\u53d1\u6325\u5931\u5e38\u3001\u538b\u529b\u7279\u522b\u5927\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\u6765\u6838\u5bf9\u4f53\u9a8c\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u5806\u53e0\u4f8b\u5b50\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "clarify", + "phase": "event_clarification", + "semantic_key": "education.2008", + "information_gain": 0.0, + "candidate_split_hash": "education:2008", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "event_quality", + "style_options": [ + { + "label": "\u53d1\u6325\u660e\u663e\u5931\u5e38\u6216\u538b\u529b\u5f88\u5927", + "answer_class": "yes" + }, + { + "label": "\u6709\u538b\u529b\u4f46\u4e0d\u7b97\u660e\u663e\u5931\u5e38", + "answer_class": "weak_yes" + }, + { + "label": "\u53d1\u6325\u6b63\u5e38\u3001\u6ca1\u6709\u660e\u663e\u5931\u5e38", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + } + ], + "evidence_collection_probes": [ + { + "year": 2011, + "year_label": "2011 \u5e74\u524d\u540e", + "domain": "relocation", + "event_family": "\u642c\u5bb6\u6216\u957f\u671f\u4f4f\u5230\u5916\u5730", + "source": "age_band", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": false, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2011 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a relocation\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u642c\u5bb6\u6216\u957f\u671f\u4f4f\u5230\u5916\u5730\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "collect", + "phase": "evidence_collection", + "semantic_key": "relocation.2011", + "information_gain": 0.0, + "candidate_split_hash": "relocation:2011", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "existence", + "style_options": [ + { + "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408", + "answer_class": "yes" + }, + { + "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31", + "answer_class": "weak_yes" + }, + { + "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + }, + { + "year": 2013, + "year_label": "2013 \u5e74\u524d\u540e", + "domain": "relationship", + "event_family": "\u5f00\u59cb\u4e00\u6bb5\u8ba4\u771f\u5173\u7cfb\u3001\u5206\u624b\u6216\u7ed3\u5a5a", + "source": "age_band", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": false, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2013 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a relationship\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5f00\u59cb\u4e00\u6bb5\u8ba4\u771f\u5173\u7cfb\u3001\u5206\u624b\u6216\u7ed3\u5a5a\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "collect", + "phase": "evidence_collection", + "semantic_key": "relationship.2013", + "information_gain": 0.0, + "candidate_split_hash": "relationship:2013", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "existence", + "style_options": [ + { + "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408", + "answer_class": "yes" + }, + { + "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31", + "answer_class": "weak_yes" + }, + { + "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + }, + { + "year": 2014, + "year_label": "2014 \u5e74\u524d\u540e", + "domain": "family", + "event_family": "\u5bb6\u4eba\u7ed3\u5a5a\u3001\u6dfb\u4e01\u6216\u4f4f\u9662", + "source": "age_band", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": false, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2014 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a family\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5bb6\u4eba\u7ed3\u5a5a\u3001\u6dfb\u4e01\u6216\u4f4f\u9662\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "collect", + "phase": "evidence_collection", + "semantic_key": "family.2014", + "information_gain": 0.0, + "candidate_split_hash": "family:2014", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "existence", + "style_options": [ + { + "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408", + "answer_class": "yes" + }, + { + "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31", + "answer_class": "weak_yes" + }, + { + "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + }, + { + "year": 2017, + "year_label": "2017 \u5e74\u524d\u540e", + "domain": "finance", + "event_family": "\u6536\u5165\u660e\u663e\u53d8\u5316\u3001\u5927\u7b14\u652f\u51fa\u6216\u6b20\u503a", + "source": "age_band", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": false, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2017 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a finance\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u6536\u5165\u660e\u663e\u53d8\u5316\u3001\u5927\u7b14\u652f\u51fa\u6216\u6b20\u503a\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "collect", + "phase": "evidence_collection", + "semantic_key": "finance.2017", + "information_gain": 0.0, + "candidate_split_hash": "finance:2017", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "existence", + "style_options": [ + { + "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408", + "answer_class": "yes" + }, + { + "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31", + "answer_class": "weak_yes" + }, + { + "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + }, + { + "year": 2018, + "year_label": "2018 \u5e74\u524d\u540e", + "domain": "health_pressure", + "event_family": "\u751f\u75c5\u3001\u53d7\u4f24\u6216\u538b\u529b\u7279\u522b\u5927", + "source": "age_band", + "tracks": [ + "vimshottari", + "narayana" + ], + "tracks_agree": false, + "unique_minute_claim": false, + "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2018 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a health_pressure\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u751f\u75c5\u3001\u53d7\u4f24\u6216\u538b\u529b\u7279\u522b\u5927\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002", + "role": "collect", + "phase": "evidence_collection", + "semantic_key": "health_pressure.2018", + "information_gain": 0.0, + "candidate_split_hash": "health_pressure:2018", + "expected_outcomes": [], + "candidate_ids": [], + "choice_kind": "existence", + "style_options": [ + { + "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408", + "answer_class": "yes" + }, + { + "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31", + "answer_class": "weak_yes" + }, + { + "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f", + "answer_class": "no" + }, + { + "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a", + "answer_class": "unsure" + } + ], + "question_contract_version": "probe-question-v1" + } + ], + "candidate_contrast_opportunities": [], + "holdout_validation_probes": [ + { + "domain": "relationship", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u611f\u60c5\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5173\u7cfb\u53d8\u5316\uff1f", + "used_for_scoring": false, + "phase": "holdout_validation", + "role": "holdout", + "source": "oos_blind" + }, + { + "domain": "family", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u5bb6\u4eba\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5bb6\u4eba\u53d8\u5316\uff1f", + "used_for_scoring": false, + "phase": "holdout_validation", + "role": "holdout", + "source": "oos_blind" + }, + { + "domain": "finance", + "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u8d22\u52a1\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u6536\u5165\u6216\u8d44\u4ea7\u53d8\u5316\uff1f", + "used_for_scoring": false, + "phase": "holdout_validation", + "role": "holdout", + "source": "oos_blind" + } + ], + "dropped_probes": [], + "dropped_asked_probe_keys": 0, + "prospective_probes": [ + { + "candidate_label": "A", + "domain": "career", + "window_label": "2027 \u5e74 1 \u6708\u9644\u8fd1", + "user_meaning": "\u5019\u9009 A \u9884\u6d4b\u4e0b\u4e00\u6b21\u4e8b\u4e1a\u53d8\u52a8\u66f4\u53ef\u80fd\u5728 2027 \u5e74 1 \u6708\u9644\u8fd1\u3002\u8fd9\u662f\u9884\u6d4b\u7a97\u53e3\uff0c\u4e0d\u662f\u627f\u8bfa\uff1b\u4e0b\u6b21\u53d1\u751f\u65f6\u56de\u6765\u8865\u4e00\u6761\uff0c\u53ef\u8fdb\u4e00\u6b65\u5206\u8fa8\u3002", + "used_for_scoring": false + }, + { + "candidate_label": "B", + "domain": "career", + "window_label": "2027 \u5e74 1 \u6708\u9644\u8fd1", + "user_meaning": "\u5019\u9009 B \u9884\u6d4b\u4e0b\u4e00\u6b21\u4e8b\u4e1a\u53d8\u52a8\u66f4\u53ef\u80fd\u5728 2027 \u5e74 1 \u6708\u9644\u8fd1\u3002\u8fd9\u662f\u9884\u6d4b\u7a97\u53e3\uff0c\u4e0d\u662f\u627f\u8bfa\uff1b\u4e0b\u6b21\u53d1\u751f\u65f6\u56de\u6765\u8865\u4e00\u6761\uff0c\u53ef\u8fdb\u4e00\u6b65\u5206\u8fa8\u3002", + "used_for_scoring": false + } + ], + "horary_observation": { + "status": "skipped", + "user_meaning": "\u6ca1\u6709\u7b2c\u4e00\u6b21\u95ee\u8d77\u8fd9\u4ef6\u4e8b\u7684\u65f6\u95f4\uff0c\u5360\u95ee\u89c2\u5bdf\u672a\u6267\u884c\u3002", + "unique_minute_claim": false + }, + "unique_minute_claim": false + }, + "candidate_feature_snapshot": { + "calculation_spec_hash": "e03dfc555a247e78549768cc0b487cb427414633982212debd9cd6e60bb67292", + "algorithm_version": "rectification-v5-matrix-scoring-7", + "candidate_count": 3, + "feature_hash": "36190982db75d3c9fe7c584607f25342531049c3d751060075519dbbd68033db", + "features": [ + { + "time": "12:00", + "ascendant_degree": 349.5527, + "ascendant_sign_index": 11, + "pada_index": 104, + "bhava_sign_index": 10, + "hora_sign_index": 1, + "ghati_sign_index": 1, + "pranapada_sign_index": 2, + "kp1_sub_index": 8, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8, + "kp_cusps": { + "status": "executed", + "house_system": "placidus", + "ayanamsa": "krishnamurti", + "houses": { + "1": { + "cusp_degree": 348.2035, + "sign": "Pisces", + "rasi_lord": "Jupiter", + "nakshatra": "Revati", + "nakshatra_lord": "Mercury", + "pada": 1, + "sub_lord": "Mercury", + "sub_sub_lord": "Jupiter", + "sub_index": 8 + }, + "2": { + "cusp_degree": 26.072, + "sign": "Aries", + "rasi_lord": "Mars", + "nakshatra": "Bharani", + "nakshatra_lord": "Venus", + "pada": 4, + "sub_lord": "Ketu", + "sub_sub_lord": "Sun", + "sub_index": 0 + }, + "3": { + "cusp_degree": 51.3992, + "sign": "Taurus", + "rasi_lord": "Venus", + "nakshatra": "Rohini", + "nakshatra_lord": "Moon", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Rahu", + "sub_index": 1 + }, + "4": { + "cusp_degree": 72.7617, + "sign": "Gemini", + "rasi_lord": "Mercury", + "nakshatra": "Ardra", + "nakshatra_lord": "Rahu", + "pada": 2, + "sub_lord": "Mercury", + "sub_sub_lord": "Mercury", + "sub_index": 8 + }, + "5": { + "cusp_degree": 95.1596, + "sign": "Cancer", + "rasi_lord": "Moon", + "nakshatra": "Pushya", + "nakshatra_lord": "Saturn", + "pada": 1, + "sub_lord": "Saturn", + "sub_sub_lord": "Rahu", + "sub_index": 7 + }, + "6": { + "cusp_degree": 124.1819, + "sign": "Leo", + "rasi_lord": "Sun", + "nakshatra": "Magha", + "nakshatra_lord": "Ketu", + "pada": 2, + "sub_lord": "Moon", + "sub_sub_lord": "Saturn", + "sub_index": 3 + }, + "7": { + "cusp_degree": 168.2035, + "sign": "Virgo", + "rasi_lord": "Mercury", + "nakshatra": "Hasta", + "nakshatra_lord": "Moon", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Venus", + "sub_index": 8 + }, + "8": { + "cusp_degree": 206.072, + "sign": "Libra", + "rasi_lord": "Venus", + "nakshatra": "Vishakha", + "nakshatra_lord": "Jupiter", + "pada": 2, + "sub_lord": "Ketu", + "sub_sub_lord": "Mars", + "sub_index": 0 + }, + "9": { + "cusp_degree": 231.3992, + "sign": "Scorpio", + "rasi_lord": "Mars", + "nakshatra": "Jyeshtha", + "nakshatra_lord": "Mercury", + "pada": 2, + "sub_lord": "Venus", + "sub_sub_lord": "Mercury", + "sub_index": 1 + }, + "10": { + "cusp_degree": 252.7617, + "sign": "Sagittarius", + "rasi_lord": "Jupiter", + "nakshatra": "Mula", + "nakshatra_lord": "Ketu", + "pada": 4, + "sub_lord": "Mercury", + "sub_sub_lord": "Rahu", + "sub_index": 8 + }, + "11": { + "cusp_degree": 275.1596, + "sign": "Capricorn", + "rasi_lord": "Saturn", + "nakshatra": "Uttara Ashadha", + "nakshatra_lord": "Sun", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Mercury", + "sub_index": 8 + }, + "12": { + "cusp_degree": 304.1819, + "sign": "Aquarius", + "rasi_lord": "Saturn", + "nakshatra": "Dhanishta", + "nakshatra_lord": "Mars", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Saturn", + "sub_index": 1 + } + }, + "indices": { + "kp1_sub_index": 8, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8 + }, + "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002" + }, + "varga_ascendants": { + "D2": 4, + "D3": 3, + "D4": 5, + "D5": 10, + "D7": 9, + "D9": 8, + "D10": 1, + "D11": 2, + "D12": 6, + "D24": 6, + "D30": 9 + }, + "arudha_signs": { + "A7": 1, + "A10": 5, + "UL": 6 + }, + "available_layers": [ + "A10", + "A7", + "Ashtakavarga", + "D1", + "D10", + "D11", + "D12", + "D2", + "D24", + "D3", + "D30", + "D4", + "D5", + "D7", + "D9", + "KP_cusps", + "Shadbala", + "UL" + ], + "blocked_layers": [], + "fingerprints": { + "natal": "076a15184607e94ed9d4bd00776c3545cab1fc29c697c75948ac2054821c5102", + "vargas": "e0b1798bb91fef314c36311fb612af9c6943a8c0b57a60ae68e681a09a49abb9", + "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0", + "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1", + "shadbala": "359058cd01c82648e7dc1906984b88a40684d22d5fa08e5684b650c5d2a001fd", + "static": "636d5aa6ab38359a36c3bb24875e57f17a4724067f33a5747ddeaa261b9334f1" + } + }, + { + "time": "12:01", + "ascendant_degree": 349.9746, + "ascendant_sign_index": 11, + "pada_index": 104, + "bhava_sign_index": 10, + "hora_sign_index": 1, + "ghati_sign_index": 1, + "pranapada_sign_index": 2, + "kp1_sub_index": 0, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8, + "kp_cusps": { + "status": "executed", + "house_system": "placidus", + "ayanamsa": "krishnamurti", + "houses": { + "1": { + "cusp_degree": 348.625, + "sign": "Pisces", + "rasi_lord": "Jupiter", + "nakshatra": "Revati", + "nakshatra_lord": "Mercury", + "pada": 1, + "sub_lord": "Ketu", + "sub_sub_lord": "Venus", + "sub_index": 0 + }, + "2": { + "cusp_degree": 26.3705, + "sign": "Aries", + "rasi_lord": "Mars", + "nakshatra": "Bharani", + "nakshatra_lord": "Venus", + "pada": 4, + "sub_lord": "Ketu", + "sub_sub_lord": "Jupiter", + "sub_index": 0 + }, + "3": { + "cusp_degree": 51.6406, + "sign": "Taurus", + "rasi_lord": "Venus", + "nakshatra": "Rohini", + "nakshatra_lord": "Moon", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Jupiter", + "sub_index": 1 + }, + "4": { + "cusp_degree": 72.9921, + "sign": "Gemini", + "rasi_lord": "Mercury", + "nakshatra": "Ardra", + "nakshatra_lord": "Rahu", + "pada": 2, + "sub_lord": "Mercury", + "sub_sub_lord": "Venus", + "sub_index": 8 + }, + "5": { + "cusp_degree": 95.4163, + "sign": "Cancer", + "rasi_lord": "Moon", + "nakshatra": "Pushya", + "nakshatra_lord": "Saturn", + "pada": 1, + "sub_lord": "Saturn", + "sub_sub_lord": "Jupiter", + "sub_index": 7 + }, + "6": { + "cusp_degree": 124.5135, + "sign": "Leo", + "rasi_lord": "Sun", + "nakshatra": "Magha", + "nakshatra_lord": "Ketu", + "pada": 2, + "sub_lord": "Moon", + "sub_sub_lord": "Ketu", + "sub_index": 3 + }, + "7": { + "cusp_degree": 168.625, + "sign": "Virgo", + "rasi_lord": "Mercury", + "nakshatra": "Hasta", + "nakshatra_lord": "Moon", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Moon", + "sub_index": 8 + }, + "8": { + "cusp_degree": 206.3705, + "sign": "Libra", + "rasi_lord": "Venus", + "nakshatra": "Vishakha", + "nakshatra_lord": "Jupiter", + "pada": 2, + "sub_lord": "Ketu", + "sub_sub_lord": "Saturn", + "sub_index": 0 + }, + "9": { + "cusp_degree": 231.6406, + "sign": "Scorpio", + "rasi_lord": "Mars", + "nakshatra": "Jyeshtha", + "nakshatra_lord": "Mercury", + "pada": 2, + "sub_lord": "Sun", + "sub_sub_lord": "Moon", + "sub_index": 2 + }, + "10": { + "cusp_degree": 252.9921, + "sign": "Sagittarius", + "rasi_lord": "Jupiter", + "nakshatra": "Mula", + "nakshatra_lord": "Ketu", + "pada": 4, + "sub_lord": "Mercury", + "sub_sub_lord": "Jupiter", + "sub_index": 8 + }, + "11": { + "cusp_degree": 275.4163, + "sign": "Capricorn", + "rasi_lord": "Saturn", + "nakshatra": "Uttara Ashadha", + "nakshatra_lord": "Sun", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Ketu", + "sub_index": 8 + }, + "12": { + "cusp_degree": 304.5135, + "sign": "Aquarius", + "rasi_lord": "Saturn", + "nakshatra": "Dhanishta", + "nakshatra_lord": "Mars", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Mercury", + "sub_index": 1 + } + }, + "indices": { + "kp1_sub_index": 0, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8 + }, + "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002" + }, + "varga_ascendants": { + "D2": 4, + "D3": 3, + "D4": 5, + "D5": 10, + "D7": 9, + "D9": 8, + "D10": 1, + "D11": 2, + "D12": 6, + "D24": 6, + "D30": 9 + }, + "arudha_signs": { + "A7": 1, + "A10": 5, + "UL": 6 + }, + "available_layers": [ + "A10", + "A7", + "Ashtakavarga", + "D1", + "D10", + "D11", + "D12", + "D2", + "D24", + "D3", + "D30", + "D4", + "D5", + "D7", + "D9", + "KP_cusps", + "Shadbala", + "UL" + ], + "blocked_layers": [], + "fingerprints": { + "natal": "fc03340c27facfe96e961087d20c5a479ef159af1aec870f2a3ab3ca5e9b5ec5", + "vargas": "e0b1798bb91fef314c36311fb612af9c6943a8c0b57a60ae68e681a09a49abb9", + "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0", + "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1", + "shadbala": "ce15456bdba61e09f8d4cf58d89965a72cadc5df65ffda9f92feb13b587f2553", + "static": "4c92d28e9105330c8a10bc418e12142bf5a9de8cf5162df62c269b1b78d15e43" + } + }, + { + "time": "12:02", + "ascendant_degree": 350.3959, + "ascendant_sign_index": 11, + "pada_index": 105, + "bhava_sign_index": 10, + "hora_sign_index": 1, + "ghati_sign_index": 1, + "pranapada_sign_index": 2, + "kp1_sub_index": 0, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8, + "kp_cusps": { + "status": "executed", + "house_system": "placidus", + "ayanamsa": "krishnamurti", + "houses": { + "1": { + "cusp_degree": 349.0467, + "sign": "Pisces", + "rasi_lord": "Jupiter", + "nakshatra": "Revati", + "nakshatra_lord": "Mercury", + "pada": 1, + "sub_lord": "Ketu", + "sub_sub_lord": "Jupiter", + "sub_index": 0 + }, + "2": { + "cusp_degree": 26.6689, + "sign": "Aries", + "rasi_lord": "Mars", + "nakshatra": "Krittika", + "nakshatra_lord": "Sun", + "pada": 1, + "sub_lord": "Sun", + "sub_sub_lord": "Sun", + "sub_index": 2 + }, + "3": { + "cusp_degree": 51.882, + "sign": "Taurus", + "rasi_lord": "Venus", + "nakshatra": "Rohini", + "nakshatra_lord": "Moon", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Saturn", + "sub_index": 1 + }, + "4": { + "cusp_degree": 73.2229, + "sign": "Gemini", + "rasi_lord": "Mercury", + "nakshatra": "Ardra", + "nakshatra_lord": "Rahu", + "pada": 2, + "sub_lord": "Mercury", + "sub_sub_lord": "Venus", + "sub_index": 8 + }, + "5": { + "cusp_degree": 95.6738, + "sign": "Cancer", + "rasi_lord": "Moon", + "nakshatra": "Pushya", + "nakshatra_lord": "Saturn", + "pada": 1, + "sub_lord": "Mercury", + "sub_sub_lord": "Mercury", + "sub_index": 8 + }, + "6": { + "cusp_degree": 124.8461, + "sign": "Leo", + "rasi_lord": "Sun", + "nakshatra": "Magha", + "nakshatra_lord": "Ketu", + "pada": 2, + "sub_lord": "Mars", + "sub_sub_lord": "Rahu", + "sub_index": 4 + }, + "7": { + "cusp_degree": 169.0467, + "sign": "Virgo", + "rasi_lord": "Mercury", + "nakshatra": "Hasta", + "nakshatra_lord": "Moon", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Rahu", + "sub_index": 8 + }, + "8": { + "cusp_degree": 206.6689, + "sign": "Libra", + "rasi_lord": "Venus", + "nakshatra": "Vishakha", + "nakshatra_lord": "Jupiter", + "pada": 3, + "sub_lord": "Venus", + "sub_sub_lord": "Venus", + "sub_index": 1 + }, + "9": { + "cusp_degree": 231.882, + "sign": "Scorpio", + "rasi_lord": "Mars", + "nakshatra": "Jyeshtha", + "nakshatra_lord": "Mercury", + "pada": 2, + "sub_lord": "Sun", + "sub_sub_lord": "Saturn", + "sub_index": 2 + }, + "10": { + "cusp_degree": 253.2229, + "sign": "Sagittarius", + "rasi_lord": "Jupiter", + "nakshatra": "Mula", + "nakshatra_lord": "Ketu", + "pada": 4, + "sub_lord": "Mercury", + "sub_sub_lord": "Saturn", + "sub_index": 8 + }, + "11": { + "cusp_degree": 275.6738, + "sign": "Capricorn", + "rasi_lord": "Saturn", + "nakshatra": "Uttara Ashadha", + "nakshatra_lord": "Sun", + "pada": 3, + "sub_lord": "Mercury", + "sub_sub_lord": "Venus", + "sub_index": 8 + }, + "12": { + "cusp_degree": 304.8461, + "sign": "Aquarius", + "rasi_lord": "Saturn", + "nakshatra": "Dhanishta", + "nakshatra_lord": "Mars", + "pada": 4, + "sub_lord": "Venus", + "sub_sub_lord": "Ketu", + "sub_index": 1 + } + }, + "indices": { + "kp1_sub_index": 0, + "kp4_sub_index": 8, + "kp7_sub_index": 8, + "kp10_sub_index": 8 + }, + "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002" + }, + "varga_ascendants": { + "D2": 4, + "D3": 7, + "D4": 5, + "D5": 10, + "D7": 9, + "D9": 9, + "D10": 1, + "D11": 2, + "D12": 7, + "D24": 7, + "D30": 7 + }, + "arudha_signs": { + "A7": 1, + "A10": 5, + "UL": 6 + }, + "available_layers": [ + "A10", + "A7", + "Ashtakavarga", + "D1", + "D10", + "D11", + "D12", + "D2", + "D24", + "D3", + "D30", + "D4", + "D5", + "D7", + "D9", + "KP_cusps", + "Shadbala", + "UL" + ], + "blocked_layers": [], + "fingerprints": { + "natal": "87c2fd4ce5a283b332f7b97caaa6edb29d6ee11e05a419a4a214933f5b7179df", + "vargas": "75d7881b48fb899ee7d0902c42e3bc878ba23e92efee34449563c5ce50286c8a", + "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0", + "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1", + "shadbala": "c7790ab436adc29223624d0e104f286bde70a9fddcbf09bc68ba975eac7d97f2", + "static": "74a2711e77bfc56684a7672e6c256cfc9a0547035fa76c162ef88d4dc1bb95aa" + } + } + ] + } +} diff --git a/tests/test_rectification_engine_memoization.py b/tests/test_rectification_engine_memoization.py new file mode 100644 index 00000000..9df9f7a7 --- /dev/null +++ b/tests/test_rectification_engine_memoization.py @@ -0,0 +1,374 @@ +"""Memoization for candidate-minute invariants in the rectification engine. + +Golden payload in tests/golden/rectification_engine_memoization_v1.json was +produced from origin/staging @ a8d29d1b before any memoization landed. +""" + +from __future__ import annotations + +import inspect +import json +from datetime import date, datetime +from pathlib import Path +from typing import Any +from unittest.mock import patch + +from scripts.active_rectification_event_engine import ( + _candidate_datetimes, + _controlled_transit_rules, + _shadbala_verified_components_auxiliary, + build_candidate_static_context, + compute_event_candidate_rows, +) +from scripts.rectification.api_service import score_candidates +from scripts.rectification.candidate_contrast import opportunity_from_probe +from scripts.rectification.contracts import normalize_rectification_request +from scripts.rectification.refinement_packet import build_refinement_packet +from scripts.rectification.scoring_service import sample_event_dates, scoreable_request +import scripts.active_rectification_event_engine as event_engine +import scripts.rectification.event_probes as event_probes +import scripts.rectification.scoring_service as scoring_service +import shadbala + +ROOT = Path(__file__).resolve().parents[1] +GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v1.json" +FROZEN_TODAY = date(2026, 9, 16) +TIMING_KEYS = frozenset({"column_compare_ms"}) +SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53" + + +def public_score_request() -> dict[str, Any]: + """Fictional events on the public 1990-01-01 Beijing smoke chart.""" + return { + "birth_date": "1990-01-01", + "start_time": "12:00", + "end_time": "12:02", + "lat": 39.9, + "lon": 116.4, + "tz": 8, + "events": [ + { + "id": "00000000-0000-4000-8000-000000000001", + "domain": "education", + "event_kind": "education_start", + "date_start": "2008-01-01", + "date_end": "2008-12-31", + "precision": "year", + "summary": "入学", + }, + { + "id": "00000000-0000-4000-8000-000000000002", + "domain": "career", + "event_kind": "career_entry", + "date_start": "2012-06-01", + "date_end": "2012-06-30", + "precision": "month", + "summary": "入职", + }, + ], + } + + +def _strip_timing(value: Any) -> Any: + if isinstance(value, dict): + return { + key: _strip_timing(item) + for key, item in value.items() + if key not in TIMING_KEYS + } + if isinstance(value, list): + return [_strip_timing(item) for item in value] + return value + + +def _normalized_request() -> dict[str, Any]: + return normalize_rectification_request(public_score_request(), today=FROZEN_TODAY) + + +def _golden_payload() -> dict[str, Any]: + scored = score_candidates(_normalized_request()) + return { + "source_commit": SOURCE_COMMIT, + "candidate_scores": scored["candidate_scores"], + "decision_receipt": _strip_timing(scored["decision_receipt"]), + "candidate_feature_snapshot": scored["candidate_feature_snapshot"], + } + + +def write_golden(path: Path = GOLDEN_PATH) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(_golden_payload(), ensure_ascii=True, indent=2) + "\n", encoding="utf-8") + return path + + +def _count_calls(monkeypatch, owner: Any, name: str, *, from_engine: bool = False) -> list[int]: + original = getattr(owner, name) + counter = [0] + + def wrapped(*args: Any, **kwargs: Any) -> Any: + if from_engine: + caller = inspect.stack()[1].filename.replace("\\", "/") + if not caller.endswith("/active_rectification_event_engine.py"): + return original(*args, **kwargs) + counter[0] += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(owner, name, wrapped) + return counter + + +def test_score_candidates_matches_baseline_golden() -> None: + expected = json.loads(GOLDEN_PATH.read_text(encoding="utf-8")) + actual = json.loads(json.dumps(_golden_payload(), ensure_ascii=True)) + assert actual["candidate_scores"] == expected["candidate_scores"] + assert actual["decision_receipt"] == expected["decision_receipt"] + + +def test_shadbala_verified_fields_match_with_and_without_birth_minute() -> None: + request = { + "birth_date": "1990-01-01", + "start_time": "12:17", + "end_time": "12:17", + "lat": 39.9, + "lon": 116.4, + "tz": 8, + "events": [], + } + candidate_at = datetime(1990, 1, 1, 12, 17) + context = build_candidate_static_context(request, candidate_at) + chart = context["chart"] + planets = chart.get("planets", {}) + sun = float(planets["Sun"]["lon"]) + moon = float(planets["Moon"]["lon"]) + sign = str(chart["ascendant"].get("sign")) + birth_hour = candidate_at.hour + candidate_at.minute / 60 + with_minute = shadbala.calc_shadbala( + planets, + sign, + birth_hour, + sun, + moon, + birth_minute=float(candidate_at.minute), + ) + without_minute = shadbala.calc_shadbala(planets, sign, birth_hour, sun, moon) + fields_with = { + planet: ( + float((row.get("sthana_bala") or {}).get("total", 0)), + float(row.get("drik_bala", 0)), + float(row.get("naisargika_bala", 0)), + ) + for planet, row in (with_minute.get("planets") or {}).items() + } + fields_without = { + planet: ( + float((row.get("sthana_bala") or {}).get("total", 0)), + float(row.get("drik_bala", 0)), + float(row.get("naisargika_bala", 0)), + ) + for planet, row in (without_minute.get("planets") or {}).items() + } + assert fields_with == fields_without + reused_rules, reused_points = _shadbala_verified_components_auxiliary( + chart, birth_hour, ("Sun", "Moon", "Mars"), shadbala_result=with_minute, + ) + fresh_rules, fresh_points = _shadbala_verified_components_auxiliary( + chart, birth_hour, ("Sun", "Moon", "Mars"), + ) + assert reused_rules == fresh_rules + assert reused_points == fresh_points + + +def test_shadbala_and_ashtakavarga_run_once_per_candidate(monkeypatch) -> None: + shadbala_calls = _count_calls(monkeypatch, event_engine.shadbala, "calc_shadbala") + ashtakavarga_calls = _count_calls(monkeypatch, event_engine.ashtakavarga, "calc_ashtakavarga") + request = _normalized_request() + scored = score_candidates(request) + candidate_count = len(scored["candidate_scores"]) + assert candidate_count >= 2 + assert len(request["events"]) >= 2 + year_samples = sample_event_dates(request["events"][0]) + assert request["events"][0]["precision"] == "year" + assert len(year_samples) >= 2 + assert shadbala_calls[0] == candidate_count + assert ashtakavarga_calls[0] == candidate_count + + +def test_dasha_timelines_run_once_per_candidate(monkeypatch) -> None: + # Year-only events skip dasha-transition proximity, which also calls these + # two functions from event_probes. The scoring path itself must stay 1×/minute. + body = public_score_request() + body["events"][1]["precision"] = "year" + body["events"][1]["date_start"] = "2012-01-01" + body["events"][1]["date_end"] = "2012-12-31" + request = normalize_rectification_request(body, today=FROZEN_TODAY) + vim_calls = _count_calls( + monkeypatch, event_engine.dasha_analyzer, "build_dasha_timeline", from_engine=True, + ) + narayana_calls = _count_calls( + monkeypatch, event_engine.narayana_dasha, "calc_narayana_mahadasha", from_engine=True, + ) + scored = score_candidates(request) + candidate_count = len(scored["candidate_scores"]) + assert len(request["events"]) >= 2 + assert vim_calls[0] == candidate_count + assert narayana_calls[0] == candidate_count + + +def test_transit_charts_match_unique_event_dates_not_candidates(monkeypatch) -> None: + transit_calls = [0] + original = event_engine.domain_calculation_service.compute_chart + + def wrapped(payload: dict[str, Any], *args: Any, **kwargs: Any) -> Any: + # Natal candidates in this fixture also sit at 12:00; transit charts use the event year. + if payload.get("hour") == 12 and payload.get("minute") == 0 and payload.get("year") != 1990: + transit_calls[0] += 1 + return original(payload, *args, **kwargs) + + monkeypatch.setattr(event_engine.domain_calculation_service, "compute_chart", wrapped) + request = _normalized_request() + scored = score_candidates(request) + candidate_count = len(scored["candidate_scores"]) + expected_dates = { + sampled + for event in scoreable_request(request)["events"] + for sampled in sample_event_dates(event) + } + assert candidate_count >= 2 + assert len(expected_dates) >= 2 + assert transit_calls[0] == len(expected_dates) + assert transit_calls[0] != candidate_count * len(expected_dates) + + +def test_year_precision_transits_still_short_circuit() -> None: + request = { + "birth_date": "1990-01-01", + "start_time": "12:00", + "end_time": "12:00", + "lat": 39.9, + "lon": 116.4, + "tz": 8, + "events": [{ + "id": "00000000-0000-4000-8000-000000000009", + "domain": "career", + "event_kind": "career_entry", + "date": "2012", + "precision": "year", + "summary": "入职", + }], + } + with patch.object(event_engine.domain_calculation_service, "compute_chart") as compute_chart: + rules = _controlled_transit_rules(request, request["events"][0], 0, (10,)) + assert rules == [] + compute_chart.assert_not_called() + + +def _probe_request_and_built() -> tuple[dict[str, Any], dict[str, Any], list[str]]: + body = public_score_request() + body["events"] = [ + { + "id": f"00000000-0000-4000-8000-{index:012d}", + "domain": domain, + "event_kind": kind, + "date_start": "2012-01-01", + "date_end": "2012-12-31", + "precision": "year", + "summary": kind, + } + for index, (domain, kind) in enumerate( + ( + ("education", "education_start"), + ("career", "career_entry"), + ("relationship", "relationship_start"), + ), + start=1, + ) + ] + request = normalize_rectification_request(body, today=FROZEN_TODAY) + times = ["12:00", "12:01", "12:02"] + event_ids = [event["id"] for event in request["events"]] + built = { + "candidate_times": times, + "matrix": { + event_id: { + clock: {"points": 4, "rule_ids": ["vim_md_domain_house"], "technique_layers": ["vim_md_domain_house"]} + for clock in times + } + for event_id in event_ids + }, + "date_sensitivity": [], + "missing_layers": [], + "static_contexts": [], + } + return request, built, times + + +def test_discriminating_probes_run_once_when_probe_times_equal_grid(monkeypatch) -> None: + calls = _count_calls(monkeypatch, event_probes, "_discriminating_event_probe_lists") + request, built, times = _probe_request_and_built() + packet = build_refinement_packet( + request, + built, + representative_time="12:00", + candidate_times=times, + ) + assert calls[0] == 1 + assert packet["candidate_contrast_opportunities"] == [ + opportunity_from_probe(probe) for probe in packet["discriminating_event_probes"] + ] + + +def test_discriminating_probes_run_twice_on_refresh_columns(monkeypatch) -> None: + calls = _count_calls(monkeypatch, event_probes, "_discriminating_event_probe_lists") + request, built, times = _probe_request_and_built() + request = {**request, "refresh_probes": True} + packet = build_refinement_packet( + request, + built, + representative_time="12:00", + candidate_times=times, + column_times=["12:00"], + ) + assert times != ["12:00"] + assert calls[0] == 2 + assert "discriminating_event_probes" in packet + assert "candidate_contrast_opportunities" in packet + + +def test_dead_row_cache_removed_from_scoring_service() -> None: + token = "_cached_" + "rows" + assert not hasattr(scoring_service, token) + hits: list[str] = [] + for folder in ("scripts", "tests"): + for path in (ROOT / folder).rglob("*.py"): + text = path.read_text(encoding="utf-8") + if token in text: + hits.append(str(path.relative_to(ROOT)).replace("\\", "/")) + assert hits == [] + + +def test_compute_event_candidate_rows_reuses_static_context_without_mutating_it() -> None: + request = { + "birth_date": "1990-01-01", + "start_time": "12:00", + "end_time": "12:01", + "lat": 39.9, + "lon": 116.4, + "tz": 8, + "events": [{ + "id": "00000000-0000-4000-8000-000000000003", + "domain": "career", + "event_kind": "career_entry", + "date": "2012-06-15", + "precision": "day", + "summary": "入职", + }], + } + contexts = [ + build_candidate_static_context(request, candidate) + for candidate in _candidate_datetimes(request) + ] + original_keys = [frozenset(context) for context in contexts] + rows = compute_event_candidate_rows(request, static_contexts=contexts) + assert len(rows) == 2 + assert [frozenset(context) for context in contexts] == original_keys + assert all("_transit_chart_cache" not in context for context in contexts)