Skip to main content
Version: 3.0.1

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

PropertyValue
IntentEvaluate 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.
InputsWell observations (role, CLS, CT, quantity, problems), Combined Outcome configurations, Historical run data (for multi-mix)
OutputsLIMS outcome code, Error code (on well and/or target)
PrecedenceBefore 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 IDTitlePriorityComplexity
REQ-RULES-COMBOUT-001Core Combined Outcome MatchingMustHigh
REQ-RULES-COMBOUT-002Multi Mix Combined OutcomesMustHigh
REQ-RULES-COMBOUT-003CLS Discrepancy CheckMustMedium
REQ-RULES-COMBOUT-004CT Discrepancy CheckMustMedium

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

PropertyValue
Programmatic NameCOMBINED_OUTCOME
PrecedenceBefore RQUAL
ScopePer-well (single) and cross-well (multi-mix)
OutputLIMS outcome code, error code

1.5 Dependencies

DirectionComponentPurpose
ConsumesWell + ObservationsWell data with PCR results
Kit ConfigurationCombined outcome mappings
Historical RunsPrior run data for multi-mix
RelatedWDCLS RuleCLS discrepancy calculation
WDCT RuleCT discrepancy calculation
PrecedesRQUAL RuleFallback outcome assignment

2. Component Architecture

2.1 Component Diagram

2.2 Component Responsibilities

ComponentFileResponsibilityREQ Trace
CombinedOutcomeRuleAnalyzer/Rules/CombinedOutcomeRule.phpEntry point, routes to single/multiAll
SingleWellCombinedOutcomeRuleAnalyzer/Rules/SingleWellCombinedOutcomeRule.phpSingle-well matchingREQ-COMBOUT-001
MultipleWellsCombinedOutcomeRuleAnalyzer/Rules/MultipleWellsCombinedOutcomeRule.phpCross-well patient matchingREQ-COMBOUT-002
SatisfiesCombinedOutcomeAnalyzer/Rules/Concerns/.../SatisfiesCombinedOutcome.phpCore matching logicREQ-COMBOUT-001
CheckCLSDiscrepancyAnalyzer/Rules/Concerns/.../CheckCLSDiscrepancy.phpCLS discrepancy conditionREQ-COMBOUT-003
CheckCTDiscrepancyAnalyzer/Rules/Concerns/.../CheckCTDiscrepancy.phpCT discrepancy conditionREQ-COMBOUT-004
CheckMixResultsAnalyzer/Rules/Concerns/.../CheckMixResults.phpMulti-mix satisfactionREQ-COMBOUT-002
SetOutcomeToWellAnalyzer/Rules/Concerns/.../SetOutcomeToWell.phpApply outcome/error to wellAll

3. Data Design

3.1 Entities

This rule reads from configuration and writes to well records:

EntityOwnerRead/WriteFields Used
wellsRUNRPTRead/Writeobservations, lims_outcome, error_code, accession, mix_id, type
observationsRUNRPTReadrole, final_cls, final_ct, final_quantity, problems
combined_outcomesKITCFGReadSee configuration structure below
runsRUNFILEReadFor 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

MethodClassPurpose
satisfies(Well, CombinedOutcome): boolSatisfiesCombinedOutcomeCheck if well matches outcome
checkCLSDiscrepancy(Observation, TargetResult): boolCheckCLSDiscrepancyEvaluate CLS discrepancy condition
checkCTDiscrepancy(Observation, TargetResult): boolCheckCTDiscrepancyEvaluate CT discrepancy condition
checkMixResults(Well[], CombinedOutcome): boolCheckMixResultsEvaluate multi-mix satisfaction
setOutcome(Well, CombinedOutcome): voidSetOutcomeToWellApply 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 configObservation has IC_FAILEDResult
truetrueCONTINUE (assign error)
truefalseSKIP
falsetrueSKIP
falsefalseCONTINUE

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_missingmix_missingWell PresentResult
falsefalseYesCONTINUE (check target results)
falsefalseNoFAIL (required mix absent)
falsetrueYesFAIL (mix should be absent)
falsetrueNoCONTINUE (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_requiredhas_cls_discrepancyResult
truetrueASSIGN
truefalseSKIP
falsetrueSKIP
falsefalseASSIGN
AnytrueASSIGN
AnyfalseASSIGN

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_requiredhas_ct_discrepancyResult
truetrueASSIGN
truefalseSKIP
falsetrueSKIP
falsefalseASSIGN
AnytrueASSIGN
AnyfalseASSIGN

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)

