Skip to main content

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

RoleNameSignatureDate
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

VersionDateAuthorChangesApproval Status
1.0.02026-02-16QA TeamInitial releaseDraft

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 TypeFrequencyNext Review DateResponsible Party
Annual Review12 months2027-02-16QA Director
Quarterly Metrics Review3 months2026-05-16QA Manager
Regulatory Update ReviewAs neededN/ARegulatory Affairs
Post-Incident ReviewAs neededN/AQA 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:

  1. Real-Time Validation - All data entry is validated according to configurable rules with severity-based enforcement
  2. Error Detection - Automated detection of transcription errors, duplicates, outliers, and inconsistencies
  3. Accuracy Metrics - Continuous monitoring of data accuracy rates with per-user, per-category, and per-module granularity
  4. Alerting & Escalation - Threshold-based alerts with defined escalation chains for quality incidents
  5. Root Cause Analysis - Systematic categorization and analysis of error patterns with corrective action tracking
  6. 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

FrameworkRequirementImplementation
FDA 21 CFR Part 11§11.10(a) - System validationValidation rule engine with test coverage
FDA 21 CFR Part 11§11.10(e) - Audit trails for data changesAll validation errors and corrections logged
ALCOA+ PrinciplesAttributable, Legible, Contemporaneous, Original, AccuratePer-principle validation checks
ALCOA+ PrinciplesComplete, Consistent, Enduring, AvailableData quality metrics for each principle
ICH Q102.5 - Data ManagementSystematic data quality monitoring
FDA Data Integrity GuidanceSection IV - ALCOA+Automated validation of data integrity
MHRA GXP Data Integrity6.9 - Data governanceRole-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

RequirementImplementationVerification Method
§11.10(a) - System ValidationValidation rule engine with comprehensive test coverageValidation test suite execution logs
§11.10(c) - Data IntegrityReal-time validation with ALCOA+ compliance checksAccuracy metrics dashboard showing >99.5% rate
§11.10(e) - Audit TrailAll validation results logged with user attributionValidation history table with complete audit trail
§11.10(k)(1) - Authority ChecksRole-based validation override with justificationOverride logs with electronic signatures

8.2 ALCOA+ Principles

PrincipleImplementationMetrics
AttributableUser ID logged for all data entry and validation100% validation events have user attribution
LegibleText pattern validation ensures readability<0.1% illegibility errors
ContemporaneousReal-time validation on data entry<1 second validation latency
OriginalSource data preserved with change trackingAudit trail captures all modifications
AccurateMulti-layer validation with error detection99.5%+ accuracy rate target
CompleteRequired field validationOmission error detection and prevention
ConsistentCross-reference validationConsistency error detection
EnduringImmutable validation historyValidation records retained per retention policy
AvailableDashboard accessible to authorized users99.9% dashboard uptime

8.3 ICH Q10

SectionRequirementImplementation
2.5 - Knowledge ManagementData quality monitoringAccuracy metrics dashboard
2.5 - Data ManagementData integrity controlsReal-time validation engine
2.6 - Quality Risk ManagementRisk-based validationSeverity-based validation rules
3.2 - Change ManagementValidation rule change controlVersion-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:

  1. Real-time validation with configurable rules and severity-based enforcement
  2. Automated error detection using statistical analysis and pattern recognition
  3. Comprehensive metrics with >99.5% accuracy rate target
  4. Intelligent alerting with escalation chains for quality incidents
  5. Root cause analysis with corrective action tracking
  6. Compliance reporting demonstrating regulatory adherence

Next Steps:

  1. Deploy validation rule engine to production
  2. Configure initial validation rules per module
  3. Establish baseline accuracy metrics
  4. Train quality team on dashboard usage
  5. Schedule first quarterly compliance review

Document End