01 · Before and afterThe pattern
// before: available for any selection, runner reads selectedRecords[0]
available: every(selected, isDraft)
// after: the rule matches what the runner does
available: selected.length === 1 && every(selected, isDraft)
// and memoise at the source instead of loosening a shared equality check
const selectedRows = useMemo(() => rows.filter(r => ids.includes(r.id)), [rows, ids]);02 · Run it on your appThe check
Pick two rows in your app's table and click each bulk action: does every one act on both rows or refuse?
If the honest answer is no or not sure, this is worth a look before it reaches production.