feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from .quality_checker import QualityChecker
from . import content_gate

View File

@@ -0,0 +1,36 @@
import random
import logging
_logger = logging.getLogger(__name__)
class ContentSourceGate:
"""Determines review requirements based on content source.
- AI-generated, not IELTS-certified → mandatory full review
- IELTS-certified entity content → spot-check (random 10% sampling)
- Otherwise → auto-approve
"""
SPOT_CHECK_RATE = 0.10
@classmethod
def check(cls, resource):
if resource.ai_generated and not resource.ielts_certified:
return 'mandatory_review'
if hasattr(resource, 'ielts_certified') and resource.ielts_certified:
if random.random() < cls.SPOT_CHECK_RATE:
return 'spot_check'
return 'auto_approve'
return 'auto_approve'
@classmethod
def apply_gate(cls, resource):
result = cls.check(resource)
if result == 'mandatory_review':
resource.write({'review_status': 'pending', 'approved': False})
elif result == 'spot_check':
resource.write({'review_status': 'pending', 'approved': False})
else:
resource.write({'review_status': 'approved', 'approved': True})
return result

View File

@@ -0,0 +1,156 @@
import logging
import re
_logger = logging.getLogger(__name__)
class QualityChecker:
"""Content quality gate: readability, CEFR alignment, grammar."""
CEFR_FLESCH_RANGES = {
'pre_a1': (90, 100), 'a1': (80, 90), 'a2': (70, 80),
'b1': (60, 70), 'b2': (50, 60), 'c1': (30, 50), 'c2': (0, 30),
}
@staticmethod
def check_readability(text):
"""Compute Flesch-Kincaid readability metrics."""
try:
import textstat
score = textstat.flesch_reading_ease(text)
grade = textstat.flesch_kincaid_grade(text)
cefr = 'b1'
for level, (low, high) in QualityChecker.CEFR_FLESCH_RANGES.items():
if low <= score < high:
cefr = level
break
if score < 0:
cefr = 'c2'
elif score >= 100:
cefr = 'pre_a1'
return {
'flesch_score': round(score, 1),
'grade_level': round(grade, 1),
'cefr_estimate': cefr,
'word_count': textstat.lexicon_count(text),
'sentence_count': textstat.sentence_count(text),
'avg_sentence_length': round(textstat.avg_sentence_length(text), 1),
}
except ImportError:
_logger.warning("textstat not installed, using fallback readability check")
return QualityChecker._fallback_readability(text)
@staticmethod
def _fallback_readability(text):
words = text.split()
sentences = re.split(r'[.!?]+', text)
sentences = [s for s in sentences if s.strip()]
word_count = len(words)
sentence_count = max(len(sentences), 1)
syllable_count = sum(QualityChecker._count_syllables(w) for w in words)
if word_count == 0:
return {
'flesch_score': 100, 'grade_level': 0, 'cefr_estimate': 'pre_a1',
'word_count': 0, 'sentence_count': 0, 'avg_sentence_length': 0,
}
asl = word_count / sentence_count
asw = syllable_count / word_count
flesch = 206.835 - 1.015 * asl - 84.6 * asw
grade = 0.39 * asl + 11.8 * asw - 15.59
cefr = 'b1'
for level, (low, high) in QualityChecker.CEFR_FLESCH_RANGES.items():
if low <= flesch < high:
cefr = level
break
return {
'flesch_score': round(flesch, 1),
'grade_level': round(max(grade, 0), 1),
'cefr_estimate': cefr,
'word_count': word_count,
'sentence_count': sentence_count,
'avg_sentence_length': round(asl, 1),
}
@staticmethod
def _count_syllables(word):
word = word.lower().strip('.,!?;:')
if not word:
return 0
count = 0
vowels = 'aeiouy'
prev_vowel = False
for char in word:
is_vowel = char in vowels
if is_vowel and not prev_vowel:
count += 1
prev_vowel = is_vowel
if word.endswith('e') and count > 1:
count -= 1
return max(count, 1)
@staticmethod
def check_cefr_alignment(text, target_cefr):
"""Check if text readability aligns with target CEFR level."""
readability = QualityChecker.check_readability(text)
actual_cefr = readability['cefr_estimate']
cefr_order = ['pre_a1', 'a1', 'a2', 'b1', 'b2', 'c1', 'c2']
actual_idx = cefr_order.index(actual_cefr) if actual_cefr in cefr_order else 3
target_idx = cefr_order.index(target_cefr) if target_cefr in cefr_order else 3
aligned = abs(actual_idx - target_idx) <= 1
return {
'aligned': aligned,
'actual_cefr': actual_cefr,
'target_cefr': target_cefr,
'deviation': actual_idx - target_idx,
'readability': readability,
}
@staticmethod
def check_grammar(text):
"""Check grammar using language_tool_python."""
try:
import language_tool_python
tool = language_tool_python.LanguageTool('en-US')
matches = tool.check(text)
errors = []
for match in matches:
errors.append({
'message': match.message,
'offset': match.offset,
'length': match.errorLength,
'rule': match.ruleId,
'category': match.category,
'replacements': match.replacements[:3] if match.replacements else [],
})
tool.close()
return {
'error_count': len(errors),
'errors': errors,
}
except ImportError:
_logger.warning("language_tool_python not installed, skipping grammar check")
return {'error_count': 0, 'errors': [], 'note': 'Grammar checker not available'}
@staticmethod
def run_all_checks(text, target_cefr='b1'):
readability = QualityChecker.check_readability(text)
alignment = QualityChecker.check_cefr_alignment(text, target_cefr)
grammar = QualityChecker.check_grammar(text)
passed = alignment['aligned'] and grammar['error_count'] < 5
return {
'passed': passed,
'readability': readability,
'cefr_alignment': alignment,
'grammar': grammar,
}