decision

Conflict is an annotation on an authoritative edge; 'weak' is not a tier

2026-08-01 · UX-164

The question

An owner review of the Payload Workbook asked for five relationship classes — Confirmed pair, Likely related, Weak, Conflict, Unpaired. Three shipped. The two that did not, Weak and Conflict, have no engine source and the correlation design carries explicit refusals against both. Should those refusals be reversed?

Constraints

  • Every correlation edge today is a BYTE-EXACT `system|value` match. There is no scoring anywhere in the pairing engine, and `PairingConfidence` is a closed two-value union assigned by a static lookup on key class, not computed.
  • `correlation.ts` refuses near-miss reporting by name: O(N²) near-misses is noise, not information. Same value under different systems is explicitly NOT the same identifier.
  • It also refuses corroboration edges: in a single-patient workbook, a shared patient or provider draws a line between every pair, so the graph would say everything is related and therefore nothing.
  • A workbook holds at most 12 cells, so N² is 144 — small enough that the cost of noise is comprehension, not compute. The argument against weak edges is that they make the strong ones harder to see, not that they are slow.
  • The distinction the whole surface rests on: a VERDICT says these two payloads are the two halves of one exchange; an EDGE says only that they carry the same value at these paths. Anything that blurs that is a regression regardless of how it renders.

Alternatives considered

  • Adopt both tiers as proposed — add Weak and Conflict as new edge classes alongside authoritative and strong.

    Rejected — Weak has no source that is not already refused for cause. The only candidates are near-misses (refused: same value under a different system is a different identifier, and saying otherwise is a false claim) and corroboration edges (refused: in a single-patient workbook they connect everything). Adding a tier whose only possible members are two categories of known noise degrades the classes that carry real information.

  • Reject both, and keep the boundary exactly as written.

    Rejected — Rejected because Conflict is not symmetric with Weak. It needs no new matching, no scoring and no near-miss: it is two exact comparisons, one match and one mismatch, over data the pairing engine already computes. Refusing it would be declining to report a contradiction we can already see.

  • Add Conflict as a new edge class in the correlation graph.

    Rejected — Wrong shape. A conflict is not a relationship between two cells that the graph was missing; it is a property of a relationship the graph already reports. As a separate class it would imply two cells can be 'conflict-related' without being related, which is meaningless.

  • Conflict as an ANNOTATION on an existing authoritative edge; Weak not adopted.

    Chosen.

The decision

Conflict is adopted as an annotation, not a class: an authoritative edge (a shared request-reference or preauth-ref) is marked in conflict when a corroborating identity field on the two cells disagrees byte-exactly. This adds no matching, no scoring and no near-miss reporting, and it does not create corroboration edges — corroboration is read only where an authoritative edge already exists, which is the precise scope the original refusal was protecting. Weak is NOT adopted: no source for it exists that is not already refused for cause. The door is left open for one narrow future case — an exact match on an identifier whose key class we do not recognise — and that would need its own record, a cap, and identity/provider fields excluded, or it becomes the corroboration graph under another name.

Why the two halves of one request got different answers

The owner's five buckets read as one feature, and the interesting part of this decision is that they are not. Three of them — Confirmed pair, Likely related, Unpaired — already existed and only needed rendering. The other two look like siblings and are not related at all.

Weak asks the engine to find something new. Every mechanism that could supply it has already been considered and refused for a stated reason. Reversing that means either reporting near-misses, which asserts that two different identifier systems carrying the same digits are the same identifier — a false claim, not a weak one — or emitting corroboration edges, which in a workbook where every cell concerns one patient draws a line between every pair. Both make the authoritative edges harder to see. Weak would not be a lower-confidence signal; it would be the absence of one, rendered as though it were a signal.

Conflict asks the engine to report something it already computes. The pairing verdict builds a corroboration[] array with an agrees flag; the refusal in correlation.ts is specifically against emitting corroboration as edges in the graph, because that is what connects everything. Reading corroboration only where an authoritative edge already exists inverts that: it is not drawing new lines, it is annotating a line already drawn.

The shape of the thing being adopted

Two cells share a preauth-ref — an authoritative key, the strongest claim the engine makes short of a verdict — and carry different member identifiers. That is not a weak signal or a near-miss. It is two byte-exact comparisons, one matching and one not, and the combination is more alarming than either alone: the transaction says these are one case and the identity says they are not.

That is worth surfacing precisely because it is rare. Its rarity is also what makes it safe: unlike a weak tier, a conflict cannot flood the panel, because it requires an authoritative edge to exist first.

What would make this wrong

If conflict annotations turn out to fire routinely, the premise is wrong and the cause is almost certainly that a "corroborating identity field" was drawn too broadly — a display name, a formatted date, anything a payer normalises differently. The field set must stay narrow and closed, and the honest response to a noisy conflict annotation is to shrink that set, not to soften the claim into a maybe. The moment it renders as "possibly conflicting", it has become the weak tier this record declined.

Where this is canonical

src/lib/validator/payload-compare/pairing/correlation.ts — The refusals this record reasons about are stated in that module's header (no near-miss reporting, no corroboration edges) and held by the `CorrelationEdge` type, which carries no direction, role or isResponseTo. The code is canonical; this is the dated argument for changing one half of it and keeping the other.