fix(rectification): anchor candidate windows to civil dates across midnight
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:
@@ -164,17 +164,18 @@ def context_time(context: dict[str, Any]) -> str | None:
|
||||
def cluster_contexts_by_signature(
|
||||
contexts: Sequence[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
buckets: dict[tuple[int | None, ...], list[dict[str, Any]]] = {}
|
||||
buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {}
|
||||
for context in contexts:
|
||||
if not isinstance(context, dict):
|
||||
continue
|
||||
time = context_time(context)
|
||||
if not time:
|
||||
continue
|
||||
buckets.setdefault(feature_signature(context), []).append(context)
|
||||
buckets.setdefault((context.get("segment_index", 0), *feature_signature(context)), []).append(context)
|
||||
clusters: list[dict[str, Any]] = []
|
||||
for signature, members in buckets.items():
|
||||
ordered = sorted(members, key=lambda item: _clock(str(context_time(item))))
|
||||
for key, members in buckets.items():
|
||||
signature = key[1:]
|
||||
ordered = sorted(members, key=lambda item: item.get("window_index", _clock(str(context_time(item)))))
|
||||
times = [str(context_time(item)) for item in ordered]
|
||||
clusters.append({
|
||||
"signature": signature,
|
||||
@@ -184,7 +185,7 @@ def cluster_contexts_by_signature(
|
||||
"representative_time": times[len(times) // 2],
|
||||
"representative": ordered[len(ordered) // 2],
|
||||
})
|
||||
clusters.sort(key=lambda item: _clock(item["representative_time"]))
|
||||
clusters.sort(key=lambda item: item["representative"].get("window_index", _clock(item["representative_time"])))
|
||||
return clusters
|
||||
|
||||
|
||||
@@ -271,7 +272,9 @@ def cap_clusters_by_adjacent_merge(
|
||||
best_index = index
|
||||
left = work[best_index]
|
||||
right = work[best_index + 1]
|
||||
merged_times = sorted(set(left["times"] + right["times"]), key=_clock)
|
||||
contexts = list(left.get("contexts") or []) + list(right.get("contexts") or [])
|
||||
order = {context_time(row): row.get("window_index", _clock(str(context_time(row)))) for row in contexts}
|
||||
merged_times = sorted(set(left["times"] + right["times"]), key=lambda clock: order.get(clock, _clock(clock)))
|
||||
work[best_index] = {
|
||||
"signature": left.get("signature"),
|
||||
"signature_key": f"{left.get('signature_key')}+{right.get('signature_key')}",
|
||||
@@ -296,7 +299,8 @@ def select_signature_representatives(
|
||||
context for context in (static_contexts or [])
|
||||
if isinstance(context, dict) and context_time(context) in by_time
|
||||
]
|
||||
if len(contexts) >= 2:
|
||||
order = {context_time(row): row.get("window_index", _clock(str(context_time(row)))) for row in contexts}
|
||||
if contexts:
|
||||
clusters = cluster_contexts_by_signature(contexts)
|
||||
else:
|
||||
clusters = _adjacent_score_clusters(list(by_time.values()))
|
||||
@@ -306,17 +310,22 @@ def select_signature_representatives(
|
||||
members = [by_time[time] for time in cluster["times"] if time in by_time]
|
||||
if not members:
|
||||
continue
|
||||
best = max(members, key=lambda row: (float(row.get("score") or 0), str(row.get("time"))))
|
||||
best = max(members, key=lambda row: (float(row.get("score") or 0), order.get(row.get("time"), _clock(str(row.get("time"))))))
|
||||
positions = [{key: context[key] for key in ("time", "candidate_date", "window_index", "window_offset_minutes", "segment_index")}
|
||||
for context in cluster.get("contexts", []) if "window_index" in context]
|
||||
representative_position = next((row for row in positions if row["time"] == best["time"]), {})
|
||||
representatives.append({
|
||||
**best,
|
||||
**representative_position,
|
||||
"cluster_times": [time for time in cluster["times"] if time in by_time],
|
||||
**({"cluster_positions": positions} if positions else {}),
|
||||
})
|
||||
representatives.sort(key=lambda row: (-float(row.get("score") or 0), str(row.get("time"))))
|
||||
representatives.sort(key=lambda row: (-float(row.get("score") or 0), order.get(row.get("time"), _clock(str(row.get("time"))))))
|
||||
return representatives or list(rows)[:1]
|
||||
|
||||
|
||||
def _adjacent_score_clusters(rows: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
ordered = sorted(rows, key=lambda row: _clock(str(_hhmm(row.get("time")))))
|
||||
ordered = sorted(rows, key=lambda row: row.get("window_index", _clock(str(_hhmm(row.get("time"))))))
|
||||
groups: list[list[dict[str, Any]]] = []
|
||||
for row in ordered:
|
||||
current = groups[-1] if groups else None
|
||||
|
||||
Reference in New Issue
Block a user