#!/usr/bin/env python3 """ CODITECT Skill Section Fixer
Auto-adds missing sections to skills that fail QA checks:
- B4_toc_if_long: Table of Contents (for skills >100 lines)
- C2_step_instructions: Steps section (numbered list)
- C4_integration: Integration section
- C5_principles: Principles section
Targets the top 4 automatable QA failures.
Usage: python3 scripts/qa/fix-skill-sections.py [--dry-run] [--verbose] [--path skills/]
ADR-161: Component Quality Assurance Framework Track: E.14 (QA Remediation) """
import os import sys import re import argparse from pathlib import Path
CODITECT_CORE = Path(file).resolve().parents[2] SKILLS_DIR = CODITECT_CORE / "skills"
FRONTMATTER_RE = re.compile(r'^(---\s*\n)(.?)(\n---\s\n)', re.DOTALL)
def find_h2_sections(body): """Extract H2 section names from body.""" return [m.group(1).strip().lower() for m in re.finditer(r'^##\s+(.+)$', body, re.MULTILINE)]
def has_toc(body): """Check if body has a Table of Contents section.""" return bool(re.search(r'##\s*table\s+of\s+contents', body, re.IGNORECASE))
def has_steps(body): """Check if body has numbered steps or a Steps section.""" has_steps_section = bool(re.search(r'##\s*steps', body, re.IGNORECASE)) has_numbered = bool(re.search(r'^\d+.', body, re.MULTILINE)) return has_steps_section or has_numbered
def has_integration(body): """Check if body has an Integration or Related section.""" return bool(re.search(r'##\s*(integration|related)', body, re.IGNORECASE))
def has_principles(body): """Check if body has a Principles or Anti-Patterns section.""" return bool(re.search(r'##\s*(principles|anti.?patterns|best\s+practices)', body, re.IGNORECASE))
def extract_h1_title(body): """Extract H1 title from body.""" m = re.search(r'^#\s+(.+)$', body, re.MULTILINE) return m.group(1).strip() if m else None
def extract_description_from_fm(fm_text): """Extract description from frontmatter.""" try: import yaml fm = yaml.safe_load(fm_text) or {} return str(fm.get('description', '')).strip() except Exception: return ''
def extract_related_agents(fm_text): """Extract related_agents from frontmatter.""" try: import yaml fm = yaml.safe_load(fm_text) or {} return fm.get('related_agents', []) or [] except Exception: return []
def generate_toc(sections): """Generate a Table of Contents from H2 sections.""" lines = ["## Table of Contents", ""] for i, section in enumerate(sections, 1): # Create anchor from section name anchor = section.lower().strip() anchor = re.sub(r'[^\w\s-]', '', anchor) anchor = re.sub(r'\s+', '-', anchor) lines.append(f"{i}. {section}") lines.append("") return "\n".join(lines)
def generate_steps(description, skill_name): """Generate a basic Steps section from description.""" steps = [ "## Steps", "", f"1. Review the patterns and configuration options in this skill", f"2. Identify applicable patterns for your use case", f"3. Apply the relevant implementation patterns", f"4. Validate the implementation against quality criteria", f"5. Iterate and refine based on results", "", ] return "\n".join(steps)
def generate_integration(skill_name, related_agents):
"""Generate an Integration section."""
lines = ["## Integration", "", "Related Components:"]
if related_agents:
for agent in related_agents[:5]:
lines.append(f"- Agent: {agent}")
else:
lines.append(f"- Skill: Related skills in the same track")
lines.append("")
return "\n".join(lines)
def generate_principles(skill_name): """Generate a Principles section.""" lines = [ "## Principles", "", "- Clarity over complexity - Prefer simple, understandable implementations", "- Validate before delivery - Always verify output meets requirements", "- Follow established patterns - Build on proven approaches", "- Document decisions - Capture rationale for key choices", "", ] return "\n".join(lines)
def fix_skill_sections(skill_dir, dry_run=False, verbose=False): """Add missing sections to a skill.""" skill_name = skill_dir.name skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
return None
with open(skill_file, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
fm_match = FRONTMATTER_RE.match(content)
if not fm_match:
return None
fm_text = fm_match.group(2)
body = content[fm_match.end():]
body_lines = body.count('\n')
# Skip short files that don't need TOC
needs_toc = body_lines > 100 and not has_toc(body)
needs_steps = not has_steps(body)
needs_integration = not has_integration(body)
needs_principles = not has_principles(body)
if not any([needs_toc, needs_steps, needs_integration, needs_principles]):
return None
changes = []
sections_to_add_before_body = [] # After H1
sections_to_add_at_end = []
# Find insertion points
h1_match = re.search(r'^#\s+.+$', body, re.MULTILINE)
h2_sections = find_h2_sections(body)
# Generate TOC
if needs_toc:
# Collect all existing H2 names plus any we'll add
all_sections = list(h2_sections)
if needs_steps and 'steps' not in [s.lower() for s in all_sections]:
all_sections.insert(0, 'Steps')
if needs_integration and not any('integration' in s.lower() or 'related' in s.lower() for s in all_sections):
all_sections.append('Integration')
if needs_principles and not any('principles' in s.lower() or 'anti' in s.lower() or 'best' in s.lower() for s in all_sections):
all_sections.append('Principles')
# Get proper-cased section names from the body
proper_sections = []
for m in re.finditer(r'^##\s+(.+)$', body, re.MULTILINE):
proper_sections.append(m.group(1).strip())
# Build TOC from proper names + additions
toc_names = []
if needs_steps:
toc_names.append('Steps')
for name in proper_sections:
toc_names.append(name)
if needs_integration:
toc_names.append('Integration')
if needs_principles:
toc_names.append('Principles')
toc = generate_toc(toc_names)
sections_to_add_before_body.append(toc)
changes.append('B4: Added Table of Contents')
# Generate Steps
if needs_steps:
desc = extract_description_from_fm(fm_text)
steps = generate_steps(desc, skill_name)
sections_to_add_before_body.append(steps)
changes.append('C2: Added Steps section')
# Generate Integration
if needs_integration:
agents = extract_related_agents(fm_text)
integration = generate_integration(skill_name, agents)
sections_to_add_at_end.append(integration)
changes.append('C4: Added Integration section')
# Generate Principles
if needs_principles:
principles = generate_principles(skill_name)
sections_to_add_at_end.append(principles)
changes.append('C5: Added Principles section')
if not changes:
return None
# Build new body
if h1_match:
# Insert TOC and Steps after H1 line
h1_end = h1_match.end()
# Find end of H1 block (next blank line or next section)
rest_after_h1 = body[h1_end:]
# Skip any immediate blank lines after H1
leading_ws = re.match(r'\n*', rest_after_h1)
insert_pos = h1_end + (leading_ws.end() if leading_ws else 0)
new_body = body[:insert_pos]
if sections_to_add_before_body:
new_body += "\n" + "\n".join(sections_to_add_before_body)
new_body += body[insert_pos:]
else:
new_body = body
if sections_to_add_before_body:
new_body = "\n".join(sections_to_add_before_body) + "\n" + new_body
# Add end sections
if sections_to_add_at_end:
# Insert before the last "---" or at the very end
new_body = new_body.rstrip()
new_body += "\n\n" + "\n".join(sections_to_add_at_end)
new_body += "\n"
if not dry_run:
new_content = content[:fm_match.end()] + new_body
with open(skill_file, 'w', encoding='utf-8') as f:
f.write(new_content)
return {
'name': skill_name,
'changes': changes,
'dry_run': dry_run,
}
def main(): parser = argparse.ArgumentParser( description='Add missing sections to CODITECT skills (TOC, Steps, Integration, Principles)') parser.add_argument('--path', default=str(SKILLS_DIR), help='Skills directory (default: skills/)') parser.add_argument('--dry-run', '-n', action='store_true', help='Preview changes without writing') parser.add_argument('--verbose', '-v', action='store_true', help='Show details for each skill') args = parser.parse_args()
target = Path(args.path)
skill_dirs = sorted([
d for d in target.iterdir()
if d.is_dir() and (d / 'SKILL.md').exists()
])
fixed = 0
skipped = 0
total = len(skill_dirs)
print(f"{'[DRY RUN] ' if args.dry_run else ''}Scanning {total} skills for missing sections...")
print(f"{'=' * 60}")
for d in skill_dirs:
result = fix_skill_sections(d, dry_run=args.dry_run, verbose=args.verbose)
if result is None:
skipped += 1
continue
fixed += 1
if args.verbose or args.dry_run:
prefix = "[DRY] " if args.dry_run else "FIXED"
print(f" {prefix} {result['name']}:")
for change in result['changes']:
print(f" {change}")
print(f"\n{'=' * 60}")
print(f"{'[DRY RUN] ' if args.dry_run else ''}Results:")
print(f" Total skills: {total}")
print(f" Fixed: {fixed}")
print(f" Already OK: {skipped}")
if args.dry_run and fixed > 0:
print(f"\nRun without --dry-run to apply {fixed} fixes.")
if name == 'main': main()