Version

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:

ComponentChange
SatisfiesCombinedOutcome.phpGuard added before addWellsToSetAssociateWellsForCombinedOutcome
SatisfiesCombinedOutcomeForMissingMixes.phpSame guard for missing-mixes path
SetOutcomeToWell.phpSkips association when outcome is Ignored

Decision logic:

Combined OutcomeAssociation RecordedReanalysis Triggered
LIMS outcome (non-Ignored)YesPossible (if well changes)
Error codeYesPossible
IgnoredNoNever

6. Error Handling

ConditionDetectionResponseFallback
No matching outcomeEmpty matches listDo not assignRQUAL rule handles
Multiple matchesmatches.length > 1Select lowest groupN/A (deterministic)
Missing history wellHistory lookup returns nullDo not assign multi-mix outcomeSingle-well rules may apply
History outcome mismatchOutcome not in required listDo not assignFall through
Invalid configurationMissing required fieldsLog error, skip outcomeOther outcomes may match

7. Configuration

SettingPathDefaultEffectREQ
groupcombined_outcome.group-Priority for conflict resolution001
ic_failedcombined_outcome.ic_failedfalseMatch IC_FAILED observations001
min_ct, max_cttarget_result.*nullCT range bounds001
min_quantity, max_quantitytarget_result.*nullQuantity range bounds001
mixes_missingcombined_outcome.mixes_missingfalseCheck for absent mixes002
allow_other_runs_to_be_usedcombined_outcome.*falseInclude history002
required_history_outcomescombined_outcome.*[]Filter history wells002
cls_discrepancy_requiredtarget_result.*AnyCLS discrepancy condition003
ct_discrepancy_requiredtarget_result.*AnyCT discrepancy condition004

See Configuration Reference for full documentation.


8. Implementation Mapping

8.1 Code Locations

ComponentPath
Entry PointAnalyzer/Rules/CombinedOutcomeRule.php
Single-WellAnalyzer/Rules/SingleWellCombinedOutcomeRule.php
Multi-WellAnalyzer/Rules/MultipleWellsCombinedOutcomeRule.php
Satisfaction CheckAnalyzer/Rules/Concerns/CombinedOutcomes/SatisfiesCombinedOutcome.php
CLS DiscrepancyAnalyzer/Rules/Concerns/CombinedOutcomes/CheckCLSDiscrepancy.php
CT DiscrepancyAnalyzer/Rules/Concerns/CombinedOutcomes/CheckCTDiscrepancy.php
Mix ResultsAnalyzer/Rules/Concerns/CombinedOutcomes/CheckMixResults.php
Outcome SettingAnalyzer/Rules/Concerns/CombinedOutcomes/SetOutcomeToWell.php

8.2 Requirement Traceability

REQ IDDesign SectionPrimary Code
REQ-RULES-COMBOUT-001§5.1, §5.2SingleWellCombinedOutcomeRule.php, SatisfiesCombinedOutcome.php
REQ-RULES-COMBOUT-002§5.3MultipleWellsCombinedOutcomeRule.php, CheckMixResults.php
REQ-RULES-COMBOUT-003§5.4CheckCLSDiscrepancy.php
REQ-RULES-COMBOUT-004§5.4CheckCTDiscrepancy.php

9. Design Decisions

DecisionRationaleAlternatives Considered
Separate single/multi-well classesDifferent complexity, testabilitySingle class with mode (rejected: too complex)
Lowest group wins for conflictsDeterministic, configurableFirst match (rejected: order-dependent), Random (rejected: non-deterministic)
Concern traits for checksReusability, single responsibilityInline in rule class (rejected: bloated)
History lookup by extraction dateBusiness logic for "most recent"By run date only (rejected: less precise)
Multi-run disabled for discrepancyTechnical limitation (cross-run comparison complex)Full support (deferred: future enhancement)

10. Performance Considerations

ScenarioConcernMitigation
Large runfiles (>384 wells)Multi-mix groupingIndex on accession, batch processing
Many combined outcomes (>50)Iteration overheadEarly exit on first match per group
History lookupsDatabase queriesCache recent runs, limit lookback period

DocumentRelevant Sections
SRS: rule-combine-outcomes.mdRequirements source
SDS: Rules EngineFramework overview
SDS: ClassificationCLS/CT discrepancy calculation
SDS: OutcomeFallback outcome assignment