ump.play()
play() is the cleanup companion to check(). It never mutates values. It only recommends what the consumer should clear or reset.
Signature
Section titled âSignatureâtype Snapshot< F extends Record<string, FieldDef>, C extends Record<string, unknown>,> = { values: InputValues conditions?: C}
ump.play( before: Snapshot<F, C>, after: Snapshot<F, C>,): Foul<F>[]Return Shape
Section titled âReturn Shapeâtype Foul<F extends Record<string, FieldDef>> = { field: keyof F & string reason: string suggestedValue: unknown}When A Recommendation Appears
Section titled âWhen A Recommendation Appearsâplay() produces a foul when a field holds a non-empty value that just fell out of play. There are two ways that can happen:
Availability foul â the field was enabled in before and is disabled in after.
Appropriateness foul â the field is still enabled, but a fairWhen predicate that was passing in before is now failing in after. The value is present and the field is available, but the selection is no longer appropriate given the current form state.
In both cases, a recommendation only appears when:
- The trigger above applies.
- The current value in
afteris still non-empty under that fieldâsisEmptyrules. - The current value differs from the suggested reset value.
Condition three matters for defaults. If a field falls out of play while it already holds its default value, recommending that same value again would be a no-op.
suggestedValue
Section titled âsuggestedValueâsuggestedValue is:
FieldDef.defaultwhen the field defines oneundefinedotherwise
const ump = umpire({ fields: { isAllDay: { default: true }, startTime: { default: '09:00' }, endTime: {}, }, rules: [],})Disabling startTime recommends '09:00'. Disabling endTime recommends undefined.
Conditions-Only Transitions
Section titled âConditions-Only TransitionsâBecause snapshots include conditions, play() works even when field values do not change.
signupUmp.play( { values: formValues, conditions: { plan: 'business' } }, { values: formValues, conditions: { plan: 'personal' } },)That is how plan switches, feature flags, or captcha expiration can still produce reset recommendations.
Convergence
Section titled âConvergenceâplay() has a useful convergence property: as the consumer applies the recommended resets, the next pass eventually returns [].
That is true even for non-empty defaults because the method suppresses no-op recommendations when the field already equals its suggestedValue.
Example
Section titled âExampleâconst fouls = signupUmp.play( { values: { companyName: 'Acme', companySize: '50', }, conditions: { plan: 'business' }, }, { values: { companyName: 'Acme', companySize: '50', }, conditions: { plan: 'personal' }, },)
// [// {// field: 'companyName',// reason: 'business plan required',// suggestedValue: undefined,// },// {// field: 'companySize',// reason: 'business plan required',// suggestedValue: undefined,// },// ]When check() is enough
Section titled âWhen check() is enoughâThink about what question youâre actually trying to answer.
In a scheduler, a user picks a date, a time, a recurrence pattern, and a timezone. When they submit, you need to know which fields are active and which of those still lack a value. That is a question about the current state of the form â and check() answers it directly:
const availability = scheduleUmp.check(values, conditions)
for (const [field, status] of Object.entries(availability)) { if (!status.enabled) continue if (!status.satisfied) errors.push(`${field} is required`)}You donât need two snapshots for this. You donât need to know what changed. You need to know whatâs true right now.
play() answers a different question: something changed â do any fields need to be cleared? The prototype for that is a recurrence toggle. The user sets a recurrence pattern, then switches the event to âall day.â The time fields fall out of play, but they still hold values. play() notices that, tells you which fields are affected, and suggests what to reset them to.
If your handler doesnât need to auto-reset anything â it just validates and saves â reach for check() and stop there. play() earns its keep when a state transition leaves stale values behind that you want to clean up before the user notices.
foulMap() â lookup by field
Section titled âfoulMap() â lookup by fieldâplay() returns an array, which is convenient for rendering a banner but requires .find() when you need the foul for a specific field. foulMap() converts the array into a field-keyed map:
import { foulMap } from '@umpire/core'
const fouls = ump.play(before, after)const byField = foulMap(fouls)
byField.companyName?.reason // 'business plan required'byField.referralCode // undefined â no foul for this fieldBoth representations are useful: the array for iterating (fouls banner, reset-all button), the map for per-field access (inline foul indicators, field-level reset buttons).
Reactive foul() in signals
Section titled âReactive foul() in signalsâThe @umpire/signals adapter exposes reactive.foul(name) for per-field foul access with fine-grained reactivity:
const reactive = reactiveUmp(ump, adapter)
// Per-field â only re-renders when this field's foul changesconst foul = reactive.foul('companyName')// â Foul | undefined
// Full array â for banner renderingconst allFouls = reactive.foulsreactive.foul(name) mirrors reactive.field(name) â availability and fouls have the same per-field accessor pattern.