fix(er): refuse score-only matches at Python merge boundary

This commit is contained in:
2026-09-07 18:18:33 +02:00
parent d0cba02ee0
commit 93c01d2264
+11 -4
View File
@@ -1,9 +1,13 @@
"""Thin adapter from the Python pipeline to 0pt's canonical TS ER engine.
Python no longer scores identity. It sends opaque records to
/api/0pt/internal/er/candidates, then only applies the returned canonical
`match` decisions. If the canonical engine is unavailable, records are
returned UNMERGED (fail closed against false identity merges).
/api/0pt/internal/er/candidates, then applies only explicit hard-rule `match`
decisions. Score/fuzzy decisions are review evidence only and can never merge
records in this adapter, even if an older or compromised bridge returns
`status=match, decided_by=score`.
If the canonical engine is unavailable, records are returned UNMERGED (fail
closed against false identity merges).
A canonical hard non-match is also a cluster-wide constraint: transitive
matches are never allowed to pull two records with conflicting stable identity
@@ -91,15 +95,18 @@ def _apply_matches(records: list[dict[str, Any]], pairs: list[dict[str, Any]]) -
members.pop(drop, None)
return True
# Defence in depth: only stable-identifier hard rules may create canonical
# identity links. Score/fuzzy "match" responses are ignored at the consumer
# boundary even if an upstream/legacy route emits one.
match_pairs = [
p for p in pairs
if p.get("status") == "match"
and p.get("decided_by") not in (None, "score")
and p.get("record_a") in known_ids
and p.get("record_b") in known_ids
]
match_pairs.sort(
key=lambda p: (
0 if p.get("decided_by") not in (None, "score") else 1,
-float(p.get("score") or 0.0),
str(p.get("candidate_id") or ""),
)