Chapter 26

One pull, four truths

Model one event that advances several positions and returns truth for each.

On this page

The first preparation model moved one piece of brass through one operation at a time. That topology was easy to trace, which was one reason the Lee and the hand-work stations made good starting points.

Production revised the map. The Dillon case feeder now advances volume 5.56 through one active station that decaps and full-length sizes in the same pass. The shellplate moves more than one case, but the configured line establishes one combined preparation result. It is a bridge between the simple hand-work flow and the live progressive topology this chapter models.

The progressive press changes the problem. A rotating plate holds several cases beneath several stations. One pull of the handle acts on all occupied positions. One case is primed. Another receives powder. Another receives a bullet. Another is crimped. Then, if the stroke succeeded, the plate advances.

Once full, the machine produces one cartridge per pull. That throughput is the reason it exists. It is also why one answer about the pull is not enough. The machine holds several truths, and one event attempts to change all of them.

The press on this bench has processed real brass. Its case feeder, station-one die, spent-primer capture, sizing adjustment, and headspace loop have evidence behind them. It has not completed a live-ammunition production cycle. Primer placement, powder drop, bullet seating, final crimp, and the interaction among those stations remain open.

Two empty 5.56 cases occupying positions on the Dillon shellplate beneath the sizing and decapping station
The exercised configuration: case feed into one combined decap-and-size responsibility. Real operation, but not yet the four live-ammunition truths modeled below.

Write the event-position matrix

Before code, put one proposed cycle into a table. The Ruby model has four stations:

Position Work before pull Facts before Attempt Report after stroke Facts after stroke if successful
Prime case D none establish primed completed, faulted, or empty primed
Charge case C primed establish charged completed, faulted, or empty primed, charged
Seat case B primed, charged establish seated completed, faulted, or empty primed, charged, seated
Crimp case A primed, charged, seated establish crimped completed, faulted, or empty all four production facts
Close view of a progressive press shellplate with four cartridge cases occupying four positions
Four occupied positions make the topology visible: one stroke acts on several pieces of state. The photograph shows the mechanism; by itself, it does not prove a live production result.

If every occupied position completes, the whole plate advances one position and case A exits. If any position faults, the successful facts remain true but the plate does not advance.

The table separates two events that are easy to blur in a machine rhythm: stations strike, then the plate advances. A strike can change facts even when the later movement is refused. That separation becomes the foundation of recovery.

Start with staggered work

A test with one case proves only that one station can act. The signature behavior needs two cases at different positions responding to the same pull:

it "carries staggered pieces through different stations on one stroke" do
  press.feed(work)
  press.cycle
  press.feed(work)

  result = press.cycle

  expect(result.outcomes.map(&:status)).to eq(%i[completed completed empty empty])
end

The test is deliberately about outcomes, not internal loops. The press may iterate, dispatch, or use another data structure later. Its contract is that one cycle operates every occupied position and reports each result.

The smallest implementation makes the two phases visible:

def cycle
  outcomes = stroke
  return CycleResult.new(advanced: false, outcomes: outcomes, ejected: nil) if faulted?(outcomes)

  CycleResult.new(advanced: true, outcomes: outcomes, ejected: advance)
end

stroke asks each station to apply its work. advance moves the plate only after every outcome is known. The CycleResult returns per-position outcomes, whether movement occurred, and any ejected work.

A result owes one answer per effect

A single success: false would preserve almost nothing. Which station failed? Did the other stations complete? Was the plate held? Did a finished item leave? An operator would have to reconstruct those answers from timing, logs, and hope.

The model instead returns a PositionOutcome for every station:

PositionOutcome = Data.define(:station, :status, :fault)

CycleResult = Data.define(:advanced, :outcomes, :ejected) do
  def advanced?
    advanced
  end
end

An empty position reports empty. A successful station reports completed. A faulted station reports faulted and names the fault. The result describes the event that occurred, not the event the caller wished had occurred.

This is the multi-position form of the rule from Build for what you need to know: make the system explain itself at the decision point. Four positions create four explanations owing.

The plate is a carrier for accumulated truth

Each piece of work is a case plus established production facts. A station does not set a general status. It returns the work with one fact added:

FactStation = Data.define(:name, :fact) do
  def apply(work)
    StationResult.new(work: work.with_fact(fact), fault: nil)
  end
end

The station refuses to know the order of the line, which cases occupy other positions, or whether the plate will advance. The press refuses to know what primed or charged means. It coordinates a protocol: station receives work, station returns work plus any fault.

The injected station list is a real joint because changing it changes the enumerated positions and their behavior. No station reaches into the press to move the plate. No press reaches into a station to establish its fact.

That division also makes the drawing possible:

ProductionRun -> ProductionLine
                    |
                    +-> ProgressivePress -> [station, station, station, station]
                    |
                    +-> PressRecovery

ProductionLine groups the press and its recovery policy because they share the same ordered stations. The production orchestrator still receives four collaborators rather than letting a long parameter list hide the relationship.

Where this machine appears in software

Strip away the brass and the topology remains:

  • one batch tick advances many records;
  • one event updates several state machines;
  • one deployment trigger moves multiple services;
  • one workflow action calls several external systems;
  • one scheduler cycle attempts work for many tenants.

For one event in your system, fill the matrix:

Position or participant State before Attempted effect Reported outcome State after

Then ask:

  1. Can you enumerate every participant the event may advance?
  2. Does the result report each participant separately?
  3. Is movement or commit distinct from the attempted effects?
  4. Which effects remain true if a later participant fails?
  5. Can an operator locate the fault without reconstructing it?

If one boolean summarizes several effects, expand the result before designing recovery. Recovery cannot preserve truth the normal path threw away.

What the model cannot establish

The green specs prove the four-station Ruby protocol. They do not prove the physical live-ammunition line's timing, feel, clearances, or behavior with primers, powder, bullets, and crimp interacting. The case-processing pass proves a narrower physical configuration. The real live line may reveal state the model does not carry.

The normal-cycle tests also do not prove recovery. They establish the facts a recovery design will need. The next chapter pulls the handle on a modeled fault, holds the plate, and asks exactly what returning control means.