116 lines
4.5 KiB
Python
116 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit whether event evidence actually distinguishes adjacent birth minutes."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections import defaultdict
|
|
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
from scripts.active_rectification_events import CandidateScoreRow
|
|
|
|
|
|
def _evidence_features(item: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"event_id": item["event_id"],
|
|
"domain": item["domain"],
|
|
"points": float(item["points"]),
|
|
"rule_ids": sorted(item["rule_ids"]),
|
|
}
|
|
|
|
|
|
def feature_fingerprint(row: CandidateScoreRow) -> str:
|
|
"""Hash only computed evidence features, never the candidate time or truth label."""
|
|
payload = {
|
|
"evidence": sorted(
|
|
(
|
|
_evidence_features(item)
|
|
for item in row["evidence"]
|
|
),
|
|
key=lambda item: (item["event_id"], item["domain"]),
|
|
),
|
|
"missing_layers": sorted(row["missing_layers"]),
|
|
}
|
|
canonical = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def analyze_candidate_rows(
|
|
rows: Sequence[CandidateScoreRow],
|
|
*,
|
|
ranking_rows: Sequence[CandidateScoreRow] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return feature-equivalence classes and real adjacent-minute transitions."""
|
|
candidates = list(rows)
|
|
if not candidates:
|
|
return {
|
|
"scope": "minute_candidate_discriminability",
|
|
"candidate_count": 0,
|
|
"unique_feature_fingerprint_count": 0,
|
|
"distinguishable_candidate_ratio": 0.0,
|
|
"equivalence_classes": [],
|
|
"adjacent_transitions": [],
|
|
"indistinguishable_adjacent_pair_count": 0,
|
|
"top_candidate_feature_unique": False,
|
|
"status": "blocked_no_candidates",
|
|
}
|
|
|
|
fingerprints = {row["time"]: feature_fingerprint(row) for row in candidates}
|
|
classes: dict[str, list[str]] = defaultdict(list)
|
|
for row in candidates:
|
|
classes[fingerprints[row["time"]]].append(row["time"])
|
|
equivalence_classes = [
|
|
{"feature_fingerprint": fingerprint, "candidate_times": times, "size": len(times)}
|
|
for fingerprint, times in sorted(classes.items(), key=lambda item: item[1][0])
|
|
]
|
|
|
|
adjacent_transitions = []
|
|
indistinguishable = 0
|
|
for previous, current in zip(candidates, candidates[1:]):
|
|
previous_by_event = {
|
|
item["event_id"]: _evidence_features(item) for item in previous["evidence"]
|
|
}
|
|
current_by_event = {
|
|
item["event_id"]: _evidence_features(item) for item in current["evidence"]
|
|
}
|
|
changed_event_ids = sorted(
|
|
event_id
|
|
for event_id in set(previous_by_event) | set(current_by_event)
|
|
if previous_by_event.get(event_id) != current_by_event.get(event_id)
|
|
)
|
|
feature_changed = fingerprints[previous["time"]] != fingerprints[current["time"]]
|
|
indistinguishable += int(not feature_changed)
|
|
adjacent_transitions.append({
|
|
"between": [previous["time"], current["time"]],
|
|
"feature_changed": feature_changed,
|
|
"changed_event_ids": changed_event_ids,
|
|
"score_delta": round(float(current["score"]) - float(previous["score"]), 4),
|
|
})
|
|
|
|
ranked = list(ranking_rows) if ranking_rows is not None else candidates
|
|
top_score = max(float(row["score"]) for row in ranked)
|
|
top_times = [row["time"] for row in ranked if float(row["score"]) == top_score]
|
|
top_unique = len(top_times) == 1 and len(classes[fingerprints[top_times[0]]]) == 1
|
|
unique_count = len(classes)
|
|
status = (
|
|
"minute_feature_unique"
|
|
if top_unique
|
|
else "range_has_differences_but_top_not_unique"
|
|
if unique_count > 1
|
|
else "blocked_feature_equivalent_range"
|
|
)
|
|
return {
|
|
"scope": "minute_candidate_discriminability",
|
|
"candidate_count": len(candidates),
|
|
"unique_feature_fingerprint_count": unique_count,
|
|
"distinguishable_candidate_ratio": round(unique_count / len(candidates), 4),
|
|
"equivalence_classes": equivalence_classes,
|
|
"adjacent_transitions": adjacent_transitions,
|
|
"indistinguishable_adjacent_pair_count": indistinguishable,
|
|
"top_candidate_times": top_times,
|
|
"top_candidate_feature_unique": top_unique,
|
|
"status": status,
|
|
"boundary": "Feature uniqueness describes this scorer's computed evidence only; it is not proof that a birth minute is true.",
|
|
}
|