Files
Jyotisha/scripts/active_rectification_events.py
T
jesse-uxandGrok (agent loop) 52b19d7a82 fix(rect): after excluding the indivisible MD/AD unit, compare on the rest (#184 round 3, BUG-1827)
打回:单元拿掉了,但 Narayana_AD 仍算 unexplained,又落回 not_reducible,
且那个分支不输出 excluded_contributions。

- 新增 _explained_by_exclusion:差异层被「所有带它的候选的不可拆 MD/AD 项」
  覆盖且没有别的缺失层时,视为已由排除解释
- not_reducible 分支也输出 excluded_contributions(非空时)
- 旧形状行(无 contributions)不受影响

Tests: 新增 4 条真实形状用例(只缺小运→common_coverage、再加一个缺失层→
not_reducible 但仍报排除、旧形状行不报、负分照计);本文件 20 条 + 既有四套
合计 56 条全过。

Co-Authored-By: Grok (agent loop)
2026-11-09 19:31:41 +08:00

733 lines
30 KiB
Python

# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
# ─── How to run ───
# .venv/bin/python -m pytest -q tests/test_active_rectification_events.py
"""Deterministically adjudicate dated events against birth-time candidates."""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, Final, Literal, NotRequired, TypedDict, assert_never
from uuid import NAMESPACE_URL, uuid5
try:
from scripts.rectification_policy import (
MAX_CONFIRMATION_WIDTH_MINUTES,
MIN_CONFIRMATION_DOMAINS,
MIN_CONFIRMATION_EVENTS,
MIN_CONFIRMATION_MARGIN_PERCENT,
)
except ModuleNotFoundError: # pragma: no cover - direct script execution
from rectification_policy import (
MAX_CONFIRMATION_WIDTH_MINUTES,
MIN_CONFIRMATION_DOMAINS,
MIN_CONFIRMATION_EVENTS,
MIN_CONFIRMATION_MARGIN_PERCENT,
)
EventPrecision = Literal["year", "month", "day"]
EventDomain = Literal[
"education",
"relocation",
"relationship",
"career",
"finance",
"health_pressure",
"family",
"appearance",
"occupation",
]
Confidence = Literal["low", "medium", "high"]
ALGORITHM_VERSION: Final = "birth-time-event-scoring-v2"
PRECISION_WEIGHTS: Final[dict[EventPrecision, float]] = {
"day": 1.0,
"month": 0.8,
"year": 0.5,
}
class CandidateEvidence(TypedDict):
event_id: str
domain: str
candidate_time: str
rule_ids: list[str]
points: float
class LifeEvent(TypedDict):
id: str
domain: EventDomain
event_kind: NotRequired[str]
date: str
precision: EventPrecision
summary: NotRequired[str]
class RectificationEventRequest(TypedDict):
birth_date: str
start_time: str
end_time: str
lat: float
lon: float
tz: float
ayanamsa: NotRequired[str]
node_mode: NotRequired[str]
minute_step: NotRequired[int]
events: list[LifeEvent]
class CandidateScoreRow(TypedDict):
time: str
score: float
evidence: list[CandidateEvidence]
missing_layers: list[str]
# Per scored event, the points of each evidence layer that was actually computed (comparability policy).
layer_points: NotRequired[dict[str, dict[str, float]]]
class WinningSegment(TypedDict):
start_time: str
end_time: str
representative_time: str
width_minutes: int
class CandidateResult(TypedDict):
result_id: str
confidence: Confidence
can_apply: bool
winning_segment: WinningSegment | None
event_count: int
domain_count: int
top_score: float
second_score: float
margin_percent: float
reasons: list[str]
evidence: list[CandidateEvidence]
algorithm_version: str
canonical_input_hash: str
calculation_contract: dict[str, Any]
stability_diagnostics: dict[str, Any]
missing_layers: list[str]
leader_status: NotRequired[str]
provisional_leader: NotRequired[WinningSegment | None]
comparable_leader: NotRequired[WinningSegment | None]
coverage_comparison: NotRequired[dict[str, Any]]
candidate_ranking_summary: NotRequired[list[dict[str, Any]]]
candidate_summary: NotRequired[dict[str, Any]]
def build_candidate_result_summary(result: dict[str, Any]) -> dict[str, Any]:
"""Project a candidate result into stable, non-confirmatory next steps."""
supported: dict[str, dict[str, Any]] = {}
unconfirmed: dict[str, dict[str, Any]] = {}
contradictory: dict[str, dict[str, Any]] = {}
for item in result.get("evidence", []):
if not isinstance(item, dict):
continue
domain = str(item.get("domain") or "unknown")
points = float(item.get("points") or 0)
bucket = supported if points > 0 else contradictory if points < 0 else unconfirmed
row = bucket.setdefault(domain, {"domain": domain, "event_count": 0, "total_points": 0.0, "rule_ids": set()})
row["event_count"] += 1
row["total_points"] += points
row["rule_ids"].update(str(rule_id) for rule_id in item.get("rule_ids", []))
def rows(bucket: dict[str, dict[str, Any]], *, include_points: bool) -> list[dict[str, Any]]:
output = []
for row in bucket.values():
item: dict[str, Any] = {
"domain": row["domain"],
"event_count": row["event_count"],
"rule_ids": sorted(row["rule_ids"]),
}
if include_points:
item["total_points"] = round(row["total_points"], 3)
output.append(item)
return sorted(output, key=lambda item: (-item.get("total_points", 0), item["domain"]))
segment = result.get("winning_segment")
next_steps: list[str] = []
if int(result.get("event_count") or 0) < 5:
next_steps.append("collect_at_least_five_events")
if float(result.get("margin_percent") or 0) <= 0 or "tied_leader" in (result.get("reasons") or []):
next_steps.append("resolve_candidate_tie_or_narrow_window")
if isinstance(segment, dict) and int(segment.get("width_minutes") or 0) > 5:
next_steps.append("narrow_window_before_minute_claim")
next_steps.append("do_not_apply_as_birth_time_truth")
return {
"claim_status": "candidate_range_not_birth_time_truth",
"candidate_range": segment,
"supporting_evidence": rows(supported, include_points=True),
"unconfirmed_evidence": rows(unconfirmed, include_points=False),
"contradictory_evidence": rows(contradictory, include_points=True),
"reasons": list(result.get("reasons") or []),
"next_step_codes": next_steps,
}
def independent_events(events: Sequence[LifeEvent]) -> tuple[list[LifeEvent], list[str]]:
"""Keep the first event of each (domain, date, precision); return the dropped ids.
The score of an event depends only on those three fields, so a repeated entry is the same test
re-run, not independent support (占星师 2026-11-08 验收清单 §6).
"""
seen: set[tuple[str, str, str]] = set()
kept: list[LifeEvent] = []
duplicates: list[str] = []
for event in events:
key = (str(event["domain"]), str(event["date"]).strip(), str(event["precision"]))
if key in seen:
duplicates.append(str(event["id"]))
continue
seen.add(key)
kept.append(event)
return kept, duplicates
def precision_weight(precision: EventPrecision) -> float:
"""Return the fixed evidence weight for a declared date precision."""
match precision:
case "day" | "month" | "year":
return PRECISION_WEIGHTS[precision]
case unreachable:
assert_never(unreachable)
def _minute_value(value: str) -> int:
hour, minute = value.split(":", maxsplit=1)
return int(hour) * 60 + int(minute)
def _is_next_minute(previous: str, current: str) -> bool:
return (_minute_value(current) - _minute_value(previous)) % (24 * 60) == 1
def _top_segments(rows: Sequence[CandidateScoreRow], top_score: float) -> list[list[CandidateScoreRow]]:
segments: list[list[CandidateScoreRow]] = []
for row in rows:
if row["score"] != top_score:
continue
if segments and _is_next_minute(segments[-1][-1]["time"], row["time"]):
segments[-1].append(row)
else:
segments.append([row])
return segments
def _winning_segment(rows: Sequence[CandidateScoreRow]) -> WinningSegment:
width = len(rows)
representative = rows[(width - 1) // 2]["time"]
return {
"start_time": rows[0]["time"],
"end_time": rows[-1]["time"],
"representative_time": representative,
"width_minutes": width,
}
def _clock_distance(left: str, right: str) -> int:
distance = abs(_minute_value(left) - _minute_value(right))
return min(distance, 24 * 60 - distance)
def _time_at_offset(value: str, offset: int) -> str:
total = (_minute_value(value) + offset) % (24 * 60)
return f"{total // 60:02d}:{total % 60:02d}"
def build_stability_diagnostics(
rows: Sequence[CandidateScoreRow],
*,
winning_segment: WinningSegment | None,
) -> dict[str, Any]:
"""Describe exact-minute neighbor separation without claiming calibrated accuracy."""
representative = winning_segment["representative_time"] if winning_segment else None
by_time = {row["time"]: row for row in rows}
representative_row = by_time.get(representative) if representative else None
neighborhoods: list[dict[str, Any]] = []
for radius in (1, 2, 5):
required_times = {
_time_at_offset(representative, -radius),
_time_at_offset(representative, radius),
} if representative else set()
neighbors = [
row for row in rows
if representative is not None
and 0 < _clock_distance(row["time"], representative) <= radius
]
if representative_row is None or not required_times.issubset(by_time):
neighborhoods.append({
"radius_minutes": radius,
"status": "blocked",
"lead_points": None,
"compared_candidate_count": len(neighbors),
"reason": "candidate_range_does_not_cover_both_sides_of_neighborhood",
})
continue
best_neighbor = max(row["score"] for row in neighbors)
lead = round(representative_row["score"] - best_neighbor, 4)
neighborhoods.append({
"radius_minutes": radius,
"status": "pass" if winning_segment["width_minutes"] == 1 and lead > 0 else "fail",
"lead_points": lead,
"compared_candidate_count": len(neighbors),
"reason": (
"unique_minute_leads_neighbor_candidates"
if winning_segment["width_minutes"] == 1 and lead > 0
else "minute_not_uniquely_separated_from_neighbors"
),
})
return {
"scope": "candidate_neighbor_stability",
"representative_time": representative,
"neighborhoods": neighborhoods,
"all_required_passed": all(item["status"] == "pass" for item in neighborhoods),
"boundary": "Neighbor separation is a diagnostic only until thresholds are frozen before public holdout replay.",
}
COMPARABILITY_POLICY: Final = "common_evidence_coverage_v1"
# Layers whose absence drops the whole event for that candidate (the event is not scored at all).
EVENT_GATING_LAYERS: Final[frozenset[str]] = frozenset({
"Narayana", "Narayana_MD_AD", "Vimshottari_MD_AD_PD",
"D2", "D3", "D4", "D6", "D7", "D8", "D9", "D10", "D11", "D12", "D24", "D30", "D60",
})
# 占星师原文层名 → 产品层名(只在「该候选整件事缺失」时适用,见 _explained_by_alias)。
ASTROLOGER_EVENT_LAYER_ALIASES: Final[dict[str, str]] = {
"Narayana_AD": "Narayana_MD_AD",
}
def _layers_by_event(unit: dict[tuple[str, str], float]) -> dict[str, set[str]]:
grouped: dict[str, set[str]] = {}
for (event_id, layer) in unit:
grouped.setdefault(event_id, set()).add(layer)
return grouped
def _explained_by_alias(layer: str, rows: Sequence[CandidateScoreRow], units: Sequence[dict[tuple[str, str], float]]) -> bool:
"""占星师原文层名能不能当作产品层名来解释这一处差异(BUG-1827)。
只有同时满足三条才算:该候选至少有一件事整件缺失;它自己覆盖单元里每一件事的层集合
与共同覆盖一致(没有「事件在、但少了某层」);它的缺失层里没有别的门控层。
"""
if layer not in ASTROLOGER_EVENT_LAYER_ALIASES:
return False
holders = [index for index, row in enumerate(rows) if layer in row["missing_layers"]]
if not holders:
return False
common = set.intersection(*[set(unit) for unit in units]) if units else set()
common_by_event = _layers_by_event(common)
all_events = {event for unit in units for (event, _) in unit}
for index in holders:
own_by_event = _layers_by_event(units[index])
# ① 至少有一件事整件缺失(别的候选有、它没有)。
if set(own_by_event) == all_events:
return False
# ② 它有的每一件事,层集合都必须与共同覆盖一致。
if any(common_by_event.get(event) != layers for event, layers in own_by_event.items()):
return False
# ③ 缺失层里不能再有别的门控层。
if any(item in EVENT_GATING_LAYERS and item != layer for item in rows[index]["missing_layers"]):
return False
return True
def _coverage_units(row: CandidateScoreRow) -> dict[tuple[str, str], float]:
"""(event, layer) -> points for every evidence unit this candidate actually computed.
Rows that carry ``layer_points`` expose each layer; older rows expose one opaque unit per scored event.
"""
explicit = row.get("layer_points")
if isinstance(explicit, dict):
return {
(str(event_id), str(layer)): float(points)
for event_id, layers in explicit.items()
for layer, points in layers.items()
}
return {(str(item["event_id"]), "event_total"): float(item["points"]) for item in row["evidence"]}
def _excluded_dasha_contributions(rows: Sequence[CandidateScoreRow]) -> list[dict[str, str]]:
"""拆不开的 MD/AD 合分:只要有一个候选的该项 decomposable 为假,就对所有候选一起排除。"""
excluded: dict[str, dict[str, str]] = {}
for row in rows:
for contribution in row.get("contributions") or []:
if not isinstance(contribution, dict):
continue
if contribution.get("layer") != "dasha_varga_core":
continue
if contribution.get("decomposable") is not False:
continue
event_id = str(contribution.get("event_id") or "")
if event_id:
excluded[event_id] = {
"event_id": event_id,
"layer": "dasha_varga_core",
"reason": "md_ad_not_decomposable",
}
return [excluded[event_id] for event_id in sorted(excluded)]
def _explained_by_exclusion(layer: str, rows: Sequence[CandidateScoreRow],
excluded: Sequence[dict[str, str]]) -> bool:
"""差异层 L 是否已由「一致排除」解释(FIX-3)。
每个带 L 的候选都必须有贡献明细、其 ``missing_reason == f"{L}_unresolved"`` 的项
全部落在 ``excluded_contributions`` 里,并且该候选没有别的缺失层。
没有 contributions 的旧形状行不适用。
"""
covered = {(item["event_id"], item["layer"]) for item in excluded}
holders = [row for row in rows if layer in row["missing_layers"]]
if not holders:
return False
for row in holders:
contributions = row.get("contributions")
if not isinstance(contributions, list) or not contributions:
return False
unresolved = [
item for item in contributions
if isinstance(item, dict) and item.get("missing_reason") == f"{layer}_unresolved"
]
if not unresolved:
return False
for item in unresolved:
if (str(item.get("event_id") or ""), "dasha_varga_core") not in covered:
return False
if [other for other in row["missing_layers"] if other != layer]:
return False
return True
def missing_mandatory_layers(rows: Sequence[CandidateScoreRow]) -> list[dict[str, Any]]:
"""逐条列出「哪个候选、缺哪一层、影响哪几件事」(占星师第七轮 P1 第 3 条)。
没有缺失时返回 ``[]``(任何情况下都不是 ``null``);旧形状行没有贡献明细,
``event_ids`` 写 ``null`` 并标 ``event_ids_source``,不假装知道影响了哪几件事。
"""
entries: list[dict[str, Any]] = []
for row in rows:
contributions = row.get("contributions")
by_layer: dict[str, list[str]] = {}
has_contributions = isinstance(contributions, list) and bool(contributions)
for contribution in contributions or []:
if not isinstance(contribution, dict):
continue
reason = contribution.get("missing_reason")
layer = contribution.get("layer")
if not isinstance(reason, str) or not isinstance(layer, str):
continue
if reason.startswith("event_not_scored:"):
layer = reason.split(":", 1)[1]
elif reason.endswith("_unresolved"):
# dasha_varga_core 里「只缺小运」:缺失的层名在原因里(Narayana_AD_unresolved)。
layer = reason[: -len("_unresolved")]
else:
continue
event_id = str(contribution.get("event_id") or "")
if event_id:
by_layer.setdefault(layer, []).append(event_id)
for layer in row["missing_layers"]:
entry: dict[str, Any] = {"candidate_time": row["time"], "layer": layer}
if layer in by_layer:
entry["event_ids"] = sorted(set(by_layer[layer]))
elif has_contributions:
entry["event_ids"] = []
else:
entry["event_ids"] = None
entry["event_ids_source"] = "legacy_row_without_contributions"
entries.append(entry)
entries.sort(key=lambda item: (item["candidate_time"], item["layer"]))
return entries
def assess_leader_comparability(
rows: Sequence[CandidateScoreRow],
provisional_leader: WinningSegment | None,
) -> dict[str, Any]:
"""Explicit comparability policy (占星师 2026-11-08 验收清单 §6 / §9 / §10).
Raw totals are only comparable when every candidate computed the same evidence units. When coverage
differs, the raw-total leader stays ``provisional_leader`` (and keeps ``winning_segment``), and the
candidates are re-ranked over the units that *every* candidate has; that leader is ``comparable_leader``.
A unit one candidate lacks is dropped for all candidates, so it never counts as 0, negative or an old
algorithm score, and a candidate cannot lead by computing less. When the differing layer is folded into
another unit (no per-layer points), the comparison is ``not_reducible`` and there is no comparable leader.
"""
units = [_coverage_units(row) for row in rows]
row_layers = [set(row["missing_layers"]) for row in rows]
unit_keys = [set(item) for item in units]
equal = all(keys == unit_keys[0] for keys in unit_keys) and all(layers == row_layers[0] for layers in row_layers)
common = set.intersection(*unit_keys) if unit_keys else set()
base: dict[str, Any] = {
"policy": COMPARABILITY_POLICY,
"candidate_count": len(rows),
"common_unit_count": len(common),
"candidate_unit_counts": {
"min": min((len(keys) for keys in unit_keys), default=0),
"max": max((len(keys) for keys in unit_keys), default=0),
},
}
separable = {layer for keys in unit_keys for _, layer in keys} - {"event_total"}
differing = set.union(*row_layers) - set.intersection(*row_layers)
# 占星师第七轮 P1 第 1 条:拆不开的 MD/AD 合分对所有候选一致排除,缺的不当 0、全的也不计。
excluded_contributions = _excluded_dasha_contributions(rows)
if excluded_contributions:
dropped = {(item["event_id"], item["layer"]) for item in excluded_contributions}
units = [{key: value for key, value in unit.items() if key not in dropped} for unit in units]
unit_keys = [set(unit) for unit in units]
common = set.intersection(*unit_keys) if unit_keys else set()
base["common_unit_count"] = len(common)
base["candidate_unit_counts"] = {
"min": min((len(keys) for keys in unit_keys), default=0),
"max": max((len(keys) for keys in unit_keys), default=0),
}
equal = all(keys == unit_keys[0] for keys in unit_keys) and all(layers == row_layers[0] for layers in row_layers)
explained_aliases = {
layer: ASTROLOGER_EVENT_LAYER_ALIASES[layer]
for layer in differing
if layer not in EVENT_GATING_LAYERS and layer not in separable
and _explained_by_alias(layer, rows, units)
}
explained_by_exclusion = {
layer for layer in differing
if layer not in EVENT_GATING_LAYERS and layer not in separable
and _explained_by_exclusion(layer, rows, excluded_contributions)
}
unexplained = sorted(
layer for layer in differing
if layer not in EVENT_GATING_LAYERS and layer not in separable
and layer not in explained_aliases and layer not in explained_by_exclusion
)
if equal:
return {
**base,
"status": "comparable",
"comparison": "raw_totals",
"candidates_with_extra_coverage": 0,
**({"excluded_contributions": excluded_contributions} if excluded_contributions else {}),
**({"layer_aliases_applied": explained_aliases} if explained_aliases else {}),
"comparable_leader": provisional_leader,
"leader_changed": False,
}
extra = sum(1 for keys in unit_keys if keys != common)
if unexplained:
return {
**base,
"status": "provisional_unequal_coverage",
"comparison": "not_reducible",
"unreducible_layers": unexplained,
**({"excluded_contributions": excluded_contributions} if excluded_contributions else {}),
"candidates_with_extra_coverage": extra,
"comparable_leader": None,
"leader_changed": None,
}
common_rows: list[CandidateScoreRow] = [
{
"time": row["time"],
"score": round(sum(points for key, points in unit.items() if key in common), 4),
"evidence": [],
"missing_layers": [],
}
for row, unit in zip(rows, units, strict=True)
]
common_top = max(row["score"] for row in common_rows)
common_segments = _top_segments(common_rows, common_top)
comparable = _winning_segment(common_segments[0]) if len(common_segments) == 1 else None
return {
**base,
"status": "provisional_unequal_coverage",
"comparison": "common_coverage",
**({"layer_aliases_applied": explained_aliases} if explained_aliases else {}),
**({"excluded_contributions": excluded_contributions} if excluded_contributions else {}),
"candidates_with_extra_coverage": extra,
"common_top_score": common_top,
"common_scores": [{"time": row["time"], "score": row["score"]} for row in common_rows],
"comparable_leader": comparable,
"comparable_tied": len(common_segments) != 1,
"leader_changed": (
comparable is None
or provisional_leader is None
or (comparable["start_time"], comparable["end_time"])
!= (provisional_leader["start_time"], provisional_leader["end_time"])
),
}
def adjudicate_candidate_rows(
rows: Sequence[CandidateScoreRow],
*,
event_count: int,
domain_count: int,
request_fingerprint: str,
canonical_input_hash: str = "",
calculation_contract: dict[str, Any] | None = None,
leave_one_event_out: dict[str, Any] | None = None,
) -> CandidateResult:
"""Rank precomputed candidate rows and apply conservative confidence gates."""
if not rows:
return {
"result_id": str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{request_fingerprint}")),
"confidence": "low",
"can_apply": False,
"winning_segment": None,
"event_count": event_count,
"domain_count": domain_count,
"top_score": 0.0,
"second_score": 0.0,
"margin_percent": 0.0,
"reasons": ["no_candidate_rows"],
"evidence": [],
"algorithm_version": ALGORITHM_VERSION,
"canonical_input_hash": canonical_input_hash,
"calculation_contract": calculation_contract or {},
"stability_diagnostics": {
"neighbor_stability": build_stability_diagnostics([], winning_segment=None),
"leave_one_event_out": leave_one_event_out or {"status": "not_evaluated", "runs": []},
},
"missing_layers": [],
"missing_mandatory_layers": [],
"top_candidate_count": 0,
"top_segment_count": 0,
"unique_time": False,
"leader_status": "no_leader",
"provisional_leader": None,
"comparable_leader": None,
"coverage_comparison": {"policy": COMPARABILITY_POLICY, "status": "no_candidate_rows"},
}
ranked_scores = sorted({row["score"] for row in rows}, reverse=True)
top_score = ranked_scores[0]
second_score = ranked_scores[1] if len(ranked_scores) > 1 else top_score
margin = round((top_score - second_score) / max(abs(top_score), 1.0) * 100, 2)
segments = _top_segments(rows, top_score)
missing_layers = sorted({layer for row in rows for layer in row["missing_layers"]})
missing_entries = missing_mandatory_layers(rows)
reasons: list[str] = []
if len(segments) != 1:
reasons.append("tied_leader")
if event_count < 3:
reasons.append("insufficient_events")
if domain_count < 2:
reasons.append("insufficient_domains")
# 占星师第七轮 P1 第 3 条:这个 reason 当且仅当列表非空。
if missing_entries:
reasons.append("missing_mandatory_layers")
segment = _winning_segment(segments[0]) if len(segments) == 1 else None
comparability = assess_leader_comparability(rows, segment)
if comparability["status"] != "comparable":
reasons.append("unequal_evidence_coverage")
if segment and segment["width_minutes"] > 15:
reasons.append("winning_interval_too_wide")
if margin < 10:
reasons.append("lead_margin_below_medium_threshold")
if reasons:
confidence: Confidence = "low"
elif (
event_count >= MIN_CONFIRMATION_EVENTS
and domain_count >= MIN_CONFIRMATION_DOMAINS
and segment is not None
and segment["width_minutes"] <= MAX_CONFIRMATION_WIDTH_MINUTES
and margin >= MIN_CONFIRMATION_MARGIN_PERCENT
):
confidence = "high"
else:
confidence = "medium"
top_rows = segments[0] if len(segments) == 1 else []
representative_row = top_rows[(len(top_rows) - 1) // 2] if top_rows else None
evidence = list(representative_row["evidence"]) if representative_row else []
neighbor_stability = build_stability_diagnostics(rows, winning_segment=segment)
if not neighbor_stability["all_required_passed"]:
reasons.append("neighbor_stability_not_passed")
if (leave_one_event_out or {}).get("status") != "pass":
reasons.append("leave_one_event_out_not_passed")
can_apply = (
confidence == "high"
and segment is not None
and neighbor_stability["all_required_passed"]
and (leave_one_event_out or {}).get("status") == "pass"
and not missing_layers
and comparability["status"] == "comparable"
and not any(reason in reasons for reason in (
"unequal_evidence_coverage",
"tied_leader",
"insufficient_events",
"insufficient_domains",
"winning_interval_too_wide",
"lead_margin_below_medium_threshold",
))
)
ranking_summary: list[dict[str, Any]] = []
for score in ranked_scores[:3]:
score_rows = sorted(
(row for row in rows if row["score"] == score),
key=lambda row: _minute_value(row["time"]),
)
if not score_rows:
continue
representative = score_rows[(len(score_rows) - 1) // 2]
ranking_summary.append({
"rank": len(ranking_summary) + 1,
"time": representative["time"],
"score": score,
"tied_minute_count": len(score_rows),
})
# 占星师 2026-11-09 第七轮 P1 第 2 条:三个计数分开,代表分钟不能冒充唯一解。
top_candidate_count = sum(1 for row in rows if row["score"] == top_score)
top_segment_count = len(segments)
return {
"result_id": str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{request_fingerprint}")),
"confidence": confidence,
"can_apply": can_apply,
"top_candidate_count": top_candidate_count,
"top_segment_count": top_segment_count,
"unique_time": top_candidate_count == 1,
"winning_segment": segment,
"event_count": event_count,
"domain_count": domain_count,
"top_score": top_score,
"second_score": second_score,
"margin_percent": margin,
"reasons": reasons,
"evidence": evidence,
"algorithm_version": ALGORITHM_VERSION,
"canonical_input_hash": canonical_input_hash,
"calculation_contract": calculation_contract or {},
"stability_diagnostics": {
"neighbor_stability": neighbor_stability,
"leave_one_event_out": leave_one_event_out or {"status": "not_evaluated", "runs": []},
},
"missing_layers": missing_layers,
"missing_mandatory_layers": missing_entries,
# winning_segment stays the raw-total leader; it is only "comparable" when coverage is equal.
"leader_status": (
"no_single_leader" if segment is None
else "comparable" if comparability["status"] == "comparable"
else "provisional_unequal_coverage"
),
"provisional_leader": segment,
"comparable_leader": comparability["comparable_leader"],
"coverage_comparison": {key: value for key, value in comparability.items() if key != "comparable_leader"},
"candidate_ranking_summary": ranking_summary,
}
def score_life_events(request: RectificationEventRequest) -> CandidateResult:
"""Compute actual candidate rows, then apply the versioned confidence gates."""
from scripts.active_rectification_event_engine import compute_event_candidate_result
result = compute_event_candidate_result(request)
result["candidate_summary"] = build_candidate_result_summary(result)
return result