fix(rectification): anchor candidate windows to civil dates across midnight
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s

Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
This commit is contained in:
jesse-ux
2026-09-21 02:55:00 +08:00
parent 3be740f84d
commit b85c4a686a
115 changed files with 106484 additions and 315 deletions
+12 -1
View File
@@ -156,6 +156,11 @@ def indistinguishable_width_minutes(candidates: Sequence[dict[str, Any]]) -> int
return 0
ranked = sorted(candidates, key=lambda row: int(row.get("rank") or 0))
top = ranked[0]
if all(row.get("cluster_intervals") for row in ranked):
from scripts.rectification.candidate_window import interval_union_width
return max(int(top.get("tied_minute_count") or 1), interval_union_width([
interval for row in ranked for interval in row["cluster_intervals"]
]), 1)
starts: list[int] = []
ends: list[int] = []
for row in ranked:
@@ -428,7 +433,12 @@ def build_candidate_decisions(
for other in all_scores
)
cluster_times, cluster_start, cluster_end = _cluster_span(row)
from scripts.rectification.candidate_window import intervals_from_positions
position = {key: row[key] for key in ("candidate_date", "window_index", "window_offset_minutes", "segment_index") if key in row}
coverage = intervals_from_positions(row.get("cluster_positions", []))
decisions.append({
**position,
**({"cluster_intervals": coverage} if coverage else {}),
"candidate_id": str(uuid5(NAMESPACE_URL, f"{POLICY_VERSION}:{result_id}:{row['time']}")),
"rank": index + 1,
"time": row["time"],
@@ -455,7 +465,8 @@ def _cluster_span(row: dict[str, Any]) -> tuple[list[str], str, str]:
if not times:
clock = str(row.get("time") or "")[:5]
times = [clock] if len(clock) == 5 and clock[2] == ":" else []
times.sort(key=lambda value: int(value[:2]) * 60 + int(value[3:5]))
positions = {item["time"]: item["window_index"] for item in row.get("cluster_positions", [])}
times.sort(key=lambda value: positions.get(value, int(value[:2]) * 60 + int(value[3:5])))
start = times[0] if times else str(row.get("time") or "")[:5]
end = times[-1] if times else start
return times, start, end