a88467ffa8
Keep unique-top and width on confirmation only, and stop lagna-frame follow-ups from blocking cards on an already-scored cluster. Co-authored-by: Cursor <cursoragent@cursor.com>
555 lines
22 KiB
Python
555 lines
22 KiB
Python
"""Server-owned P0/P1 refinement packet for birth-time rectification.
|
||
|
||
Produces candidate-narrowing structure only. Never grants a unique minute
|
||
and never copies raw scores into public copy. D9/D10 sign names are method
|
||
contrast for the skill verification report, not unique-minute proof.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Sequence
|
||
|
||
from scripts.rectification.house_table import PLANET_ZH, SIGN_LORDS, SIGNS, SIGNS_CN
|
||
|
||
NAKSHATRA_SPAN = 40.0 / 3.0
|
||
NAKSHATRA_BOUNDARY_DEGREES = 2.0
|
||
MATCH_LABELS = {
|
||
"strong": "强相关",
|
||
"medium": "有关联",
|
||
"weak": "弱关联",
|
||
"none": "未见对应",
|
||
}
|
||
# Everyday A/B traits only. No Sanskrit names and no D9/D10 personality tables.
|
||
NAKSHATRA_TRAITS: tuple[tuple[str, str], ...] = (
|
||
("起步快、敢先动手", "更愿意把第一步走完再看"),
|
||
("事情来了会立刻表态", "先把感受压一压再开口"),
|
||
("喜欢把节奏拉开、自己掌握步调", "更在意别人是否跟得上"),
|
||
("照顾身边的人会放在前面", "需要先把自己安顿好"),
|
||
("愿意站到台前把话说清楚", "更习惯在旁边把事情理顺"),
|
||
("对细节和次序很敏感", "更看重大方向有没有走偏"),
|
||
("希望两边都能说得过去", "必要时会直接选边"),
|
||
("碰到转折会往深处想", "更想尽快回到能做事的状态"),
|
||
("愿意把视野拉远一点再决定", "更盯着眼前能落地的一步"),
|
||
("愿意为长期结果多熬一阵", "更怕把时间耗在看不见的地方"),
|
||
("想法一多就想换条路试试", "更想把一条路走稳"),
|
||
("情绪来了会先自己消化", "更需要说出来才过得去"),
|
||
("新开始会让人兴奋", "新开始会让人先观察一阵"),
|
||
("承诺一旦出口就很难收回", "承诺前会反复确认自己是不是真想"),
|
||
("变化来时先问值不值得", "变化来时先问自己扛不扛得住"),
|
||
("家里的事会牵动判断", "更想把家里的事和工作分开"),
|
||
("被看见会更有劲", "被看见反而会先退半步"),
|
||
("计划乱了会先整理清单", "计划乱了会先找一个人商量"),
|
||
("两边关系都想维持住", "维持不住时会干脆拉开距离"),
|
||
("压力大时会往内部找原因", "压力大时会先改外部条件"),
|
||
("愿意把决定放到更大的时间尺度", "更相信眼前这一段就够判断"),
|
||
("愿意为结构稳定让步", "稳定如果太闷就会想拆掉重来"),
|
||
("对规则和例外都很敏感", "更想先有一个能用的规则"),
|
||
("说不清的感受会先放着", "说不清就会反复确认"),
|
||
("一有机会就想动手试", "会先把退路看清楚再动"),
|
||
("对人的反应比对事情本身更敏感", "对事情进度比对气氛更敏感"),
|
||
("收尾时会想把未完成的交代清", "收尾时会想尽快开始下一件"),
|
||
)
|
||
|
||
|
||
def _clock(value: str) -> int:
|
||
return int(value[:2]) * 60 + int(value[3:5])
|
||
|
||
|
||
def _feature_time(feature: dict[str, Any]) -> str | None:
|
||
raw = feature.get("time")
|
||
if isinstance(raw, str) and len(raw) >= 5:
|
||
return raw[:5]
|
||
return None
|
||
|
||
|
||
def _features(built: dict[str, Any]) -> list[dict[str, Any]]:
|
||
rows: list[dict[str, Any]] = []
|
||
for context in built.get("static_contexts") or []:
|
||
if not isinstance(context, dict):
|
||
continue
|
||
feature = context.get("feature")
|
||
if not isinstance(feature, dict):
|
||
continue
|
||
time = _feature_time(feature)
|
||
if time:
|
||
rows.append(feature)
|
||
rows.sort(key=lambda item: _clock(str(_feature_time(item))))
|
||
return rows
|
||
|
||
|
||
def _sign_names(indices: set[int]) -> list[str]:
|
||
names: list[str] = []
|
||
for idx in sorted(indices):
|
||
if isinstance(idx, int) and 0 <= idx <= 11:
|
||
names.append(SIGNS_CN[SIGNS[idx]])
|
||
return names
|
||
|
||
|
||
def _has_gochara(rule_ids: Sequence[str]) -> bool:
|
||
return any("controlled_transit" in str(item) or str(item).startswith("gochara") for item in rule_ids)
|
||
|
||
|
||
def match_level(rule_ids: Sequence[str]) -> str:
|
||
ids = [str(item) for item in rule_ids]
|
||
if not ids or ids == ["no_domain_activation"]:
|
||
return "none"
|
||
if any(
|
||
item.startswith("vim_md_domain") or item.startswith("narayana_md_domain")
|
||
for item in ids
|
||
):
|
||
return "strong"
|
||
if any(
|
||
item.startswith("vim_ad_") or item.startswith("narayana_ad_")
|
||
for item in ids
|
||
):
|
||
return "medium"
|
||
if any(item.startswith("vim_") or item.startswith("narayana_") for item in ids):
|
||
return "weak"
|
||
if _has_gochara(ids):
|
||
return "medium"
|
||
return "none"
|
||
|
||
|
||
def _tracks(rule_ids: Sequence[str]) -> list[str]:
|
||
tracks: list[str] = []
|
||
if any(str(item).startswith("vim_") for item in rule_ids):
|
||
tracks.append("vimshottari")
|
||
if any(str(item).startswith("narayana_") for item in rule_ids):
|
||
tracks.append("narayana")
|
||
if _has_gochara(rule_ids):
|
||
tracks.append("gochara")
|
||
return tracks
|
||
|
||
|
||
def _split_track_points(rule_ids: Sequence[str], points: float) -> tuple[float, float]:
|
||
vim = sum(str(item).startswith("vim_") for item in rule_ids)
|
||
narayana = sum(str(item).startswith("narayana_") for item in rule_ids)
|
||
total = vim + narayana
|
||
if total == 0:
|
||
return 0.0, 0.0
|
||
return points * vim / total, points * narayana / total
|
||
|
||
|
||
_LAYER_LABEL = {
|
||
"d1": "本命上升",
|
||
"d9": "D9",
|
||
"d10": "D10",
|
||
"d4": "D4",
|
||
"d5": "D5",
|
||
"d7": "D7",
|
||
"d12": "D12",
|
||
"d24": "D24",
|
||
"d2": "D2",
|
||
"d11": "D11",
|
||
"d30": "D30",
|
||
"pada": "Nakshatra pada",
|
||
"hora": "Hora Lagna",
|
||
"ghati": "Ghati Lagna",
|
||
"bhava": "Bhava Lagna",
|
||
"pranapada": "Pranapada Lagna",
|
||
"kp1": "KP 1宫子主",
|
||
"kp4": "KP 4宫子主",
|
||
"kp7": "KP 7宫子主",
|
||
"kp10": "KP 10宫子主",
|
||
}
|
||
|
||
|
||
def _scan_layer_value(feature: dict[str, Any], layer: str) -> int | None:
|
||
vargas = feature.get("varga_ascendants") if isinstance(feature.get("varga_ascendants"), dict) else {}
|
||
raw = {
|
||
"d1": feature.get("ascendant_sign_index"),
|
||
"d9": vargas.get("D9"),
|
||
"d10": vargas.get("D10"),
|
||
"d4": vargas.get("D4"),
|
||
"d5": vargas.get("D5"),
|
||
"d7": vargas.get("D7"),
|
||
"d12": vargas.get("D12"),
|
||
"d24": vargas.get("D24"),
|
||
"d2": vargas.get("D2"),
|
||
"d11": vargas.get("D11"),
|
||
"d30": vargas.get("D30"),
|
||
"pada": feature.get("pada_index"),
|
||
"hora": feature.get("hora_sign_index"),
|
||
"ghati": feature.get("ghati_sign_index"),
|
||
"bhava": feature.get("bhava_sign_index"),
|
||
"pranapada": feature.get("pranapada_sign_index"),
|
||
"kp1": feature.get("kp1_sub_index"),
|
||
"kp4": feature.get("kp4_sub_index"),
|
||
"kp7": feature.get("kp7_sub_index"),
|
||
"kp10": feature.get("kp10_sub_index"),
|
||
}.get(layer)
|
||
return raw if isinstance(raw, int) else None
|
||
|
||
|
||
def window_scan(
|
||
built: dict[str, Any],
|
||
*,
|
||
start_minute: int | None = None,
|
||
end_minute: int | None = None,
|
||
) -> dict[str, Any]:
|
||
"""D1/D9/D10/D4/D5/D7/D12/D24/D2/D11/D30 plus display-only pada/Hora/Ghati/Bhava/Pranapada/KP."""
|
||
counts: dict[str, set[int]] = {layer: set() for layer in _LAYER_LABEL}
|
||
transitions: list[dict[str, Any]] = []
|
||
previous: dict[str, int | None] | None = None
|
||
for feature in _features(built):
|
||
time = _feature_time(feature)
|
||
if time and start_minute is not None and end_minute is not None:
|
||
clock = _clock(time)
|
||
if clock < start_minute or clock > end_minute:
|
||
continue
|
||
current = {layer: _scan_layer_value(feature, layer) for layer in _LAYER_LABEL}
|
||
for layer, bucket in counts.items():
|
||
value = current[layer]
|
||
if isinstance(value, int):
|
||
bucket.add(value)
|
||
if previous and time:
|
||
for layer, label in _LAYER_LABEL.items():
|
||
before = previous[layer]
|
||
after = current[layer]
|
||
if isinstance(before, int) and isinstance(after, int) and before != after:
|
||
transitions.append({
|
||
"layer": layer,
|
||
"at": time,
|
||
"user_meaning": f"{label} 在 {time} 发生变化",
|
||
})
|
||
previous = current
|
||
payload: dict[str, Any] = {
|
||
"scanned": True,
|
||
"confirmation_allowed": False,
|
||
"unique_minute_claim": False,
|
||
"transitions": transitions,
|
||
}
|
||
for layer in _LAYER_LABEL:
|
||
payload[f"{layer}_lagna_count" if layer.startswith("d") else f"{layer}_count"] = len(counts[layer])
|
||
payload[f"{layer}_candidates_differ"] = len(counts[layer]) > 1
|
||
payload["d9_sign_names"] = _sign_names(counts["d9"])
|
||
payload["d10_sign_names"] = _sign_names(counts["d10"])
|
||
return payload
|
||
|
||
|
||
def event_dasha_ledger(
|
||
request: dict[str, Any],
|
||
built: dict[str, Any],
|
||
representative_time: str | None,
|
||
) -> list[dict[str, Any]]:
|
||
if not representative_time:
|
||
return []
|
||
matrix = built.get("matrix") or {}
|
||
rows: list[dict[str, Any]] = []
|
||
for event in request.get("events") or []:
|
||
if not isinstance(event, dict):
|
||
continue
|
||
contribution = (matrix.get(event.get("id")) or {}).get(representative_time)
|
||
if not isinstance(contribution, dict):
|
||
continue
|
||
rule_ids = contribution.get("rule_ids") or []
|
||
level = match_level(rule_ids)
|
||
summary = str(event.get("summary") or "").strip() or "这条经历"
|
||
tracks = _tracks(rule_ids)
|
||
track_text = "、".join(
|
||
"主限" if track == "vimshottari" else "分盘大运" if track == "narayana" else "受控行运"
|
||
for track in tracks
|
||
) or "现有大运层"
|
||
gochara_hit = _has_gochara(rule_ids)
|
||
rows.append({
|
||
"summary": summary[:80],
|
||
"match": level,
|
||
"match_label": MATCH_LABELS[level],
|
||
"tracks": tracks,
|
||
"gochara": "activated" if gochara_hit else "not_seen",
|
||
"user_meaning": (
|
||
f"{summary[:40]}:{MATCH_LABELS[level]}({track_text}"
|
||
f"{';Gochara 激活相关宫' if gochara_hit else ';Gochara 未见对应'})"
|
||
),
|
||
})
|
||
return rows
|
||
|
||
|
||
def event_fit_rate(rows: Sequence[dict[str, Any]]) -> dict[str, Any]:
|
||
total = len(rows)
|
||
matched = sum(1 for row in rows if row.get("match") in {"strong", "medium"})
|
||
if total == 0:
|
||
return {
|
||
"matched": 0,
|
||
"total": 0,
|
||
"percent": None,
|
||
"band": "insufficient",
|
||
"label": "事件不足,无法计算吻合率",
|
||
"unique_minute_claim": False,
|
||
"user_meaning": "事件–Dasha–Gochara 表还没有可评分行。这不是唯一分钟确认。",
|
||
}
|
||
percent = round(100 * matched / total)
|
||
band = "high" if percent >= 80 else "medium" if percent >= 60 else "low"
|
||
label = (
|
||
"高度吻合(事件吻合率 ≥80%)" if band == "high"
|
||
else "中度吻合(事件吻合率 60–80%)" if band == "medium"
|
||
else "低度吻合(事件吻合率 <60%)"
|
||
)
|
||
return {
|
||
"matched": matched,
|
||
"total": total,
|
||
"percent": percent,
|
||
"band": band,
|
||
"label": label,
|
||
"unique_minute_claim": False,
|
||
"user_meaning": (
|
||
f"当前窗 {matched}/{total} 件已确认事件与 Dasha/Gochara 吻合,{label}。"
|
||
"这是相对拟合,不是已确认唯一出生分钟。"
|
||
),
|
||
}
|
||
|
||
|
||
def dasha_agreement(built: dict[str, Any], candidate_times: Sequence[str]) -> dict[str, Any]:
|
||
times = [str(item)[:5] for item in candidate_times if isinstance(item, str) and len(str(item)) >= 5]
|
||
if not times:
|
||
return {
|
||
"status": "unavailable",
|
||
"vimshottari_top": None,
|
||
"narayana_top": None,
|
||
"user_meaning": "还没有足够的大运对照。",
|
||
}
|
||
vim_scores = {time: 0.0 for time in times}
|
||
narayana_scores = {time: 0.0 for time in times}
|
||
for contributions in (built.get("matrix") or {}).values():
|
||
if not isinstance(contributions, dict):
|
||
continue
|
||
for time in times:
|
||
cell = contributions.get(time)
|
||
if not isinstance(cell, dict):
|
||
continue
|
||
vim_points, narayana_points = _split_track_points(
|
||
cell.get("rule_ids") or [],
|
||
float(cell.get("points") or 0),
|
||
)
|
||
vim_scores[time] += vim_points
|
||
narayana_scores[time] += narayana_points
|
||
if all(value == 0 for value in vim_scores.values()) or all(value == 0 for value in narayana_scores.values()):
|
||
return {
|
||
"status": "partial",
|
||
"vimshottari_top": max(times, key=lambda time: vim_scores[time]) if any(vim_scores.values()) else None,
|
||
"narayana_top": max(times, key=lambda time: narayana_scores[time]) if any(narayana_scores.values()) else None,
|
||
"user_meaning": "主限和分盘大运还不能做成完整对照,只作观察。",
|
||
}
|
||
vim_top = max(times, key=lambda time: (vim_scores[time], -_clock(time)))
|
||
narayana_top = max(times, key=lambda time: (narayana_scores[time], -_clock(time)))
|
||
if vim_top == narayana_top:
|
||
return {
|
||
"status": "agree",
|
||
"vimshottari_top": vim_top,
|
||
"narayana_top": narayana_top,
|
||
"user_meaning": "主限和分盘大运都更支持同一段代表性时间。这仍不是唯一分钟确认。",
|
||
}
|
||
return {
|
||
"status": "conflict",
|
||
"vimshottari_top": vim_top,
|
||
"narayana_top": narayana_top,
|
||
"user_meaning": f"主限更偏向 {vim_top},分盘大运更偏向 {narayana_top}。冲突时不能按更高把握收口。",
|
||
}
|
||
|
||
|
||
def _house_lord_zh(asc_idx: int, house: int) -> str | None:
|
||
sign = SIGNS[(asc_idx + house - 1) % 12]
|
||
lord = SIGN_LORDS.get(sign)
|
||
return PLANET_ZH.get(lord) if lord else None
|
||
|
||
|
||
def lagna_contrast(built: dict[str, Any]) -> dict[str, Any] | None:
|
||
features = _features(built)
|
||
if not features:
|
||
return None
|
||
intervals: list[dict[str, Any]] = []
|
||
current: dict[str, Any] | None = None
|
||
for feature in features:
|
||
time = _feature_time(feature)
|
||
index = feature.get("ascendant_sign_index")
|
||
if not time or not isinstance(index, int) or index < 0 or index > 11:
|
||
continue
|
||
if current and current["d1_lagna_index"] == index:
|
||
current["end"] = time
|
||
continue
|
||
if current:
|
||
intervals.append(current)
|
||
sign = SIGNS_CN.get(SIGNS[index])
|
||
current = {
|
||
"start": time,
|
||
"end": time,
|
||
"d1_lagna_index": index,
|
||
"lagna": sign,
|
||
"lords": {
|
||
"l1": _house_lord_zh(index, 1),
|
||
"l4": _house_lord_zh(index, 4),
|
||
"l7": _house_lord_zh(index, 7),
|
||
"l10": _house_lord_zh(index, 10),
|
||
},
|
||
}
|
||
if current:
|
||
intervals.append(current)
|
||
if len(intervals) < 2:
|
||
return None
|
||
left, right = intervals[0], intervals[1]
|
||
return {
|
||
"intervals": intervals[:3],
|
||
"user_meaning": (
|
||
f"窗口里出现两段本命上升:{left['start']}-{left['end']} 为{left['lagna']},"
|
||
f"{right['start']}-{right['end']} 为{right['lagna']}。"
|
||
"可并列 D9/D10 类型表作校时方法,不是命运承诺,也不能确认唯一分钟。"
|
||
),
|
||
"unique_minute_claim": False,
|
||
}
|
||
|
||
|
||
def nakshatra_boundary(built: dict[str, Any], representative_time: str | None) -> dict[str, Any] | None:
|
||
features = {
|
||
_feature_time(feature): feature
|
||
for feature in _features(built)
|
||
if _feature_time(feature)
|
||
}
|
||
feature = features.get(representative_time or "") or (list(features.values())[0] if features else None)
|
||
if not isinstance(feature, dict):
|
||
return None
|
||
longitude = feature.get("ascendant_degree")
|
||
if not isinstance(longitude, (int, float)):
|
||
return None
|
||
wrapped = float(longitude) % 360.0
|
||
index = int(wrapped / NAKSHATRA_SPAN) % 27
|
||
position = wrapped % NAKSHATRA_SPAN
|
||
distance = min(position, NAKSHATRA_SPAN - position)
|
||
if distance > NAKSHATRA_BOUNDARY_DEGREES:
|
||
return {
|
||
"near_boundary": False,
|
||
"distance_degrees": round(distance, 4),
|
||
"user_meaning": None,
|
||
"options": [],
|
||
}
|
||
earlier_index = index if position <= NAKSHATRA_SPAN / 2 else (index - 1) % 27
|
||
later_index = (earlier_index + 1) % 27
|
||
earlier = NAKSHATRA_TRAITS[earlier_index]
|
||
later = NAKSHATRA_TRAITS[later_index]
|
||
return {
|
||
"near_boundary": True,
|
||
"distance_degrees": round(distance, 4),
|
||
"options": [
|
||
{
|
||
"key": "A",
|
||
"time_bias": "earlier",
|
||
"traits": list(earlier),
|
||
},
|
||
{
|
||
"key": "B",
|
||
"time_bias": "later",
|
||
"traits": list(later),
|
||
},
|
||
],
|
||
"user_meaning": (
|
||
"升点靠近两段日常节奏的交界。哪一组更像你近年的处事方式?"
|
||
f"A:{earlier[0]};{earlier[1]}。"
|
||
f"B:{later[0]};{later[1]}。"
|
||
"这只用来偏置时间窗,不能确认唯一分钟。"
|
||
),
|
||
}
|
||
|
||
|
||
def precision_stage(scan: dict[str, Any], event_count: int) -> dict[str, Any]:
|
||
if event_count <= 0:
|
||
current = "collect_events"
|
||
meaning = "还需要带大概时间的经历,才能开始缩小窗口。"
|
||
elif scan.get("d1_candidates_differ"):
|
||
current = "lagna_frame"
|
||
meaning = "本命上升还可能落在两段里。先补能分开这两段的带日期经历。"
|
||
elif scan.get("d9_candidates_differ"):
|
||
current = "d9_refine"
|
||
meaning = "本命上升已较稳,关系盘仍会换升。可再补一件记得时间的感情或关系变化。"
|
||
elif scan.get("d10_candidates_differ"):
|
||
current = "d10_refine"
|
||
meaning = "关系盘已较稳,事业盘仍会换升。可再补一件记得时间的工作变化。"
|
||
elif scan.get("d4_candidates_differ"):
|
||
current = "d4_refine"
|
||
meaning = "事业盘已较稳,居所盘仍会换升。可再补一件记得时间的搬家或住处变化。"
|
||
elif scan.get("d5_candidates_differ") or scan.get("d24_candidates_differ"):
|
||
current = "d5_refine"
|
||
meaning = "居所盘已较稳,成就盘或学业盘仍会换升。可再补一件记得时间的学业、考试或被委以责任的变化。"
|
||
else:
|
||
current = "ready_to_adopt"
|
||
meaning = "核心分盘已不再换升。可以采用代表性时间看盘,也可以再补主题经历。"
|
||
return {
|
||
"current": current,
|
||
"can_stop": current in {"d9_refine", "d10_refine", "d4_refine", "d5_refine", "theme_refine", "ready_to_adopt"},
|
||
"user_meaning": meaning,
|
||
"unique_minute_claim": False,
|
||
}
|
||
|
||
|
||
def oos_blind_prompts(request: dict[str, Any]) -> list[dict[str, Any]]:
|
||
covered = {
|
||
str(event.get("domain"))
|
||
for event in request.get("events") or []
|
||
if isinstance(event, dict) and event.get("domain")
|
||
}
|
||
catalog = (
|
||
("relationship", "校时还没用过感情这条线。有没有一件没提过、但记得大概时间的关系变化?"),
|
||
("career", "校时还没用过事业这条线。有没有一件没提过、但记得大概时间的工作变化?"),
|
||
("family", "校时还没用过家人这条线。有没有一件没提过、但记得大概时间的家人变化?"),
|
||
("education", "校时还没用过学习这条线。有没有一件没提过、但记得大概时间的学业变化?"),
|
||
("finance", "校时还没用过财务这条线。有没有一件没提过、但记得大概时间的收入或资产变化?"),
|
||
("health_pressure", "校时还没用过健康压力这条线。有没有一件没提过、但记得大概时间的身体或压力变化?"),
|
||
)
|
||
prompts = [
|
||
{"domain": domain, "user_meaning": meaning, "used_for_scoring": False}
|
||
for domain, meaning in catalog
|
||
if domain not in covered
|
||
]
|
||
return prompts[:3]
|
||
|
||
|
||
def cluster_scan(
|
||
built: dict[str, Any],
|
||
candidate_times: Sequence[str],
|
||
representative_time: str | None,
|
||
width_minutes: int | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Scan only the indistinguishable candidate cluster, not the full declared range."""
|
||
clocks: list[int] = []
|
||
for raw in [*candidate_times, representative_time]:
|
||
if isinstance(raw, str) and len(raw) >= 5:
|
||
clocks.append(_clock(raw[:5]))
|
||
if not clocks:
|
||
return window_scan(built)
|
||
lo, hi = min(clocks), max(clocks)
|
||
span = hi - lo + 1
|
||
width = max(int(width_minutes or 0), span, 1)
|
||
if width > span:
|
||
extra = width - span
|
||
lo -= extra // 2
|
||
hi += extra - extra // 2
|
||
return window_scan(
|
||
built,
|
||
start_minute=max(0, lo),
|
||
end_minute=min(24 * 60 - 1, hi),
|
||
)
|
||
|
||
|
||
def build_refinement_packet(
|
||
request: dict[str, Any],
|
||
built: dict[str, Any],
|
||
*,
|
||
representative_time: str | None,
|
||
candidate_times: Sequence[str],
|
||
cluster_width_minutes: int | None = None,
|
||
) -> dict[str, Any]:
|
||
scan = window_scan(built)
|
||
cluster = cluster_scan(built, candidate_times, representative_time, cluster_width_minutes)
|
||
ledger = event_dasha_ledger(request, built, representative_time)
|
||
agreement = dasha_agreement(built, candidate_times)
|
||
return {
|
||
"window_scan": scan,
|
||
"event_dasha_ledger": ledger,
|
||
"event_fit_rate": event_fit_rate(ledger),
|
||
"dasha_agreement": agreement,
|
||
"lagna_contrast": lagna_contrast(built),
|
||
"nakshatra_boundary": nakshatra_boundary(built, representative_time),
|
||
"precision_stage": precision_stage(cluster, len(request.get("events") or [])),
|
||
"oos_blind_prompts": oos_blind_prompts(request),
|
||
"unique_minute_claim": False,
|
||
"confirmation_allowed": False,
|
||
}
|