- [Type family instances](#type-family-instances)
- [Type-family resolution in the viewer](#type-family-resolution-in-the-viewer)
- [Are the resolution chains deterministic?](#are-the-resolution-chains-deterministic)
+- [Predicate nodes and how they dedup](#predicate-nodes-and-how-they-dedup)
- [What the viewer is *not* doing](#what-the-viewer-is-not-doing)
## Pipeline at a glance
(`pdInstances`, `pdFamInstances`, `tfEquations`, etc.) is ground
truth.
+## Predicate nodes and how they dedup
+
+The instance view introduces a synthetic node type — `kind: 'predicate'`
+— that doesn't appear in the schema. Two cases produce one:
+
+- **Context constraint** (the predicate sits in `iiContext`). Edge
+ from focused instance → predicate node, labelled `instance context`.
+- **Unmatched superclass** (a `ciSuperclasses` entry whose substituted
+ args don't match any instance in `pdInstances`). Edge from focused
+ instance → predicate node, labelled `superclass constraint`. If the
+ predicate happens to be structurally identical to a context
+ predicate the same node receives both edges; otherwise a fresh node
+ with `role: 'extern'` is created (rendered grey).
+
+This is a viewer-only abstraction. The Haskell side knows nothing about
+it; predicate nodes have no on-disk representation. The dedup is what
+makes the "context AND unmatched superclass" case collapse into a
+single node — which is what a typical reader expects to see when the
+same constraint is required twice.
+
+A few things to be aware of:
+
+- **Dedup key is a structural hash.** Two predicates collapse to one
+ node when their class `QualName` and the JSON-stringified
+ `[TypeArg]` are byte-identical. The hash sees through tyvar names
+ (TyVarRef carries an integer index, not a name), so two instances
+ with `[a, b]` vs `[x, y]` produce the same id structurally. Their
+ *labels* differ (rendered against each instance's own
+ `iiTyVars`) — first call wins on the label.
+
+- **Reduction asymmetry between context and superclass.** Context
+ predicates carry `piArgs` as written. Superclass requirements go
+ through `reduceTypeArg` so we can match them against
+ `pdInstances`. If a fam-instance reduction changes the arg shape —
+ the context says `Foo (F a)`, the reduced superclass says
+ `Foo Int` — the dedup id differs and the user sees two predicate
+ nodes for what is morally one constraint. Realistic but rare in
+ practice.
+
+- **Tyvar index aliasing across instances.** When the focused class
+ has many instances and they happen to produce structurally
+ identical context predicates (different tyvar names, same indices),
+ the predicate node is shared across all of them. That's *probably*
+ what the user wants — fewer noisy duplicates — but means the
+ arrows from several instance nodes land on one shared predicate.
+ No information is lost; the labels just match the first instance
+ that contributed.
+
+- **'extern' role is sticky to the first observation.** If a
+ predicate is observed first as a context constraint (`role:
+ 'context'`) and later as an unmatched superclass, the node keeps
+ the `'context'` role and just gains a second edge. If observed in
+ the reverse order — superclass first, context second — same thing
+ happens because context predicates are processed before superclass
+ requirements in `buildInstanceView`. This is intentional; the
+ styling shouldn't downgrade once we know the predicate is also a
+ legitimate context constraint.
+
+- **Predicate nodes don't survive view changes.** The set is rebuilt
+ from scratch every time the user enters an instance view; nothing
+ is persisted. The same predicate, viewed from a different focused
+ class, gets a different node id (because the focused-instance
+ arg/tyvar context differs). That's fine — predicate nodes are
+ scoped to "this view".
+
## What the viewer is *not* doing
A few things we deliberately don't do, in case you wonder: