0027 — An SPC signal raises a quality hold, and an inspected characteristic with a specification is a check¶
- Status: accepted
- Date: 2026-09-14
- Deciders: scottkalw
Context¶
GET /quality/spc/{material}/{characteristic} has run four Western Electric
rules over the stored checks since the chart was written, and returned them
as signals. Nothing read them. No hold, no non-conformance, no trigger, no
audit row — the rules fired for whoever happened to have the screen open, and
at two in the morning that is nobody.
Underneath it, the readings mostly were not there to fire on. The simulator's
inspection path (InspSeq / InspPass / Insp_* → unit_inspections) wrote
a UnitInspection per unit and never a QualityCheck. QualityCheck is the
only table the chart reads, so a station measuring four characteristics ten
times a second was invisible to the only code that could have judged them. A
scrap burst on a moulding line produced a wall of scrapped units and a
perfectly flat control chart.
So the two halves of the gap: the readings did not reach the chart, and the chart's verdict reached nothing.
This came out of the lab's third round (2026-09-14, finding F15). The lab wants to measure how long after the first bad part does a hold exist — and could not, because the only thing that could link a scrap burst to a hold was the lab itself, which would have been the lab timing its own actor.
Options considered¶
| Option | For | Against |
|---|---|---|
| Leave the rules on the GET; let a plant write a trigger on the tag if it wants action | No new state, no new table | A control chart nobody has open computes nothing. And there is no tag: the rule is a statement about a series, not about a reading, so there is nothing for a tag-watching trigger to watch |
| Evaluate on the write, raise a non-conformance, once per rule per window of readings; and turn an inspected characteristic that has a specification into a check | The rule fires when the plant is still running the thing that tripped it; the hold carries the rule and the points, so a supervisor is not asked to take the machine's word for it; the plant still decides what else happens | A new table, an evaluation on a hot write path, and a judgement call about how often "the same excursion" may raise a record |
| Evaluate on the write and act directly — hold the lot, stop the line | Fastest containment | The MES would be stopping lines on a statistical inference. Rule 4 (eight points one side of centre) is an early warning, not a fault, and a plant that has its line stopped by one learns to switch the feature off |
| Make every inspected attribute a check | Nothing to configure; the chart sees everything | A row in quality_checks for every attribute of every unit at line rate. The cutlery lab alone is 160,000 rows a shift for charts nobody asked for |
Decision¶
An SPC signal is an event the MES acts on. The four Western Electric rules
are evaluated when a check is recorded — on the write path, in
fsmes.services.spc.evaluate — as well as on the chart endpoint, which is
unchanged. A rule that fires is recorded as an SpcSignal row carrying the
rule, what it means, the reading it fired on, and the chart as it stood:
centre, sigma, the ±3σ limits, how many readings they came from, and the
points of its window. Recomputing those numbers a day later gives different
ones, and then nobody can see what the MES actually acted on.
A signal raises a non-conformance, with that evidence on it
(non_conformances.evidence), against the material and characteristic, the
work order the reading was taken against when it named one, and the station
that measured when one was recorded. It raises nothing else: it does not hold
a lot, stop a line, scrap stock or write to a machine. What a plant does next
is a person's act, or a trigger the plant configures — the trigger
catalogue gains spc.signal, a tag no PLC publishes, raised by the MES on the
station whose reading tripped the rule, with the rule number as its value. A
plant that wants rule 1 to stop a moulder writes that trigger; it is not
written into this code.
The hold is raised open, not under review. Decision 0024 says who took
each step is recorded and a step nobody took is null. Nobody has picked this
up — a machine raised it. under_review with no reviewer would be the MES
claiming a person looked.
Once per signal. A signal's identity is its rule plus the window of
readings it judged — the first and last check id of rule 1's single point,
rule 2's three, rule 3's five, rule 4's eight — held unique in spc_signals.
Re-running the rules over the same stored readings finds the row and raises
nothing, which is what makes evaluating on every write safe. And while the
non-conformance a rule raised is still unresolved, a further firing of that
rule on that characteristic joins it rather than opening another: an excursion
that lasts twenty readings is one thing that went wrong, not twenty. Once
somebody has dispositioned it, the next firing is a new finding.
An inspected characteristic that has a specification is a check. A station
event's Insp_<Name> reading becomes a QualityCheck when the plant has
written a QualitySpec for that material and that characteristic name, judged
in or out of spec by that specification. One with no specification stays a
UnitInspection and is named in the ingest's uncharted list and its audit
row — reported as uncharted, never silently dropped. Which characteristics are
charted is therefore the plant's configuration, not this code's guess, and
that is also what keeps the row count to what somebody asked for.
Such a reading opens no per-reading non-conformance. The station has already judged the unit and the unit is already good or scrapped; a record per bad piece on a machine inspecting ten a second is noise a supervisor learns to scroll past. What raises the hold is the signal. A reading a person records still opens one when it is out of spec, as it always has: a person measuring one bottle has found one bad bottle, and that is a finding.
quality_checks gains equipment_id: the station that took the reading, for
readings a station took. A person with a gauge records none, and null there
means not recorded, not "no machine" — deriving one from the order's route
would name a machine nobody stood at.
Migration a9c4e17b3d60 adds the table and the three columns and changes no
data.
Consequences¶
Easier: a scrap burst now reaches the chart and the chart now reaches a supervisor, in the seconds it takes the readings to arrive rather than the next time somebody opens a screen. The lab can measure the distance between the first bad part and the hold, because the product closes that loop and the lab only watches it. The hold carries its own evidence, so the first question a supervisor asks — why do you think so — is answered on the record.
Harder: quality.record_check now returns three things, not two — the check,
the non-conformance and the signals. That is a breaking change to a service
signature in a 0.x release and is in the changelog under Honesty. The
rules run on a write path; they read at most the last 200 readings of the one
characteristic that changed, and a batch of station readings evaluates once
per characteristic, not once per reading.
To revisit: whether the window that raises a hold should be the plant's setting (some plants act on rule 1 only), and whether a signal should be able to name a lot. It cannot today: a quality check records its work order, not a lot, and guessing the lot from the order would be inventing the traceability this MES exists to keep honest.
House rules touched¶
Never invent production (1): a hold raised by a rule states which rule,
which readings, and the limits it judged them against; a station the MES did
not record is null and reaches no trigger, rather than being derived.
Unknown is a valid answer (2): fewer than twelve readings raises nothing
and the chart says why, as it already did. Unlabelled data is reported as
unlabelled (3): a measured characteristic with no specification is counted
and named in uncharted, never quietly dropped. Config, not code (4):
which characteristics are charted is the plant's specifications, and what a
signal sets off beyond the hold is the plant's trigger. Tests are prose
(5): tests/test_spc_signals.py names each rule after the behaviour it pins.