Files
claude-skills-reference/ra-qm-team/quality-documentation-manager/scripts/document_validator.py
Alireza Rezvani 15266e99f2 fix(skill): rewrite quality-documentation-manager with document control content (#80) (#155)
- Remove placeholder files (example.py, api_reference.md, example_asset.txt)
- Add 12 trigger phrases for skill discoverability
- Add Table of Contents for navigation
- Remove marketing language ("comprehensive", "robust", "seamless", "expert-level")
- Create 5 numbered workflows with validation checkpoints:
  - Document Control Workflow with lifecycle stages
  - Document Numbering System with format and category codes
  - Approval and Review Process with comment disposition
  - Change Control Process with classification criteria
  - 21 CFR Part 11 Compliance with electronic controls
- Create document-control-procedures.md (~400 lines):
  - Document numbering format and workflow
  - Lifecycle stages and transitions
  - Review and approval matrix
  - Change control classification and impact assessment
  - Distribution methods and access control
  - Record retention periods and disposal
- Create 21cfr11-compliance-guide.md (~450 lines):
  - Part 11 scope and applicability
  - Electronic record requirements (§11.10)
  - Electronic signature requirements with manifestation
  - System controls (administrative, operational, technical)
  - Validation approach and documentation
  - Compliance checklist and gap assessment template
- Create document_validator.py (~450 lines):
  - Document numbering convention validation
  - Status and lifecycle validation
  - Date validation (effective, review due)
  - Approval requirements checking
  - Change history completeness
  - 21 CFR Part 11 controls validation
  - Interactive mode and JSON output

SKILL.md reduced from 266 to 438 lines with actionable workflows.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 17:54:40 +01:00

647 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Document Validator - Quality Documentation Compliance Checker
Validates document metadata, numbering conventions, and control requirements
for ISO 13485 and 21 CFR Part 11 compliance.
Usage:
python document_validator.py --doc document.json
python document_validator.py --interactive
python document_validator.py --doc document.json --output json
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
class DocumentType(Enum):
QM = "Quality Manual"
SOP = "Standard Operating Procedure"
WI = "Work Instruction"
TF = "Template/Form"
POL = "Policy"
SPEC = "Specification"
PLN = "Plan"
RPT = "Report"
class DocumentStatus(Enum):
DRAFT = "Draft"
REVIEW = "Under Review"
APPROVED = "Approved"
EFFECTIVE = "Effective"
SUPERSEDED = "Superseded"
OBSOLETE = "Obsolete"
class Severity(Enum):
CRITICAL = "Critical"
MAJOR = "Major"
MINOR = "Minor"
INFO = "Info"
@dataclass
class ValidationFinding:
rule: str
severity: Severity
message: str
recommendation: str
@dataclass
class Document:
number: str
title: str
doc_type: str
revision: str
status: str
effective_date: Optional[str] = None
review_date: Optional[str] = None
author: Optional[str] = None
approver: Optional[str] = None
approval_date: Optional[str] = None
change_history: List[Dict] = field(default_factory=list)
has_audit_trail: bool = False
has_electronic_signature: bool = False
signature_components: int = 0
@dataclass
class ValidationResult:
document_number: str
validation_date: str
total_findings: int
critical_findings: int
major_findings: int
minor_findings: int
compliance_score: float
findings: List[Dict]
recommendations: List[str]
class DocumentValidator:
"""Validator for quality documentation compliance."""
# Document number pattern: PREFIX-CATEGORY-SEQUENCE-REVISION
DOC_NUMBER_PATTERN = r'^([A-Z]{2,4})-(\d{2,3})-(\d{3,4})(?:-([A-Z]|\d{2}))?$'
# Valid document type prefixes
VALID_PREFIXES = ['QM', 'SOP', 'WI', 'TF', 'POL', 'SPEC', 'PLN', 'RPT']
# Category codes
VALID_CATEGORIES = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10']
def __init__(self, document: Document):
self.document = document
self.today = datetime.now()
self.findings: List[ValidationFinding] = []
def validate(self) -> ValidationResult:
"""Run all validation checks."""
self._validate_document_number()
self._validate_title()
self._validate_status_lifecycle()
self._validate_dates()
self._validate_approvals()
self._validate_change_history()
self._validate_electronic_controls()
# Calculate compliance score
score = self._calculate_compliance_score()
# Generate recommendations
recommendations = self._generate_recommendations()
# Count findings by severity
critical = len([f for f in self.findings if f.severity == Severity.CRITICAL])
major = len([f for f in self.findings if f.severity == Severity.MAJOR])
minor = len([f for f in self.findings if f.severity == Severity.MINOR])
return ValidationResult(
document_number=self.document.number,
validation_date=self.today.strftime("%Y-%m-%d"),
total_findings=len(self.findings),
critical_findings=critical,
major_findings=major,
minor_findings=minor,
compliance_score=round(score, 1),
findings=[asdict(f) for f in self.findings],
recommendations=recommendations
)
def _validate_document_number(self):
"""Validate document numbering convention."""
number = self.document.number
if not number:
self.findings.append(ValidationFinding(
rule="DOC-NUM-001",
severity=Severity.CRITICAL,
message="Document number is missing",
recommendation="Assign document number per numbering procedure"
))
return
match = re.match(self.DOC_NUMBER_PATTERN, number)
if not match:
self.findings.append(ValidationFinding(
rule="DOC-NUM-002",
severity=Severity.MAJOR,
message=f"Document number '{number}' does not match standard format",
recommendation="Use format: PREFIX-CATEGORY-SEQUENCE[-REVISION] (e.g., SOP-02-001-A)"
))
return
prefix, category, sequence, revision = match.groups()
if prefix not in self.VALID_PREFIXES:
self.findings.append(ValidationFinding(
rule="DOC-NUM-003",
severity=Severity.MAJOR,
message=f"Invalid document type prefix: {prefix}",
recommendation=f"Use one of: {', '.join(self.VALID_PREFIXES)}"
))
if category not in self.VALID_CATEGORIES:
self.findings.append(ValidationFinding(
rule="DOC-NUM-004",
severity=Severity.MINOR,
message=f"Non-standard category code: {category}",
recommendation=f"Standard categories are: {', '.join(self.VALID_CATEGORIES)}"
))
def _validate_title(self):
"""Validate document title."""
title = self.document.title
if not title:
self.findings.append(ValidationFinding(
rule="DOC-TTL-001",
severity=Severity.MAJOR,
message="Document title is missing",
recommendation="Provide descriptive document title"
))
return
if len(title) < 10:
self.findings.append(ValidationFinding(
rule="DOC-TTL-002",
severity=Severity.MINOR,
message="Document title is very short",
recommendation="Use descriptive title that clearly identifies content"
))
if len(title) > 100:
self.findings.append(ValidationFinding(
rule="DOC-TTL-003",
severity=Severity.MINOR,
message="Document title exceeds recommended length",
recommendation="Keep title under 100 characters"
))
def _validate_status_lifecycle(self):
"""Validate document status and lifecycle."""
status = self.document.status
if not status:
self.findings.append(ValidationFinding(
rule="DOC-STS-001",
severity=Severity.MAJOR,
message="Document status is missing",
recommendation="Assign appropriate document status"
))
return
valid_statuses = [s.value for s in DocumentStatus]
if status not in valid_statuses:
self.findings.append(ValidationFinding(
rule="DOC-STS-002",
severity=Severity.MAJOR,
message=f"Invalid document status: {status}",
recommendation=f"Use one of: {', '.join(valid_statuses)}"
))
# Check status-specific requirements
if status == DocumentStatus.EFFECTIVE.value:
if not self.document.effective_date:
self.findings.append(ValidationFinding(
rule="DOC-STS-003",
severity=Severity.MAJOR,
message="Effective document missing effective date",
recommendation="Add effective date for effective documents"
))
if status == DocumentStatus.APPROVED.value:
if not self.document.approval_date:
self.findings.append(ValidationFinding(
rule="DOC-STS-004",
severity=Severity.MAJOR,
message="Approved document missing approval date",
recommendation="Add approval date for approved documents"
))
def _validate_dates(self):
"""Validate document dates."""
# Check effective date
if self.document.effective_date:
try:
eff_date = datetime.strptime(self.document.effective_date, "%Y-%m-%d")
if eff_date > self.today:
self.findings.append(ValidationFinding(
rule="DOC-DTE-001",
severity=Severity.INFO,
message="Effective date is in the future",
recommendation="Verify planned effective date is correct"
))
except ValueError:
self.findings.append(ValidationFinding(
rule="DOC-DTE-002",
severity=Severity.MINOR,
message="Invalid effective date format",
recommendation="Use YYYY-MM-DD format for dates"
))
# Check review date
if self.document.review_date:
try:
review_date = datetime.strptime(self.document.review_date, "%Y-%m-%d")
if review_date < self.today:
self.findings.append(ValidationFinding(
rule="DOC-DTE-003",
severity=Severity.MAJOR,
message="Document is overdue for review",
recommendation="Initiate periodic review process"
))
elif review_date < self.today + timedelta(days=30):
self.findings.append(ValidationFinding(
rule="DOC-DTE-004",
severity=Severity.MINOR,
message="Document review due within 30 days",
recommendation="Plan for upcoming review"
))
except ValueError:
self.findings.append(ValidationFinding(
rule="DOC-DTE-005",
severity=Severity.MINOR,
message="Invalid review date format",
recommendation="Use YYYY-MM-DD format for dates"
))
else:
if self.document.status == DocumentStatus.EFFECTIVE.value:
self.findings.append(ValidationFinding(
rule="DOC-DTE-006",
severity=Severity.MINOR,
message="Effective document missing review date",
recommendation="Add next review date (typically 1-3 years from effective)"
))
def _validate_approvals(self):
"""Validate document approval information."""
if self.document.status in [DocumentStatus.APPROVED.value, DocumentStatus.EFFECTIVE.value]:
if not self.document.author:
self.findings.append(ValidationFinding(
rule="DOC-APR-001",
severity=Severity.MAJOR,
message="Document author not identified",
recommendation="Document author on signature page"
))
if not self.document.approver:
self.findings.append(ValidationFinding(
rule="DOC-APR-002",
severity=Severity.CRITICAL,
message="Document approver not identified",
recommendation="Obtain required approval signatures"
))
def _validate_change_history(self):
"""Validate change history completeness."""
history = self.document.change_history
if not history:
self.findings.append(ValidationFinding(
rule="DOC-CHG-001",
severity=Severity.MAJOR,
message="Document change history is missing",
recommendation="Include change history table with revision descriptions"
))
return
for i, entry in enumerate(history):
if not entry.get('revision'):
self.findings.append(ValidationFinding(
rule="DOC-CHG-002",
severity=Severity.MINOR,
message=f"Change history entry {i+1} missing revision number",
recommendation="Include revision number for each history entry"
))
if not entry.get('description'):
self.findings.append(ValidationFinding(
rule="DOC-CHG-003",
severity=Severity.MINOR,
message=f"Change history entry {i+1} missing description",
recommendation="Include description of changes for each revision"
))
if not entry.get('date'):
self.findings.append(ValidationFinding(
rule="DOC-CHG-004",
severity=Severity.MINOR,
message=f"Change history entry {i+1} missing date",
recommendation="Include date for each history entry"
))
def _validate_electronic_controls(self):
"""Validate 21 CFR Part 11 requirements for electronic documents."""
# Audit trail check
if not self.document.has_audit_trail:
self.findings.append(ValidationFinding(
rule="P11-AUD-001",
severity=Severity.MAJOR,
message="Electronic document lacks audit trail",
recommendation="Enable audit trail for 21 CFR Part 11 compliance"
))
# Electronic signature check
if self.document.has_electronic_signature:
if self.document.signature_components < 2:
self.findings.append(ValidationFinding(
rule="P11-SIG-001",
severity=Severity.CRITICAL,
message="Electronic signature uses fewer than 2 identification components",
recommendation="Use at least 2 components (e.g., user ID + password)"
))
else:
if self.document.status in [DocumentStatus.APPROVED.value, DocumentStatus.EFFECTIVE.value]:
self.findings.append(ValidationFinding(
rule="P11-SIG-002",
severity=Severity.INFO,
message="Document uses handwritten signatures",
recommendation="Consider electronic signatures for efficiency"
))
def _calculate_compliance_score(self) -> float:
"""Calculate compliance score based on findings."""
if not self.findings:
return 100.0
# Weight by severity
deductions = {
Severity.CRITICAL: 25,
Severity.MAJOR: 10,
Severity.MINOR: 3,
Severity.INFO: 0
}
total_deduction = sum(deductions[f.severity] for f in self.findings)
score = max(0, 100 - total_deduction)
return score
def _generate_recommendations(self) -> List[str]:
"""Generate prioritized recommendations."""
recommendations = []
# Critical findings
critical = [f for f in self.findings if f.severity == Severity.CRITICAL]
if critical:
recommendations.append(
f"URGENT: {len(critical)} critical finding(s) require immediate attention"
)
# Major findings
major = [f for f in self.findings if f.severity == Severity.MAJOR]
if major:
recommendations.append(
f"ACTION: {len(major)} major finding(s) should be addressed within 30 days"
)
# Review overdue
review_overdue = [f for f in self.findings if f.rule == "DOC-DTE-003"]
if review_overdue:
recommendations.append(
"REVIEW: Document is overdue for periodic review. Initiate review process."
)
# Part 11 gaps
p11_findings = [f for f in self.findings if f.rule.startswith("P11")]
if p11_findings:
recommendations.append(
f"COMPLIANCE: {len(p11_findings)} 21 CFR Part 11 gap(s) identified"
)
if not recommendations:
recommendations.append("Document passes validation checks")
return recommendations
def format_text_output(result: ValidationResult) -> str:
"""Format validation result as text report."""
lines = [
"=" * 70,
"DOCUMENT VALIDATION REPORT",
"=" * 70,
f"Document: {result.document_number}",
f"Validation Date: {result.validation_date}",
f"Compliance Score: {result.compliance_score}%",
"",
"FINDINGS SUMMARY",
"-" * 40,
f" Critical: {result.critical_findings}",
f" Major: {result.major_findings}",
f" Minor: {result.minor_findings}",
f" Total: {result.total_findings}",
]
if result.findings:
lines.extend([
"",
"DETAILED FINDINGS",
"-" * 40,
])
for finding in result.findings:
severity = finding['severity']
lines.append(f"\n[{severity}] {finding['rule']}")
lines.append(f" Issue: {finding['message']}")
lines.append(f" Action: {finding['recommendation']}")
lines.extend([
"",
"RECOMMENDATIONS",
"-" * 40,
])
for i, rec in enumerate(result.recommendations, 1):
lines.append(f"{i}. {rec}")
lines.append("=" * 70)
return "\n".join(lines)
def interactive_mode():
"""Run interactive document validation."""
print("=" * 60)
print("Document Validator - Interactive Mode")
print("=" * 60)
print("\nEnter document information:\n")
number = input("Document Number (e.g., SOP-02-001): ").strip()
title = input("Document Title: ").strip()
print("\nDocument Types: QM, SOP, WI, TF, POL, SPEC, PLN, RPT")
doc_type = input("Document Type: ").strip().upper()
revision = input("Revision (e.g., 01 or A): ").strip()
print("\nStatuses: Draft, Under Review, Approved, Effective, Superseded, Obsolete")
status = input("Status: ").strip()
effective_date = input("Effective Date (YYYY-MM-DD, or Enter to skip): ").strip() or None
review_date = input("Next Review Date (YYYY-MM-DD, or Enter to skip): ").strip() or None
author = input("Author Name (or Enter to skip): ").strip() or None
approver = input("Approver Name (or Enter to skip): ").strip() or None
has_audit = input("Has Audit Trail? (y/n): ").strip().lower() == 'y'
has_esig = input("Uses Electronic Signatures? (y/n): ").strip().lower() == 'y'
sig_components = 0
if has_esig:
sig_input = input("Number of signature components (e.g., 2): ").strip()
sig_components = int(sig_input) if sig_input.isdigit() else 0
doc = Document(
number=number,
title=title,
doc_type=doc_type,
revision=revision,
status=status,
effective_date=effective_date,
review_date=review_date,
author=author,
approver=approver,
has_audit_trail=has_audit,
has_electronic_signature=has_esig,
signature_components=sig_components
)
validator = DocumentValidator(doc)
result = validator.validate()
print("\n" + format_text_output(result))
def main():
parser = argparse.ArgumentParser(
description="Quality Documentation Validator"
)
parser.add_argument(
"--doc",
type=str,
help="JSON file with document metadata"
)
parser.add_argument(
"--output",
choices=["text", "json"],
default="text",
help="Output format"
)
parser.add_argument(
"--interactive",
action="store_true",
help="Run in interactive mode"
)
parser.add_argument(
"--sample",
action="store_true",
help="Generate sample document JSON"
)
args = parser.parse_args()
if args.interactive:
interactive_mode()
return
if args.sample:
sample = {
"number": "SOP-02-001",
"title": "Document Control Procedure",
"doc_type": "SOP",
"revision": "03",
"status": "Effective",
"effective_date": "2024-01-15",
"review_date": "2025-01-15",
"author": "J. Smith",
"approver": "M. Jones",
"approval_date": "2024-01-10",
"change_history": [
{"revision": "01", "date": "2022-01-01", "description": "Initial release"},
{"revision": "02", "date": "2023-01-15", "description": "Updated approval workflow"},
{"revision": "03", "date": "2024-01-15", "description": "Added electronic signature requirements"}
],
"has_audit_trail": True,
"has_electronic_signature": True,
"signature_components": 2
}
print(json.dumps(sample, indent=2))
return
if args.doc:
with open(args.doc, "r") as f:
data = json.load(f)
doc = Document(
number=data.get("number", ""),
title=data.get("title", ""),
doc_type=data.get("doc_type", ""),
revision=data.get("revision", ""),
status=data.get("status", ""),
effective_date=data.get("effective_date"),
review_date=data.get("review_date"),
author=data.get("author"),
approver=data.get("approver"),
approval_date=data.get("approval_date"),
change_history=data.get("change_history", []),
has_audit_trail=data.get("has_audit_trail", False),
has_electronic_signature=data.get("has_electronic_signature", False),
signature_components=data.get("signature_components", 0)
)
else:
# Demo document
doc = Document(
number="SOP-02-001",
title="Document Control",
doc_type="SOP",
revision="01",
status="Effective",
effective_date="2024-01-15",
author="J. Smith",
has_audit_trail=True,
has_electronic_signature=True,
signature_components=2
)
validator = DocumentValidator(doc)
result = validator.validate()
if args.output == "json":
print(json.dumps(asdict(result), indent=2))
else:
print(format_text_output(result))
if __name__ == "__main__":
main()