Chapter 22

Three surprises, three exits

Preserve meaning through wrong context, foreign material, and quarantine.

On this page

The tray held four kinds of thing.

The 5.56 was expected. The 9mm was good brass in the wrong process. The .50 caliber cases were also good brass in the wrong process, although they had been put into the tumbler deliberately. The stripper clip was not a case.

Later in preparation, inspection found a third kind of refusal: cases that belonged to the 5.56 process but had crushed mouths. They were mine. Their identity was right. Their condition was not.

They went into a zip bag, physically out of the loading blocks and off the normal flow. The damaged mouths remained visible through the plastic. The bag stayed at the bench, but nothing in it could drift back into production by looking enough like the material beside it.

The bag is the quarantine.

Wrong context, foreign material, and damaged owned material all receive a "no." That does not make them the same.

Labeled buckets, a tub of mixed brass, ammunition crates, and a case tumbler arranged on the garage floor
The exits exist before the sorting starts. Each "no" already has somewhere to go, and none of the destinations can leak back into the flow.

What does each no preserve?

Imagine one bucket under the bench marked REJECTS. Put the 9mm, the .50 caliber cases, the stripper clip, and the crushed 5.56 into it.

The bucket is simple. It is also an information shredder.

The 9mm still has value when the correct process exists. The stripper clip does not belong to any case-loading process. The crushed 5.56 may teach us something about a range lot, a preparation tool, or rough handling, but only if its identity and reason remain attached. One bucket preserves none of those distinctions. A full bucket tells us only that the system has said no a number of times.

An exit should preserve the information needed for the next legitimate decision. That is why the first artifact in this chapter is not a list of validation rules. It is an exit table.

Material class Identity preserved Reason preserved Destination Allowed re-entry
Expected 5.56 case and batch accepted at classification caliber-specific loading block continues normally
Wrong-context 9mm or .50 caliber case and caliber wrong caliber for this run labeled bin for that caliber only through the correct process from its admission boundary
Foreign stripper clip description of object not a cartridge case leaves the brass workflow none
Defective owned 5.56 case, lot when known, visible condition observed defect quarantine bag, out of normal flow no automatic re-entry; explicit disposition required

The final column carries most of the design. Wrong-context material may have a future. Quarantined material does not get to grant itself one. The same code path that says no should not quietly decide that the reason has been repaired. Release from quarantine is a separate decision, owned by a person or process with enough evidence to make it.

The tumbler owns none of this

The easiest place to add a caliber check appears to be the first machine. Stop the wrong object before work is spent cleaning it.

But the tumbler cannot see the decision we need. Its useful contract is smaller: dirty material in, range dirt removed on output. It processed the .50 caliber cases deliberately and the 9mm accidentally in exactly the same way. Teaching it today's expected caliber would join cleaning to one downstream run and still would not tell us what to do with damaged 5.56 after inspection.

A processor should not enforce a rule it does not own.

Classification belongs immediately after the first cleaning, where the process has enough context to ask what belongs next. Defect inspection belongs later, where prepared material can be seen well enough to judge. The three exits do not require one giant classifier. They require each boundary to keep the refusal it owns and preserve the result for the rest of the system.

This gives us an important distinction:

  • A station transforms material.
  • A boundary decides whether material may cross.
  • A carrier makes some invalid placements physically difficult or impossible.
  • An exit preserves a refusal for its next legitimate disposition.

The tumbler is a station. The classification point is a boundary. The loading block is a carrier. The labeled bin, foreign-material exit, and quarantine bag are exits.

The tray becomes a test

The inner-loop test begins with the observed output. It names the protocol of the classifier before its implementation matters:

it "separates the observed cleaning output into three exits" do
  five_fifty_six = CartridgeCase.new(caliber: "5.56")
  nine_millimeter = CartridgeCase.new(caliber: "9mm")
  fifty_caliber = CartridgeCase.new(caliber: ".50 cal")
  stripper_clip = ForeignObject.new(description: "stripper clip")

  classification = described_class.new(expected_caliber: "5.56").classify(
    [five_fifty_six, nine_millimeter, fifty_caliber, stripper_clip]
  )

  expect(classification.accepted).to eq([five_fifty_six])
  expect(classification.wrong_context).to eq([nine_millimeter, fifty_caliber])
  expect(classification.foreign).to eq([stripper_clip])
