#!/usr/bin/env python3
"""Deep audit of all 20 ELA weeks - handles all edge cases."""
import ast
import os
import re
import sys
import subprocess
from collections import Counter
from typing import List, Dict, Tuple, Any, Optional
BASE = os.path.expanduser("~/Home_School/2nd_Grade/English_Language_Arts")
PAGE_TARGETS = {"Monday": 4, "Tuesday": 3, "Wednesday": 4, "Thursday": 4, "Friday": 3, "Teacher_Guide": 6}
def extract_words(generator_path: str) -> List[Tuple]:
"""Extract WORDS or words list from generator."""
with open(generator_path) as f:
source = f.read()
# Try AST extraction
try:
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in ("WORDS", "words"):
if isinstance(node.value, (ast.List, ast.Tuple)):
words = []
for elt in node.value.elts:
if isinstance(elt, ast.Tuple):
vals = []
for val in elt.elts:
if isinstance(val, ast.Constant):
vals.append(val.value)
if len(vals) >= 2:
words.append(tuple(vals))
return words
except:
pass
# Regex fallback - handle both WORDS and words
m = re.search(r'^(\w+?_?WORDS?)\s*=\s*\[(.*?)\]', source, re.DOTALL | re.MULTILINE)
if m:
words = []
for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\'](?:\s*,\s*["\']([^"\']*)["\'](?:\s*,\s*["\']([^"\']*)["\'])?)?\)', m.group(2)):
vals = [tm.group(i) for i in range(1, 5) if tm.group(i) is not None]
if vals:
words.append(tuple(vals))
return words
return []
def extract_word_names(words: List[Tuple]) -> List[str]:
return [w[0] for w in words]
def extract_story(source: str, day_key: str) -> str:
"""Extract story/reading text for a given day."""
keys = {
"Monday": [r'MONDAY_STORY\s*=\s*\((.*?)\)', r'MONDAY_STORY\s*=\s*"([^"]*)"'],
"Thursday": [r'THURSDAY_STORY\s*=\s*\((.*?)\)', r'THURSDAY_STORY\s*=\s*"([^"]*)"'],
"Friday": [r'FRIDAY_REVIEW_STORY\s*=\s*\((.*?)\)', r'FRIDAY_REVIEW_STORY\s*=\s*"([^"]*)"'],
"Tuesday": [r'TUESDAY_READING\s*=\s*\((.*?)\)', r'TUESDAY_READING\s*=\s*"([^"]*)"'],
"Wednesday": [r'WEDNESDAY_READING\s*=\s*\((.*?)\)', r'WEDNESDAY_READING\s*=\s*"([^"]*)"'],
}
for pat in keys.get(day_key, []):
m = re.search(pat, source, re.DOTALL)
if m:
text = m.group(1).replace('\\n', ' ').replace('"', '').strip()
return ' '.join(text.split())
return ""
def check_words_in_story(story: str, word_names: List[str]) -> Tuple[List[str], List[str]]:
"""Check which words appear in a story (case-insensitive)."""
if not story:
return [], word_names
found, missing = [], []
story_lower = story.lower()
for w in word_names:
if w.lower() in story_lower:
found.append(w)
else:
missing.append(w)
return found, missing
def extract_swap_data(source: str) -> List[Dict]:
"""Extract swap data from both Tuesday and Thursday.
Format: (original_sentence, (original_word1, replacement1), (original_word2, replacement2))
Returns list of dicts with 'original_words' and 'replacement_words'.
"""
swaps = []
for prefix in ["TUESDAY_SWAP_DATA", "THURSDAY_SWAP_DATA"]:
m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
block = m.group(1)
# Find each triple: (sentence, (w1, r1), (w2, r2))
pattern = r'\(\s*["\']([^"\']*)["\']\s*,\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)\s*,\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)'
for tm in re.finditer(pattern, block):
swaps.append({
"sentence": tm.group(1),
"original_word1": tm.group(2),
"replacement1": tm.group(3),
"original_word2": tm.group(4),
"replacement2": tm.group(5),
})
return swaps
def extract_fill_data(source: str) -> List[Tuple]:
fills = []
for prefix in ["MONDAY_FILL_DATA", "MONDAY_FILL", "WEDNESDAY_FILL", "THURSDAY_FILL", "FRIDAY_FILL"]:
m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
fills.append((tm.group(1), tm.group(2)))
return fills
def extract_sort_answers(source: str) -> List[str]:
"""Extract all words from sort answers."""
all_sorted = []
for prefix in ["TEACHER_MONDAY_SORT_ANSWERS", "TEACHER_TUESDAY_SORT_ANSWERS",
"TEACHER_THURSDAY_SORT_ANSWERS", "TEACHER_WEDNESDAY_SORT_ANSWERS"]:
m = re.search(rf'{prefix}\s*=\s*(\{{.*?\}})', source, re.DOTALL)
if m:
try:
data = ast.literal_eval(m.group(1))
for cat_words in data.values():
if isinstance(cat_words, list):
all_sorted.extend(cat_words)
except:
pass
return all_sorted
def extract_pattern_hunt(source: str) -> Tuple[List, List]:
"""Extract pattern hunt words and match answers."""
hunt_words = []
hunt_matches = []
for prefix in ["TUESDAY_PATTERN_HUNT_WORDS", "THURSDAY_PATTERN_HUNT_WORDS"]:
m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
hunt_words.append(tm.group(1))
for prefix in ["TEACHER_TUESDAY_PATTERN_HUNT", "TEACHER_THURSDAY_PATTERN_HUNT"]:
m = re.search(rf'{prefix}\s*=\s*(\{{.*?\}})', source, re.DOTALL)
if m:
try:
data = ast.literal_eval(m.group(1))
if "match" in data:
hunt_matches.extend(data["match"])
if "decoy" in data:
hunt_matches # decoys exist
except:
pass
return hunt_words, hunt_matches
def extract_correct_sentences(source: str) -> List[str]:
sentences = []
m = re.search(r'WEDNESDAY_CORRECT\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
sentences.append(tm.group(1))
return sentences
def extract_paragraph_label_data(source: str) -> Tuple[List, List]:
data, answers = [], []
m = re.search(r'WEDNESDAY_PARAGRAPH_LABEL_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
data.append(tm.group(1))
m = re.search(r'TEACHER_WEDNESDAY_PARAGRAPH_LABEL_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
answers.append((tm.group(1), tm.group(2)))
return data, answers
def extract_detective_answers(source: str) -> List[Tuple]:
answers = []
for prefix in ["TEACHER_WEDNESDAY_DETECTIVE_ANSWERS", "TEACHER_THURSDAY_DETECTIVE_ANSWERS"]:
m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
answers.append((tm.group(1), tm.group(2)))
return answers
def extract_opposite_words(source: str) -> Tuple[List, List]:
words, answers = [], []
m = re.search(r'FRIDAY_OPPOSITE_WORDS\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
words.append(tm.group(1))
m = re.search(r'TEACHER_FRIDAY_OPPOSITE_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
answers.append((tm.group(1), tm.group(2)))
return words, answers
def count_pdf_pages(pdf_path: str) -> int:
try:
result = subprocess.run(["pdfinfo", pdf_path], capture_output=True, text=True, timeout=5)
for line in result.stdout.split('\n'):
if line.startswith("Pages:"):
return int(line.split(":")[1].strip())
except:
pass
return 0
def audit_week(week_num: int) -> Dict:
issues = []
week_dir = os.path.join(BASE, f"Week_{week_num:02d}")
# Find generator
generator = None
for f in sorted(os.listdir(week_dir)):
if f.startswith("generate_week") and f.endswith(".py"):
if any(x in f for x in ["_backup", "_CORRUPTED", "_new", "_old"]):
continue
generator = os.path.join(week_dir, f)
break
if not generator:
return {"week": week_num, "status": "FAIL", "issues": ["No generator found"], "page_counts": {}, "word_count": 0}
result = {"week": week_num, "status": "PASS", "issues": [], "page_counts": {}, "word_count": 0}
source = open(generator).read()
# 1. Try running generator
try:
env = os.environ.copy()
env["PYTHONPATH"] = BASE
proc = subprocess.run(
["python3", os.path.basename(generator)],
cwd=week_dir, capture_output=True, text=True, timeout=120, env=env
)
if proc.returncode != 0:
err = proc.stderr.strip()[:300] if proc.stderr.strip() else proc.stdout.strip()[:300]
issues.append(f"Generator exit code {proc.returncode}: {err[:200]}")
except subprocess.TimeoutExpired:
issues.append("Generator timed out (>120s)")
except Exception as e:
issues.append(f"Generator exception: {e}")
# 2. Page counts (from existing PDFs)
day_dirs = {}
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Teacher_Guide"]:
day_subdir = os.path.join(week_dir, day)
if os.path.isdir(day_subdir):
pdfs = [f for f in os.listdir(day_subdir) if f.endswith('.pdf')]
if pdfs:
pages = count_pdf_pages(os.path.join(day_subdir, pdfs[0]))
day_dirs[day] = {"pdfs": len(pdfs), "pages": pages}
for day, info in day_dirs.items():
target = PAGE_TARGETS.get(day, 0)
result["page_counts"][day] = f"{info['pages']} pages (target: {target})"
if info["pages"] > target:
issues.append(f"{day}: {info['pages']} pages (target: {target}) - OVERFLOW")
elif info["pages"] < target and target > 0:
issues.append(f"{day}: {info['pages']} pages (target: {target}) - UNDER")
# 3. Extract words
words = extract_words(generator)
word_names = extract_word_names(words)
word_set = {w.lower() for w in word_names}
result["word_count"] = len(words)
if len(words) != 12:
issues.append(f"Expected 12 words, found {len(words)}")
# 4. Word coverage in stories
for day_key in ["Monday", "Thursday", "Friday", "Tuesday", "Wednesday"]:
story = extract_story(source, day_key)
if story and words:
found, missing = check_words_in_story(story, word_names)
if missing:
issues.append(f"{day_key} story missing words: {missing}")
# 5. Swap words validation
swaps = extract_swap_data(source)
for sw in swaps:
# Check original words are in word list (they should be - these are the words to swap OUT)
if sw["original_word1"].lower() not in word_set:
issues.append(f"Swap original word '{sw['original_word1']}' not in word list (sentence: {sw['sentence'][:40]}...)")
if sw["original_word2"].lower() not in word_set:
issues.append(f"Swap original word '{sw['original_word2']}' not in word list (sentence: {sw['sentence'][:40]}...)")
# Check replacement words are in word list
if sw["replacement1"].lower() not in word_set:
issues.append(f"Swap replacement '{sw['replacement1']}' not in word list")
if sw["replacement2"].lower() not in word_set:
issues.append(f"Swap replacement '{sw['replacement2']}' not in word list")
# Check for degenerate swaps (word replaced with itself)
if sw["original_word1"].lower() == sw["replacement1"].lower():
issues.append(f"Degenerate swap: '{sw['original_word1']}' → '{sw['replacement1']}' (same word)")
if sw["original_word2"].lower() == sw["replacement2"].lower():
issues.append(f"Degenerate swap: '{sw['original_word2']}' → '{sw['replacement2']}' (same word)")
# 6. Sort coverage
if words:
all_sorted = extract_sort_answers(source)
if all_sorted:
freq = Counter(w.lower() for w in all_sorted)
if len(all_sorted) != 12:
issues.append(f"Sort answers cover {len(all_sorted)} words, expected 12")
for w, c in freq.items():
if c != 1:
issues.append(f"Sort word '{w}' appears {c} times (expected 1)")
for w in word_names:
if w.lower() not in freq:
issues.append(f"Word '{w}' not covered in sort answers")
# 7. Pattern hunt has decoys
hunt_words, hunt_matches = extract_pattern_hunt(source)
if hunt_words and hunt_matches:
if len(hunt_words) == len(hunt_matches):
issues.append(f"Pattern hunt: all {len(hunt_matches)} words are targets (no decoys)")
# 8. Correct sentences need capitalization
correct_sents = extract_correct_sentences(source)
for sent in correct_sents:
if sent and sent[0].isupper():
issues.append(f"Correct sentence already capitalized: '{sent[:50]}'")
# 9. Paragraph label counts
pl_data, pl_answers = extract_paragraph_label_data(source)
if pl_data and pl_answers:
if len(pl_data) != len(pl_answers):
issues.append(f"Paragraph labels: {len(pl_data)} data items vs {len(pl_answers)} answers")
# 10. Data format mismatches (tuple repr rendering)
if 'TEACHER_TUESDAY_SCRAMBLE_ANSWERS' in source:
m = re.search(r'TEACHER_TUESDAY_SCRAMBLE_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
if m:
# Check that it's proper Python tuple format, not repr
if not re.search(r'\(\s*["\']', m.group(1)):
issues.append("Scramble answers may have tuple repr format issue")
# 11. Wednesday detective types - check for valid context clue types
detective_answers = extract_detective_answers(source)
valid_context_clue_types = {"Definition", "Example", "Contrast", "Description", "Restatement", "Inference"}
valid_verb_types = {"Action", "Linking", "Helping", "Auxiliary", "Transitive", "Intransitive"}
valid_noun_types = {"Compound", "Common", "Proper", "Collective", "Abstract", "Concrete", "Singular", "Plural"}
all_valid = valid_context_clue_types | valid_verb_types | valid_noun_types
for _, ans_type in detective_answers:
if ans_type.strip() and ans_type.strip() not in all_valid:
issues.append(f"Detective answer type '{ans_type}' is non-standard")
# 12. Friday opposite words in word list
opp_words, opp_answers = extract_opposite_words(source)
if opp_words and word_set:
for w in opp_words:
if w.lower() not in word_set:
issues.append(f"Friday opposite word '{w}' not in word list")
# 13. Fill answers in word list
fills = extract_fill_data(source)
if fills and word_set:
for sentence, answer in fills:
if answer.lower() not in word_set:
issues.append(f"Fill answer '{answer}' not in word list")
# 14. Check for duplicate swap data (Tuesday == Thursday)
tue_swaps = [s for s in swaps if any('TUESDAY' in source[:source.find(str(s["sentence"]))] for _ in [1])]
# Simplified: just check if same sentences appear in both TUESDAY_SWAP and THURSDAY_SWAP
tue_block = re.search(r'TUESDAY_SWAP_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
thu_block = re.search(r'THURSDAY_SWAP_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
if tue_block and thu_block:
tue_sents = re.findall(r'\(\s*"([^"]+)"', tue_block.group(1))
thu_sents = re.findall(r'\(\s*"([^"]+)"', thu_block.group(1))
common = set(tue_sents) & set(thu_sents)
if common:
issues.append(f"Swap data: {len(common)} identical sentence(s) in Tuesday AND Thursday")
result["issues"] = issues
result["status"] = "FAIL" if issues else "PASS"
return result
def main():
print("=" * 80)
print("ELA WEEK DEEP AUDIT - ALL 20 WEEKS")
print("=" * 80)
all_results = []
total_issues = 0
for week_num in range(1, 21):
result = audit_week(week_num)
all_results.append(result)
issues = result["issues"]
total_issues += len(issues)
print(f"\n{'─'*60}")
print(f"Week {week_num}: {result['status']} (words: {result['word_count']})")
print(f"{'─'*60}")
if issues:
for i, issue in enumerate(issues, 1):
print(f" {i}. {issue}")
pc = result.get("page_counts", {})
if pc:
print(f" Pages: {', '.join(f'{k}={v}' for k, v in pc.items())}")
print(f"\n{'='*80}")
print(f"AUDIT SUMMARY")
print(f"{'='*80}")
passed = sum(1 for r in all_results if r["status"] == "PASS")
failed = sum(1 for r in all_results if r["status"] == "FAIL")
print(f"\n PASS: {passed}/20")
print(f" FAIL: {failed}/20")
print(f" Total issues: {total_issues}")
# Issue categories
cat_counts = Counter()
issue_categories = {
"Generator Error": lambda x: "Generator" in x,
"Word Count": lambda x: "Expected 12 words" in x,
"Story Word Coverage": lambda x: "story missing words" in x,
"Swap Word Not in List": lambda x: "Swap" in x and "not in word list" in x,
"Degenerate Swap": lambda x: "Degenerate swap" in x,
"Swap Data Duplicate": lambda x: "identical sentence" in x,
"Sort Coverage": lambda x: "Sort" in x.lower() and ("expected 12" in x or "not covered" in x or "appears" in x),
"Pattern Hunt No Decoys": lambda x: "no decoys" in x,
"Already Capitalized": lambda x: "already capitalized" in x.lower(),
"Paragraph Label Mismatch": lambda x: "Paragraph labels" in x,
"Detective Type": lambda x: "Detective" in x,
"Opposite Not in List": lambda x: "opposite" in x.lower() and "not in word list" in x,
"Fill Answer Not in List": lambda x: "Fill answer" in x,
"Page Count Overflow": lambda x: "OVERFLOW" in x,
"Page Count Under": lambda x: "UNDER" in x,
}
for r in all_results:
for issue in r["issues"]:
matched = False
for cat, check_fn in issue_categories.items():
if check_fn(issue):
cat_counts[cat] += 1
matched = True
break
if not matched:
cat_counts["Other"] += 1
print(f"\n Issue breakdown:")
for cat, count in cat_counts.most_common():
print(f" {cat}: {count}")
print(f"\n Failed weeks detail:")
for r in all_results:
if r["status"] == "FAIL":
print(f" Week {r['week']}: {len(r['issues'])} issues")
for issue in r["issues"]:
print(f" - {issue}")
if __name__ == "__main__":
main()