#!/usr/bin/env python3
"""
Comprehensive Math Curriculum Audit
Validates curriculum structure, topic coverage, and lesson correctness.
"""
import os
import re
import sys
from pathlib import Path
from typing import List, Dict, Tuple, Any
BASE = os.path.expanduser("~/Home_School/2nd_Grade/Math")
# Define the expected curriculum structure
CURRICULUM = {
"Phase 1: Number Sense & Operations": {
"weeks": [1, 2, 3, 4, 5, 6],
"topics": {
1: "Counting to 100",
2: "Place Value (Tens & Ones)",
3: "Comparing Numbers",
4: "Addition Facts to 20",
5: "Subtraction Facts",
6: "Phase 1 Review",
},
"skills": ["counting", "place value", "comparing", "addition", "subtraction"]
},
"Phase 2: Addition & Subtraction": {
"weeks": [7, 8, 9, 10, 11, 12, 13, 14],
"topics": {
7: "Addition Strategies",
8: "Subtraction Strategies",
9: "Add to 100",
10: "Subtract from 100",
11: "Missing Addends",
12: "Add/Sub Word Problems",
13: "Phase 2 Review",
14: "Word Problems Checkpoint",
},
"skills": ["addition", "subtraction", "word problems", "strategies"]
},
"Phase 3: Measurement, Time & Money": {
"weeks": [15, 16, 17, 18, 19, 20],
"topics": {
15: "Length - Inches & Centimeters",
16: "Telling Time",
17: "Introduction to Fractions",
18: "Money & Fractions",
19: "Money: Adding & Change",
20: "Data: Picture & Bar Graphs",
},
"skills": ["measurement", "time", "money", "fractions", "data"]
},
"Phase 4: Geometry & Fractions": {
"weeks": [21, 22, 23, 24, 25, 26],
"topics": {
21: "2D Shapes & Attributes",
22: "3D Shapes",
23: "Lines, Angles & Symmetry",
24: "Area & Perimeter",
25: "Patterns & Rule Finding",
26: "Phase 4 Review",
},
"skills": ["geometry", "shapes", "area", "perimeter", "patterns"]
},
"Phase 5: Multiplication, Division & Patterns": {
"weeks": [27, 28, 29, 30, 31, 32],
"topics": {
27: "Equal Groups & Arrays",
28: "Repeated Addition to Multiplication",
29: "Multiplication Facts 0-5",
30: "Multiplication Facts 6-10",
31: "Division as Equal Sharing",
32: "Final Review & Assessment",
},
"skills": ["multiplication", "division", "arrays", "patterns"]
}
}
# Expected daily structure template
DAILY_TEMPLATE = {
"Monday": "Introduction/New Concept",
"Tuesday": "Practice/Application",
"Wednesday": "Deepening Understanding",
"Thursday": "Extension/Challenge",
"Friday": "Review/Assessment"
}
class CurriculumError:
def __init__(self, week: int, day: str, error_type: str, message: str):
self.week = week
self.day = day
self.error_type = error_type # "MISSING", "WRONG_TOPIC", "WRONG_PHASE", "STRUCTURE"
self.message = message
def __str__(self):
day_str = self.day if self.day != "N/A" else "Week"
return f"Week {self.week} {day_str}: [{self.error_type}] {self.message}"
class CurriculumAuditor:
def __init__(self):
self.errors: List[CurriculumError] = []
self.warnings: List[CurriculumError] = []
self.stats = {
"weeks_audited": 0,
"weeks_missing": 0,
"topics_validated": 0,
"days_validated": 0,
}
def log_error(self, week: int, day: str, error_type: str, message: str):
self.errors.append(CurriculumError(week, day, error_type, message))
def log_warning(self, week: int, day: str, error_type: str, message: str):
self.warnings.append(CurriculumError(week, day, error_type, message))
def get_phase_for_week(self, week_num: int) -> Tuple[str, Dict]:
"""Get the phase information for a given week."""
for phase_name, phase_info in CURRICULUM.items():
if week_num in phase_info["weeks"]:
return phase_name, phase_info
return "Unknown", {}
def extract_week_info(self, filepath: Path) -> Dict:
"""Extract week number, topic, and phase from a generator file."""
with open(filepath, 'r') as f:
content = f.read()
week_num = int(filepath.stem.replace('generate_week', ''))
# Extract topic from docstring
topic = ""
phase = ""
for line in content.split('\n')[:15]:
if 'Week' in line and ':' in line:
topic = re.sub(r'[^a-zA-Z0-9\s&\-]', '', line).strip()
if 'Phase' in line:
phase = line.strip()
# Extract day topics
days = {}
for day_name in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']:
pattern = rf'def {day_name}\(\):.*?""".*?"""'
match = re.search(pattern, content, re.DOTALL)
if match:
doc_content = match.group(0)
lines = doc_content.split('\n')
if len(lines) > 1:
day_topic = lines[1].strip().replace('"""', '').strip()
if day_topic and not day_topic.startswith('#'):
days[day_name.capitalize()] = day_topic[:80]
return {
"week_num": week_num,
"topic": topic,
"phase": phase,
"days": days,
"content": content
}
def validate_week_topic(self, week_num: int, actual_topic: str, expected_topic: str):
"""Validate that a week covers the expected topic."""
# Allow some flexibility in topic naming
expected_keywords = expected_topic.lower().split()
actual_lower = actual_topic.lower()
# Check if key words match
matching_keywords = sum(1 for kw in expected_keywords if kw in actual_lower)
if matching_keywords >= len(expected_keywords) * 0.6:
return True, "Topic matches"
elif actual_topic:
return False, f"Expected '{expected_topic}', got '{actual_topic[:50]}'"
else:
return False, "No topic found"
def validate_phase_order(self, week_num: int, actual_phase: str, expected_phase: str):
"""Validate that a week is in the correct phase."""
if not actual_phase:
# Some weeks might not have explicit phase declarations - warn but don't error
return True, "No phase declaration (optional)"
# Check if expected phase name appears in actual phase
# Also allow checkpoint/review variations
if expected_phase.lower() in actual_phase.lower():
return True, "Phase matches"
# Allow checkpoint/review variations
checkpoint_keywords = ["checkpoint", "review", "assessment", "bridge to", "finale"]
if any(kw in actual_phase.lower() for kw in checkpoint_keywords):
return True, "Phase checkpoint/review accepted"
return False, f"Expected '{expected_phase}', got '{actual_phase[:50]}'"
def validate_daily_structure(self, week_num: int, days: Dict):
"""Validate that all 5 days are present with proper structure."""
required_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
missing_days = []
for day in required_days:
if day not in days:
missing_days.append(day)
self.log_error(week_num, day, "MISSING", "Day function not found")
# Check Friday is a review
if "Friday" in days:
friday_topic = days["Friday"].lower()
if not any(word in friday_topic for word in ["review", "assessment", "challenge", "finale"]):
self.log_warning(week_num, "Friday", "STRUCTURE",
f"Friday should be review/assessment, got: '{days['Friday'][:40]}'")
def validate_activity_count(self, week_num: int, content: str):
"""Validate that each day has appropriate number of activities."""
for day_name in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']:
pattern = rf'def {day_name}\(\):.*?(?=\ndef |\Z)'
match = re.search(pattern, content, re.DOTALL)
if match:
day_content = match.group(0)
activity_count = len(re.findall(r's\d+\s*=', day_content))
if activity_count < 4:
self.log_warning(week_num, day_name.capitalize(), "STRUCTURE",
f"Only {activity_count} activities (too few)")
elif activity_count > 8:
self.log_warning(week_num, day_name.capitalize(), "STRUCTURE",
f"{activity_count} activities (too many, max 6 recommended)")
def validate_math_correctness(self, week_num: int, content: str):
"""Validate mathematical correctness in the week."""
# Skip-counting validation
skip_sequences = re.findall(r'class="skip-num">(\d+)', content)
if skip_sequences:
nums = [int(x) for x in skip_sequences]
if len(nums) >= 4:
# Check for count-by-10 sequences
if all(n % 10 == 0 for n in nums[:4]):
for i in range(min(3, len(nums)-1)):
diff = nums[i+1] - nums[i]
if diff != 0 and abs(diff) not in [10, 20, 30, 40, 50, 60, 70, 80, 90]:
self.log_error(week_num, "N/A", "CORRECTNESS",
f"Count-by-10s error: jump from {nums[i]} to {nums[i+1]}")
# Check for count-by-5 sequences
if skip_sequences:
nums = [int(x) for x in skip_sequences]
if len(nums) >= 4 and all(n % 5 == 0 for n in nums) and not all(n % 10 == 0 for n in nums):
for i in range(min(3, len(nums)-1)):
diff = nums[i+1] - nums[i]
if diff != 0 and abs(diff) not in [5, 10, 15, 20]:
self.log_error(week_num, "N/A", "CORRECTNESS",
f"Count-by-5s error: jump from {nums[i]} to {nums[i+1]}")
def audit_week(self, filepath: Path):
"""Audit a single week file."""
try:
info = self.extract_week_info(filepath)
week_num = info["week_num"]
# Get expected phase and topic
expected_phase_name, expected_phase_info = self.get_phase_for_week(week_num)
expected_topic = expected_phase_info.get("topics", {}).get(week_num, "Unknown")
# Validate topic
topic_ok, topic_msg = self.validate_week_topic(week_num, info["topic"], expected_topic)
if topic_ok:
self.stats["topics_validated"] += 1
else:
self.log_error(week_num, "N/A", "WRONG_TOPIC", topic_msg)
# Validate phase
if expected_phase_name != "Unknown":
phase_ok, phase_msg = self.validate_phase_order(week_num, info["phase"], expected_phase_name)
if not phase_ok:
self.log_error(week_num, "N/A", "WRONG_PHASE", phase_msg)
# Validate daily structure
self.validate_daily_structure(week_num, info["days"])
self.stats["days_validated"] += len(info["days"])
# Validate activity counts
self.validate_activity_count(week_num, info["content"])
# Validate math correctness
self.validate_math_correctness(week_num, info["content"])
self.stats["weeks_audited"] += 1
except Exception as e:
self.log_error(week_num, "N/A", "ERROR", f"Failed to parse: {e}")
def audit_missing_weeks(self, week_files: List[Path]):
"""Check for missing week files."""
existing_weeks = {int(wf.stem.replace('generate_week', '')) for wf in week_files}
for phase_name, phase_info in CURRICULUM.items():
for week_num in phase_info["weeks"]:
if week_num not in existing_weeks:
self.log_error(week_num, "N/A", "MISSING",
f"Missing week file: generate_week{week_num}.py ({phase_name})")
self.stats["weeks_missing"] += 1
def audit_curriculum_flow(self, week_files: List[Path]):
"""Validate that curriculum flows logically through phases."""
existing_weeks = sorted([int(wf.stem.replace('generate_week', '')) for wf in week_files])
# Check for gaps in week numbers
for i in range(1, 33): # 32 weeks total
if i not in existing_weeks:
# Already logged in missing weeks check
pass
# Check review weeks are at phase boundaries
if i in [6, 13, 14, 20, 26, 32]: # Review/assessment weeks
# These should have review topics
pass # Will be checked in individual week audit
def print_report(self):
"""Print comprehensive audit report."""
print(f"\n{'='*80}")
print("CURRICULUM AUDIT REPORT")
print(f"{'='*80}\n")
print("STATISTICS:")
print("-" * 40)
print(f" Weeks audited: {self.stats['weeks_audited']}")
print(f" Weeks missing: {self.stats['weeks_missing']}")
print(f" Topics validated: {self.stats['topics_validated']}")
print(f" Days validated: {self.stats['days_validated']}")
print()
# Print phase coverage
print("PHASE COVERAGE:")
print("-" * 40)
for phase_name, phase_info in CURRICULUM.items():
existing_weeks = [w for w in phase_info["weeks"] if
any(int(f.stem.replace('generate_week', '')) == w
for f in Path(BASE).glob("generate_week*.py"))]
status = "✓" if len(existing_weeks) == len(phase_info["weeks"]) else "✗"
print(f" {status} {phase_name}")
print(f" Weeks {phase_info['weeks'][0]}-{phase_info['weeks'][-1]}: "
f"{len(existing_weeks)}/{len(phase_info['weeks'])} files present")
print()
# Print errors
if self.errors:
print(f"ERRORS ({len(self.errors)}):")
print("-" * 40)
# Group by type
by_type = {}
for error in self.errors:
if error.error_type not in by_type:
by_type[error.error_type] = []
by_type[error.error_type].append(error)
for error_type, errors in by_type.items():
print(f"\n {error_type}:")
for error in errors[:10]: # Limit output
print(f" ✗ {error}")
if len(errors) > 10:
print(f" ... and {len(errors) - 10} more")
print()
# Print warnings
if self.warnings:
print(f"WARNINGS ({len(self.warnings)}):")
print("-" * 40)
by_type = {}
for warning in self.warnings:
if warning.error_type not in by_type:
by_type[warning.error_type] = []
by_type[warning.error_type].append(warning)
for error_type, warnings in by_type.items():
print(f"\n {error_type}:")
for warning in warnings[:10]:
print(f" ⚠ {warning}")
if len(warnings) > 10:
print(f" ... and {len(warnings) - 10} more")
print()
if not self.errors and not self.warnings:
print("✓ Curriculum audit passed! All weeks are on track.")
print(f"\n{'='*80}")
print("WHAT THIS AUDIT CHECKS:")
print("-" * 40)
print(" • All 32 weeks present (6 phases)")
print(" • Each week covers expected topic")
print(" • Phase declarations are correct")
print(" • All 5 days present per week")
print(" • Activity counts (4-8 per day)")
print(" • Math correctness (skip-counting, etc.)")
print(f"\n{'='*80}")
return len(self.errors) == 0
def main():
auditor = CurriculumAuditor()
print(f"\n{'='*80}")
print("COMPREHENSIVE CURRICULUM AUDIT")
print(f"{'='*80}\n")
# Get all week files
week_files = sorted(Path(BASE).glob("generate_week*.py"))
print(f"Found {len(week_files)} week files...\n")
# Check for missing weeks
auditor.audit_missing_weeks(week_files)
# Audit each week
for filepath in week_files:
week_num = int(filepath.stem.replace('generate_week', ''))
print(f"Auditing {filepath.name}... ", end="")
auditor.audit_week(filepath)
print("✓")
# Print report
success = auditor.print_report()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()