Branch exploration: contamination_ppm had ridden along on the SoupStream bus since the bus was first defined, wired to a constant 0 at every producer and read by nothing downstream — a signal shaped like a requirement with no behavior behind it. SR-GS-007 requires contamination detection before sealing at 99% sensitivity or better, and the QC station that should have been doing that detection had never been asked to. This branch gives BehQCStation an actual contamination model, sweeps the incidence rate the requirement has to hold across, and converts SR-GS-007 from unexercised to verified-by-executed-test.
Artifacts: the two new model arguments and outports on the shared QC library model, instance-parameter wiring in ../behavior/build/buildInlineBehaviors.m, ../analysis/sweeps/runContaminationSweep.m, ../behavior/tests/tBehQCStation.m and ../tests/analysis/tContamination.m, the Contamination suite in ../tests/system/GalacticSoupSystemTests.mldatx. Decision: ADR-027 in 07_decision_log.md.
SoupStream carries a contamination_ppm element that has been present in the bus definition for as long as the bus has existed, and every producer in the behavioral layer sets it to a constant 0. Nothing downstream ever branched on it — the QC station's job, as originally modeled, was purely a quality-pass/quality-reject split on the existing quality signal, with no contamination logic anywhere in the chain. SR-GS-007 was consequently in the same bucket gravity dependence occupied before doc 14: a requirement with a component named for the job (the QC station), no implementation of the job, and no way for a test to exercise it. The fix is not touching the bus element itself — contamination_ppm stays as-is for now — but giving BehQCStation a real contamination path with its own detected/escaped accounting.
BehQCStation gains two model arguments with neutral defaults, so every existing caller is unaffected until a dictionary override says otherwise:
| Argument | Default | Meaning |
|---|---|---|
ContamIncidence |
0 | Fraction of the quality-passed stream that is contaminated |
DetectSensitivity |
0.995 | Fraction of contaminated flow the station catches |
Contamination is applied to the quality-passed stream, passflow_q, not the raw input — the station only has to worry about contamination in soup it would otherwise have shipped:
contam = passflow_q * ContamIncidence
detected = contam * DetectSensitivity -> routed to the reject stream
escaped = contam * (1 - DetectSensitivity) -> ships with the passed flow
Two new outports, contamDetected_bps and contamEscaped_bps, expose the split for logging and for test criteria. At ContamIncidence = 0, contam is identically zero regardless of DetectSensitivity, so the model is exactly neutral — every pre-existing throughput baseline stands unchanged: 308.4 / 196.8 / 231.9 bph for HyperCook, LeanBroth, and EverSimmer respectively, the same figures carried since doc 12's nominal suite.
DetectSensitivity's default of 0.995 sits above the 0.99 requirement floor deliberately — margin against the requirement, not a value backed into from a measurement. The sweep in §3 includes a case that tests the floor itself.
Both new arguments are exposed at the top level via the shared dictionary as QC_ContamIncidence (default 0) and QC_DetectSensitivity (default 0.995), mapped into each BehQCStation instance through instance-parameter expressions in buildInlineBehaviors.m — the same generator, and the same per-run override mechanism (setVariable(..., 'Workspace', model)), that fault injection and the gravity sweep (docs 13 and 14) already established.
runContaminationSweep.m sweeps QC_ContamIncidence over 0.5%, 1%, 2%, and 5% across all three variants, plus one boundary case with QC_DetectSensitivity set to 0.99 — the requirement floor itself, rather than the 0.995 design default — run on EverSimmer only. Results land in contaminationResults.mat and contaminationSweep.csv, with a summary figure at ../docs/figures/contamination_sensitivity.png.
Measured sensitivity is flat at 0.9950 for every variant at every incidence point — as it must be, since the detection split is deterministic flow math; the sweep verifies the plumbing end-to-end through three different architectures rather than sampling a statistic. What varies is the cost of detection: rejecting detected contamination trims throughput roughly in proportion to incidence × sensitivity, and at the worst swept point (5% incidence) the variants land at 293.1 / 187.9 / 220.4 bph (HyperCook / LeanBroth / EverSimmer). No compliance verdict flips anywhere in the sweep: HyperCook and EverSimmer clear the 200 bph floor with room at 5% contamination, and LeanBroth stays below it as it already was at zero.
The boundary case measures exactly 0.9900 — a detector specified at the requirement floor meets the requirement with zero margin, which is precisely why the design value is 0.995 and not 0.99. The escaped-contamination rate at 2% incidence works out to about 103 ppm of packaged flow for the single-QC variants and 34 ppm for EverSimmer, whose per-cell QC stations each see only a third of the plant's flow.
That per-cell number is the one architectural observation in the sweep: contamination detection composes the same way fault tolerance does. EverSimmer's distributed QC doesn't detect any better (sensitivity is identical), but a contamination excursion in one cell is confined to a third of production, which is the same isolation argument that won it the fault-response comparison — showing up unprompted in a completely different quality metric.
Signal-logging marks go on contamDetected_bps and contamEscaped_bps for the QC instance in each variant — QCBench in LeanBroth, InlineQCScanner in HyperCook, and Cell 1's station only in EverSimmer (its other cells are not marked, consistent with the single-instance logging pattern established for gravity-related signals in doc 14). Simulink Test criteria read both signals from test.sltest_simout.get('logsout') and compute:
measured sensitivity = detected / (detected + escaped)
The Contamination suite in GalacticSoupSystemTests.mldatx adds two cases, both at 2% incidence:
| Case | Model | Criteria | Verify link |
|---|---|---|---|
| HyperCook contamination 2 percent | PhysicalHyperCook |
measured sensitivity ≥ 0.99 AND throughput floor still met at 2% incidence | SR-GS-007 |
| EverSimmer contamination 2 percent | PhysicalEverSimmer |
measured sensitivity ≥ 0.99 AND throughput floor still met at 2% incidence | SR-GS-007 |
LeanBroth gets neither a case nor a link, per the Verify-link semantics rule from doc 12 §3: LeanBroth's nominal throughput already sits below the SR-GS-002 floor, so a passing contamination case on top of an already-noncompliant baseline would not demonstrate SR-GS-007 being genuinely met in a shippable configuration. LeanBroth's numbers are still worth protecting — they're covered by the sweep in §3, which baselines them as a regression baseline without asserting requirement satisfaction.
Below the suite, tBehQCStation.m gains a component-level contamination method that checks the exact split ratios (detected/contam, escaped/contam) on the library model directly, isolated from the physical-variant simulations. tContamination.m, in the analysis tier, baselines the sweep results from §3 and the sensitivity-margin conclusion (0.995 design vs. 0.99 floor) as golden values.
With these two cases linked, the verified-by-test count in the requirements coverage summary grows from 5 to 6 of 28.
- Dictionary-side names must differ from model-argument names. The first attempt named the dictionary entry
ContamIncidence— identical to theBehQCStationmodel argument it feeds. Every simulation warned that the symbol defined in the model workspace shadows the symbol inBehParamsCommon.sldd. The fix is the sameQC_prefix convention already used elsewhere in the dictionary: the top-level override isQC_ContamIncidence, mapped into the model argumentContamIncidencevia the instance-parameter expression, and the two names never collide again. - Contamination applies to the passed stream, not the input. Computing
contamfrom the station's raw input rather thanpassflow_qwould double-count flow already rejected on quality grounds — a bug worth checking for explicitly in review, since both signals are available at the same point in the chart and the wrong one type-checks identically. - Neutrality at zero is what protects every existing baseline. Because
contam = passflow_q * ContamIncidenceand the default is 0, the entire contamination path multiplies out to exactly zero with no rounding residue — the same "exactly neutral at the default" property doc 14 relied on for gravity scaling at 1 g, and the reason the 308.4/196.8/231.9 bph baselines needed no re-verification, only a neutrality check. - Verify-link asymmetry follows the compliance picture, not the test suite's convenience. LeanBroth would pass a contamination criterion at 2% incidence in isolation — the QC station's detection math doesn't care which variant hosts it — but linking that pass to SR-GS-007 would claim the requirement is met by a variant that fails the throughput requirement it ships alongside. The link is withheld for the same reason doc 12 withheld LeanBroth's throughput link, not because the contamination behavior itself is suspect.