#!/usr/bin/env python3 """ CODITECT Task ID Validator (ADR-074)
Validates task IDs in files and tool call logs. Used for compliance checking and governance enforcement.
Usage:
python validate-task-ids.py
import argparse import json import re import sys from pathlib import Path from typing import List, Dict, Tuple, Optional
Task ID regex pattern
TASK_ID_PATTERN = re.compile(r'^[A-H].\d+(.\d+)*$')
Track definitions
TRACKS = { 'A': {'name': 'Backend API', 'agents': ['senior-architect', 'database-architect']}, 'B': {'name': 'Frontend UI', 'agents': ['frontend-react-typescript-expert']}, 'C': {'name': 'DevOps/Infrastructure', 'agents': ['devops-engineer', 'cloud-architect']}, 'D': {'name': 'Security', 'agents': ['security-specialist']}, 'E': {'name': 'Testing/QA', 'agents': ['testing-specialist']}, 'F': {'name': 'Documentation', 'agents': ['codi-documentation-writer']}, 'G': {'name': 'DMS', 'agents': ['prompt-analyzer-specialist']}, 'H': {'name': 'Innovation/Research', 'agents': ['senior-architect']} }
def validate_task_id(task_id: str) -> Tuple[bool, str, Optional[Dict]]: """Validate a task ID.
Args:
task_id: Task ID to validate (e.g., "A.9.1.3")
Returns:
(is_valid, message, metadata)
"""
if not task_id:
return False, "Empty task ID", None
task_id = task_id.strip()
if not TASK_ID_PATTERN.match(task_id):
return False, f"Invalid format: '{task_id}'. Expected: Track.Section.Task (e.g., A.9.1.3)", None
track = task_id[0]
if track not in TRACKS:
return False, f"Unknown track: '{track}'", None
# Parse components
parts = task_id.split('.')
metadata = {
'track': track,
'track_name': TRACKS[track]['name'],
'section': int(parts[1]) if len(parts) > 1 else None,
'task': int(parts[2]) if len(parts) > 2 else None,
'subtask': int(parts[3]) if len(parts) > 3 else None,
'depth': len(parts),
'suggested_agents': TRACKS[track]['agents']
}
return True, f"Valid task ID: {task_id} ({metadata['track_name']})", metadata
def extract_task_ids_from_text(text: str) -> List[str]: """Extract all task IDs from text.""" # Pattern to find task IDs in context (e.g., "A.9.1.3:" or "[A.9.1.3]") pattern = r'\b([A-H].\d+(?:.\d+)*)\b' matches = re.findall(pattern, text) return list(set(matches))
def validate_file(file_path: Path) -> Dict: """Validate task IDs in a file.
Returns:
Validation report
"""
content = file_path.read_text()
task_ids = extract_task_ids_from_text(content)
results = {
'file': str(file_path),
'total_task_ids': len(task_ids),
'valid': [],
'invalid': [],
'tracks_used': set()
}
for task_id in task_ids:
is_valid, message, metadata = validate_task_id(task_id)
if is_valid:
results['valid'].append({'id': task_id, 'metadata': metadata})
results['tracks_used'].add(metadata['track'])
else:
results['invalid'].append({'id': task_id, 'error': message})
results['tracks_used'] = list(results['tracks_used'])
return results
def check_session_log(log_path: Path) -> Dict: """Check a session log for task ID compliance.
Returns:
Compliance report
"""
lines = log_path.read_text().splitlines()
results = {
'file': str(log_path),
'total_tool_calls': 0,
'compliant_calls': 0,
'non_compliant_calls': 0,
'violations': [],
'compliance_rate': 0.0
}
for i, line in enumerate(lines):
# Look for tool calls (various formats)
if 'Bash(' in line or 'Edit(' in line or 'Write(' in line or 'Read(' in line:
results['total_tool_calls'] += 1
# Check for task ID in description
task_ids = extract_task_ids_from_text(line)
if task_ids:
results['compliant_calls'] += 1
else:
results['non_compliant_calls'] += 1
results['violations'].append({
'line': i + 1,
'content': line[:100] + ('...' if len(line) > 100 else '')
})
if results['total_tool_calls'] > 0:
results['compliance_rate'] = (results['compliant_calls'] / results['total_tool_calls']) * 100
return results
def print_report(report: Dict, verbose: bool = False): """Print validation report.""" print(f"\n{'='*60}") print(f"Task ID Validation Report") print(f"{'='*60}") print(f"File: {report['file']}")
if 'compliance_rate' in report:
# Session log compliance report
print(f"\nTool Call Compliance:")
print(f" Total calls: {report['total_tool_calls']}")
print(f" Compliant: {report['compliant_calls']}")
print(f" Violations: {report['non_compliant_calls']}")
print(f" Rate: {report['compliance_rate']:.1f}%")
if report['violations'] and verbose:
print(f"\nViolations:")
for v in report['violations'][:10]: # Limit to 10
print(f" Line {v['line']}: {v['content']}")
else:
# File validation report
print(f"\nTask IDs Found: {report['total_task_ids']}")
print(f" Valid: {len(report['valid'])}")
print(f" Invalid: {len(report['invalid'])}")
print(f" Tracks: {', '.join(report['tracks_used']) or 'None'}")
if report['invalid']:
print(f"\nInvalid Task IDs:")
for item in report['invalid']:
print(f" - {item['id']}: {item['error']}")
if verbose and report['valid']:
print(f"\nValid Task IDs:")
for item in report['valid']:
m = item['metadata']
print(f" - {item['id']}: {m['track_name']} (depth: {m['depth']})")
def main(): parser = argparse.ArgumentParser( description="Validate CODITECT task IDs", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python validate-task-ids.py plan.md python validate-task-ids.py --check-log session.jsonl python validate-task-ids.py --format A.9.1.3 python validate-task-ids.py --tracks """ )
parser.add_argument('file', nargs='?', help='File to validate')
parser.add_argument('--format', metavar='ID', help='Validate a single task ID')
parser.add_argument('--check-log', metavar='LOG', help='Check session log for compliance')
parser.add_argument('--tracks', action='store_true', help='List all tracks')
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
parser.add_argument('--json', action='store_true', help='Output as JSON')
args = parser.parse_args()
# List tracks
if args.tracks:
print("\nCODITECT Track Nomenclature:")
print("="*50)
for track, info in TRACKS.items():
print(f" {track}: {info['name']}")
print(f" Agents: {', '.join(info['agents'])}")
return 0
# Validate single task ID
if args.format:
is_valid, message, metadata = validate_task_id(args.format)
if args.json:
print(json.dumps({'valid': is_valid, 'message': message, 'metadata': metadata}))
else:
status = "VALID" if is_valid else "INVALID"
print(f"[{status}] {message}")
if metadata:
print(f" Track: {metadata['track']} - {metadata['track_name']}")
print(f" Depth: {metadata['depth']}")
print(f" Suggested agents: {', '.join(metadata['suggested_agents'])}")
return 0 if is_valid else 1
# Check session log
if args.check_log:
log_path = Path(args.check_log)
if not log_path.exists():
print(f"Error: Log file not found: {log_path}", file=sys.stderr)
return 1
report = check_session_log(log_path)
if args.json:
print(json.dumps(report, indent=2))
else:
print_report(report, args.verbose)
return 0 if report['compliance_rate'] >= 80 else 1
# Validate file
if args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"Error: File not found: {file_path}", file=sys.stderr)
return 1
report = validate_file(file_path)
if args.json:
print(json.dumps(report, indent=2))
else:
print_report(report, args.verbose)
return 0 if not report['invalid'] else 1
parser.print_help()
return 0
if name == "main": sys.exit(main())