Detect exact-tuple overlaps across a tool registration set.
Intentionally NOT flagged:
Broad vs. narrow targets (e.g. an untargeted click alongside
click on empty) — the dispatcher's specificity ordering resolves
those cleanly, and the broad one is usually the intended fallback.
Different modifier requirements on the same target — they fire on
different inputs.
A binding whose action declines via enabled() so a lower-priority
one can take the gesture. Detecting that intent would mean evaluating
the action; consumers can suppress known-intentional compositions in
their UI layer.
Two { kindOf } predicate targets. The route grammar renders every
predicate as the single token PREDICATE_TARGET, so bucketing on
the rendered target alone reported select's resize / rotate / move
drags — three genuinely different predicates — as a three-way conflict.
Predicate entries bucket by function identity instead, which means two
separately written but equivalent predicates go unflagged. That's the
right way to be wrong here: this check has to be silent when nothing is
wrong or nobody will keep it on.
Note that two bindings sharing a tuple on the SAME tool are now possible
(bindings are an array, where phase tables were objects with unique
keys) — so a conflict may name one tool twice.
This is the raw same-tuple detector. Feeding it a whole tool registry
over-reports, because registry tools take turns in the active slot and
can't collide with each other — see findScopedConflicts.
Detect exact-tuple overlaps across a tool registration set.
Intentionally NOT flagged:
Broad vs. narrow targets (e.g. an untargeted
clickalongsideclickonempty) — the dispatcher's specificity ordering resolves those cleanly, and the broad one is usually the intended fallback.Different modifier requirements on the same target — they fire on different inputs.
A binding whose action declines via
enabled()so a lower-priority one can take the gesture. Detecting that intent would mean evaluating the action; consumers can suppress known-intentional compositions in their UI layer.Two
{ kindOf }predicate targets. The route grammar renders every predicate as the single token PREDICATE_TARGET, so bucketing on the rendered target alone reported select'sresize/rotate/movedrags — three genuinely different predicates — as a three-way conflict. Predicate entries bucket by function identity instead, which means two separately written but equivalent predicates go unflagged. That's the right way to be wrong here: this check has to be silent when nothing is wrong or nobody will keep it on.Note that two bindings sharing a tuple on the SAME tool are now possible (bindings are an array, where phase tables were objects with unique keys) — so a conflict may name one tool twice.
This is the raw same-tuple detector. Feeding it a whole tool registry over-reports, because registry tools take turns in the active slot and can't collide with each other — see findScopedConflicts.