Skip to main content

#!/usr/bin/env python3 """ Enforce Naming Conventions

Validates and optionally fixes file/directory naming against CODITECT standards.

Usage: python enforce-naming-conventions.py [path] [--fix] [--check]

Examples: python enforce-naming-conventions.py # Check current directory python enforce-naming-conventions.py --fix # Fix violations python enforce-naming-conventions.py --check # Exit non-zero on violations """

import argparse import os import re import subprocess import sys from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple

ANSI colors

Shared Colors module (consolidates 36 duplicate definitions)

_script_dir = Path(file).parent sys.path.insert(0, str(_script_dir / "core")) from colors import Colors

class Violation: """Represents a naming violation.""" path: Path current_name: str expected_name: str rule: str is_dir: bool

Naming rules

RULES = { 'directory': { 'pattern': r'^[a-z][a-z0-9-]$', 'description': 'kebab-case', 'transform': lambda s: s.lower().replace('', '-').replace(' ', '-') }, 'python_package': { 'pattern': r'^[a-z][a-z0-9]$', 'description': 'snake_case', 'transform': lambda s: s.lower().replace('-', '').replace(' ', '') }, 'markdown_standard': { 'pattern': r'^(README|CLAUDE|LICENSE|CHANGELOG|CONTRIBUTING|CODE_OF_CONDUCT).md$', 'description': 'UPPERCASE.md', 'transform': lambda s: s.upper().replace('.MD', '.md') }, 'markdown_doc': { 'pattern': r'^[A-Z][A-Z0-9-].md$', 'description': 'UPPER-KEBAB-CASE.md', 'transform': lambda s: s.upper().replace('', '-').replace(' ', '-') }, 'python_file': { 'pattern': r'^[a-z][a-z0-9].py$', 'description': 'snake_case.py', 'transform': lambda s: s.lower().replace('-', '').replace(' ', '') }, 'typescript': { 'pattern': r'^[a-z][a-z0-9-].ts$', 'description': 'kebab-case.ts', 'transform': lambda s: s.lower().replace('_', '-').replace(' ', '-') }, 'react_component': { 'pattern': r'^[A-Z][a-zA-Z0-9].tsx$', 'description': 'PascalCase.tsx', 'transform': lambda s: ''.join(w.capitalize() for w in re.split(r'[-\s]', s.replace('.tsx', ''))) + '.tsx' }, 'shell': { 'pattern': r'^[a-z][a-z0-9-]*.sh$', 'description': 'kebab-case.sh', 'transform': lambda s: s.lower().replace('', '-').replace(' ', '-') }, 'json_config': { 'pattern': r'^[a-z][a-z0-9.-]*.json$', 'description': 'kebab-case.json', 'transform': lambda s: s.lower().replace('_', '-').replace(' ', '-') } }

Standard docs that must be uppercase

STANDARD_DOCS = ['readme.md', 'claude.md', 'license', 'license.md', 'changelog.md', 'contributing.md', 'code_of_conduct.md']

def get_rule_for_file(path: Path) -> Optional[Tuple[str, dict]]: """Determine which naming rule applies to a file.""" name = path.name.lower() suffix = path.suffix.lower()

# Standard docs
if name in STANDARD_DOCS:
return 'markdown_standard', RULES['markdown_standard']

# By extension
if suffix == '.md':
return 'markdown_doc', RULES['markdown_doc']
elif suffix == '.py':
return 'python_file', RULES['python_file']
elif suffix == '.ts':
return 'typescript', RULES['typescript']
elif suffix == '.tsx':
return 'react_component', RULES['react_component']
elif suffix == '.sh':
return 'shell', RULES['shell']
elif suffix == '.json':
return 'json_config', RULES['json_config']

return None

def check_name(path: Path) -> Optional[Violation]: """Check if a file/directory name follows conventions.""" name = path.name

# Skip hidden files/dirs and special cases
if name.startswith('.') or name.startswith('__'):
return None

if path.is_dir():
# Check if it's a Python package (has __init__.py)
is_python_pkg = (path / '__init__.py').exists()
rule_name = 'python_package' if is_python_pkg else 'directory'
rule = RULES[rule_name]

if not re.match(rule['pattern'], name):
expected = rule['transform'](name)
return Violation(
path=path,
current_name=name,
expected_name=expected,
rule=rule['description'],
is_dir=True
)
else:
rule_info = get_rule_for_file(path)
if rule_info:
rule_name, rule = rule_info
if not re.match(rule['pattern'], name):
expected = rule['transform'](name)
return Violation(
path=path,
current_name=name,
expected_name=expected,
rule=rule['description'],
is_dir=False
)

return None

