Combined Outcomes Rule Design
Document Type: Rule Design (Tier 2) Domain: RULES-COMBOUT Domain Character: Algorithmic SRS Reference: rule-combine-outcomes.md Status: Draft Last Updated: 2026-03-05
1. Overview
1.0 Rule Summary
| Property | Value |
|---|---|
| Intent | Evaluate well observation results against configured mappings to assign LIMS outcomes and error codes, supporting single-well matching, multi-well patient matching across mixes, and discrepancy-based conditions. |
| Inputs | Well observations (role, CLS, CT, quantity, problems), Combined Outcome configurations, Historical run data (for multi-mix) |
| Outputs | LIMS outcome code, Error code (on well and/or target) |
| Precedence | Before RQUAL; after WDCLS and WDCT (for discrepancy flags) |
1.1 Purpose
The Combined Outcomes rule evaluates well observation results against configured mappings to assign LIMS outcomes and error codes. It is a core component of the PCR analysis rules engine, executing before the RQUAL rule.
The rule supports:
- Single-well outcome matching based on role, classification, CT, quantity, and IC failure
- Multi-well outcome matching across mixes for the same patient
- Discrepancy-based matching (CLS and CT discrepancies)
- Conflict resolution via priority groups
1.2 Requirements Covered
| REQ ID | Title | Priority | Complexity |
|---|---|---|---|
| REQ-RULES-COMBOUT-001 | Core Combined Outcome Matching | Must | High |
| REQ-RULES-COMBOUT-002 | Multi Mix Combined Outcomes | Must | High |
| REQ-RULES-COMBOUT-003 | CLS Discrepancy Check | Must | Medium |
| REQ-RULES-COMBOUT-004 | CT Discrepancy Check | Must | Medium |
1.3 Constraints
Tier 2 Constraint: This document describes ownership, patterns, and design rationale. It links to reference docs for full schemas.
1.4 Rule Properties
| Property | Value |
|---|---|
| Programmatic Name | COMBINED_OUTCOME |
| Precedence | Before RQUAL |
| Scope | Per-well (single) and cross-well (multi-mix) |
| Output | LIMS outcome code, error code |
1.5 Dependencies
| Direction | Component | Purpose |
|---|---|---|
| Consumes | Well + Observations | Well data with PCR results |
| Kit Configuration | Combined outcome mappings | |
| Historical Runs | Prior run data for multi-mix | |
| Related | WDCLS Rule | CLS discrepancy calculation |
| WDCT Rule | CT discrepancy calculation | |
| Precedes | RQUAL Rule | Fallback outcome assignment |
2. Component Architecture
2.1 Component Diagram
2.2 Component Responsibilities
| Component | File | Responsibility | REQ Trace |
|---|---|---|---|
CombinedOutcomeRule | Analyzer/Rules/CombinedOutcomeRule.php | Entry point, routes to single/multi | All |
SingleWellCombinedOutcomeRule | Analyzer/Rules/SingleWellCombinedOutcomeRule.php | Single-well matching | REQ-COMBOUT-001 |
MultipleWellsCombinedOutcomeRule | Analyzer/Rules/MultipleWellsCombinedOutcomeRule.php | Cross-well patient matching | REQ-COMBOUT-002 |
SatisfiesCombinedOutcome | Analyzer/Rules/Concerns/.../SatisfiesCombinedOutcome.php | Core matching logic | REQ-COMBOUT-001 |
CheckCLSDiscrepancy | Analyzer/Rules/Concerns/.../CheckCLSDiscrepancy.php | CLS discrepancy condition | REQ-COMBOUT-003 |
CheckCTDiscrepancy | Analyzer/Rules/Concerns/.../CheckCTDiscrepancy.php | CT discrepancy condition | REQ-COMBOUT-004 |
CheckMixResults | Analyzer/Rules/Concerns/.../CheckMixResults.php | Multi-mix satisfaction | REQ-COMBOUT-002 |
SetOutcomeToWell | Analyzer/Rules/Concerns/.../SetOutcomeToWell.php | Apply outcome/error to well | All |
3. Data Design
3.1 Entities
This rule reads from configuration and writes to well records:
| Entity | Owner | Read/Write | Fields Used |
|---|---|---|---|
wells | RUNRPT | Read/Write | observations, lims_outcome, error_code, accession, mix_id, type |
observations | RUNRPT | Read | role, final_cls, final_ct, final_quantity, problems |
combined_outcomes | KITCFG | Read | See configuration structure below |
runs | RUNFILE | Read | For history lookup |
See Database Reference for full schema.
3.2 Configuration Structure
interface CombinedOutcome {
id: string;
name: string;
group: number; // Priority (lower = higher)
type: 'outcome' | 'error';
lims_code: string | null; // null if error type
error_code: string | null; // null if outcome type
ic_failed: boolean; // Match on IC failure
mix_results: MixResult[];
// Multi-mix settings
mixes_missing: boolean;
allow_other_runs_to_be_used: boolean;
required_history_outcomes: string[];
mix_level_outcomes: MixLevelOutcome[];
}
interface MixResult {
mix_id: string;
mix_missing: boolean; // Expected to be absent
target_results: TargetResult[];
}
interface TargetResult {
target_id: string;
role: string; // Must match observation role
result: 'Any' | 'Classification/Discrepancy' | string; // CLS value
min_ct: number | null;
max_ct: number | null;
min_quantity: number | null;
max_quantity: number | null;
cls_discrepancy_required: boolean | 'Any';
ct_discrepancy_required: boolean | 'Any';
}
interface MixLevelOutcome {
mix_id: string;
outcome: string; // Override outcome for this mix
}
3.3 Matching State
interface MatchingContext {
well: Well;
observation: Observation;
combined_outcome: CombinedOutcome;
target_result: TargetResult;
// Computed during matching
role_matches: boolean;
result_matches: boolean;
ct_in_range: boolean;
quantity_in_range: boolean;
ic_failed_matches: boolean;
cls_discrepancy_matches: boolean;
ct_discrepancy_matches: boolean;
// For multi-mix
patient_wells: Well[];
history_wells: Well[];
mix_results_satisfied: boolean;
}
4. Interface Design
4.1 Rule Interface
interface RuleInterface {
public function execute(Well $well, Kit $kit): void;
public function getName(): string;
public function getPrecedence(): int;
}
4.2 Internal APIs
| Method | Class | Purpose |
|---|---|---|
satisfies(Well, CombinedOutcome): bool | SatisfiesCombinedOutcome | Check if well matches outcome |
checkCLSDiscrepancy(Observation, TargetResult): bool | CheckCLSDiscrepancy | Evaluate CLS discrepancy condition |
checkCTDiscrepancy(Observation, TargetResult): bool | CheckCTDiscrepancy | Evaluate CT discrepancy condition |
checkMixResults(Well[], CombinedOutcome): bool | CheckMixResults | Evaluate multi-mix satisfaction |
setOutcome(Well, CombinedOutcome): void | SetOutcomeToWell | Apply outcome to well |
4.3 Events
This rule does not emit events. It modifies well state directly.
5. Behavioral Design
5.1 Core Matching Algorithm (REQ-COMBOUT-001)
Algorithm: Evaluate Single Well Combined Outcome
Inputs:
- well: Well - The well being evaluated
- observations: Observation[] - Well's observation results
- combined_outcomes: CombinedOutcome[] - Configured outcome mappings
Outputs:
- void (modifies well.lims_outcome, well.error_code)
Assumptions:
- Observations have been calculated (final_cls, final_ct, final_quantity populated)
- Combined outcomes are loaded and valid
- WDCLS/WDCT rules have run (discrepancy flags set)
Steps:
1. Initialize matches = []
2. For each combined_outcome in combined_outcomes:
For each observation in well.observations:
For each mix_result in combined_outcome.mix_results:
For each target_result in mix_result.target_results:
a. Role match: IF target_result.role != observation.role: CONTINUE
b. IC_FAILED check: IF NOT check_ic_failed(combined_outcome, observation): CONTINUE
c. Result/CLS match:
- IF target_result.result == 'Any': match = true
- ELSE IF target_result.result == 'Classification/Discrepancy':
match = 'CLASSIFICATION' IN observation.problems
- ELSE: match = (target_result.result == observation.final_cls)
- IF NOT match: CONTINUE
d. CT range check:
IF NOT in_range(observation.final_ct, target_result.min_ct, target_result.max_ct):
CONTINUE
e. Quantity range check:
IF NOT in_range(observation.final_quantity, target_result.min_quantity, target_result.max_quantity):
CONTINUE
f. CLS discrepancy check: IF NOT check_cls_discrepancy(observation, target_result): CONTINUE
g. CT discrepancy check: IF NOT check_ct_discrepancy(observation, target_result): CONTINUE
h. All conditions passed: matches.append({ combined_outcome, group })
3. IF matches.length == 0: RETURN (fall through to RQUAL)
4. Conflict resolution: sort matches by group ascending, winner = matches[0]
5. Apply outcome:
- IF winner.type == 'error': set well.error_code, well.lims_outcome = null
- ELSE: set well.lims_outcome = winner.lims_code
Notes:
- Evaluation order: role → IC_FAILED → result → ranges → discrepancies
- All conditions must pass; first failure skips to next outcome
- Lowest group number = highest priority (group 1 beats group 2)
Helper: in_range
Algorithm: Check Value in Range
Inputs:
- value: number|null - The value to check
- min: number|null - Lower bound (null = unbounded)
- max: number|null - Upper bound (null = unbounded)
Outputs:
- bool - Whether value is within bounds
Steps:
1. IF min == null AND max == null: RETURN true
2. IF min != null AND value < min: RETURN false
3. IF max != null AND value > max: RETURN false
4. RETURN true
5.1.1 IC_FAILED Decision Logic
ic_failed config | Observation has IC_FAILED | Result |
|---|---|---|
| true | true | CONTINUE (assign error) |
| true | false | SKIP |
| false | true | SKIP |
| false | false | CONTINUE |
Precedence: Evaluated after role match, before result match. Default: ic_failed defaults to false, so observations with IC_FAILED are skipped unless explicitly configured. Unreachable: None.
5.2 Decision Flowchart
5.3 Multi-Mix Algorithm (REQ-COMBOUT-002)
Algorithm: Evaluate Multi-Mix Combined Outcome
Inputs:
- well: Well - The well being evaluated (used for patient identification)
- run: Run - Current run containing all wells
- combined_outcome: CombinedOutcome - The multi-mix outcome configuration
- history_enabled: bool - Whether to consider historical runs
Outputs:
- bool - Whether outcome was successfully assigned
Assumptions:
- Wells have accession field populated for patient identification
- Historical runs are accessible when allow_other_runs_to_be_used is true
- Mix-level outcomes are configured for each expected mix
Steps:
1. Filter: IF well.type != 'Sample': RETURN false (only sample wells)
2. Group wells by patient:
patient_wells = run.wells.where(w => w.accession == well.accession)
3. For each mix_result in combined_outcome.mix_results:
a. Find well for this mix: mix_well = patient_wells.find(w => w.mix_id == mix_result.mix_id)
b. Handle mix_missing logic (see decision table below):
- IF mixes_missing=false AND mix_missing=true AND mix_well exists: RETURN false
- IF mixes_missing=false AND mix_missing=false AND mix_well missing: RETURN false
c. Check target results:
- IF NOT satisfies_target_results(mix_well, mix_result.target_results):
- IF history_enabled AND allow_other_runs_to_be_used:
- history_well = find_most_recent_history(well.accession, mix_result.mix_id)
- IF history_well == null: RETURN false
- IF NOT check_history_outcome(history_well, required_history_outcomes): RETURN false
- IF NOT satisfies_target_results(history_well, target_results): RETURN false
- ELSE: RETURN false
4. All mix results satisfied - apply outcomes:
FOR each mix_well in patient_wells:
outcome = get_mix_level_outcome(combined_outcome, mix_well.mix_id)
apply_outcome(mix_well, outcome)
RETURN true
Notes:
- Different wells for same patient may receive different outcomes (mix-level)
- History lookup uses extraction_date DESC, then run.created_at DESC
- Only most recent history well is evaluated, not all historical wells
Helper: find_most_recent_history
Algorithm: Find Most Recent History Well
Inputs:
- accession: string - Patient identifier
- mix_id: string - Mix to find
Outputs:
- Well|null - Most recent matching history well, or null
Steps:
1. Query all runs except current
2. Filter wells: accession matches AND mix_id matches
3. Order by: extraction_date DESC, run.created_at DESC
4. Return first result or null
5.3.1 Mix Missing Decision Logic
mixes_missing | mix_missing | Well Present | Result |
|---|---|---|---|
| false | false | Yes | CONTINUE (check target results) |
| false | false | No | FAIL (required mix absent) |
| false | true | Yes | FAIL (mix should be absent) |
| false | true | No | CONTINUE (expected absence) |
| true | * | * | (mixes_missing=true logic not shown - different path) |
Precedence: Evaluated before target result checking. Default: mixes_missing defaults to false. Unreachable: None.
5.4 Discrepancy Check Logic (REQ-COMBOUT-003, 004)
Algorithm: Check CLS Discrepancy
Inputs:
- observation: Observation - Contains has_cls_discrepancy flag (set by WDCLS rule)
- target_result: TargetResult - Contains cls_discrepancy_required setting
Outputs:
- bool - Whether observation satisfies discrepancy condition
Assumptions:
- WDCLS rule has already executed and set has_cls_discrepancy
- cls_discrepancy_required is one of: true, false, or 'Any'
Steps:
1. required = target_result.cls_discrepancy_required
2. has_discrepancy = observation.has_cls_discrepancy
3. IF required == 'Any': RETURN true
4. IF required == true: RETURN has_discrepancy == true
5. IF required == false: RETURN has_discrepancy == false
Notes:
- 'Any' acts as a wildcard - discrepancy status is irrelevant
- Discrepancy calculation logic is owned by WDCLS rule, not this rule
5.4.1 CLS Discrepancy Decision Logic
cls_discrepancy_required | has_cls_discrepancy | Result |
|---|---|---|
| true | true | ASSIGN |
| true | false | SKIP |
| false | true | SKIP |
| false | false | ASSIGN |
| Any | true | ASSIGN |
| Any | false | ASSIGN |
Precedence: Evaluated after range checks, before CT discrepancy check. Default: cls_discrepancy_required defaults to 'Any' (no constraint). Unreachable: None.
5.4.2 CT Discrepancy Decision Logic
CT discrepancy follows identical logic with ct_discrepancy_required and has_ct_discrepancy:
ct_discrepancy_required | has_ct_discrepancy | Result |
|---|---|---|
| true | true | ASSIGN |
| true | false | SKIP |
| false | true | SKIP |
| false | false | ASSIGN |
| Any | true | ASSIGN |
| Any | false | ASSIGN |
Precedence: Evaluated after CLS discrepancy check (final condition). Default: ct_discrepancy_required defaults to 'Any' (no constraint). Unreachable: None. Limitation: Multi-run CT discrepancy checking is disabled; only current run wells evaluated.
5.5 Cross-Run Association Guard (v3.0.1)
Added in v3.0.1 (commit a7ecd68, BT-5811).
Problem: Cross-run combined outcome logic could create invalid well associations when the outcome was "Ignored", causing spurious reanalysis triggers and potentially infinite reanalysis loops (ISSUE-010/KI-005).
Fix: SetOutcomeToWell now checks whether the combined outcome being assigned is "Ignored" before setting the associate_well_ids_for_combined_outcome field. If the outcome is Ignored, the association is skipped — the well receives the Ignored outcome but does not record a cross-run dependency.
Affected components:
| Component | Change |
|---|---|
SatisfiesCombinedOutcome.php | Guard added before addWellsToSetAssociateWellsForCombinedOutcome |
SatisfiesCombinedOutcomeForMissingMixes.php | Same guard for missing-mixes path |
SetOutcomeToWell.php | Skips association when outcome is Ignored |
Decision logic:
| Combined Outcome | Association Recorded | Reanalysis Triggered |
|---|---|---|
| LIMS outcome (non-Ignored) | Yes | Possible (if well changes) |
| Error code | Yes | Possible |
| Ignored | No | Never |
6. Error Handling
| Condition | Detection | Response | Fallback |
|---|---|---|---|
| No matching outcome | Empty matches list | Do not assign | RQUAL rule handles |
| Multiple matches | matches.length > 1 | Select lowest group | N/A (deterministic) |
| Missing history well | History lookup returns null | Do not assign multi-mix outcome | Single-well rules may apply |
| History outcome mismatch | Outcome not in required list | Do not assign | Fall through |
| Invalid configuration | Missing required fields | Log error, skip outcome | Other outcomes may match |
7. Configuration
| Setting | Path | Default | Effect | REQ |
|---|---|---|---|---|
group | combined_outcome.group | - | Priority for conflict resolution | 001 |
ic_failed | combined_outcome.ic_failed | false | Match IC_FAILED observations | 001 |
min_ct, max_ct | target_result.* | null | CT range bounds | 001 |
min_quantity, max_quantity | target_result.* | null | Quantity range bounds | 001 |
mixes_missing | combined_outcome.mixes_missing | false | Check for absent mixes | 002 |
allow_other_runs_to_be_used | combined_outcome.* | false | Include history | 002 |
required_history_outcomes | combined_outcome.* | [] | Filter history wells | 002 |
cls_discrepancy_required | target_result.* | Any | CLS discrepancy condition | 003 |
ct_discrepancy_required | target_result.* | Any | CT discrepancy condition | 004 |
See Configuration Reference for full documentation.
8. Implementation Mapping
8.1 Code Locations
| Component | Path |
|---|---|
| Entry Point | Analyzer/Rules/CombinedOutcomeRule.php |
| Single-Well | Analyzer/Rules/SingleWellCombinedOutcomeRule.php |
| Multi-Well | Analyzer/Rules/MultipleWellsCombinedOutcomeRule.php |
| Satisfaction Check | Analyzer/Rules/Concerns/CombinedOutcomes/SatisfiesCombinedOutcome.php |
| CLS Discrepancy | Analyzer/Rules/Concerns/CombinedOutcomes/CheckCLSDiscrepancy.php |
| CT Discrepancy | Analyzer/Rules/Concerns/CombinedOutcomes/CheckCTDiscrepancy.php |
| Mix Results | Analyzer/Rules/Concerns/CombinedOutcomes/CheckMixResults.php |
| Outcome Setting | Analyzer/Rules/Concerns/CombinedOutcomes/SetOutcomeToWell.php |
8.2 Requirement Traceability
| REQ ID | Design Section | Primary Code |
|---|---|---|
| REQ-RULES-COMBOUT-001 | §5.1, §5.2 | SingleWellCombinedOutcomeRule.php, SatisfiesCombinedOutcome.php |
| REQ-RULES-COMBOUT-002 | §5.3 | MultipleWellsCombinedOutcomeRule.php, CheckMixResults.php |
| REQ-RULES-COMBOUT-003 | §5.4 | CheckCLSDiscrepancy.php |
| REQ-RULES-COMBOUT-004 | §5.4 | CheckCTDiscrepancy.php |
9. Design Decisions
| Decision | Rationale | Alternatives Considered |
|---|---|---|
| Separate single/multi-well classes | Different complexity, testability | Single class with mode (rejected: too complex) |
| Lowest group wins for conflicts | Deterministic, configurable | First match (rejected: order-dependent), Random (rejected: non-deterministic) |
| Concern traits for checks | Reusability, single responsibility | Inline in rule class (rejected: bloated) |
| History lookup by extraction date | Business logic for "most recent" | By run date only (rejected: less precise) |
| Multi-run disabled for discrepancy | Technical limitation (cross-run comparison complex) | Full support (deferred: future enhancement) |
10. Performance Considerations
| Scenario | Concern | Mitigation |
|---|---|---|
| Large runfiles (>384 wells) | Multi-mix grouping | Index on accession, batch processing |
| Many combined outcomes (>50) | Iteration overhead | Early exit on first match per group |
| History lookups | Database queries | Cache recent runs, limit lookback period |
11. Related Documents
| Document | Relevant Sections |
|---|---|
| SRS: rule-combine-outcomes.md | Requirements source |
| SDS: Rules Engine | Framework overview |
| SDS: Classification | CLS/CT discrepancy calculation |
| SDS: Outcome | Fallback outcome assignment |