Prevention Rules Engine — Validation Gates (v0)
Overview
The Prevention Rules Engine is a validation gate system that runs quality checks on appeals and precedents before submission. v0 implements basic checks to prevent obviously flawed submissions. Future versions will add ML-powered prediction.Architecture
Two Gate Levels
PR-U (Pre-Upstream) — User-facing validation- Runs before user can submit
- Returns errors and guidance (helps user fix)
- Example: "Your narrative is too short (28 words), please expand to 50+" PR-D (Pre-Download) — Final validation
- Runs before export/download
- Returns warnings (non-blocking)
- Example: "This precedent has pending outcome — data incomplete"
- Check:
narrative IS NOT NULL AND LENGTH(narrative) > 0 - Severity: ERROR
- Message: "Please provide appeal narrative (clinical justification for overturning denial)"
- Guidance: "Include why the patient meets medical necessity criteria and what would happen if denied" #### Gate: Has Precedents
- Check:
ARRAY_LENGTH(selected_precedent_ids, 1) > 0 - Severity: ERROR
- Message: "Please select at least one precedent as evidence"
- Guidance: "Browse marketplace or your library to find similar cases" #### Gate: Valid Precedents
- Check: All IDs in
selected_precedent_idsexist inprecedent_objects - Severity: ERROR
- Message: "One or more selected precedents no longer exist"
- Guidance: "Refresh and re-select your precedents" #### Gate: Narrative Length
- Check:
LENGTH(narrative) >= 50 - Severity: WARNING
- Message: "Narrative seems short (50+ words recommended)"
- Guidance: "Expand with clinical detail: GAF score, functional impact, evidence integration" #### Gate: Precedent Relevance
- Check: Compare appeal payer/denial_type with selected precedent payers/denial_types
- Severity: WARNING
- Message: "Some precedents are for different denials (Cigna vs. UHC). Verify relevance."
- Guidance: "Appeals are strongest when precedents are for same payer/denial type"
- Check:
evidence_score IS NOT NULL - Severity: ERROR
- Message: "Please rate evidence strength (Minimal, Moderate, Strong)"
- Guidance: "Strong = complete documentation; Moderate = partial; Minimal = limited" #### Gate: Outcome Set (If Complete)
- Check: IF
status = 'Complete'THENoutcome IS NOT NULL - Severity: ERROR
- Message: "Complete precedents must have outcome (Won, Lost, Partial, Abandoned)"
- Guidance: "Set outcome after appeal resolves" #### Gate: Policy Reference Complete
- Check:
policy_referencehaspolicy_sectionandexact_language - Severity: WARNING
- Message: "Policy reference incomplete — missing section or exact language"
- Guidance: "Include specific policy section and quoted language that triggered denial" #### Gate: Denial Trigger Specified
- Check:
denial_trigger IS NOT NULL AND LENGTH(denial_trigger) > 0 - Severity: ERROR
- Message: "Specify exact denial trigger (e.g., 'Frequency limit exceeded')"
- Guidance: "Use X.12 code if available, or describe in payer's language" #### Gate: Evidence Kit Non-Empty
- Check:
JSONB_ARRAY_LENGTH(evidence_kit.required) > 0 OR evidence_kit.recommended > 0 - Severity: WARNING
- Message: "Evidence kit is empty. What evidence is needed to appeal this?"
- Guidance: "Populate required/recommended/optional arrays so others know what to gather"
- Check: IF
status = 'Complete'THENoutcome != 'Pending' - Severity: WARNING
- Message: "Outcome still pending — data is incomplete"
- Guidance: "This precedent will be more valuable once appeal resolves" #### Gate: SUD Compliance (42 CFR Part 2)
- Check: IF
data_origin_type = 'SUD'THENsharing_restrictions != 'none' - Severity: ERROR (blocks download)
- Message: "SUD precedent cannot be exported without consent. Set sharing_restrictions."
- Guidance: "SUD data requires 'consent_required' or 'originator_only' sharing tag" #### Gate: Traceability Complete
- Check:
JSONB_ARRAY_LENGTH(traceability_ledger) >= 2(denial + at least 1 action) - Severity: WARNING
- Message: "Traceability ledger is sparse — missing decision history"
- Guidance: "Document each step: denial date, appeal filed, decisions, outcome"
- Check: All PR-U gates pass + no urgent warnings
- Severity: ERROR
- Message: "Appeal not ready to submit. Address errors above."
- Guidance: (User sees specific error list)
Gate Severity
| Level | Blocking? | Example |
| Error | YES | Missing narrative, no precedents selected |
| Warning | NO | Narrative is short, outcome is pending |
| Info | NO | Cluster is newly Emerging (congratulations!) |
| 'warnings' |
async function validateAppeal(appeal: Appeal, context: AuthContext): Promise<ValidationResult> { const result: ValidationResult = { status: 'clean', checks_passed: [], warnings: [], errors: [], };
// Gate: Has Narrative if (!appeal.narrative || appeal.narrative.trim().length === 0) { result.errors.push({ check: 'has_narrative', message: 'Please provide appeal narrative', guidance: '...', }); } else { result.checks_passed.push('has_narrative'); } // Gate: Has Precedents if (!appeal.selected_precedent_ids || appeal.selected_precedent_ids.length === 0) { result.errors.push({ check: 'has_precedents', message: 'Please select at least one precedent', guidance: '...', }); } else { result.checks_passed.push('has_precedents'); } // ... (more gates) // Determine overall status if (result.errors.length > 0) { result.status = 'errors'; } else if (result.warnings.length > 0) { result.status = 'warnings'; } else { result.status = 'clean'; }return result; }
API Endpoint
// src/routes/validation.ts
router.post('/api/validation/appeal', tenant, async (req, res) => {
const appeal = req.body;
const result = await validateAppeal(appeal, req.context);
res.json(result);
});
router.post('/api/validation/precedent', tenant, async (req, res) => {
const precedent = req.body;
const result = await validatePrecedent(precedent, req.context);
res.json(result);
});
Error Responses
422 Validation Error (User Can Fix)
Example: User submits appeal without narrative{
"status": "error",
"type": "validation_failed",
"message": "Appeal validation failed. Please address errors below.",
"validation_result": {
"status": "errors",
"errors": [
{
"check": "has_narrative",
"message": "Narrative is required",
"guidance": "Provide 50+ word clinical justification"
}
],
"warnings": []
}
}
Response code: 422 Unprocessable Entity (user can fix and retry)
400 Invalid Data (Data Error)
Example: Selected precedent doesn't exist{
"status": "error",
"type": "data_error",
"message": "One or more referenced precedents no longer exist",
"details": {
"missing_ids": ["precedent-uuid-1"]
}
}
Response code: 400 Bad Request (data problem, not validation)
Future Enhancements (v1+)
ML-Powered Prediction
Goal: Estimate win probability before submissionPR-P1: Win Probability Estimator
Input: narrative, precedent_ids, payer, denial_type
Output: estimated win probability (0–100%)
Example: "Based on similar cases, this appeal has ~45% win probability"
Data: Train on historical precedent outcomes + evidence combinations
Narrative Scoring
PR-N1: Narrative Quality Score
Readability: Simple language, short paragraphs
Completeness: Covers all required elements
Persuasiveness: Cites evidence, addresses counterarguments
Score: 0–100
Evidence Recommendation
PR-E1: Evidence Recommender
Input: payer, denial_type, loc, narrative
Output: ranked list of evidence types (from registry)
Example: "For Cigna IOP frequency limits, 92% of successful appeals included psychiatric evaluation"
See also
Code reference
src/services/validation.ts — Validation logic (PR-U and PR-D gates)src/routes/validation.ts — Validation API endpointssrc/utils/validation.ts — Zod schemas (input validation, separate from business rules)src/routes/appeals.ts — Appeal submit endpoint (runs validation before accepting)