Skip to Content
TechnicalPublic Norwood checkerThe tool state machine

The tool state machine

How it works, in a paragraph. The checker is one client component holding one reducer. Its state is a discriminated union rather than a bag of booleans, which means the combinations that would be bugs are not merely avoided — they cannot be written down. There is exactly one place that decides what the visitor sees, and every failure the tool can encounter is a named member of that union rather than an error boundary.

The six steps. referred and outcome are terminal for that submission; failed returns to capture because none of its causes are the visitor’s mistake.

The shape is the safety argument

The outcome is a tagged union, so a stage only exists on the member that has one:

MemberCarries
unreadablenothing — the photos could not be read
excludednothing — the pattern is not one we will estimate
stagethe stage, and a confidence of medium or high

Two absences are doing the work. unreadable and excluded have no stage field, so “a Norwood verdict on a photo we said we could not read” has no representation. And confidence has no low — low confidence does not produce a stage, it produces an abstention, so the risky combination cannot be constructed by a caller who forgets the rule.

The same union is stated three times, in the three places a wrong record could be created: this type, the API’s response schema, and a check constraint in the database. Any one of them alone would be a convention; together they are a guarantee.

Failures are states, not exceptions

quota, capacity, provider and offline are members of a Failure union and each renders its own panel. They are kept separate from the outcome deliberately, because none of them consumes one of the visitor’s checks — they describe something that went wrong on the way rather than a conclusion about their scalp.

Two of them look alike and are not. quota means this visitor has used their allowance today; capacity means the tool as a whole has. Telling a first-time visitor they have used up their checks would be a lie, so the distinction survives all the way to the panel they see.

Per-photo state, and why converting exists

Each of the two photo slots is its own small union — empty, converting, ready, invalid — where an upload widget would normally offer two states. The extra ones earn their place:

  • converting covers the seconds an iPhone HEIC takes to become a JPEG in the browser. Without it the interface looks frozen at exactly the moment most visitors are using it.
  • invalid carries a reason — type, size, dimensions, heic — so the slot can say what to fix rather than that something is wrong.

The gate blocks by default

The gate has three answers, not two: yes, no, and not sure. A binary would force everyone who genuinely does not know into “no”, which is precisely the population the gate exists to catch.

The blocking condition is written as “not no” rather than “yes or unsure”. That is not a stylistic choice: a fourth answer added later blocks by default instead of silently falling through to the permissive branch.

One submit, whatever the visitor does

The submission is keyed on entering the working step rather than on the click, so a double-tap or an impatient second press cannot start two checks. The same submission also reuses one idempotency key across retries, so a retry of a failed attempt does not spend a second check from the allowance.

Deliberately not exported

The helpers that decide whether both photos are ready and whether the gate blocks stay private to the module. Exporting them would invite a second caller to re-derive “can this be submitted”, and two answers to that question is how a disabled button and a live handler end up disagreeing.

Last updated on