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.
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 |
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:
- Can you enumerate every participant the event may advance?
- Does the result report each participant separately?
- Is movement or commit distinct from the attempted effects?
- Which effects remain true if a later participant fails?
- 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.