def scan_directory(path: Path, recursive: bool = True) -> List[Violation]: """Scan directory for naming violations.""" violations = []

skip_dirs = {'.git', 'node_modules', 'venv', '.venv', '__pycache__', '.pytest_cache'}

try:
items = list(path.iterdir())
except PermissionError:
return violations

for item in items:
if item.name in skip_dirs:
continue

violation = check_name(item)
if violation:
violations.append(violation)

if recursive and item.is_dir() and not item.name.startswith('.'):
violations.extend(scan_directory(item, recursive))

return violations

def fix_violation(violation: Violation, dry_run: bool = False) -> bool: """Fix a naming violation using git mv.""" new_path = violation.path.parent / violation.expected_name

if new_path.exists():
print(f" {Colors.RED}Cannot fix: {new_path} already exists{Colors.RESET}")
return False

if dry_run:
print(f" Would rename: {violation.current_name} → {violation.expected_name}")
return True

try:
# Try git mv first
result = subprocess.run(
['git', 'mv', str(violation.path), str(new_path)],
capture_output=True,
text=True
)

if result.returncode == 0:
print(f" {Colors.GREEN}Fixed:{Colors.RESET} {violation.current_name} → {violation.expected_name}")
return True
else:
# Fall back to regular rename
violation.path.rename(new_path)
print(f" {Colors.GREEN}Renamed:{Colors.RESET} {violation.current_name} → {violation.expected_name}")
return True

except Exception as e:
print(f" {Colors.RED}Error fixing {violation.current_name}: {e}{Colors.RESET}")
return False

def print_report(violations: List[Violation], total_checked: int): """Print violations report.""" compliance = 1 - (len(violations) / total_checked) if total_checked > 0 else 1.0

print(f"\n{Colors.BOLD}{'='*60}{Colors.RESET}")
print(f"{Colors.BOLD} NAMING CONVENTION REPORT{Colors.RESET}")
print(f"{Colors.BOLD}{'='*60}{Colors.RESET}\n")

color = Colors.GREEN if compliance >= 0.95 else (Colors.YELLOW if compliance >= 0.8 else Colors.RED)
print(f" {Colors.BOLD}Compliance:{Colors.RESET} {color}{compliance*100:.1f}%{Colors.RESET}")
print(f" {Colors.BOLD}Violations:{Colors.RESET} {len(violations)}")
print()

if violations:
# Group by rule
by_rule: Dict[str, List[Violation]] = {}
for v in violations:
by_rule.setdefault(v.rule, []).append(v)

for rule, rule_violations in by_rule.items():
print(f" {Colors.CYAN}{Colors.BOLD}{rule} violations ({len(rule_violations)}):{Colors.RESET}")
for v in rule_violations[:5]: # Show first 5
print(f" {v.current_name} → {v.expected_name}")
if len(rule_violations) > 5:
print(f" ... and {len(rule_violations) - 5} more")
print()

print(f"{Colors.BOLD}{'='*60}{Colors.RESET}\n")

def main(): parser = argparse.ArgumentParser( description='Validate and fix naming conventions' ) parser.add_argument('path', nargs='?', default='.', help='Path to check') parser.add_argument('--fix', action='store_true', help='Fix violations') parser.add_argument('--dry-run', action='store_true', help='Show what would be fixed') parser.add_argument('--check', action='store_true', help='Exit non-zero on violations') parser.add_argument('--json', action='store_true', help='Output as JSON')

args = parser.parse_args()

path = Path(args.path).resolve()
if not path.exists():
print(f"Error: Path does not exist: {path}", file=sys.stderr)
sys.exit(1)

# Scan for violations
violations = scan_directory(path)

# Count total items checked (rough estimate)
total_items = sum(1 for _ in path.rglob('*') if not any(
p in str(_) for p in ['.git', 'node_modules', 'venv', '__pycache__']
))

if args.json:
output = {
'compliance': 1 - (len(violations) / total_items) if total_items > 0 else 1.0,
'violations': [
{
'path': str(v.path),
'current': v.current_name,
'expected': v.expected_name,
'rule': v.rule,
'is_directory': v.is_dir
}
for v in violations
]
}
print(json.dumps(output, indent=2))
elif args.fix or args.dry_run:
print(f"\n{Colors.BOLD}Fixing naming violations...{Colors.RESET}\n")
fixed = 0
for v in violations:
if fix_violation(v, dry_run=args.dry_run):
fixed += 1
print(f"\n{Colors.GREEN}Fixed {fixed} of {len(violations)} violations{Colors.RESET}\n")
else:
print_report(violations, total_items)

if args.check and violations:
sys.exit(1)

sys.exit(0)

if name == 'main': main()