#!/usr/bin/env python3 """ CODITECT Workflow QA Grader
Grades workflows against CODITECT-STANDARD-WORKFLOWS.md criteria.
Weights: Prerequisites (15%), Step Completeness (30%), Examples (25%), Integration (15%), Troubleshooting (15%)
Usage: python3 scripts/qa/grade-workflows.py [path] [--json output.json] [--verbose]
ADR-161: Component Quality Assurance Framework """
import os import sys import re import json import argparse from pathlib import Path
sys.path.insert(0, os.path.dirname(file)) from qa_common import ( parse_frontmatter, count_words, grade_from_score, compute_weighted_score, aggregate_results, output_results )
CODITECT_CORE = Path(file).resolve().parents[2] WORKFLOWS_DIR = CODITECT_CORE / "workflows"
def grade_workflow(filepath): """Grade a single workflow file.""" filename = os.path.basename(filepath) wf_name = re.sub(r'.(workflow)?.md$', '', filename)
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
fm, body = parse_frontmatter(content)
body_lower = body.lower()
word_count = count_words(body)
scores = {}
# A. PREREQUISITES (15%)
scores['A1_prerequisites_section'] = 1 if re.search(r'##\s*(prerequisites|requirements|before\s+you\s+begin|setup)', body_lower) else 0
scores['A2_inputs_specified'] = 1 if fm.get('inputs') or re.search(r'(inputs?:|required\s+inputs?|parameters?:)', body_lower) else 0
# B. STEP COMPLETENESS (30%)
# Numbered/ordered steps
numbered_steps = re.findall(r'^\d+\.\s+', body, re.MULTILINE)
scores['B1_numbered_steps'] = 1 if len(numbered_steps) >= 3 else 0
# Steps with expected output
scores['B2_expected_output'] = 1 if re.search(r'(expected\s+output|you\s+should\s+see|result:|output:)', body_lower) else 0
# Steps reference tools or commands
scores['B3_tool_references'] = 1 if re.search(r'(Task\(|/\w+|python3\s+|bash\s+|```)', body) else 0
# Step validation or checkpoints
scores['B4_validation_steps'] = 1 if re.search(r'(verify|validate|confirm|check\s+that|assert)', body_lower) else 0
# C. EXAMPLES (25%)
code_blocks = re.findall(r'```[\w]*\n.*?```', body, re.DOTALL)
scores['C1_walkthrough'] = 1 if len(code_blocks) >= 2 else 0
scores['C2_code_examples'] = 1 if re.search(r'```\w+', body) else 0
scores['C3_variant_paths'] = 1 if re.search(r'(alternative|variant|option\s+\d|if\s+.*instead)', body_lower) else 0
# D. INTEGRATION (15%)
scores['D1_related_components'] = 1 if re.search(r'(##\s*related|##\s*integration|agent.*:|skill.*:|command.*:)', body_lower) else 0
scores['D2_output_locations'] = 1 if re.search(r'(output.*location|generates?.*file|saves?\s+to|writes?\s+to|output.*path)', body_lower) or fm.get('outputs') else 0
# E. TROUBLESHOOTING (15%)
scores['E1_common_issues'] = 1 if re.search(r'##\s*(troubleshooting|common\s+issues|known\s+issues|faq)', body_lower) else 0
scores['E2_debug_approach'] = 1 if re.search(r'(debug|--verbose|logging|error\s+message|if\s+.*fails)', body_lower) else 0
categories = [
('A_prerequisites', 15, ['A1_prerequisites_section', 'A2_inputs_specified']),
('B_step_completeness', 30, ['B1_numbered_steps', 'B2_expected_output', 'B3_tool_references', 'B4_validation_steps']),
('C_examples', 25, ['C1_walkthrough', 'C2_code_examples', 'C3_variant_paths']),
('D_integration', 15, ['D1_related_components', 'D2_output_locations']),
('E_troubleshooting', 15, ['E1_common_issues', 'E2_debug_approach']),
]
total_base, category_scores = compute_weighted_score(scores, categories)
return {
'name': wf_name,
'scores': scores,
'category_scores': category_scores,
'total_base': total_base,
'grade': grade_from_score(total_base),
'word_count': word_count,
'track': fm.get('track', fm.get('category', 'N/A')),
}
def main(): parser = argparse.ArgumentParser(description='Grade CODITECT workflows') parser.add_argument('path', nargs='?', default=str(WORKFLOWS_DIR), help='Workflow file or directory') parser.add_argument('--json', dest='json_output', help='Output JSON to file') parser.add_argument('--verbose', action='store_true') args = parser.parse_args()
target = Path(args.path)
if target.is_file():
wf_files = [target]
else:
# Find all .workflow.md files and workflow .md files
wf_files = sorted(set(
list(target.rglob('*.workflow.md')) +
[f for f in target.glob('*.md') if f.name != 'README.md']
))
results = []
errors = []
for filepath in wf_files:
try:
results.append(grade_workflow(str(filepath)))
except Exception as e:
errors.append({'file': filepath.name, 'error': str(e)})
data = aggregate_results(results, 'workflows')
data['errors'] = errors
if args.json_output:
output_results(data, args.json_output, 'json')
output_results(data, format='summary')
if args.verbose:
sorted_results = sorted(results, key=lambda x: x['total_base'], reverse=True)
print(f"\nTOP 10:")
for r in sorted_results[:10]:
print(f" {r['grade']} {r['total_base']:5.1f}% | {r['name']}")
print(f"\nBOTTOM 10:")
for r in sorted_results[-10:]:
print(f" {r['grade']} {r['total_base']:5.1f}% | {r['name']}")
if name == 'main': main()