Affinity
A location-eligibility predicate — the "can I execute here" question, riding the opaque task envelope (H8, design §14.6, Model A).
An Affinity is a small serializable expression over the capability tokens a peer advertises in its CapSet. It rides the TaskDescriptor the same way H6's Lane does: warp core carries it and evaluates it with matches, but assigns the tokens no meaning and depends on no module that interprets them. Placement then consistent-hashes over the eligible subset of the roster — the peers whose advertised CapSet satisfies this predicate — rather than the whole roster (see TaskRing.owner). Every peer computes the same eligible set from the same convergent capability view, so determinism holds for the same reason the ring does.
Eligibility is independent of entitlement: this type lives in :kuilt-warp and names no :kuilt-heddle type. It composes with a lane (a task may carry both an Affinity and a Lane) but does not live inside one — eligibility answers where, a lane answers how much.
Because it is serializable it cannot capture a Kotlin lambda; instead it is a closed algebra of leaf predicates (Has, Attr) and combinators (And, Or, Not), with Anywhere as the no-requirement default. Build predicates fluently:
val where = Affinity.has("GPU") and Affinity.attr("region", "us-east")Samples
// "must run on a GPU node in us-east" — a composable predicate, not a lambda (it rides the wire).
val where = Affinity.has("GPU") and Affinity.attr("region", "us-east")
val gpuUsEast = CapSet(tokens = setOf("GPU"), attributes = mapOf("region" to "us-east"))
val cpuUsWest = CapSet(tokens = setOf("CPU"), attributes = mapOf("region" to "us-west"))
check(where.matches(gpuUsEast)) // eligible
check(!where.matches(cpuUsWest)) // not eligible
check(Affinity.Anywhere.matches(cpuUsWest)) // the default requires nothing
// Tag a task with the requirement; placement then hashes over only the eligible peers.
val task = TaskDescriptor(OpId("train"), byteArrayOf(1, 2, 3)).where(where)
check(task.affinity == where)Inheritors
Types
The no-requirement default: every peer is eligible, so placement is over the whole roster — warp's pre-H8 behaviour, bit-for-bit. This is the TaskDescriptor.affinity default; a descriptor that never sets an affinity is byte-for-byte identical on the wire to a pre-H8 descriptor (CBOR omits a field at its default).