Files
claude-skills-reference/ra-qm-team/capa-officer/scripts/capa_tracker.py
Alireza Rezvani 63c35e361e fix(skill): rewrite capa-officer with comprehensive CAPA management content (#79) (#153)
- 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 ("Senior", "Expert-level")
- Create 4 numbered workflows with validation checkpoints:
  - CAPA Investigation Workflow with necessity determination
  - Root Cause Analysis with method selection decision tree
  - Corrective Action Planning with action types and templates
  - Effectiveness Verification with timeline guidelines
- Create rca-methodologies.md (~450 lines):
  - Method selection matrix and decision tree
  - 5 Why analysis template with calibration example
  - Fishbone diagram (6M) categories and template
  - Fault Tree Analysis for safety-critical issues
  - Human Factors Analysis (HFACS) framework
  - FMEA with RPN calculation scales
- Create effectiveness-verification-guide.md (~350 lines):
  - Verification planning requirements
  - Five verification methods with evidence requirements
  - SMART effectiveness criteria guidance
  - Closure requirements by severity
  - Ineffective CAPA process workflow
  - Documentation templates
- Create capa_tracker.py (~480 lines):
  - CAPA status and metrics calculation
  - Aging analysis by time buckets
  - Effectiveness rate tracking
  - Overdue CAPA identification
  - Interactive mode for manual entry
  - JSON and text output formats
  - Actionable recommendations engine

SKILL.md grew from 191 to 435 lines with concrete decision criteria.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 14:18:45 +01:00

639 lines
20 KiB
Python

#!/usr/bin/env python3
"""
CAPA Tracker - Corrective and Preventive Action Management Tool
Tracks CAPA status, calculates metrics, identifies overdue items,
and generates reports for management review.
Usage:
python capa_tracker.py --capas capas.json
python capa_tracker.py --interactive
python capa_tracker.py --capas capas.json --output json
"""
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from enum import Enum
class CAPAStatus(Enum):
OPEN = "Open"
INVESTIGATION = "Investigation"
ACTION_PLANNING = "Action Planning"
IMPLEMENTATION = "Implementation"
VERIFICATION = "Verification"
CLOSED_EFFECTIVE = "Closed - Effective"
CLOSED_INEFFECTIVE = "Closed - Ineffective"
class CAPASeverity(Enum):
CRITICAL = "Critical"
MAJOR = "Major"
MINOR = "Minor"
class CAPASource(Enum):
COMPLAINT = "Customer Complaint"
AUDIT = "Internal Audit"
EXTERNAL_AUDIT = "External Audit"
NONCONFORMANCE = "Nonconformance"
MANAGEMENT_REVIEW = "Management Review"
TREND_ANALYSIS = "Trend Analysis"
REGULATORY = "Regulatory Feedback"
OTHER = "Other"
@dataclass
class CAPA:
capa_number: str
title: str
description: str
source: CAPASource
severity: CAPASeverity
status: CAPAStatus
open_date: str
target_date: str
owner: str
root_cause: str = ""
corrective_action: str = ""
verification_date: Optional[str] = None
close_date: Optional[str] = None
days_open: int = 0
is_overdue: bool = False
@dataclass
class CAPAMetrics:
total_capas: int
open_capas: int
closed_capas: int
overdue_capas: int
avg_cycle_time: float
effectiveness_rate: float
by_status: Dict[str, int]
by_severity: Dict[str, int]
by_source: Dict[str, int]
overdue_list: List[Dict]
recommendations: List[str]
class CAPATracker:
"""CAPA tracking and metrics calculator."""
# Target cycle times by severity (days)
TARGET_CYCLE_TIMES = {
CAPASeverity.CRITICAL: 30,
CAPASeverity.MAJOR: 60,
CAPASeverity.MINOR: 90,
}
def __init__(self, capas: List[CAPA]):
self.capas = capas
self.today = datetime.now()
self._calculate_derived_fields()
def _calculate_derived_fields(self):
"""Calculate days open and overdue status."""
for capa in self.capas:
open_date = datetime.strptime(capa.open_date, "%Y-%m-%d")
if capa.close_date:
close_date = datetime.strptime(capa.close_date, "%Y-%m-%d")
capa.days_open = (close_date - open_date).days
else:
capa.days_open = (self.today - open_date).days
target_date = datetime.strptime(capa.target_date, "%Y-%m-%d")
if not capa.close_date and self.today > target_date:
capa.is_overdue = True
def calculate_metrics(self) -> CAPAMetrics:
"""Calculate comprehensive CAPA metrics."""
total = len(self.capas)
# Status counts
closed_statuses = [CAPAStatus.CLOSED_EFFECTIVE, CAPAStatus.CLOSED_INEFFECTIVE]
open_capas = [c for c in self.capas if c.status not in closed_statuses]
closed_capas = [c for c in self.capas if c.status in closed_statuses]
overdue_capas = [c for c in self.capas if c.is_overdue]
# Average cycle time (closed CAPAs only)
if closed_capas:
avg_cycle = sum(c.days_open for c in closed_capas) / len(closed_capas)
else:
avg_cycle = 0.0
# Effectiveness rate
effective = [c for c in self.capas if c.status == CAPAStatus.CLOSED_EFFECTIVE]
ineffective = [c for c in self.capas if c.status == CAPAStatus.CLOSED_INEFFECTIVE]
if effective or ineffective:
effectiveness = len(effective) / (len(effective) + len(ineffective)) * 100
else:
effectiveness = 0.0
# Counts by category
by_status = {}
for status in CAPAStatus:
count = len([c for c in self.capas if c.status == status])
if count > 0:
by_status[status.value] = count
by_severity = {}
for severity in CAPASeverity:
count = len([c for c in self.capas if c.severity == severity])
if count > 0:
by_severity[severity.value] = count
by_source = {}
for source in CAPASource:
count = len([c for c in self.capas if c.source == source])
if count > 0:
by_source[source.value] = count
# Overdue list
overdue_list = []
for capa in sorted(overdue_capas, key=lambda c: c.days_open, reverse=True):
target = datetime.strptime(capa.target_date, "%Y-%m-%d")
days_overdue = (self.today - target).days
overdue_list.append({
"capa_number": capa.capa_number,
"title": capa.title,
"severity": capa.severity.value,
"status": capa.status.value,
"days_overdue": days_overdue,
"owner": capa.owner
})
# Generate recommendations
recommendations = self._generate_recommendations(
open_capas, overdue_capas, effectiveness, avg_cycle
)
return CAPAMetrics(
total_capas=total,
open_capas=len(open_capas),
closed_capas=len(closed_capas),
overdue_capas=len(overdue_capas),
avg_cycle_time=round(avg_cycle, 1),
effectiveness_rate=round(effectiveness, 1),
by_status=by_status,
by_severity=by_severity,
by_source=by_source,
overdue_list=overdue_list,
recommendations=recommendations
)
def _generate_recommendations(
self,
open_capas: List[CAPA],
overdue_capas: List[CAPA],
effectiveness: float,
avg_cycle: float
) -> List[str]:
"""Generate actionable recommendations."""
recommendations = []
# Overdue CAPAs
if overdue_capas:
critical_overdue = [c for c in overdue_capas if c.severity == CAPASeverity.CRITICAL]
if critical_overdue:
recommendations.append(
f"URGENT: {len(critical_overdue)} critical CAPA(s) overdue. "
"Escalate to management immediately."
)
else:
recommendations.append(
f"ACTION: {len(overdue_capas)} CAPA(s) overdue. "
"Review and update target dates or expedite closure."
)
# Effectiveness rate
if effectiveness < 80 and effectiveness > 0:
recommendations.append(
f"CONCERN: Effectiveness rate at {effectiveness:.0f}%. "
"Review root cause analysis quality and corrective action adequacy."
)
# Cycle time
if avg_cycle > 60:
recommendations.append(
f"IMPROVEMENT: Average cycle time is {avg_cycle:.0f} days. "
"Target is 60 days. Review investigation and approval bottlenecks."
)
# Investigation backlog
in_investigation = [c for c in open_capas if c.status == CAPAStatus.INVESTIGATION]
if len(in_investigation) > 5:
recommendations.append(
f"WORKLOAD: {len(in_investigation)} CAPAs in investigation phase. "
"Consider additional resources or prioritization."
)
# Stuck in verification
in_verification = [c for c in open_capas if c.status == CAPAStatus.VERIFICATION]
old_verification = [c for c in in_verification if c.days_open > 120]
if old_verification:
recommendations.append(
f"STALLED: {len(old_verification)} CAPA(s) in verification >120 days. "
"Complete effectiveness checks or extend with justification."
)
# Source patterns
complaint_capas = [c for c in self.capas if c.source == CAPASource.COMPLAINT]
if len(complaint_capas) > len(self.capas) * 0.4:
recommendations.append(
"TREND: >40% of CAPAs from customer complaints. "
"Review preventive action effectiveness and quality controls."
)
if not recommendations:
recommendations.append(
"CAPA program operating within targets. "
"Continue monitoring key metrics."
)
return recommendations
def get_aging_report(self) -> Dict:
"""Generate aging analysis of open CAPAs."""
open_statuses = [
CAPAStatus.OPEN, CAPAStatus.INVESTIGATION,
CAPAStatus.ACTION_PLANNING, CAPAStatus.IMPLEMENTATION,
CAPAStatus.VERIFICATION
]
open_capas = [c for c in self.capas if c.status in open_statuses]
aging_buckets = {
"0-30 days": [],
"31-60 days": [],
"61-90 days": [],
"91-120 days": [],
">120 days": []
}
for capa in open_capas:
days = capa.days_open
if days <= 30:
bucket = "0-30 days"
elif days <= 60:
bucket = "31-60 days"
elif days <= 90:
bucket = "61-90 days"
elif days <= 120:
bucket = "91-120 days"
else:
bucket = ">120 days"
aging_buckets[bucket].append({
"capa_number": capa.capa_number,
"title": capa.title,
"days_open": days,
"status": capa.status.value,
"severity": capa.severity.value
})
return aging_buckets
def format_text_output(metrics: CAPAMetrics, aging: Dict) -> str:
"""Format metrics as text report."""
lines = [
"=" * 70,
"CAPA STATUS REPORT",
"=" * 70,
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
"",
"SUMMARY METRICS",
"-" * 40,
f"Total CAPAs: {metrics.total_capas}",
f"Open CAPAs: {metrics.open_capas}",
f"Closed CAPAs: {metrics.closed_capas}",
f"Overdue CAPAs: {metrics.overdue_capas}",
f"Avg Cycle Time: {metrics.avg_cycle_time} days",
f"Effectiveness Rate: {metrics.effectiveness_rate}%",
"",
"STATUS DISTRIBUTION",
"-" * 40,
]
for status, count in metrics.by_status.items():
bar = "" * min(count, 20)
lines.append(f" {status:<25} {bar} {count}")
lines.extend([
"",
"SEVERITY DISTRIBUTION",
"-" * 40,
])
for severity, count in metrics.by_severity.items():
bar = "" * min(count, 20)
lines.append(f" {severity:<25} {bar} {count}")
lines.extend([
"",
"SOURCE DISTRIBUTION",
"-" * 40,
])
for source, count in metrics.by_source.items():
bar = "" * min(count, 20)
lines.append(f" {source:<25} {bar} {count}")
lines.extend([
"",
"AGING ANALYSIS",
"-" * 40,
])
for bucket, capas in aging.items():
lines.append(f" {bucket}: {len(capas)} CAPA(s)")
if metrics.overdue_list:
lines.extend([
"",
"OVERDUE CAPAs",
"-" * 40,
f"{'CAPA #':<12} {'Title':<25} {'Days':<6} {'Owner':<15}",
"-" * 60,
])
for item in metrics.overdue_list[:10]:
title = item["title"][:24] if len(item["title"]) > 24 else item["title"]
lines.append(
f"{item['capa_number']:<12} {title:<25} "
f"{item['days_overdue']:<6} {item['owner']:<15}"
)
if len(metrics.overdue_list) > 10:
lines.append(f"... and {len(metrics.overdue_list) - 10} more")
lines.extend([
"",
"RECOMMENDATIONS",
"-" * 40,
])
for i, rec in enumerate(metrics.recommendations, 1):
lines.append(f"{i}. {rec}")
lines.append("=" * 70)
return "\n".join(lines)
def interactive_mode():
"""Run interactive CAPA entry mode."""
print("=" * 60)
print("CAPA Tracker - Interactive Mode")
print("=" * 60)
capas = []
print("\nEnter CAPAs (blank CAPA number to finish):\n")
while True:
capa_num = input("CAPA Number (e.g., CAPA-2024-001): ").strip()
if not capa_num:
break
title = input("Title: ").strip()
description = input("Description: ").strip()
print("Source options: C=Complaint, A=Audit, N=Nonconformance, M=Management Review, T=Trend, O=Other")
source_input = input("Source [C/A/N/M/T/O]: ").strip().upper()
source_map = {
"C": CAPASource.COMPLAINT,
"A": CAPASource.AUDIT,
"N": CAPASource.NONCONFORMANCE,
"M": CAPASource.MANAGEMENT_REVIEW,
"T": CAPASource.TREND_ANALYSIS,
"O": CAPASource.OTHER
}
source = source_map.get(source_input, CAPASource.OTHER)
print("Severity: C=Critical, M=Major, I=Minor")
severity_input = input("Severity [C/M/I]: ").strip().upper()
severity_map = {
"C": CAPASeverity.CRITICAL,
"M": CAPASeverity.MAJOR,
"I": CAPASeverity.MINOR
}
severity = severity_map.get(severity_input, CAPASeverity.MINOR)
print("Status: O=Open, I=Investigation, P=Action Planning, M=Implementation, V=Verification, E=Closed Effective, N=Closed Ineffective")
status_input = input("Status [O/I/P/M/V/E/N]: ").strip().upper()
status_map = {
"O": CAPAStatus.OPEN,
"I": CAPAStatus.INVESTIGATION,
"P": CAPAStatus.ACTION_PLANNING,
"M": CAPAStatus.IMPLEMENTATION,
"V": CAPAStatus.VERIFICATION,
"E": CAPAStatus.CLOSED_EFFECTIVE,
"N": CAPAStatus.CLOSED_INEFFECTIVE
}
status = status_map.get(status_input, CAPAStatus.OPEN)
open_date = input("Open Date (YYYY-MM-DD): ").strip()
target_date = input("Target Date (YYYY-MM-DD): ").strip()
owner = input("Owner: ").strip()
close_date = None
if status in [CAPAStatus.CLOSED_EFFECTIVE, CAPAStatus.CLOSED_INEFFECTIVE]:
close_date = input("Close Date (YYYY-MM-DD): ").strip()
capas.append(CAPA(
capa_number=capa_num,
title=title,
description=description,
source=source,
severity=severity,
status=status,
open_date=open_date,
target_date=target_date,
owner=owner,
close_date=close_date if close_date else None
))
print(f"\nAdded: {capa_num}\n")
if not capas:
print("No CAPAs entered. Exiting.")
return
tracker = CAPATracker(capas)
metrics = tracker.calculate_metrics()
aging = tracker.get_aging_report()
print("\n" + format_text_output(metrics, aging))
def main():
parser = argparse.ArgumentParser(
description="CAPA Tracking and Metrics Tool"
)
parser.add_argument(
"--capas",
type=str,
help="JSON file with CAPA data"
)
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 CAPA data file"
)
args = parser.parse_args()
if args.interactive:
interactive_mode()
return
if args.sample:
sample_data = {
"capas": [
{
"capa_number": "CAPA-2024-001",
"title": "Calibration overdue for pH meter",
"description": "pH meter EQ-042 found 2 months overdue",
"source": "AUDIT",
"severity": "MAJOR",
"status": "VERIFICATION",
"open_date": "2024-06-15",
"target_date": "2024-08-15",
"owner": "J. Smith",
"root_cause": "No trigger for schedule update at equipment purchase",
"corrective_action": "Updated SOP-EQ-001 to require schedule update"
},
{
"capa_number": "CAPA-2024-002",
"title": "Customer complaint - labeling error",
"description": "Wrong lot number on product label",
"source": "COMPLAINT",
"severity": "CRITICAL",
"status": "INVESTIGATION",
"open_date": "2024-09-01",
"target_date": "2024-10-01",
"owner": "M. Jones"
},
{
"capa_number": "CAPA-2024-003",
"title": "Training records incomplete",
"description": "Missing effectiveness verification for 3 operators",
"source": "AUDIT",
"severity": "MINOR",
"status": "CLOSED_EFFECTIVE",
"open_date": "2024-03-10",
"target_date": "2024-06-10",
"owner": "A. Brown",
"close_date": "2024-05-20"
}
]
}
print(json.dumps(sample_data, indent=2))
return
if args.capas:
with open(args.capas, "r") as f:
data = json.load(f)
capas = []
for c in data.get("capas", []):
try:
source = CAPASource[c.get("source", "OTHER").upper()]
except KeyError:
source = CAPASource.OTHER
try:
severity = CAPASeverity[c.get("severity", "MINOR").upper()]
except KeyError:
severity = CAPASeverity.MINOR
try:
status = CAPAStatus[c.get("status", "OPEN").upper()]
except KeyError:
status = CAPAStatus.OPEN
capas.append(CAPA(
capa_number=c["capa_number"],
title=c.get("title", ""),
description=c.get("description", ""),
source=source,
severity=severity,
status=status,
open_date=c["open_date"],
target_date=c["target_date"],
owner=c.get("owner", ""),
root_cause=c.get("root_cause", ""),
corrective_action=c.get("corrective_action", ""),
verification_date=c.get("verification_date"),
close_date=c.get("close_date")
))
else:
# Demo data if no file provided
capas = [
CAPA(
capa_number="CAPA-2024-001",
title="Calibration overdue",
description="pH meter overdue",
source=CAPASource.AUDIT,
severity=CAPASeverity.MAJOR,
status=CAPAStatus.VERIFICATION,
open_date="2024-06-15",
target_date="2024-08-15",
owner="J. Smith"
),
CAPA(
capa_number="CAPA-2024-002",
title="Labeling error complaint",
description="Wrong lot number",
source=CAPASource.COMPLAINT,
severity=CAPASeverity.CRITICAL,
status=CAPAStatus.INVESTIGATION,
open_date="2024-09-01",
target_date="2024-10-01",
owner="M. Jones"
),
CAPA(
capa_number="CAPA-2024-003",
title="Training records incomplete",
description="Missing effectiveness verification",
source=CAPASource.AUDIT,
severity=CAPASeverity.MINOR,
status=CAPAStatus.CLOSED_EFFECTIVE,
open_date="2024-03-10",
target_date="2024-06-10",
owner="A. Brown",
close_date="2024-05-20"
)
]
tracker = CAPATracker(capas)
metrics = tracker.calculate_metrics()
aging = tracker.get_aging_report()
if args.output == "json":
output = {
"metrics": asdict(metrics),
"aging": aging
}
print(json.dumps(output, indent=2))
else:
print(format_text_output(metrics, aging))
if __name__ == "__main__":
main()