Spaces:
Sleeping
Sleeping
| """Tier 3 QA verification: hand-compute readiness per demo persona and compare | |
| to `compute_gap_report`. | |
| Run via: ./venv/Scripts/python.exe manage.py shell < scripts/qa_verify_personas.py | |
| """ | |
| import os, sys, django # noqa | |
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') | |
| django.setup() | |
| from apps.accounts.models import User | |
| from apps.roles.models import UserTargetRole, RoleSkill | |
| from apps.skills.models import UserSkill | |
| from apps.analysis.services import compute_gap_report, LEVEL_THRESHOLDS, MANDATORY_CAP | |
| def hand_compute(user, role): | |
| """Independent re-derivation from the design doc §Module 4.""" | |
| role_skills = list(RoleSkill.objects.filter(role=role).select_related('skill')) | |
| if not role_skills: | |
| return {'readiness': 100.0, 'no_requirements': True, 'cap': False} | |
| user_prof = { | |
| us.skill_id: us.proficiency | |
| for us in UserSkill.objects.filter(user=user) | |
| } | |
| sat_sum = 0.0 | |
| weight_sum = 0.0 | |
| mandatory_short = False | |
| gap_rows = [] | |
| for rs in role_skills: | |
| T = LEVEL_THRESHOLDS[rs.required_level] | |
| U = user_prof.get(rs.skill_id, 0) | |
| S = min(U / T, 1.0) | |
| sat_sum += S * rs.weight | |
| weight_sum += rs.weight | |
| if rs.is_mandatory and S < 1.0: | |
| mandatory_short = True | |
| # severity | |
| gap = max(T - U, 0) | |
| if U >= T: | |
| gt, sev = 'MET', None | |
| elif U == 0: | |
| gt, sev = 'MISSING', ('CRITICAL' if rs.is_mandatory else 'MEDIUM') | |
| else: | |
| ratio = gap / T | |
| if rs.is_mandatory or ratio >= 0.5: | |
| sev = 'HIGH' | |
| elif ratio >= 0.25: | |
| sev = 'MEDIUM' | |
| else: | |
| sev = 'LOW' | |
| gt = 'INSUFFICIENT' | |
| gap_rows.append((rs.skill.skill_name, rs.required_level, U, S, rs.weight, | |
| rs.is_mandatory, gt, sev)) | |
| if weight_sum == 0: | |
| return {'readiness': 100.0, 'no_requirements': True, 'cap': False, | |
| 'raw': None, 'gaps': gap_rows} | |
| raw = sat_sum / weight_sum * 100.0 | |
| cap = mandatory_short and raw > MANDATORY_CAP | |
| final = MANDATORY_CAP if cap else raw | |
| return {'readiness': final, 'no_requirements': False, 'cap': cap, | |
| 'raw': raw, 'gaps': gap_rows} | |
| DEMO_EMAILS = [ | |
| 'demo.weak@gapguide.test', | |
| 'demo.partial@gapguide.test', | |
| 'demo.ready@gapguide.test', | |
| 'demo.closedloop@gapguide.test', | |
| ] | |
| mismatches = [] | |
| for email in DEMO_EMAILS: | |
| try: | |
| u = User.objects.get(email=email) | |
| except User.DoesNotExist: | |
| print(f'[MISS] {email} not in DB') | |
| continue | |
| target = UserTargetRole.objects.filter(user=u, is_active=True).select_related('role').first() | |
| if not target: | |
| print(f'[MISS] {email} has no active target') | |
| continue | |
| role = target.role | |
| expected = hand_compute(u, role) | |
| actual = compute_gap_report(u, role) | |
| exp_r = round(expected['readiness'], 2) | |
| act_r = round(actual.readiness, 2) | |
| ok = abs(exp_r - act_r) <= 0.1 and expected['cap'] == actual.mandatory_cap_applied | |
| print(f'\n=== {email} -> {role.role_name} ===') | |
| print(f' expected readiness: {exp_r} (raw={expected["raw"]}, cap={expected["cap"]})') | |
| print(f' actual readiness: {act_r} (band={actual.band}, cap={actual.mandatory_cap_applied})') | |
| print(f' match: {ok}') | |
| if not ok: | |
| mismatches.append((email, expected, actual)) | |
| # Also spot-check one severity vs hand-computed | |
| actual_severities = {(g.skill_name, g.severity) for g in actual.gaps} | |
| expected_severities = {(row[0], row[7]) for row in expected['gaps']} | |
| diff = actual_severities ^ expected_severities | |
| if diff: | |
| print(f' severity diff: {diff}') | |
| if mismatches: | |
| print(f'\n*** {len(mismatches)} persona readiness mismatches ***') | |
| sys.exit(1) | |
| else: | |
| print('\n*** All persona readiness values match hand computation ***') | |