Initial commit
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Analyze skill structure and validate against Anthropic standards.
|
||||
|
||||
Usage:
|
||||
python analyze_skill_structure.py --skill skills/category/skill-name
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Configure stdout for UTF-8 encoding (prevents Windows encoding errors)
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
def parse_frontmatter(content: str) -> Tuple[Dict[str, str], str]:
|
||||
"""Extract and parse YAML frontmatter from SKILL.md"""
|
||||
if not content.startswith('---'):
|
||||
return {}, content
|
||||
|
||||
match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
|
||||
if not match:
|
||||
return {}, content
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
body = match.group(2)
|
||||
|
||||
# Parse frontmatter
|
||||
frontmatter = {}
|
||||
for line in frontmatter_text.split('\n'):
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
|
||||
return frontmatter, body
|
||||
|
||||
def check_name_convention(name: str) -> List[str]:
|
||||
"""Check if name follows hyphen-case convention"""
|
||||
issues = []
|
||||
|
||||
if not re.match(r'^[a-z0-9-]+$', name):
|
||||
issues.append(f"Name '{name}' should use only lowercase letters, digits, and hyphens")
|
||||
|
||||
if name.startswith('-') or name.endswith('-'):
|
||||
issues.append(f"Name '{name}' cannot start or end with hyphen")
|
||||
|
||||
if '--' in name:
|
||||
issues.append(f"Name '{name}' cannot contain consecutive hyphens")
|
||||
|
||||
return issues
|
||||
|
||||
def check_description(description: str) -> List[str]:
|
||||
"""Check description quality"""
|
||||
issues = []
|
||||
|
||||
if len(description) > 1024:
|
||||
issues.append(f"Description is {len(description)} characters (max 1024)")
|
||||
|
||||
if '<' in description or '>' in description:
|
||||
issues.append("Description cannot contain angle brackets")
|
||||
|
||||
# Check for third-person voice
|
||||
first_person_patterns = [
|
||||
r'\bI\s+',
|
||||
r'\bwe\s+',
|
||||
r'\bour\s+',
|
||||
r'\bmy\s+',
|
||||
]
|
||||
for pattern in first_person_patterns:
|
||||
if re.search(pattern, description, re.IGNORECASE):
|
||||
issues.append("Description should use third-person voice, not first-person")
|
||||
break
|
||||
|
||||
# Check if it includes trigger terms
|
||||
if 'trigger' not in description.lower() and 'include' not in description.lower():
|
||||
issues.append("Description should explicitly mention trigger terms")
|
||||
|
||||
return issues
|
||||
|
||||
def check_second_person_usage(body: str) -> List[Tuple[int, str]]:
|
||||
"""Find second-person usage in skill body"""
|
||||
violations = []
|
||||
|
||||
patterns = [
|
||||
r'\byou\s+should\b',
|
||||
r'\byou\s+can\b',
|
||||
r'\byou\s+need\s+to\b',
|
||||
r'\byou\s+have\s+to\b',
|
||||
r'\byou\s+must\b',
|
||||
r'\byou\s+will\b',
|
||||
r'\byour\b',
|
||||
]
|
||||
|
||||
lines = body.split('\n')
|
||||
for i, line in enumerate(lines, start=1):
|
||||
# Skip code blocks
|
||||
if line.strip().startswith('```') or line.strip().startswith(' '):
|
||||
continue
|
||||
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, line, re.IGNORECASE):
|
||||
violations.append((i, line.strip()))
|
||||
break
|
||||
|
||||
return violations
|
||||
|
||||
def check_emoji_usage(content: str) -> List[Tuple[int, str]]:
|
||||
"""Find emoji usage in skill content"""
|
||||
violations = []
|
||||
|
||||
# Common emojis that should be replaced with ASCII
|
||||
# Using Unicode ranges to avoid encoding issues
|
||||
emoji_pattern = re.compile(
|
||||
r'[\U0001F300-\U0001F9FF' # Miscellaneous Symbols and Pictographs, Supplemental
|
||||
r'\u2600-\u26FF' # Miscellaneous Symbols
|
||||
r'\u2700-\u27BF' # Dingbats
|
||||
r'\U0001F600-\U0001F64F' # Emoticons
|
||||
r'\U0001F680-\U0001F6FF' # Transport and Map
|
||||
r'\U0001F1E0-\U0001F1FF]' # Regional Indicator Symbols
|
||||
)
|
||||
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines, start=1):
|
||||
if emoji_pattern.search(line):
|
||||
violations.append((i, line.strip()[:100])) # Limit to 100 chars
|
||||
|
||||
return violations
|
||||
|
||||
def analyze_skill(skill_path: Path) -> Dict:
|
||||
"""Analyze skill structure and return findings"""
|
||||
results = {
|
||||
'valid': True,
|
||||
'critical_issues': [],
|
||||
'warnings': [],
|
||||
'suggestions': [],
|
||||
}
|
||||
|
||||
# Check if SKILL.md exists
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
if not skill_md.exists():
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append('SKILL.md not found')
|
||||
return results
|
||||
|
||||
# Read SKILL.md
|
||||
content = skill_md.read_text(encoding='utf-8')
|
||||
|
||||
# Parse frontmatter
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
if not frontmatter:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append('No YAML frontmatter found')
|
||||
return results
|
||||
|
||||
# Check required fields
|
||||
if 'name' not in frontmatter:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append('Missing "name" field in frontmatter')
|
||||
else:
|
||||
name = frontmatter['name']
|
||||
name_issues = check_name_convention(name)
|
||||
if name_issues:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].extend(name_issues)
|
||||
|
||||
if 'description' not in frontmatter:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append('Missing "description" field in frontmatter')
|
||||
else:
|
||||
description = frontmatter['description']
|
||||
desc_issues = check_description(description)
|
||||
if desc_issues:
|
||||
for issue in desc_issues:
|
||||
if 'angle brackets' in issue or 'characters' in issue:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append(issue)
|
||||
else:
|
||||
results['warnings'].append(issue)
|
||||
|
||||
# Check for second-person usage
|
||||
second_person = check_second_person_usage(body)
|
||||
if second_person:
|
||||
results['warnings'].append(f"Found {len(second_person)} instances of second-person usage")
|
||||
results['second_person_violations'] = second_person[:5] # First 5
|
||||
|
||||
# Check for emoji usage (critical issue)
|
||||
emoji_violations = check_emoji_usage(content)
|
||||
if emoji_violations:
|
||||
results['valid'] = False
|
||||
results['critical_issues'].append(f"Found {len(emoji_violations)} emoji characters (must use ASCII alternatives)")
|
||||
results['emoji_violations'] = emoji_violations[:5] # First 5
|
||||
|
||||
# Check resource directories
|
||||
resources = {
|
||||
'scripts': skill_path / 'scripts',
|
||||
'references': skill_path / 'references',
|
||||
'assets': skill_path / 'assets',
|
||||
}
|
||||
|
||||
mentioned_resources = {}
|
||||
for resource_type, resource_path in resources.items():
|
||||
pattern = f'{resource_type}/'
|
||||
if pattern in body:
|
||||
mentioned_resources[resource_type] = resource_path.exists()
|
||||
|
||||
for resource_type, exists in mentioned_resources.items():
|
||||
if not exists:
|
||||
results['suggestions'].append(
|
||||
f"Skill mentions {resource_type}/ but directory doesn't exist"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Analyze skill structure')
|
||||
parser.add_argument('--skill', required=True, help='Path to skill directory')
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_path = Path(args.skill)
|
||||
|
||||
if not skill_path.exists():
|
||||
print(f"Error: Skill directory not found: {skill_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Analyzing skill: {skill_path}")
|
||||
print("=" * 60)
|
||||
|
||||
results = analyze_skill(skill_path)
|
||||
|
||||
# Print results
|
||||
if results['valid']:
|
||||
print("[PASS] Skill structure is valid!")
|
||||
else:
|
||||
print("[FAIL] Skill structure has critical issues")
|
||||
|
||||
print()
|
||||
|
||||
if results['critical_issues']:
|
||||
print("CRITICAL ISSUES:")
|
||||
for issue in results['critical_issues']:
|
||||
print(f" [ERROR] {issue}")
|
||||
print()
|
||||
|
||||
if results['warnings']:
|
||||
print("WARNINGS:")
|
||||
for warning in results['warnings']:
|
||||
print(f" [WARN] {warning}")
|
||||
print()
|
||||
|
||||
if 'second_person_violations' in results:
|
||||
print("SECOND-PERSON USAGE (first 5):")
|
||||
for line_num, line in results['second_person_violations']:
|
||||
print(f" Line {line_num}: {line}")
|
||||
print()
|
||||
|
||||
if 'emoji_violations' in results:
|
||||
print("EMOJI USAGE DETECTED (first 5):")
|
||||
print(" Emojis must be replaced with ASCII alternatives:")
|
||||
print(" [OK]/[PASS], [ERROR]/[FAIL], [WARN], [TIP], [INFO], [X]")
|
||||
for line_num, line in results['emoji_violations']:
|
||||
print(f" Line {line_num}: {line}")
|
||||
print()
|
||||
|
||||
if results['suggestions']:
|
||||
print("SUGGESTIONS:")
|
||||
for suggestion in results['suggestions']:
|
||||
print(f" [TIP] {suggestion}")
|
||||
print()
|
||||
|
||||
sys.exit(0 if results['valid'] else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user