#!/usr/bin/env python3
"""Audit all ELA week generators for quality issues.
Checks:
1. Word swap challenges where swap words = original words (no cognitive gain)
2. Story quality indicators (forced word cramming, nonsensical sentences)
3. Duplicate if __name__ blocks (syntax error risk)
4. Indentation errors
5. Swap data where grammatical roles don't match
6. Story text that's too short or too long
"""
import ast
import os
import re
import sys
from pathlib import Path
ELA_ROOT = Path(os.path.expanduser("~/Home_School/2nd_Grade/English_Language_Arts"))
ISSUES = {} # file -> list of (line, severity, message)
def scan_file(filepath):
"""Scan a single generator file for quality issues."""
issues = []
rel = filepath.relative_to(ELA_ROOT)
try:
source = filepath.read_text()
except Exception as e:
issues.append((0, "ERROR", f"Cannot read file: {e}"))
return issues
# Check 1: Duplicate if __name__ blocks
name_main_count = source.count('if __name__ == "__main__":')
if name_main_count > 1:
issues.append((0, "HIGH", f"Duplicate if __name__ == '__main__' blocks ({name_main_count} found)"))
# Check 2: Try to parse as Python to catch syntax errors
try:
tree = ast.parse(source)
except SyntaxError as e:
issues.append((e.lineno or 0, "HIGH", f"Syntax error: {e.msg} (line {e.lineno})"))
# Can't do deeper analysis without valid AST
return issues
# Check 3: Story text analysis
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
text = node.value
# Look for story-like text (long strings with HTML)
if len(text) > 100 and '<strong>' in text:
# Check for forced word cramming indicators
words_in_story = re.findall(r'<strong>(\w+)</strong>', text)
plain_words = re.findall(r'\b(\w+)\b', text.replace('<', '').replace('>', ''))
# Story too short (< 50 words)
if len(plain_words) < 50 and len(words_in_story) > 3:
issues.append((node.lineno, "MEDIUM", f"Story at line {node.lineno} has only {len(plain_words)} words - may be too brief"))
# Story too long (> 400 words)
if len(plain_words) > 400:
issues.append((node.lineno, "LOW", f"Story at line {node.lineno} has {len(plain_words)} words - may be too long for grade level"))
# Check for nonsensical word combinations
# Look for "insect" with wrong articles ("a insect" instead of "an insect")
if re.search(r'\b[aA]\s+insect\b', text):
issues.append((node.lineno, "MEDIUM", f"Story at line {node.lineno}: 'a insect' should be 'an insect'"))
# Check for "octopus" with wrong articles
if re.search(r'\b[aA]\s+octopus\b', text):
issues.append((node.lineno, "MEDIUM", f"Story at line {node.lineno}: 'a octopus' should be 'an octopus'"))
# Check for forced/nonsensical combinations
if re.search(r'\b(bubble|insect|helmet|kitten)\s+(bubble|insect|helmet|kitten)\b', text):
issues.append((node.lineno, "MEDIUM", f"Story at line {node.lineno}: possible repetitive word usage"))
# Check 4: Word swap challenge analysis
for node in ast.walk(tree):
if isinstance(node, ast.List):
# Look for swap_data lists
if hasattr(node, 'ctx'):
# Find assignment target
for parent in ast.walk(tree):
if isinstance(parent, ast.Assign) and isinstance(parent.targets[0], ast.Name) and 'swap' in parent.targets[0].id.lower():
if parent.value == node or (isinstance(parent.value, ast.ListComp) or isinstance(parent.value, ast.List)):
# Check swap data
for item in node.elts:
if isinstance(item, ast.Tuple):
# Try to evaluate the swap data
try:
if len(item.elts) >= 3:
# First element should be sentence, others should be tuples
sentence_node = item.elts[0]
if isinstance(sentence_node, ast.Constant) and isinstance(sentence_node.value, str):
sentence = sentence_node.value
# Extract words from sentence
sentence_words = set(re.findall(r'\b(\w+)\b', sentence.replace('<', '').replace('>', '')))
# Check swap words
for swap_tuple in item.elts[1:]:
if isinstance(swap_tuple, ast.Tuple) and len(swap_tuple.elts) >= 2:
orig_word = swap_tuple.elts[0].value if isinstance(swap_tuple.elts[0], ast.Constant) else None
new_word = swap_tuple.elts[1].value if isinstance(swap_tuple.elts[1], ast.Constant) else None
if orig_word and new_word:
# Check if swap words are the same as original
if orig_word.lower() == new_word.lower():
issues.append((item.lineno, "HIGH", f"Word swap uses same word: '{orig_word}' -> '{new_word}' (no change)"))
elif orig_word.lower() not in sentence_words:
issues.append((item.lineno, "MEDIUM", f"Word swap: '{orig_word}' not found in sentence"))
elif new_word.lower() in sentence_words:
issues.append((item.lineno, "MEDIUM", f"Word swap: '{new_word}' already appears in sentence"))
except (AttributeError, IndexError, TypeError):
pass
return issues
def main():
print("Auditing ELA generators...\n")
# Find all week generators
generators = []
for week_dir in sorted(ELA_ROOT.glob("Week_*")):
gen_file = week_dir / "generate_week*.py"
for f in gen_file.glob("*.py"):
if "generate" in f.name.lower():
generators.append(f)
# Also check shared day templates
shared = [
"tuesday.py", "wednesday.py", "thursday.py", "friday.py", "monday.py",
"tuesday_wp.py", "wednesday_wp.py", "thursday_wp.py", "friday_wp.py", "monday_wp.py"
]
for s in shared:
f = ELA_ROOT / s
if f.exists():
generators.append(f)
print(f"Found {len(generators)} files to audit\n")
print("=" * 80)
for filepath in generators:
file_issues = scan_file(filepath)
if file_issues:
rel = filepath.relative_to(ELA_ROOT)
print(f"\n{rel}:")
for line, severity, msg in file_issues:
marker = "🔴" if severity == "HIGH" else "🟡" if severity == "MEDIUM" else "🔵"
print(f" {marker} Line {line}: {msg}")
print("\n" + "=" * 80)
print("Audit complete.")
if __name__ == "__main__":
main()