fix(er): refuse score-only matches at Python merge boundary
This commit is contained in:
@@ -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 ""),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user