STD: Combined Outcomes Rule (COMBOUT)
Version: v1.0.0 Status: Draft SRS Source:
docusaurus/docs/srs/rules/rule-combine-outcomes.mdRule Name: COMBINED_OUTCOME Domain: RULES-COMBOUT
Overview
This document specifies tests for the Combined Outcomes rule using decision tables and test vectors. The rule evaluates well observations against configured Combined Outcome mappings to assign LIMS outcomes and error codes.
Rule Characteristics:
- Pure business logic (no UI)
- Complex condition matching (role, CLS, CT, quantity, discrepancy)
- Conflict resolution via group priority
- Multi-mix patient matching
Test Method: TM-API (per Test Plan §3.3 - Rules use automated API tests)
Verification Approach: Rule verification is performed using data-driven test vectors. Each row in a decision table represents a complete verification scenario with defined inputs and expected outputs. This format enables exhaustive condition coverage while remaining concise and auditable.
Coverage Summary
| REQ ID | Title | Conditions | Test Vectors | Coverage | Gaps |
|---|---|---|---|---|---|
| REQ-RULES-COMBOUT-001 | Core Matching | 15 | 24 | 100% | None |
| REQ-RULES-COMBOUT-002 | Multi-Mix Outcomes | 22 | 25 | 100% | None |
| REQ-RULES-COMBOUT-003 | CLS Discrepancy Check | 6 | 6 | 100% | None |
| REQ-RULES-COMBOUT-004 | CT Discrepancy Check | 6 | 6 | 100% | None |
Totals: 4 REQs, 49 Conditions, 61 Test Vectors, 100% Coverage
REQ-RULES-COMBOUT-001: Core Matching
Input Variables
| Variable | Type | Valid Values | Description |
|---|---|---|---|
config.role | string | NEC, PEC, Patient, ... | Combined outcome role |
obs.role | string | NEC, PEC, Patient, ... | Observation role |
config.result | string | Any, Pos, Neg, Amb, CLS/Discrepancy | Expected result |
obs.final_cls | string | Pos, Neg, Amb, ... | Observation classification |
obs.problems | array | [], [IC_FAILED], [CLASSIFICATION] | Observation problems |
config.ic_failed | bool | true, false | IC failure trigger |
config.min_ct | float? | null, numeric | CT lower bound |
config.max_ct | float? | null, numeric | CT upper bound |
obs.final_ct | float | numeric | Observation CT value |
config.min_quantity | float? | null, numeric | Quantity lower bound |
config.max_quantity | float? | null, numeric | Quantity upper bound |
obs.final_quantity | float | numeric | Observation quantity |
config.type | string | Outcome, Error | Outcome type |
config.group | int | 1, 2, 3, ... | Priority (lower wins) |
Output Variables
| Variable | Type | Description |
|---|---|---|
well.lims | string? | LIMS outcome code (null if error) |
well.error_code | string? | Error code (null if outcome) |
matched | bool | Whether the combined outcome was assigned |
Decision Table: Role Matching
| TV | config.role | obs.role | matched | Covers |
|---|---|---|---|---|
| TV-001-001 | NEC | NEC | true | AC: Role match |
| TV-001-002 | NEC | PEC | false | AC: Role mismatch |
| TV-001-003 | PEC | PEC | true | AC: Role match |
| TV-001-004 | Patient | NEC | false | AC: Role mismatch |
Decision Table: IC_FAILED Handling
| TV | config.ic_failed | obs.problems | matched | error_assigned | Covers |
|---|---|---|---|---|---|
| TV-001-005 | true | [IC_FAILED] | true | true | AC: IC_FAILED config=true, problem present |
| TV-001-006 | true | [] | false | false | AC: IC_FAILED config=true, problem absent |
| TV-001-007 | false | [IC_FAILED] | false | false | AC: IC_FAILED config=false, problem present |
| TV-001-008 | false | [] | true | false | AC: IC_FAILED config=false, problem absent |
Decision Table: Result/Classification Matching
| TV | config.result | obs.final_cls | obs.problems | matched | Covers |
|---|---|---|---|---|---|
| TV-001-009 | Any | Pos | [] | true | AC: "Any" matches any |
| TV-001-010 | Any | Neg | [] | true | AC: "Any" matches any |
| TV-001-011 | Pos | Pos | [] | true | AC: Exact CLS match |
| TV-001-012 | Pos | Neg | [] | false | AC: CLS mismatch |
| TV-001-013 | CLS/Discrepancy | Pos | [CLASSIFICATION] | true | AC: Discrepancy match |
| TV-001-014 | CLS/Discrepancy | Pos | [] | false | AC: No discrepancy |
Decision Table: Quantity Range Matching
| TV | config.min_qty | config.max_qty | obs.final_qty | matched | Covers |
|---|---|---|---|---|---|
| TV-001-015 | null | null | 1000 | true | AC: No bounds = match any |
| TV-001-016 | 900 | 1000 | 1000 | true | AC: Within bounds (inclusive) |
| TV-001-017 | 900 | null | 1000 | true | AC: Above min, no max |
| TV-001-018 | null | 999 | 1000 | false | AC: Exceeds max |
| TV-001-019 | 1001 | null | 1000 | false | AC: Below min |
Decision Table: CT Range Matching
| TV | config.min_ct | config.max_ct | obs.final_ct | matched | Covers |
|---|---|---|---|---|---|
| TV-001-020 | null | null | 35 | true | AC: No bounds = match any |
| TV-001-021 | 30 | 40 | 35 | true | AC: Within bounds |
| TV-001-022 | 30 | 40 | 41 | false | AC: Exceeds max |
| TV-001-022B | 30 | 40 | 37 | true | BVA: Below min boundary, inconclusive |
Decision Table: Combined CT + Quantity
| TV | min_ct | max_ct | final_ct | min_qty | max_qty | final_qty | matched | Covers |
|---|---|---|---|---|---|---|---|---|
| TV-001-023 | 31 | null | 30 | 2000 | null | 1000 | false | AC: Both conditions must match |
Decision Table: Conflict Resolution (Multiple Matches)
| TV | Outcome 1 | Outcome 2 | obs matches | selected | Covers |
|---|---|---|---|---|---|
| TV-001-024 | group=2, lims=DETECTED | group=1, lims=NOT_DETECTED | both | NOT_DETECTED | AC: Lowest group wins |
Decision Table: Error vs Outcome Type
| TV | config.type | error_code | expected_lims | expected_error | Covers |
|---|---|---|---|---|---|
| TV-001-025 | Outcome | null | DETECTED | null | AC: Outcome sets LIMS |
| TV-001-026 | Error | Error_A | null | Error_A | AC: Error sets error, LIMS=null |
REQ-RULES-COMBOUT-002: Multi-Mix Outcomes
Input Variables
| Variable | Type | Description |
|---|---|---|
well.type | string | Sample, NEC, PEC |
well.accession | string | Patient identifier |
well.mix | string | Mix identifier |
well.specimen | string | Specimen type (Serum, Plasma, etc.) |
config.mix_results | array | Required mix configurations |
config.mixes_missing | bool | Whether to check for absent mixes |
config.allow_other_runs | bool | Use historical runs |
config.required_history_outcomes | array | Required outcomes from history |
config.is_repeat | bool | Whether this is a repeat outcome check |
config.use_latest_uploaded_well | bool | Select by upload time vs extraction date |
config.use_sample_type | bool | Whether to filter by specimen type |
config.specimen | string? | Required specimen type (null = any) |
Decision Table: Sample Type Filter
| TV | well.type | triggered | Covers |
|---|---|---|---|
| TV-002-001 | Sample | true | AC: Trigger for samples |
| TV-002-002 | NEC | false | AC: Skip non-samples |
Decision Table: Patient Matching (Accession)
| TV | well_A.accession | well_B.accession | mix_results | satisfied | Covers |
|---|---|---|---|---|---|
| TV-002-003 | A | A | [Mix_A, Mix_B] | true | AC: Same patient |
| TV-002-004 | A | B | [Mix_A, Mix_B] | false | AC: Different patient |
Decision Table: Mixes Missing Logic
| TV | mixes_missing | mix_results | wells_present | satisfied | Covers |
|---|---|---|---|---|---|
| TV-002-005 | false | A(missing=F), B(missing=T), C(missing=F) | [A, C] | true | AC: Expected absence |
| TV-002-006 | false | A(missing=F), B(missing=T), C(missing=F) | [A, B, C] | false | AC: Unexpected presence |
Decision Table: History Runs
| TV | allow_other_runs | current_run_wells | history_wells | satisfied | Covers |
|---|---|---|---|---|---|
| TV-002-007 | true | [Mix_A] | [Mix_B] | true | AC: Uses history |
| TV-002-008 | false | [Mix_A] | [Mix_B] | false | AC: No history |
Decision Table: Required History Outcomes
| TV | required_outcomes | history_well.outcome | satisfied | Covers |
|---|---|---|---|---|
| TV-002-009 | [LIMS_A] | LIMS_A | true | AC: Outcome match |
| TV-002-010 | [LIMS_A] | LIMS_B | false | AC: Outcome mismatch |
Decision Table: Most Recent History Selection
| TV | history_wells | expected_selected | reason | Covers |
|---|---|---|---|---|
| TV-002-011 | [W1(e_date=1, outcome=B), W2(e_date=2, outcome=A)] | W2 | Most recent | AC: Date ordering |
| TV-002-012 | [W1(e_date=1, outcome=A), W2(e_date=2, outcome=B)] | W2 | Most recent | AC: Date ordering |
Decision Table: Mix-Level Outcomes
| TV | well.mix | mix_outcomes | expected_outcome | Covers |
|---|---|---|---|---|
| TV-002-013 | Mix_A | {Mix_A: Error_A, Mix_B: Error_B} | Error_A | AC: Per-mix outcome |
| TV-002-014 | Mix_B | {Mix_A: Error_A, Mix_B: Error_B} | Error_B | AC: Per-mix outcome |
Decision Table: IS_REPEAT History Match
| TV | is_repeat | history_exists | history_outcome_matches | satisfied | Covers |
|---|---|---|---|---|---|
| TV-002-015 | true | true | true | true | AC: Repeat with matching history outcome |
| TV-002-016 | true | false | n/a | false | AC: Repeat with no history well |
| TV-002-017 | true | true | false | false | AC: Repeat with wrong history outcome |
Decision Table: USE_LATEST_UPLOADED_WELL Selection
| TV | use_latest_uploaded | wells_available | expected_selection | Covers |
|---|---|---|---|---|
| TV-002-018 | true | [W1(upload=Jan1), W2(upload=Jan5)] | W2 | AC: Selects most recently uploaded |
| TV-002-019 | true | [W1(upload=Jan1, same_run)] | W1 | AC: Falls back to same run when no cross-run |
| TV-002-020 | false | [W1(e_date=Jan1), W2(e_date=Jan5)] | W2 | AC: Default selects by nearest extraction date |
Decision Table: SPECIMEN Filter
| TV | use_sample_type | config.specimen | well.specimen | satisfied | Covers |
|---|---|---|---|---|---|
| TV-002-021 | true | Serum | Plasma | false | AC: Specimen type mismatch rejects |
| TV-002-022 | true | Serum | Serum | true | AC: Specimen type match satisfies |
| TV-002-023 | false | Serum | Plasma | true | AC: use_sample_type=false bypasses check |
| TV-002-024 | true | null | Serum | true | AC: Null config specimen bypasses check |
| TV-002-025 | true | Serum | [mixed] | filtered | AC: Filters history wells by specimen |
REQ-RULES-COMBOUT-003: CLS Discrepancy Check
Decision Table: CLS Discrepancy Matching
| TV | cls_discrepancy_required | has_cls_discrepancy | matched | Covers |
|---|---|---|---|---|
| TV-003-001 | true | true | true | AC: Required and present |
| TV-003-002 | true | false | false | AC: Required but absent |
| TV-003-003 | false | true | false | AC: Not required but present |
| TV-003-004 | false | false | true | AC: Not required and absent |
| TV-003-005 | Any | true | true | AC: Any accepts present |
| TV-003-006 | Any | false | true | AC: Any accepts absent |
REQ-RULES-COMBOUT-004: CT Discrepancy Check
Decision Table: CT Discrepancy Matching
| TV | ct_discrepancy_required | has_ct_discrepancy | matched | Covers |
|---|---|---|---|---|
| TV-004-001 | true | true | true | AC: Required and present |
| TV-004-002 | true | false | false | AC: Required but absent |
| TV-004-003 | false | true | false | AC: Not required but present |
| TV-004-004 | false | false | true | AC: Not required and absent |
| TV-004-005 | Any | true | true | AC: Any accepts present |
| TV-004-006 | Any | false | true | AC: Any accepts absent |
Implementation Notes
Parameterized Test Structure
/**
* @dataProvider icFailedProvider
*/
public function testIcFailedHandling(
bool $configIcFailed,
array $obsProblems,
bool $expectedMatch,
bool $expectedError
): void {
// Setup combined outcome config
// Setup observation
// Execute rule
// Assert match/error state
}
public static function icFailedProvider(): array
{
return [
'TV-001-005' => [true, ['IC_FAILED'], true, true],
'TV-001-006' => [true, [], false, false],
'TV-001-007' => [false, ['IC_FAILED'], false, false],
'TV-001-008' => [false, [], true, false],
];
}
Test File Locations
| Requirement | Test File | Method |
|---|---|---|
| REQ-RULES-COMBOUT-001 | tests/Unit/Rules/CombinedOutcomeRuleTest.php | TM-API |
| REQ-RULES-COMBOUT-002 | tests/Unit/Rules/MultipleWellsCombinedOutcomeRuleTest.php | TM-API |
| REQ-RULES-COMBOUT-003 | tests/Unit/Rules/CombinedOutcomeCLSDiscrepancyTest.php | TM-API |
| REQ-RULES-COMBOUT-004 | tests/Unit/Rules/CombinedOutcomeCTDiscrepancyTest.php | TM-API |
Traceability to Existing Tests
| Requirement | Jira Tests | Status |
|---|---|---|
| REQ-RULES-COMBOUT-001 | BT-406, BT-407, BT-3661, BT-3660, BT-3879, BT-3885, BT-3543 | Existing |
| REQ-RULES-COMBOUT-002 | Pending | Gap |
| REQ-RULES-COMBOUT-003 | BT-5228 | Existing |
| REQ-RULES-COMBOUT-004 | Pending | Gap |
Gap Analysis
Identified Gaps
| Gap | Requirement | Description | Priority | Owner |
|---|---|---|---|---|
| GAP-001 | REQ-RULES-COMBOUT-002 | No Jira test ticket for multi-mix outcomes | High | TBD |
| GAP-002 | REQ-RULES-COMBOUT-004 | No Jira test ticket for CT discrepancy | Medium | TBD |
Remediation Plan
- GAP-001: Create test ticket covering TV-002-001 through TV-002-025 (includes new behavioral ACs)
- GAP-002: Create test ticket covering TV-004-001 through TV-004-006