#!/usr/bin/env python3
"""
Curriculum Week Validator
Run BEFORE generating PDFs to catch data errors early.
Usage:
python3 validate_week.py generate_week9.py
"""
import sys
import re
import os
INAPPROPRIATE_WORDS = {
"undress", "dress", "naked", "bare", "strip", "stripped",
"bath", "shower", "toilet", "potty",
}
errors = []
warnings = []
def error(msg):
errors.append(msg)
print(f" ā {msg}")
def warn(msg):
warnings.append(msg)
print(f" ā ļø {msg}")
def ok(msg):
print(f" ā
{msg}")
def check_words(words, topic=""):
if not isinstance(words, (list, tuple)):
error(f"Words ({topic}): not a list/tuple ā {type(words)}")
return []
word_names = []
for item in words:
if isinstance(item, tuple):
word = item[0]
elif isinstance(item, str):
word = item
else:
error(f"Words ({topic}): invalid item ā {item}")
continue
word_names.append(word)
if len(word) < 6:
error(f"Words: '{word}' is only {len(word)} letters ā minimum is 6")
if word.lower() in INAPPROPRIATE_WORDS:
error(f"Words: '{word}' is inappropriate for 2nd grade")
seen = set()
for w in word_names:
if w.lower() in seen:
error(f"Words: duplicate ā '{w}'")
seen.add(w.lower())
if len(word_names) != 12:
warn(f"Words: expected 12 words, found {len(word_names)}")
return word_names
def check_story(story, words, topic=""):
story_lower = story.lower()
missing = [w for w in words if w.lower() not in story_lower]
if missing:
error(f"Story ({topic}): missing words: {', '.join(missing)}")
else:
ok(f"Story ({topic}): all {len(words)} words present")
# Grammar checks
patterns = {
r'\bunsafe\s+\w+\b': "unsafe used as verb? (should be adjective)",
r'\bdishonest\s+(?:place|thing|box|room|house|building|car|park|garden|tree|path|door|wall)\b': "dishonest used for non-person?",
r'\bremaken\b': "remaken ā should be 'remade'",
r'\bundress': "undress ā inappropriate for 2nd grade",
}
for pattern, msg in patterns.items():
if re.search(pattern, story, re.IGNORECASE):
error(f"Story ({topic}): {msg}")
def check_fill_data(data, topic=""):
if not isinstance(data, (list, tuple)):
error(f"Fill ({topic}): not a list/tuple")
return
for i, item in enumerate(data):
if isinstance(item, tuple):
if len(item) != 2:
error(f"Fill ({topic}) item {i}: expected (sentence, answer), got {len(item)} items")
elif "__" not in item[0]:
warn(f"Fill ({topic}) item {i}: sentence has no blank markers (__)")
else:
error(f"Fill ({topic}) item {i}: expected (sentence, answer) tuple, got {type(item).__name__}")
def check_tuple_list(data, expected_len, name, topic=""):
if not isinstance(data, (list, tuple)):
error(f"{name} ({topic}): not a list/tuple")
return
for i, item in enumerate(data):
if isinstance(item, tuple):
if len(item) != expected_len:
error(f"{name} ({topic}) item {i}: expected {expected_len} items, got {len(item)}")
else:
error(f"{name} ({topic}) item {i}: expected tuple, got {type(item).__name__}")
def main():
if len(sys.argv) < 2:
print("Usage: python3 validate_week.py <generate_weekX.py>")
sys.exit(1)
filepath = sys.argv[1]
if not os.path.exists(filepath):
print(f"File not found: {filepath}")
sys.exit(1)
with open(filepath, 'r') as f:
content = f.read()
week_match = re.search(r'week(\d+)', filepath, re.IGNORECASE)
week_num = f"Week {week_match.group(1)}" if week_match else "Unknown"
print(f"š Validating: {os.path.basename(filepath)}")
print(f" {week_num}")
print("=" * 60)
# ---- Core Fields ----
print("\nš¦ Core Fields:")
core_fields = [
("WEEK", "Week directory path"),
("WORDS", "Word list"),
("LEARNING_TEXT", "Learning box text"),
]
for field, desc in core_fields:
if f"{field} = " in content or f"{field}=" in content:
ok(f"{field} ā {desc}")
else:
error(f"Missing: {field} ā {desc}")
# ---- Day Story Fields ----
print("\nš Day Stories:")
story_fields = [
("MONDAY_STORY", "Monday"),
("TUESDAY_READING", "Tuesday"),
("WEDNESDAY_READING", "Wednesday"),
("THURSDAY_STORY", "Thursday"),
("FRIDAY_REVIEW_STORY", "Friday"),
]
for field, day in story_fields:
if field in content:
ok(f"{field} ā {day} story")
else:
error(f"Missing: {field} ā {day} story")
# ---- Activity Data ----
print("\nš Activity Data:")
# Monday fields ā grammar practice varies (strings for sentence fixing, tuples for prefixes)
monday_fields = [
("MONDAY_GRAMMAR_PRACTICE", "Monday grammar practice"),
("MONDAY_FILL_DATA", "Monday fill-in-blank"),
("MONDAY_OPPOSITE_WORDS", "Monday opposites"),
("MONDAY_SENTENCE_WORDS", "Monday sentence words"),
]
for field, name in monday_fields:
if field in content:
ok(f"{field} ā {name}")
else:
error(f"Missing: {field} ā {name}")
tuesday_fields = [
("TUESDAY_LEARNING", "Tuesday learning"),
("TUESDAY_COMP_QUESTIONS", "Tuesday comp questions"),
("TUESDAY_REMEMBER_1", "Tuesday remember 1"),
("TUESDAY_REMEMBER_2", "Tuesday remember 2"),
("TUESDAY_SWAP_DATA", "Tuesday word swap"),
("TUESDAY_BUILDER_PROMPTS", "Tuesday sentence builder"),
]
for field, name in tuesday_fields:
if field in content:
ok(f"{field} ā {name}")
else:
error(f"Missing: {field} ā {name}")
wednesday_fields = [
("WEDNESDAY_LEARNING", "Wednesday learning"),
("WEDNESDAY_VOCAB_Q", "Wednesday vocab question"),
("WEDNESDAY_SYN_ANT", "Wednesday synonyms/antonyms"),
("WEDNESDAY_SORT_DATA", "Wednesday sentence sort"),
("WEDNESDAY_CORRECT", "Wednesday correct sentences"),
("WEDNESDAY_FILL", "Wednesday fill-in-blank"),
("WEDNESDAY_CONTEXT", "Wednesday context clues"),
]
for field, name in wednesday_fields:
if field in content:
ok(f"{field} ā {name}")
else:
error(f"Missing: {field} ā {name}")
thursday_fields = [
("THURSDAY_LEARNING", "Thursday learning"),
("THURSDAY_COMP_QUESTIONS", "Thursday comp questions"),
("THURSDAY_SORT_DATA", "Thursday sentence sort"),
("THURSDAY_DETECTIVE", "Thursday spelling detective"),
("THURSDAY_SWAP_DATA", "Thursday word swap"),
("THURSDAY_PARA_PROMPTS", "Thursday paragraph builder"),
]
for field, name in thursday_fields:
if field in content:
ok(f"{field} ā {name}")
else:
error(f"Missing: {field} ā {name}")
friday_fields = [
("FRIDAY_STORY_QUESTIONS", "Friday story questions"),
("FRIDAY_OPPOSITE_WORDS", "Friday opposite pairs"),
("FRIDAY_BUILDER_WORDS", "Friday sentence builder"),
("FRIDAY_JOURNAL_WORDS", "Friday word journal"),
]
for field, name in friday_fields:
if field in content:
ok(f"{field} ā {name}")
else:
error(f"Missing: {field} ā {name}")
# ---- Teacher Guide Data ----
print("\nšØāš« Teacher Guide Data:")
# Teacher guide ā check core fields, some days skip separate answer keys
teacher_fields = [
("TEACHER_MONDAY_GRAMMAR_EXPLANATION", "Monday grammar explanation"),
("TEACHER_MONDAY_FILL_ANSWERS", "Monday fill answers"),
("TEACHER_MONDAY_OPPOSITE_ANSWERS", "Monday opposite answers"),
("TEACHER_MONDAY_SENTENCE_WORDS", "Monday sentence words"),
("TEACHER_TUESDAY_SCRAMBLE_ANSWERS", "Tuesday scramble answers"),
("TEACHER_TUESDAY_COMP_ANSWERS", "Tuesday comp answers"),
("TEACHER_THURSDAY_DETECTIVE_ANSWERS", "Thursday detective answers"),
]
for field, name in teacher_fields:
if field in content:
ok(f"Teacher: {name}")
else:
warn(f"Missing teacher data: {field} ({name})")
# Friday comp answers ā optional (may be inline with questions)
if "TEACHER_FRIDAY_COMP_ANSWERS" in content or "FRIDAY_COMP_ANSWERS" in content:
ok("Teacher: Friday comp answers")
else:
warn("No separate Friday comp answers (may be inline)")
# ---- Word List ----
print("\nš Word List:")
try:
words_match = re.search(r'WORDS\s*=\s*(\[.*?\])', content, re.DOTALL)
if words_match:
words = eval(words_match.group(1))
word_names = check_words(words, "Main")
# Check story coverage
for day, key in [("Monday", "MONDAY_STORY"), ("Tuesday", "TUESDAY_READING"),
("Wednesday", "WEDNESDAY_READING"), ("Thursday", "THURSDAY_STORY"),
("Friday", "FRIDAY_REVIEW_STORY")]:
story_match = re.search(rf'{key}\s*=\s*\((.*?)\)', content, re.DOTALL)
if story_match:
story = story_match.group(1).replace('\\n', '\n')
check_story(story, word_names, day)
except Exception as e:
warn(f"Could not parse word list: {e}")
# ---- Main Block ----
print("\nš Main Block:")
# Check for main execution ā some weeks use if __name__, some just call at top level
has_guarded = "if __name__" in content or "if '__name__'" in content
has_direct_calls = all(f"generate_{day}(" in content for day in ["monday", "tuesday", "wednesday", "thursday", "friday"])
if has_guarded or has_direct_calls:
ok(f"Execution: {'guarded block' if has_guarded else 'direct calls at top level'}")
else:
error("No generation calls found")
for gen in ["generate_monday", "generate_tuesday", "generate_wednesday",
"generate_thursday", "generate_friday"]:
if f"{gen}(" in content:
ok(f"Call to {gen}")
else:
error(f"Missing call to {gen}")
if "generate_teacher_guide" in content or "generate_teacher" in content:
ok("Teacher guide generation")
else:
warn("No teacher guide generation")
# ---- Fill Data Format ----
print("\nš Fill Data Format:")
fill_fields = [
("MONDAY_FILL_DATA", "Monday fill data"),
]
for field, name in fill_fields:
if field in content:
try:
match = re.search(rf'{field}\s*=\s*(\[.*?\])', content, re.DOTALL)
if match:
data = eval(match.group(1))
check_fill_data(data, name)
except Exception as e:
error(f"Could not parse {name}: {e}")
# ---- Summary ----
print("\n" + "=" * 60)
print(f"\nš Validation Results:")
print(f" Errors: {len(errors)}")
print(f" Warnings: {len(warnings)}")
if errors:
print(f"\nā FAILED ā {len(errors)} error(s) to fix:")
for e in errors:
print(f" ⢠{e}")
sys.exit(1)
elif warnings:
print(f"\nā ļø PASSED with {len(warnings)} warning(s):")
for w in warnings:
print(f" ⢠{w}")
else:
print(f"\nš ALL CHECKS PASSED! Safe to generate PDFs.")
if __name__ == "__main__":
main()