end

The full spec is spec/case_classifier_spec.rb.

The test says three things about the joint.

First, the classifier receives a collection and returns a classification. It does not move items to bins or call the next station. Second, valid material in the wrong context retains its identity. Third, foreign material is not smuggled into the wrong-context collection merely because both were refused.

Only then does the smallest implementation appear:

class CaseClassifier
  def initialize(expected_caliber:)
    @expected_caliber = expected_caliber
  end

  def classify(items)
    brass, foreign = items.partition { |item| item.respond_to?(:caliber) }
    accepted, wrong_context = brass.partition { |item| expected?(item) }

    Classification.new(accepted:, wrong_context:, foreign:)
  end

  private

  def expected?(item)
    item.caliber == @expected_caliber
  end
end

The object has one job: separate the observed input into exits. It refuses to know whether a 5.56 case is free of residue, long enough, or damaged. It refuses to store the accepted cases. It refuses to decide what the wrong-context bin does next.

Those refusals keep the next joint visible.

The block enforces placement

Classification returns information. The caliber-specific loading block makes the decision difficult to bypass at the physical bench. The .50 caliber cases do not fit its holes. The 9mm sits obviously wrong, loose and short. The stripper clip has no position to try.

The Ruby carrier is a CaliberBatch. Its first test is the ordinary path:

it "accepts a case of its own caliber" do
  cartridge_case = CartridgeCase.new(caliber: "5.56")

  placement = batch.accept(cartridge_case)

  expect(placement).to be_placed
  expect(batch.contents).to eq([cartridge_case])
end

The load-bearing refusal test checks both the answer and the state left behind:

it "refuses a case of another caliber and does not hold it" do
  placement = batch.accept(CartridgeCase.new(caliber: "9mm"))

  expect(placement).to be_refused
  expect(placement.reason).to eq(:wrong_caliber)
  expect(batch.contents).to be_empty
end

accept returns a Placement for every attempt. Expected refusal is data, not an exception. The caller can count reasons, route the refused item, or show an operator what happened. An exception remains available for a breached programming contract, but wrong-caliber brass at a sorting boundary is not a surprise to the program. It is one of the reasons the boundary exists.

Notice the two proofs do different work. The classifier proves that the three categories remain distinct. The carrier proves that refused material never entered accepted state. Returning the right reason while still appending the 9mm would be transformed output wrapped around a broken invariant.

Quarantine is not another classifier branch

The crushed 5.56 passes the caliber boundary. Its shape at that boundary is close enough, and its identity is correct. The defect becomes visible at inspection, under the magnifier, after more facts have been established.

That is not a failure of the loading block. A boundary can only reject what it can sense.

Trying to make the first classifier detect every future defect would give it responsibility for facts that do not exist yet. It would need to know cleaning quality, primer-pocket preparation, case length, and every later production fact. Soon the whole system would point into one object called Validator, and every change would land there.

Instead, the classification boundary promises only this: the material is case-like and belongs to the caliber context. The readiness gate later promises more. Final inspection promises more again. Each promise is narrow enough to draw and strong enough for the next part to trust.

Build your exit table

Choose a boundary in your own system. Start with something it has actually refused, not a hypothetical taxonomy.

Refusal class Identity to preserve Reason to preserve Destination Re-entry authority

Then ask:

  1. Which refusals are valuable in a different context?
  2. Which inputs were never valid material for this system?
  3. Which inputs belong here but need quarantine because their condition is uncertain or defective?
  4. Can anything re-enter merely by being submitted again?
  5. Which object owns each decision, and what does it refuse to know?

If every row leads to the same dead-letter queue, error log, or status flag, you may be preserving storage while destroying meaning.

What the exits cannot establish

The physical loading block senses shape imperfectly. A damaged 5.56 case can fit. The Ruby model senses a caliber message, which is also only a chosen approximation of case-like shape. An unrelated object could answer that message. A cartridge case could carry a false value. Neither carrier proves quality or truth by itself.

The quarantine bag has another limit. Here the crushed mouths make the reason visible, so the material carries much of its own story. An invisible defect would need a reason attached in a notebook or record. A bag is separation. It is not automatically provenance.

The exits preserve what the next decision needs, no more. In the next chapter, one of the preparation facts turns out to require a tool the original plan did not contain.