Data Accuracy Monitoring Dashboard
Document ID: CODITECT-BIO-ACC-MONITOR-001 Version: 1.0.0 Effective Date: 2026-02-16 Classification: Internal - Restricted Owner: Quality Assurance Director
Document Control
Approval History
| Role | Name | Signature | Date |
|---|---|---|---|
| Quality Assurance Director | [Pending] | [Digital Signature] | YYYY-MM-DD |
| Chief Information Security Officer | [Pending] | [Digital Signature] | YYYY-MM-DD |
| VP Engineering | [Pending] | [Digital Signature] | YYYY-MM-DD |
| Regulatory Affairs Director | [Pending] | [Digital Signature] | YYYY-MM-DD |
Revision History
| Version | Date | Author | Changes | Approval Status |
|---|---|---|---|---|
| 1.0.0 | 2026-02-16 | QA Team | Initial release | Draft |
Distribution List
- Quality Assurance Team
- Data Governance Committee
- Information Security Team
- Engineering Leadership
- Compliance and Regulatory Affairs
- Internal Audit
- External Auditors (as needed)
Review Schedule
| Review Type | Frequency | Next Review Date | Responsible Party |
|---|---|---|---|
| Annual Review | 12 months | 2027-02-16 | QA Director |
| Quarterly Metrics Review | 3 months | 2026-05-16 | QA Manager |
| Regulatory Update Review | As needed | N/A | Regulatory Affairs |
| Post-Incident Review | As needed | N/A | QA Director |
1. Executive Summary
1.1 Purpose
This document establishes the comprehensive Data Accuracy Monitoring Dashboard for the CODITECT Biosciences Quality Management System (BIO-QMS) Platform, ensuring:
- Real-Time Validation - All data entry is validated according to configurable rules with severity-based enforcement
- Error Detection - Automated detection of transcription errors, duplicates, outliers, and inconsistencies
- Accuracy Metrics - Continuous monitoring of data accuracy rates with per-user, per-category, and per-module granularity
- Alerting & Escalation - Threshold-based alerts with defined escalation chains for quality incidents
- Root Cause Analysis - Systematic categorization and analysis of error patterns with corrective action tracking
- Compliance Reporting - Dashboard components and reports demonstrating adherence to FDA 21 CFR Part 11, ALCOA+ principles, and ICH Q10
1.2 Scope
This specification applies to:
- All data entry points in the BIO-QMS platform
- All record types subject to regulatory requirements
- All users with data entry permissions
- All modules: Document Management, Change Control, Deviation Management, Training, Audit Management, CAPA, Risk Management
Out of Scope:
- System-generated data (logs, timestamps) - covered under electronic record controls
- Integration data from validated external systems - covered under interface validation
- Archive/backup data integrity - covered under data retention policies
1.3 Regulatory Context
| Framework | Requirement | Implementation |
|---|---|---|
| FDA 21 CFR Part 11 | §11.10(a) - System validation | Validation rule engine with test coverage |
| FDA 21 CFR Part 11 | §11.10(e) - Audit trails for data changes | All validation errors and corrections logged |
| ALCOA+ Principles | Attributable, Legible, Contemporaneous, Original, Accurate | Per-principle validation checks |
| ALCOA+ Principles | Complete, Consistent, Enduring, Available | Data quality metrics for each principle |
| ICH Q10 | 2.5 - Data Management | Systematic data quality monitoring |
| FDA Data Integrity Guidance | Section IV - ALCOA+ | Automated validation of data integrity |
| MHRA GXP Data Integrity | 6.9 - Data governance | Role-based accuracy metrics and training |
1.4 System Architecture Overview
2. Data Entry Validation
2.1 Validation Rule Engine
2.1.1 Rule Schema
PostgreSQL Schema:
-- Validation rule definitions
CREATE TABLE validation_rules (
rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_code VARCHAR(50) UNIQUE NOT NULL,
rule_name VARCHAR(255) NOT NULL,
description TEXT,
-- Scope
module VARCHAR(50) NOT NULL, -- document, change_control, deviation, etc.
form_type VARCHAR(100), -- specific form type, NULL for all forms
field_name VARCHAR(100), -- specific field, NULL for form-level rules
-- Rule definition
rule_type VARCHAR(50) NOT NULL, -- numeric_range, date_format, pattern_match, etc.
rule_config JSONB NOT NULL, -- type-specific configuration
-- Severity and enforcement
severity VARCHAR(20) NOT NULL CHECK (severity IN ('ERROR', 'WARNING', 'INFO')),
is_active BOOLEAN DEFAULT TRUE,
-- Metadata
created_by UUID NOT NULL REFERENCES users(user_id),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- Multi-tenancy
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Audit
version INTEGER DEFAULT 1,
change_reason TEXT
);
-- Rule configuration examples stored in rule_config JSONB:
-- Numeric range: {"min": 0, "max": 100, "decimal_places": 2, "unit": "°C"}
-- Date format: {"format": "YYYY-MM-DD", "allow_future": false, "min_date": "2020-01-01"}
-- Pattern match: {"regex": "^[A-Z]{3}-[0-9]{5}$", "example": "DOC-12345"}
-- Reference: {"table": "products", "column": "product_id", "display_field": "product_name"}
-- Calculated: {"formula": "field_a + field_b", "tolerance": 0.01}
-- Validation execution history
CREATE TABLE validation_history (
history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Context
record_id UUID NOT NULL,
record_type VARCHAR(50) NOT NULL,
field_name VARCHAR(100),
-- Validation
rule_id UUID REFERENCES validation_rules(rule_id),
rule_code VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
-- Result
validation_result VARCHAR(20) NOT NULL CHECK (validation_result IN ('PASS', 'FAIL')),
error_message TEXT,
field_value TEXT, -- encrypted for sensitive fields
-- Action taken
action_taken VARCHAR(50), -- saved, corrected, overridden, rejected
override_reason TEXT,
override_by UUID REFERENCES users(user_id),
-- Metadata
validated_by UUID NOT NULL REFERENCES users(user_id),
validated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- Multi-tenancy
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Performance tracking
execution_time_ms INTEGER
);
-- Indexes for performance
CREATE INDEX idx_validation_history_record ON validation_history(record_id, record_type);
CREATE INDEX idx_validation_history_tenant_time ON validation_history(tenant_id, validated_at);
CREATE INDEX idx_validation_history_rule ON validation_history(rule_id);
CREATE INDEX idx_validation_history_user ON validation_history(validated_by);
CREATE INDEX idx_validation_history_result ON validation_history(validation_result) WHERE validation_result = 'FAIL';
2.1.2 Rule Types and Implementation
TypeScript Validation Engine:
// src/services/validation/ValidationEngine.ts
import { z } from 'zod';
export enum RuleType {
NUMERIC_RANGE = 'numeric_range',
DATE_FORMAT = 'date_format',
TEXT_PATTERN = 'text_pattern',
REFERENCE_EXISTS = 'reference_exists',
CALCULATED_FIELD = 'calculated_field',
CONTROLLED_VOCABULARY = 'controlled_vocabulary',
CROSS_FIELD_CONSISTENCY = 'cross_field_consistency',
UNIQUE_VALUE = 'unique_value',
}
export enum Severity {
ERROR = 'ERROR', // Blocks save operation
WARNING = 'WARNING', // Requires justification
INFO = 'INFO', // Suggestion only
}
export interface ValidationRule {
ruleId: string;
ruleCode: string;
ruleName: string;
module: string;
formType?: string;
fieldName?: string;
ruleType: RuleType;
ruleConfig: Record<string, any>;
severity: Severity;
isActive: boolean;
tenantId: string;
}
export interface ValidationResult {
ruleCode: string;
severity: Severity;
passed: boolean;
errorMessage?: string;
fieldName?: string;
fieldValue?: any;
executionTimeMs: number;
}
export class ValidationEngine {
private ruleRegistry: Map<string, ValidationRule> = new Map();
/**
* Numeric range validation
* Config: { min, max, decimal_places, unit, allow_null }
*/
private validateNumericRange(
value: any,
config: Record<string, any>
): { passed: boolean; message?: string } {
if (value === null || value === undefined || value === '') {
if (config.allow_null) {
return { passed: true };
}
return { passed: false, message: 'Value is required' };
}
const numValue = parseFloat(value);
if (isNaN(numValue)) {
return { passed: false, message: 'Value must be numeric' };
}
if (config.min !== undefined && numValue < config.min) {
return { passed: false, message: `Value must be >= ${config.min}${config.unit ? ' ' + config.unit : ''}` };
}
if (config.max !== undefined && numValue > config.max) {
return { passed: false, message: `Value must be <= ${config.max}${config.unit ? ' ' + config.unit : ''}` };
}
if (config.decimal_places !== undefined) {
const decimalPart = numValue.toString().split('.')[1];
if (decimalPart && decimalPart.length > config.decimal_places) {
return {
passed: false,
message: `Value must have at most ${config.decimal_places} decimal places`
};
}
}
return { passed: true };
}
/**
* Date format and logic validation
* Config: { format, allow_future, min_date, max_date }
*/
private validateDateFormat(
value: any,
config: Record<string, any>
): { passed: boolean; message?: string } {
if (!value) {
return { passed: false, message: 'Date is required' };
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return { passed: false, message: `Invalid date format. Expected: ${config.format || 'YYYY-MM-DD'}` };
}
const now = new Date();
if (!config.allow_future && date > now) {
return { passed: false, message: 'Date cannot be in the future' };
}
if (config.min_date) {
const minDate = new Date(config.min_date);
if (date < minDate) {
return { passed: false, message: `Date must be on or after ${config.min_date}` };
}
}
if (config.max_date) {
const maxDate = new Date(config.max_date);
if (date > maxDate) {
return { passed: false, message: `Date must be on or before ${config.max_date}` };
}
}
return { passed: true };
}
/**
* Pattern matching validation
* Config: { regex, example, case_sensitive }
*/
private validateTextPattern(
value: any,
config: Record<string, any>
): { passed: boolean; message?: string } {
if (!value) {
return { passed: false, message: 'Value is required' };
}
const strValue = String(value);
const flags = config.case_sensitive ? '' : 'i';
const regex = new RegExp(config.regex, flags);
if (!regex.test(strValue)) {
return {
passed: false,
message: `Value does not match required pattern${config.example ? `. Example: ${config.example}` : ''}`
};
}
return { passed: true };
}
/**
* Reference existence validation
* Config: { table, column, display_field }
*/
private async validateReferenceExists(
value: any,
config: Record<string, any>,
tenantId: string
): Promise<{ passed: boolean; message?: string }> {
if (!value) {
return { passed: false, message: 'Reference value is required' };
}
// Execute query to check if referenced record exists
const exists = await this.checkReferenceExists(
config.table,
config.column,
value,
tenantId
);
if (!exists) {
return {
passed: false,
message: `Referenced ${config.display_field || 'record'} does not exist`
};
}
return { passed: true };
}
/**
* Calculated field validation
* Config: { formula, tolerance, dependent_fields }
*/
private validateCalculatedField(
value: any,
config: Record<string, any>,
formData: Record<string, any>
): { passed: boolean; message?: string } {
if (value === null || value === undefined) {
return { passed: false, message: 'Calculated value is required' };
}
// Evaluate formula with form data
const expectedValue = this.evaluateFormula(config.formula, formData);
if (expectedValue === null) {
return { passed: false, message: 'Cannot calculate expected value - missing dependencies' };
}
const tolerance = config.tolerance || 0.001;
const diff = Math.abs(parseFloat(value) - expectedValue);
if (diff > tolerance) {
return {
passed: false,
message: `Calculated value ${value} does not match expected ${expectedValue} (tolerance: ±${tolerance})`
};
}
return { passed: true };
}
/**
* Controlled vocabulary validation
* Config: { allowed_values, case_sensitive }
*/
private validateControlledVocabulary(
value: any,
config: Record<string, any>
): { passed: boolean; message?: string } {
if (!value) {
return { passed: false, message: 'Value is required' };
}
const strValue = String(value);
const allowedValues = config.allowed_values || [];
const isAllowed = config.case_sensitive
? allowedValues.includes(strValue)
: allowedValues.some((v: string) => v.toLowerCase() === strValue.toLowerCase());
if (!isAllowed) {
return {
passed: false,
message: `Value must be one of: ${allowedValues.join(', ')}`
};
}
return { passed: true };
}
/**
* Execute validation for a single rule
*/
async executeRule(
rule: ValidationRule,
value: any,
formData: Record<string, any>
): Promise<ValidationResult> {
const startTime = Date.now();
let result: { passed: boolean; message?: string };
try {
switch (rule.ruleType) {
case RuleType.NUMERIC_RANGE:
result = this.validateNumericRange(value, rule.ruleConfig);
break;
case RuleType.DATE_FORMAT:
result = this.validateDateFormat(value, rule.ruleConfig);
break;
case RuleType.TEXT_PATTERN:
result = this.validateTextPattern(value, rule.ruleConfig);
break;
case RuleType.REFERENCE_EXISTS:
result = await this.validateReferenceExists(value, rule.ruleConfig, rule.tenantId);
break;
case RuleType.CALCULATED_FIELD:
result = this.validateCalculatedField(value, rule.ruleConfig, formData);
break;
case RuleType.CONTROLLED_VOCABULARY:
result = this.validateControlledVocabulary(value, rule.ruleConfig);
break;
default:
result = { passed: false, message: `Unknown rule type: ${rule.ruleType}` };
}
} catch (error) {
result = { passed: false, message: `Validation error: ${error.message}` };
}
const executionTimeMs = Date.now() - startTime;
return {
ruleCode: rule.ruleCode,
severity: rule.severity,
passed: result.passed,
errorMessage: result.message,
fieldName: rule.fieldName,
fieldValue: value,
executionTimeMs,
};
}
/**
* Validate entire form
*/
async validateForm(
module: string,
formType: string,
formData: Record<string, any>,
tenantId: string
): Promise<ValidationResult[]> {
// Load applicable rules
const rules = await this.loadRules(module, formType, tenantId);
const results: ValidationResult[] = [];
for (const rule of rules) {
if (!rule.isActive) continue;
const fieldValue = rule.fieldName ? formData[rule.fieldName] : formData;
const result = await this.executeRule(rule, fieldValue, formData);
results.push(result);
// Log validation result
await this.logValidationResult(result, formData, tenantId);
}
return results;
}
/**
* Check if validation allows save
*/
canSave(results: ValidationResult[]): { allowed: boolean; blockers: ValidationResult[] } {
const blockers = results.filter(r => !r.passed && r.severity === Severity.ERROR);
return {
allowed: blockers.length === 0,
blockers,
};
}
/**
* Helper methods
*/
private async checkReferenceExists(
table: string,
column: string,
value: any,
tenantId: string
): Promise<boolean> {
// Database query implementation
return true; // Placeholder
}
private evaluateFormula(formula: string, data: Record<string, any>): number | null {
// Safe formula evaluation implementation
// Use expression parser, NOT eval()
return null; // Placeholder
}
private async loadRules(
module: string,
formType: string,
tenantId: string
): Promise<ValidationRule[]> {
// Database query implementation
return []; // Placeholder
}
private async logValidationResult(
result: ValidationResult,
formData: Record<string, any>,
tenantId: string
): Promise<void> {
// Database insert implementation
}
}
2.1.3 React Form Integration
React Component with Real-Time Validation:
// src/components/forms/ValidatedForm.tsx
import React, { useState, useCallback } from 'react';
import { ValidationEngine, ValidationResult, Severity } from '@/services/validation';
interface ValidatedFormProps {
module: string;
formType: string;
initialData?: Record<string, any>;
onSubmit: (data: Record<string, any>) => Promise<void>;
}
export const ValidatedForm: React.FC<ValidatedFormProps> = ({
module,
formType,
initialData = {},
onSubmit,
}) => {
const [formData, setFormData] = useState(initialData);
const [validationResults, setValidationResults] = useState<ValidationResult[]>([]);
const [isValidating, setIsValidating] = useState(false);
const [showOverrideDialog, setShowOverrideDialog] = useState(false);
const validationEngine = new ValidationEngine();
/**
* Real-time field validation on blur
*/
const handleFieldBlur = useCallback(async (fieldName: string) => {
setIsValidating(true);
try {
const results = await validationEngine.validateForm(
module,
formType,
formData,
currentTenantId
);
// Filter to this field's results
const fieldResults = results.filter(r => r.fieldName === fieldName);
// Merge with existing results
setValidationResults(prev => [
...prev.filter(r => r.fieldName !== fieldName),
...fieldResults,
]);
} finally {
setIsValidating(false);
}
}, [module, formType, formData]);
/**
* Full form validation before submit
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsValidating(true);
try {
// Run all validation rules
const results = await validationEngine.validateForm(
module,
formType,
formData,
currentTenantId
);
setValidationResults(results);
// Check if save is allowed
const { allowed, blockers } = validationEngine.canSave(results);
if (!allowed) {
// Show error messages
toast.error(`Cannot save: ${blockers.length} validation error(s)`);
return;
}
// Check for warnings requiring justification
const warnings = results.filter(r => !r.passed && r.severity === Severity.WARNING);
if (warnings.length > 0) {
// Show override dialog
setShowOverrideDialog(true);
return;
}
// All validations passed
await onSubmit(formData);
} finally {
setIsValidating(false);
}
};
/**
* Render validation messages for a field
*/
const renderFieldValidation = (fieldName: string) => {
const fieldResults = validationResults.filter(r => r.fieldName === fieldName);
if (fieldResults.length === 0) return null;
return (
<div className="validation-messages">
{fieldResults.map((result, idx) => {
if (result.passed) return null;
const severityClass = {
ERROR: 'validation-error',
WARNING: 'validation-warning',
INFO: 'validation-info',
}[result.severity];
return (
<div key={idx} className={severityClass}>
<span className="validation-icon">
{result.severity === Severity.ERROR && '❌'}
{result.severity === Severity.WARNING && '⚠️'}
{result.severity === Severity.INFO && 'ℹ️'}
</span>
<span className="validation-message">{result.errorMessage}</span>
</div>
);
})}
</div>
);
};
return (
<form onSubmit={handleSubmit} className="validated-form">
{/* Form fields with validation */}
<div className="form-field">
<label htmlFor="temperature">Temperature (°C)</label>
<input
type="number"
id="temperature"
value={formData.temperature || ''}
onChange={(e) => setFormData({ ...formData, temperature: e.target.value })}
onBlur={() => handleFieldBlur('temperature')}
step="0.01"
/>
{renderFieldValidation('temperature')}
</div>
{/* Submit button */}
<button type="submit" disabled={isValidating}>
{isValidating ? 'Validating...' : 'Save'}
</button>
{/* Override dialog for warnings */}
{showOverrideDialog && (
<OverrideJustificationDialog
warnings={validationResults.filter(r => !r.passed && r.severity === Severity.WARNING)}
onSubmit={async (justification) => {
// Log override and proceed
await onSubmit({ ...formData, _override_justification: justification });
setShowOverrideDialog(false);
}}
onCancel={() => setShowOverrideDialog(false)}
/>
)}
</form>
);
};
3. Transcription Error Detection
3.1 Pattern Analysis
3.1.1 Digit Transposition Detection
Algorithm Implementation:
// src/services/accuracy/TranscriptionDetector.ts
export class TranscriptionDetector {
/**
* Detect digit transposition (e.g., 123 vs 132)
* Uses Levenshtein distance with character position weighting
*/
detectDigitTransposition(
enteredValue: string,
expectedValue: string
): { isTransposition: boolean; confidence: number; suggestion?: string } {
// Only check numeric values
if (!/^\d+$/.test(enteredValue) || !/^\d+$/.test(expectedValue)) {
return { isTransposition: false, confidence: 0 };
}
// Must be same length for transposition
if (enteredValue.length !== expectedValue.length) {
return { isTransposition: false, confidence: 0 };
}
// Count position differences
let differences = 0;
const diffPositions: number[] = [];
for (let i = 0; i < enteredValue.length; i++) {
if (enteredValue[i] !== expectedValue[i]) {
differences++;
diffPositions.push(i);
}
}
// Classic transposition: exactly 2 adjacent positions swapped
if (differences === 2 && diffPositions.length === 2) {
const [pos1, pos2] = diffPositions;
if (Math.abs(pos1 - pos2) === 1) {
// Check if characters are actually swapped
if (
enteredValue[pos1] === expectedValue[pos2] &&
enteredValue[pos2] === expectedValue[pos1]
) {
return {
isTransposition: true,
confidence: 0.95,
suggestion: expectedValue,
};
}
}
}
// Extended transposition: up to 3 non-adjacent swaps
if (differences <= 6 && differences % 2 === 0) {
// Check if all differences can be explained by swaps
const swapCount = differences / 2;
if (swapCount <= 3) {
return {
isTransposition: true,
confidence: 0.7 - (swapCount * 0.15),
suggestion: expectedValue,
};
}
}
return { isTransposition: false, confidence: 0 };
}
/**
* Detect common OCR errors
* Common confusions: 0/O, 1/I/l, 5/S, 8/B, etc.
*/
detectOCRError(
enteredValue: string,
expectedValue: string
): { isOCRError: boolean; confidence: number; suggestion?: string } {
const ocrConfusions: Record<string, string[]> = {
'0': ['O', 'o'],
'O': ['0', 'o'],
'1': ['I', 'l', '|'],
'I': ['1', 'l', '|'],
'l': ['1', 'I', '|'],
'5': ['S', 's'],
'S': ['5', 's'],
'8': ['B', 'b'],
'B': ['8', 'b'],
'6': ['G', 'g'],
'G': ['6', 'g'],
'2': ['Z', 'z'],
'Z': ['2', 'z'],
};
if (enteredValue.length !== expectedValue.length) {
return { isOCRError: false, confidence: 0 };
}
let confusionCount = 0;
for (let i = 0; i < enteredValue.length; i++) {
if (enteredValue[i] !== expectedValue[i]) {
const confusables = ocrConfusions[expectedValue[i]] || [];
if (confusables.includes(enteredValue[i])) {
confusionCount++;
} else {
// Non-OCR difference found
return { isOCRError: false, confidence: 0 };
}
}
}
if (confusionCount > 0) {
return {
isOCRError: true,
confidence: Math.min(0.9, 0.6 + (confusionCount * 0.1)),
suggestion: expectedValue,
};
}
return { isOCRError: false, confidence: 0 };
}
}
3.2 Duplicate Detection
Fuzzy Matching Implementation:
// src/services/accuracy/DuplicateDetector.ts
import { distance } from 'fastest-levenshtein';
export class DuplicateDetector {
/**
* Find near-duplicate records using fuzzy matching
*/
async findNearDuplicates(
record: Record<string, any>,
recordType: string,
tenantId: string,
similarityThreshold: number = 0.85
): Promise<Array<{ recordId: string; similarity: number; matchedFields: string[] }>> {
// Load recent records of same type (last 90 days)
const recentRecords = await this.loadRecentRecords(recordType, tenantId, 90);
const duplicates: Array<{ recordId: string; similarity: number; matchedFields: string[] }> = [];
for (const existing of recentRecords) {
const similarity = this.calculateRecordSimilarity(record, existing);
if (similarity >= similarityThreshold) {
duplicates.push({
recordId: existing.id,
similarity,
matchedFields: this.identifyMatchedFields(record, existing),
});
}
}
return duplicates.sort((a, b) => b.similarity - a.similarity);
}
/**
* Calculate overall record similarity
*/
private calculateRecordSimilarity(
record1: Record<string, any>,
record2: Record<string, any>
): number {
const fields = Object.keys(record1).filter(k => !k.startsWith('_'));
let totalWeight = 0;
let matchWeight = 0;
// Field weights (customize per record type)
const weights: Record<string, number> = {
title: 3.0,
description: 2.0,
product_code: 5.0,
batch_number: 5.0,
date: 1.0,
// ... other fields
};
for (const field of fields) {
const weight = weights[field] || 1.0;
totalWeight += weight;
const value1 = String(record1[field] || '');
const value2 = String(record2[field] || '');
const fieldSimilarity = this.calculateFieldSimilarity(value1, value2);
matchWeight += fieldSimilarity * weight;
}
return totalWeight > 0 ? matchWeight / totalWeight : 0;
}
/**
* Calculate field-level similarity
*/
private calculateFieldSimilarity(value1: string, value2: string): number {
if (value1 === value2) return 1.0;
if (!value1 || !value2) return 0;
// Normalize
const norm1 = value1.toLowerCase().trim();
const norm2 = value2.toLowerCase().trim();
if (norm1 === norm2) return 1.0;
// Levenshtein distance
const maxLen = Math.max(norm1.length, norm2.length);
const dist = distance(norm1, norm2);
return 1 - (dist / maxLen);
}
/**
* Identify which fields matched
*/
private identifyMatchedFields(
record1: Record<string, any>,
record2: Record<string, any>
): string[] {
const matched: string[] = [];
for (const field of Object.keys(record1)) {
if (field.startsWith('_')) continue;
const similarity = this.calculateFieldSimilarity(
String(record1[field] || ''),
String(record2[field] || '')
);
if (similarity >= 0.9) {
matched.push(field);
}
}
return matched;
}
private async loadRecentRecords(
recordType: string,
tenantId: string,
days: number
): Promise<Array<Record<string, any>>> {
// Database query implementation
return [];
}
}
3.3 Outlier Detection
Statistical Analysis:
// src/services/accuracy/OutlierDetector.ts
export class OutlierDetector {
/**
* Detect statistical outliers using z-score
*/
async detectOutliers(
fieldName: string,
value: number,
recordType: string,
tenantId: string,
zScoreThreshold: number = 3.0
): Promise<{ isOutlier: boolean; zScore: number; mean: number; stdDev: number }> {
// Load historical values for this field (last 12 months)
const historicalValues = await this.loadHistoricalValues(
fieldName,
recordType,
tenantId,
365
);
if (historicalValues.length < 30) {
// Insufficient data for statistical analysis
return { isOutlier: false, zScore: 0, mean: 0, stdDev: 0 };
}
// Calculate mean and standard deviation
const mean = this.calculateMean(historicalValues);
const stdDev = this.calculateStdDev(historicalValues, mean);
// Calculate z-score
const zScore = stdDev > 0 ? Math.abs((value - mean) / stdDev) : 0;
return {
isOutlier: zScore > zScoreThreshold,
zScore,
mean,
stdDev,
};
}
/**
* Detect outliers using IQR (Interquartile Range)
*/
async detectOutliersIQR(
fieldName: string,
value: number,
recordType: string,
tenantId: string
): Promise<{ isOutlier: boolean; quartiles: [number, number, number]; iqr: number }> {
const historicalValues = await this.loadHistoricalValues(
fieldName,
recordType,
tenantId,
365
);
if (historicalValues.length < 30) {
return { isOutlier: false, quartiles: [0, 0, 0], iqr: 0 };
}
const sorted = [...historicalValues].sort((a, b) => a - b);
const q1 = this.percentile(sorted, 25);
const q2 = this.percentile(sorted, 50);
const q3 = this.percentile(sorted, 75);
const iqr = q3 - q1;
const lowerBound = q1 - (1.5 * iqr);
const upperBound = q3 + (1.5 * iqr);
return {
isOutlier: value < lowerBound || value > upperBound,
quartiles: [q1, q2, q3],
iqr,
};
}
private calculateMean(values: number[]): number {
return values.reduce((sum, v) => sum + v, 0) / values.length;
}
private calculateStdDev(values: number[], mean: number): number {
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
return Math.sqrt(variance);
}
private percentile(sorted: number[], p: number): number {
const index = (p / 100) * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
const weight = index - lower;
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
private async loadHistoricalValues(
fieldName: string,
recordType: string,
tenantId: string,
days: number
): Promise<number[]> {
// Database query implementation
return [];
}
}
3.4 Cross-Reference Consistency Checks
PostgreSQL Functions:
-- Cross-table consistency validation
CREATE OR REPLACE FUNCTION check_cross_reference_consistency(
p_record_id UUID,
p_record_type VARCHAR,
p_tenant_id UUID
)
RETURNS TABLE(
check_name VARCHAR,
passed BOOLEAN,
error_message TEXT
) AS $$
BEGIN
-- Example: Deviation must reference valid product
IF p_record_type = 'deviation' THEN
RETURN QUERY
SELECT
'product_exists'::VARCHAR,
EXISTS(
SELECT 1 FROM products p
WHERE p.product_id = (
SELECT d.product_id FROM deviations d WHERE d.deviation_id = p_record_id
) AND p.tenant_id = p_tenant_id
),
'Referenced product does not exist'::TEXT;
-- Deviation effective date must be >= product creation date
RETURN QUERY
SELECT
'date_consistency'::VARCHAR,
(
SELECT d.effective_date >= p.created_at
FROM deviations d
JOIN products p ON d.product_id = p.product_id
WHERE d.deviation_id = p_record_id
),
'Deviation date predates product creation'::TEXT;
END IF;
-- Example: Change control must have valid approvers
IF p_record_type = 'change_control' THEN
RETURN QUERY
SELECT
'approvers_valid'::VARCHAR,
NOT EXISTS(
SELECT 1 FROM change_control_approvers cca
LEFT JOIN users u ON cca.approver_user_id = u.user_id
WHERE cca.change_control_id = p_record_id
AND u.user_id IS NULL
),
'Change control has invalid approver references'::TEXT;
END IF;
RETURN;
END;
$$ LANGUAGE plpgsql;
4. Accuracy Metrics
4.1 Metrics Calculation
PostgreSQL Views and Functions:
-- Monthly accuracy rate per data category
CREATE MATERIALIZED VIEW mv_monthly_accuracy_by_category AS
SELECT
tenant_id,
DATE_TRUNC('month', validated_at) AS month,
record_type AS category,
COUNT(*) AS total_validations,
COUNT(*) FILTER (WHERE validation_result = 'PASS') AS passed_validations,
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS accuracy_rate_pct,
COUNT(*) FILTER (WHERE severity = 'ERROR' AND validation_result = 'FAIL') AS error_count,
COUNT(*) FILTER (WHERE severity = 'WARNING' AND validation_result = 'FAIL') AS warning_count
FROM validation_history
GROUP BY tenant_id, DATE_TRUNC('month', validated_at), record_type;
CREATE UNIQUE INDEX idx_mv_monthly_accuracy_category
ON mv_monthly_accuracy_by_category(tenant_id, month, category);
-- Per-user accuracy scores
CREATE MATERIALIZED VIEW mv_user_accuracy_scores AS
SELECT
tenant_id,
validated_by AS user_id,
DATE_TRUNC('month', validated_at) AS month,
COUNT(*) AS total_validations,
COUNT(*) FILTER (WHERE validation_result = 'PASS') AS passed_validations,
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS accuracy_score_pct,
COUNT(*) FILTER (WHERE severity = 'ERROR' AND validation_result = 'FAIL') AS error_count
FROM validation_history
WHERE validated_by IS NOT NULL
GROUP BY tenant_id, validated_by, DATE_TRUNC('month', validated_at);
CREATE UNIQUE INDEX idx_mv_user_accuracy
ON mv_user_accuracy_scores(tenant_id, user_id, month);
-- Error type distribution
CREATE MATERIALIZED VIEW mv_error_distribution AS
SELECT
tenant_id,
DATE_TRUNC('month', validated_at) AS month,
rule_code,
r.rule_name,
r.rule_type AS error_type,
COUNT(*) AS error_count,
ROUND(
100.0 * COUNT(*) / SUM(COUNT(*)) OVER (PARTITION BY tenant_id, DATE_TRUNC('month', validated_at)),
2
) AS percentage_of_errors
FROM validation_history vh
JOIN validation_rules r ON vh.rule_id = r.rule_id
WHERE vh.validation_result = 'FAIL'
GROUP BY tenant_id, DATE_TRUNC('month', validated_at), rule_code, r.rule_name, r.rule_type
ORDER BY error_count DESC;
CREATE INDEX idx_mv_error_distribution
ON mv_error_distribution(tenant_id, month);
-- Accuracy trend analysis (3-month rolling average)
CREATE MATERIALIZED VIEW mv_accuracy_trends AS
SELECT
tenant_id,
month,
category,
accuracy_rate_pct,
AVG(accuracy_rate_pct) OVER (
PARTITION BY tenant_id, category
ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rolling_avg_3month,
LAG(accuracy_rate_pct) OVER (
PARTITION BY tenant_id, category
ORDER BY month
) AS prev_month_rate,
accuracy_rate_pct - LAG(accuracy_rate_pct) OVER (
PARTITION BY tenant_id, category
ORDER BY month
) AS month_over_month_change
FROM mv_monthly_accuracy_by_category;
CREATE INDEX idx_mv_accuracy_trends
ON mv_accuracy_trends(tenant_id, month, category);
-- Refresh materialized views (scheduled via cron)
CREATE OR REPLACE FUNCTION refresh_accuracy_metrics()
RETURNS VOID AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_accuracy_by_category;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_user_accuracy_scores;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_error_distribution;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_accuracy_trends;
END;
$$ LANGUAGE plpgsql;
4.2 Metrics API
TypeScript Service:
// src/services/metrics/AccuracyMetricsService.ts
export interface AccuracyMetrics {
overall: {
accuracyRate: number;
totalValidations: number;
passedValidations: number;
errorCount: number;
warningCount: number;
};
byCategory: Array<{
category: string;
accuracyRate: number;
totalValidations: number;
trend: 'improving' | 'stable' | 'declining';
}>;
byUser: Array<{
userId: string;
userName: string;
accuracyScore: number;
errorCount: number;
rank: number;
}>;
errorDistribution: Array<{
errorType: string;
count: number;
percentage: number;
}>;
trends: {
currentMonth: number;
previousMonth: number;
threeMonthAverage: number;
sixMonthAverage: number;
};
}
export class AccuracyMetricsService {
async getAccuracyMetrics(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<AccuracyMetrics> {
const [overall, byCategory, byUser, errorDistribution, trends] = await Promise.all([
this.getOverallAccuracy(tenantId, startDate, endDate),
this.getAccuracyByCategory(tenantId, startDate, endDate),
this.getAccuracyByUser(tenantId, startDate, endDate),
this.getErrorDistribution(tenantId, startDate, endDate),
this.getAccuracyTrends(tenantId, endDate),
]);
return {
overall,
byCategory,
byUser,
errorDistribution,
trends,
};
}
private async getOverallAccuracy(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<AccuracyMetrics['overall']> {
const result = await db.query(`
SELECT
COUNT(*) AS total_validations,
COUNT(*) FILTER (WHERE validation_result = 'PASS') AS passed_validations,
COUNT(*) FILTER (WHERE severity = 'ERROR' AND validation_result = 'FAIL') AS error_count,
COUNT(*) FILTER (WHERE severity = 'WARNING' AND validation_result = 'FAIL') AS warning_count,
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS accuracy_rate
FROM validation_history
WHERE tenant_id = $1
AND validated_at BETWEEN $2 AND $3
`, [tenantId, startDate, endDate]);
return {
accuracyRate: parseFloat(result.rows[0].accuracy_rate),
totalValidations: parseInt(result.rows[0].total_validations),
passedValidations: parseInt(result.rows[0].passed_validations),
errorCount: parseInt(result.rows[0].error_count),
warningCount: parseInt(result.rows[0].warning_count),
};
}
// ... other metric calculation methods
}
5. Alerting & Escalation
5.1 Alert Configuration
Database Schema:
CREATE TABLE accuracy_alert_rules (
rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_name VARCHAR(255) NOT NULL,
-- Trigger conditions
metric_type VARCHAR(50) NOT NULL, -- accuracy_rate, error_count, trend
threshold_value NUMERIC NOT NULL,
threshold_operator VARCHAR(10) NOT NULL CHECK (threshold_operator IN ('<', '<=', '>', '>=', '=')),
evaluation_period VARCHAR(50), -- 'daily', 'weekly', 'monthly'
-- Scope
scope_type VARCHAR(50) NOT NULL, -- 'global', 'category', 'user', 'module'
scope_value VARCHAR(100), -- specific category/user/module if applicable
-- Actions
severity VARCHAR(20) NOT NULL CHECK (severity IN ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL')),
notification_channels JSONB, -- ['email', 'slack', 'sms']
escalation_chain JSONB, -- array of {role, delay_minutes}
-- Status
is_active BOOLEAN DEFAULT TRUE,
-- Multi-tenancy
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE accuracy_alerts (
alert_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_id UUID REFERENCES accuracy_alert_rules(rule_id),
-- Alert details
metric_type VARCHAR(50) NOT NULL,
metric_value NUMERIC NOT NULL,
threshold_value NUMERIC NOT NULL,
scope_type VARCHAR(50),
scope_value VARCHAR(100),
-- Status
status VARCHAR(20) DEFAULT 'OPEN' CHECK (status IN ('OPEN', 'ACKNOWLEDGED', 'RESOLVED', 'DISMISSED')),
acknowledged_by UUID REFERENCES users(user_id),
acknowledged_at TIMESTAMPTZ,
resolved_by UUID REFERENCES users(user_id),
resolved_at TIMESTAMPTZ,
resolution_notes TEXT,
-- Escalation
escalation_level INTEGER DEFAULT 0,
last_escalated_at TIMESTAMPTZ,
-- Multi-tenancy
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Metadata
triggered_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_accuracy_alerts_tenant_status ON accuracy_alerts(tenant_id, status);
CREATE INDEX idx_accuracy_alerts_triggered ON accuracy_alerts(triggered_at);
5.2 Alert Engine
TypeScript Implementation:
// src/services/alerting/AccuracyAlertEngine.ts
export interface AlertRule {
ruleId: string;
ruleName: string;
metricType: 'accuracy_rate' | 'error_count' | 'trend';
thresholdValue: number;
thresholdOperator: '<' | '<=' | '>' | '>=' | '=';
evaluationPeriod: 'daily' | 'weekly' | 'monthly';
scopeType: 'global' | 'category' | 'user' | 'module';
scopeValue?: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
notificationChannels: string[];
escalationChain: Array<{ role: string; delayMinutes: number }>;
isActive: boolean;
tenantId: string;
}
export class AccuracyAlertEngine {
/**
* Evaluate all active alert rules
*/
async evaluateAlertRules(tenantId: string): Promise<void> {
const rules = await this.loadActiveRules(tenantId);
for (const rule of rules) {
await this.evaluateRule(rule);
}
}
/**
* Evaluate single alert rule
*/
private async evaluateRule(rule: AlertRule): Promise<void> {
// Calculate current metric value
const metricValue = await this.calculateMetricValue(rule);
// Check if threshold is breached
const isBreached = this.checkThreshold(
metricValue,
rule.thresholdOperator,
rule.thresholdValue
);
if (isBreached) {
// Check if alert already exists and is open
const existingAlert = await this.findOpenAlert(rule);
if (!existingAlert) {
// Create new alert
await this.createAlert(rule, metricValue);
} else {
// Update existing alert and check escalation
await this.checkEscalation(existingAlert, rule);
}
} else {
// Auto-resolve any open alerts for this rule
await this.autoResolveAlerts(rule);
}
}
/**
* Calculate metric value based on rule configuration
*/
private async calculateMetricValue(rule: AlertRule): Promise<number> {
const endDate = new Date();
let startDate: Date;
switch (rule.evaluationPeriod) {
case 'daily':
startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);
break;
case 'weekly':
startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'monthly':
startDate = new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
}
let query = '';
const params: any[] = [rule.tenantId, startDate, endDate];
if (rule.metricType === 'accuracy_rate') {
query = `
SELECT
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS metric_value
FROM validation_history
WHERE tenant_id = $1
AND validated_at BETWEEN $2 AND $3
`;
if (rule.scopeType === 'category') {
query += ` AND record_type = $4`;
params.push(rule.scopeValue);
} else if (rule.scopeType === 'user') {
query += ` AND validated_by = $4`;
params.push(rule.scopeValue);
}
} else if (rule.metricType === 'error_count') {
query = `
SELECT COUNT(*) AS metric_value
FROM validation_history
WHERE tenant_id = $1
AND validated_at BETWEEN $2 AND $3
AND validation_result = 'FAIL'
AND severity = 'ERROR'
`;
if (rule.scopeType === 'user') {
query += ` AND validated_by = $4`;
params.push(rule.scopeValue);
}
} else if (rule.metricType === 'trend') {
// Calculate 3-month trend (current vs 3 months ago)
query = `
WITH current_period AS (
SELECT
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS rate
FROM validation_history
WHERE tenant_id = $1
AND validated_at BETWEEN $2 AND $3
),
previous_period AS (
SELECT
ROUND(
100.0 * COUNT(*) FILTER (WHERE validation_result = 'PASS') / NULLIF(COUNT(*), 0),
2
) AS rate
FROM validation_history
WHERE tenant_id = $1
AND validated_at BETWEEN $2 - INTERVAL '3 months' AND $3 - INTERVAL '3 months'
)
SELECT (c.rate - p.rate) AS metric_value
FROM current_period c, previous_period p
`;
}
const result = await db.query(query, params);
return parseFloat(result.rows[0]?.metric_value || '0');
}
/**
* Check if threshold is breached
*/
private checkThreshold(
value: number,
operator: string,
threshold: number
): boolean {
switch (operator) {
case '<': return value < threshold;
case '<=': return value <= threshold;
case '>': return value > threshold;
case '>=': return value >= threshold;
case '=': return Math.abs(value - threshold) < 0.01;
default: return false;
}
}
/**
* Create new alert
*/
private async createAlert(rule: AlertRule, metricValue: number): Promise<void> {
const alertId = await db.query(`
INSERT INTO accuracy_alerts (
rule_id, metric_type, metric_value, threshold_value,
scope_type, scope_value, tenant_id
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING alert_id
`, [
rule.ruleId,
rule.metricType,
metricValue,
rule.thresholdValue,
rule.scopeType,
rule.scopeValue,
rule.tenantId,
]);
// Send notifications
await this.sendNotifications(rule, metricValue, alertId.rows[0].alert_id);
}
/**
* Send alert notifications
*/
private async sendNotifications(
rule: AlertRule,
metricValue: number,
alertId: string
): Promise<void> {
const message = this.formatAlertMessage(rule, metricValue);
for (const channel of rule.notificationChannels) {
switch (channel) {
case 'email':
await this.sendEmailNotification(rule, message, alertId);
break;
case 'slack':
await this.sendSlackNotification(rule, message, alertId);
break;
case 'sms':
await this.sendSMSNotification(rule, message, alertId);
break;
}
}
}
/**
* Format alert message
*/
private formatAlertMessage(rule: AlertRule, metricValue: number): string {
return `
🚨 Accuracy Alert: ${rule.ruleName}
Metric: ${rule.metricType}
Current Value: ${metricValue}
Threshold: ${rule.thresholdOperator} ${rule.thresholdValue}
Severity: ${rule.severity}
Scope: ${rule.scopeType}${rule.scopeValue ? ` (${rule.scopeValue})` : ''}
Please investigate and take corrective action.
`.trim();
}
/**
* Check and execute escalation
*/
private async checkEscalation(alert: any, rule: AlertRule): Promise<void> {
const minutesSinceLastEscalation =
(Date.now() - new Date(alert.last_escalated_at || alert.triggered_at).getTime()) / (1000 * 60);
const currentLevel = alert.escalation_level || 0;
const nextEscalation = rule.escalationChain[currentLevel];
if (nextEscalation && minutesSinceLastEscalation >= nextEscalation.delayMinutes) {
// Escalate
await db.query(`
UPDATE accuracy_alerts
SET escalation_level = escalation_level + 1,
last_escalated_at = CURRENT_TIMESTAMP
WHERE alert_id = $1
`, [alert.alert_id]);
// Notify escalation target
await this.sendEscalationNotification(alert, rule, nextEscalation);
}
}
private async loadActiveRules(tenantId: string): Promise<AlertRule[]> {
// Database query
return [];
}
private async findOpenAlert(rule: AlertRule): Promise<any> {
// Database query
return null;
}
private async autoResolveAlerts(rule: AlertRule): Promise<void> {
// Database update
}
private async sendEmailNotification(rule: AlertRule, message: string, alertId: string): Promise<void> {
// Email service integration
}
private async sendSlackNotification(rule: AlertRule, message: string, alertId: string): Promise<void> {
// Slack webhook integration
}
private async sendSMSNotification(rule: AlertRule, message: string, alertId: string): Promise<void> {
// SMS service integration
}
private async sendEscalationNotification(alert: any, rule: AlertRule, escalation: any): Promise<void> {
// Escalation notification
}
}
6. Root Cause Analysis
6.1 Error Categorization Taxonomy
Database Schema:
CREATE TABLE error_categories (
category_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category_code VARCHAR(50) UNIQUE NOT NULL,
category_name VARCHAR(255) NOT NULL,
parent_category_id UUID REFERENCES error_categories(category_id),
description TEXT,
-- Examples and patterns
common_patterns JSONB, -- array of pattern examples
identification_rules JSONB, -- automated categorization rules
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Predefined taxonomy
INSERT INTO error_categories (category_code, category_name, description) VALUES
('HUMAN_ERROR', 'Human Error', 'Errors caused by manual data entry mistakes'),
('SYSTEM_ERROR', 'System Error', 'Errors caused by software bugs or system issues'),
('PROCESS_GAP', 'Process Gap', 'Errors due to missing or unclear procedures'),
('TRAINING_GAP', 'Training Gap', 'Errors due to insufficient user training');
-- Sub-categories for Human Error
INSERT INTO error_categories (category_code, category_name, parent_category_id, description)
SELECT
'HUMAN_TRANSCRIPTION', 'Transcription Error', category_id, 'Digit/character transposition or misreading'
FROM error_categories WHERE category_code = 'HUMAN_ERROR';
INSERT INTO error_categories (category_code, category_name, parent_category_id, description)
SELECT
'HUMAN_OMISSION', 'Omission Error', category_id, 'Missing required data fields'
FROM error_categories WHERE category_code = 'HUMAN_ERROR';
INSERT INTO error_categories (category_code, category_name, parent_category_id, description)
SELECT
'HUMAN_CALCULATION', 'Calculation Error', category_id, 'Incorrect manual calculations'
FROM error_categories WHERE category_code = 'HUMAN_ERROR';
-- Root cause analysis table
CREATE TABLE error_root_cause_analysis (
analysis_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Error reference
validation_history_id UUID REFERENCES validation_history(history_id),
error_pattern VARCHAR(255),
-- Categorization
category_id UUID REFERENCES error_categories(category_id),
sub_category_id UUID REFERENCES error_categories(category_id),
auto_categorized BOOLEAN DEFAULT FALSE,
-- Analysis
root_cause TEXT,
contributing_factors JSONB, -- array of factors
impact_assessment TEXT,
-- Corrective action
corrective_action_required BOOLEAN DEFAULT TRUE,
capa_id UUID, -- link to CAPA system
preventive_measures JSONB,
-- Status
analysis_status VARCHAR(20) DEFAULT 'DRAFT' CHECK (analysis_status IN ('DRAFT', 'REVIEW', 'APPROVED', 'CLOSED')),
analyzed_by UUID NOT NULL REFERENCES users(user_id),
reviewed_by UUID REFERENCES users(user_id),
approved_by UUID REFERENCES users(user_id),
-- Multi-tenancy
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
6.2 Pattern Identification
TypeScript Service:
// src/services/analysis/RootCauseAnalyzer.ts
export class RootCauseAnalyzer {
/**
* Identify common error patterns
*/
async identifyErrorPatterns(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<Array<{
pattern: string;
category: string;
frequency: number;
affectedUsers: number;
affectedRecords: number;
trend: 'increasing' | 'stable' | 'decreasing';
}>> {
const query = `
WITH error_patterns AS (
SELECT
vh.rule_code,
vr.rule_name AS pattern,
ec.category_name AS category,
COUNT(*) AS frequency,
COUNT(DISTINCT vh.validated_by) AS affected_users,
COUNT(DISTINCT vh.record_id) AS affected_records,
DATE_TRUNC('week', vh.validated_at) AS week
FROM validation_history vh
JOIN validation_rules vr ON vh.rule_id = vr.rule_id
LEFT JOIN error_categories ec ON vr.error_category_id = ec.category_id
WHERE vh.tenant_id = $1
AND vh.validated_at BETWEEN $2 AND $3
AND vh.validation_result = 'FAIL'
GROUP BY vh.rule_code, vr.rule_name, ec.category_name, DATE_TRUNC('week', vh.validated_at)
),
pattern_trends AS (
SELECT
rule_code,
pattern,
category,
SUM(frequency) AS total_frequency,
SUM(affected_users) AS total_affected_users,
SUM(affected_records) AS total_affected_records,
CASE
WHEN REGR_SLOPE(frequency, EXTRACT(EPOCH FROM week)) > 1 THEN 'increasing'
WHEN REGR_SLOPE(frequency, EXTRACT(EPOCH FROM week)) < -1 THEN 'decreasing'
ELSE 'stable'
END AS trend
FROM error_patterns
GROUP BY rule_code, pattern, category
)
SELECT * FROM pattern_trends
ORDER BY total_frequency DESC
LIMIT 20
`;
const result = await db.query(query, [tenantId, startDate, endDate]);
return result.rows;
}
/**
* Auto-categorize errors using ML patterns
*/
async autoCategorizeError(
validationHistoryId: string,
ruleCode: string,
errorMessage: string,
fieldValue: string
): Promise<{ categoryId: string; confidence: number }> {
// Pattern matching rules
const patterns = [
{
categoryCode: 'HUMAN_TRANSCRIPTION',
patterns: [/transpos/i, /digit.*swap/i, /132.*123/i],
confidence: 0.9,
},
{
categoryCode: 'HUMAN_OMISSION',
patterns: [/required/i, /missing/i, /blank/i, /null/i],
confidence: 0.95,
},
{
categoryCode: 'HUMAN_CALCULATION',
patterns: [/formula/i, /calculate/i, /sum/i, /total/i],
confidence: 0.85,
},
{
categoryCode: 'SYSTEM_ERROR',
patterns: [/timeout/i, /500/i, /database/i, /connection/i],
confidence: 0.95,
},
];
for (const { categoryCode, patterns: regexes, confidence } of patterns) {
for (const regex of regexes) {
if (regex.test(errorMessage)) {
const category = await this.getCategoryByCode(categoryCode);
return { categoryId: category.category_id, confidence };
}
}
}
// Default: human error
const defaultCategory = await this.getCategoryByCode('HUMAN_ERROR');
return { categoryId: defaultCategory.category_id, confidence: 0.5 };
}
/**
* Generate root cause analysis report
*/
async generateRCAReport(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<{
summary: {
totalErrors: number;
byCategory: Record<string, number>;
topPatterns: Array<{ pattern: string; count: number }>;
};
recommendations: Array<{
category: string;
priority: 'HIGH' | 'MEDIUM' | 'LOW';
action: string;
expectedImpact: string;
}>;
}> {
const patterns = await this.identifyErrorPatterns(tenantId, startDate, endDate);
// Categorize errors
const byCategory: Record<string, number> = {};
let totalErrors = 0;
for (const pattern of patterns) {
byCategory[pattern.category] = (byCategory[pattern.category] || 0) + pattern.frequency;
totalErrors += pattern.frequency;
}
// Generate recommendations
const recommendations = this.generateRecommendations(patterns, byCategory);
return {
summary: {
totalErrors,
byCategory,
topPatterns: patterns.slice(0, 10).map(p => ({
pattern: p.pattern,
count: p.frequency,
})),
},
recommendations,
};
}
/**
* Generate recommendations based on error patterns
*/
private generateRecommendations(
patterns: any[],
byCategory: Record<string, number>
): Array<{
category: string;
priority: 'HIGH' | 'MEDIUM' | 'LOW';
action: string;
expectedImpact: string;
}> {
const recommendations = [];
// Training gap recommendations
if (byCategory['Training Gap'] > 10) {
recommendations.push({
category: 'Training Gap',
priority: 'HIGH' as const,
action: 'Conduct targeted training sessions for users with high error rates. Focus on data entry best practices and validation rule understanding.',
expectedImpact: `Reduce training-related errors by 50-70% within 30 days`,
});
}
// Process gap recommendations
if (byCategory['Process Gap'] > 5) {
recommendations.push({
category: 'Process Gap',
priority: 'HIGH' as const,
action: 'Review and update SOPs for data entry procedures. Add clarifying examples and decision trees.',
expectedImpact: `Reduce process-related errors by 40-60% within 60 days`,
});
}
// System error recommendations
if (byCategory['System Error'] > 0) {
recommendations.push({
category: 'System Error',
priority: 'CRITICAL' as const,
action: 'Investigate and fix software bugs causing validation failures. Consider adding defensive programming checks.',
expectedImpact: `Eliminate system-caused errors within 14 days`,
});
}
// Transcription error recommendations
const transcriptionPattern = patterns.find(p => p.pattern.includes('transposition'));
if (transcriptionPattern && transcriptionPattern.frequency > 5) {
recommendations.push({
category: 'Human Error',
priority: 'MEDIUM' as const,
action: 'Implement double-entry verification for critical numeric fields. Add visual confirmation dialogs for high-value changes.',
expectedImpact: `Reduce transcription errors by 60-80%`,
});
}
return recommendations.sort((a, b) => {
const priorityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
}
private async getCategoryByCode(code: string): Promise<any> {
// Database query
return { category_id: '' };
}
}
7. Dashboard Components
7.1 Overall Accuracy Scorecard
React Component:
// src/components/dashboards/AccuracyScorecard.tsx
import React from 'react';
import { Gauge } from '@/components/charts/Gauge';
import { TrendIndicator } from '@/components/charts/TrendIndicator';
interface AccuracyScorecardProps {
metrics: {
overall: number;
byCategory: Record<string, number>;
trend: number;
target: number;
};
}
export const AccuracyScorecard: React.FC<AccuracyScorecardProps> = ({ metrics }) => {
const getStatusColor = (value: number): string => {
if (value >= 99.5) return '#22c55e'; // Green
if (value >= 98.0) return '#eab308'; // Yellow
return '#ef4444'; // Red
};
return (
<div className="accuracy-scorecard">
<div className="scorecard-header">
<h2>Data Accuracy Scorecard</h2>
<span className="scorecard-period">Last 30 Days</span>
</div>
<div className="scorecard-main">
<Gauge
value={metrics.overall}
max={100}
min={0}
target={metrics.target}
color={getStatusColor(metrics.overall)}
label="Overall Accuracy"
unit="%"
/>
<div className="scorecard-details">
<div className="metric-row">
<span className="metric-label">Target:</span>
<span className="metric-value">{metrics.target}%</span>
</div>
<div className="metric-row">
<span className="metric-label">Current:</span>
<span className="metric-value" style={{ color: getStatusColor(metrics.overall) }}>
{metrics.overall.toFixed(2)}%
</span>
</div>
<div className="metric-row">
<span className="metric-label">Trend (30d):</span>
<TrendIndicator value={metrics.trend} unit="%" />
</div>
</div>
</div>
<div className="scorecard-categories">
<h3>By Category</h3>
{Object.entries(metrics.byCategory).map(([category, value]) => (
<div key={category} className="category-row">
<span className="category-name">{category}</span>
<div className="category-bar">
<div
className="category-bar-fill"
style={{
width: `${value}%`,
backgroundColor: getStatusColor(value),
}}
/>
</div>
<span className="category-value">{value.toFixed(1)}%</span>
</div>
))}
</div>
</div>
);
};
7.2 Error Trend Charts
React Component:
// src/components/dashboards/ErrorTrendChart.tsx
import React from 'react';
import { Line } from 'react-chartjs-2';
interface ErrorTrendChartProps {
data: Array<{
date: string;
errorCount: number;
accuracyRate: number;
}>;
timeframe: '30d' | '60d' | '90d' | '180d';
}
export const ErrorTrendChart: React.FC<ErrorTrendChartProps> = ({ data, timeframe }) => {
const chartData = {
labels: data.map(d => d.date),
datasets: [
{
label: 'Error Count',
data: data.map(d => d.errorCount),
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
yAxisID: 'y-errors',
},
{
label: 'Accuracy Rate (%)',
data: data.map(d => d.accuracyRate),
borderColor: '#22c55e',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
yAxisID: 'y-accuracy',
},
],
};
const options = {
responsive: true,
interaction: {
mode: 'index' as const,
intersect: false,
},
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: `Error Trends - ${timeframe}`,
},
},
scales: {
'y-errors': {
type: 'linear' as const,
display: true,
position: 'left' as const,
title: {
display: true,
text: 'Error Count',
},
},
'y-accuracy': {
type: 'linear' as const,
display: true,
position: 'right' as const,
min: 95,
max: 100,
title: {
display: true,
text: 'Accuracy Rate (%)',
},
grid: {
drawOnChartArea: false,
},
},
},
};
return <Line data={chartData} options={options} />;
};
7.3 Compliance Reporting
Export Service:
// src/services/reporting/AccuracyReportExporter.ts
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
export class AccuracyReportExporter {
/**
* Generate compliance report PDF
*/
async generateComplianceReport(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<Buffer> {
const metrics = await this.getAccuracyMetrics(tenantId, startDate, endDate);
const rcaData = await this.getRCAData(tenantId, startDate, endDate);
const doc = new jsPDF();
// Title page
doc.setFontSize(20);
doc.text('Data Accuracy Compliance Report', 20, 20);
doc.setFontSize(12);
doc.text(`Reporting Period: ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]}`, 20, 30);
doc.text(`Generated: ${new Date().toISOString()}`, 20, 40);
// Executive summary
doc.setFontSize(16);
doc.text('Executive Summary', 20, 60);
doc.setFontSize(10);
doc.text(`Overall Accuracy Rate: ${metrics.overall.accuracyRate.toFixed(2)}%`, 20, 70);
doc.text(`Target: 99.5%`, 20, 80);
doc.text(`Status: ${metrics.overall.accuracyRate >= 99.5 ? 'PASS' : 'FAIL'}`, 20, 90);
// Accuracy by category table
autoTable(doc, {
startY: 110,
head: [['Category', 'Accuracy Rate', 'Total Validations', 'Errors', 'Status']],
body: metrics.byCategory.map(cat => [
cat.category,
`${cat.accuracyRate.toFixed(2)}%`,
cat.totalValidations.toString(),
cat.errorCount.toString(),
cat.accuracyRate >= 99.5 ? '✓ PASS' : '✗ FAIL',
]),
});
// Regulatory compliance mapping
doc.addPage();
doc.setFontSize(16);
doc.text('Regulatory Compliance Mapping', 20, 20);
autoTable(doc, {
startY: 30,
head: [['Requirement', 'Implementation', 'Evidence']],
body: [
[
'FDA 21 CFR Part 11 §11.10(a) - Validation',
'Validation rule engine with 100% coverage',
`${metrics.overall.totalValidations} validations executed`,
],
[
'ALCOA+ - Accurate',
'Real-time validation with error detection',
`${metrics.overall.accuracyRate.toFixed(2)}% accuracy rate`,
],
[
'ALCOA+ - Complete',
'Required field validation',
`${metrics.errorsByType.omission || 0} omission errors detected and corrected`,
],
[
'ALCOA+ - Consistent',
'Cross-reference validation',
`${metrics.errorsByType.consistency || 0} consistency errors detected`,
],
[
'ICH Q10 - Data Management',
'Continuous monitoring dashboard',
'Daily metrics refresh, 24/7 alerting',
],
],
});
// Root cause analysis summary
doc.addPage();
doc.setFontSize(16);
doc.text('Root Cause Analysis', 20, 20);
autoTable(doc, {
startY: 30,
head: [['Error Category', 'Count', 'Percentage', 'Corrective Action']],
body: rcaData.byCategory.map(cat => [
cat.category,
cat.count.toString(),
`${cat.percentage.toFixed(1)}%`,
cat.correctiveAction,
]),
});
return Buffer.from(doc.output('arraybuffer'));
}
/**
* Generate training needs report
*/
async generateTrainingNeedsReport(
tenantId: string,
startDate: Date,
endDate: Date
): Promise<Buffer> {
const userMetrics = await this.getUserMetrics(tenantId, startDate, endDate);
const doc = new jsPDF();
doc.setFontSize(20);
doc.text('Training Needs Assessment', 20, 20);
// Users requiring training (accuracy < 99% or >5 errors/month)
const needsTraining = userMetrics.filter(
u => u.accuracyScore < 99.0 || u.errorCount > 5
);
autoTable(doc, {
startY: 40,
head: [['User', 'Accuracy Score', 'Error Count', 'Top Error Types', 'Priority']],
body: needsTraining.map(user => [
user.userName,
`${user.accuracyScore.toFixed(1)}%`,
user.errorCount.toString(),
user.topErrorTypes.join(', '),
user.errorCount > 10 ? 'HIGH' : user.accuracyScore < 98 ? 'MEDIUM' : 'LOW',
]),
});
return Buffer.from(doc.output('arraybuffer'));
}
private async getAccuracyMetrics(tenantId: string, startDate: Date, endDate: Date): Promise<any> {
// Implementation
return {};
}
private async getRCAData(tenantId: string, startDate: Date, endDate: Date): Promise<any> {
// Implementation
return { byCategory: [] };
}
private async getUserMetrics(tenantId: string, startDate: Date, endDate: Date): Promise<any[]> {
// Implementation
return [];
}
}
8. Compliance Mapping
8.1 FDA 21 CFR Part 11
| Requirement | Implementation | Verification Method |
|---|---|---|
| §11.10(a) - System Validation | Validation rule engine with comprehensive test coverage | Validation test suite execution logs |
| §11.10(c) - Data Integrity | Real-time validation with ALCOA+ compliance checks | Accuracy metrics dashboard showing >99.5% rate |
| §11.10(e) - Audit Trail | All validation results logged with user attribution | Validation history table with complete audit trail |
| §11.10(k)(1) - Authority Checks | Role-based validation override with justification | Override logs with electronic signatures |
8.2 ALCOA+ Principles
| Principle | Implementation | Metrics |
|---|---|---|
| Attributable | User ID logged for all data entry and validation | 100% validation events have user attribution |
| Legible | Text pattern validation ensures readability | <0.1% illegibility errors |
| Contemporaneous | Real-time validation on data entry | <1 second validation latency |
| Original | Source data preserved with change tracking | Audit trail captures all modifications |
| Accurate | Multi-layer validation with error detection | 99.5%+ accuracy rate target |
| Complete | Required field validation | Omission error detection and prevention |
| Consistent | Cross-reference validation | Consistency error detection |
| Enduring | Immutable validation history | Validation records retained per retention policy |
| Available | Dashboard accessible to authorized users | 99.9% dashboard uptime |
8.3 ICH Q10
| Section | Requirement | Implementation |
|---|---|---|
| 2.5 - Knowledge Management | Data quality monitoring | Accuracy metrics dashboard |
| 2.5 - Data Management | Data integrity controls | Real-time validation engine |
| 2.6 - Quality Risk Management | Risk-based validation | Severity-based validation rules |
| 3.2 - Change Management | Validation rule change control | Version-controlled rule registry |
9. Conclusion
This Data Accuracy Monitoring Dashboard provides comprehensive data integrity controls for the BIO-QMS platform, ensuring compliance with FDA 21 CFR Part 11, ALCOA+ principles, and ICH Q10 requirements. The system delivers:
- Real-time validation with configurable rules and severity-based enforcement
- Automated error detection using statistical analysis and pattern recognition
- Comprehensive metrics with >99.5% accuracy rate target
- Intelligent alerting with escalation chains for quality incidents
- Root cause analysis with corrective action tracking
- Compliance reporting demonstrating regulatory adherence
Next Steps:
- Deploy validation rule engine to production
- Configure initial validation rules per module
- Establish baseline accuracy metrics
- Train quality team on dashboard usage
- Schedule first quarterly compliance review
Document End