gapguide-api / scripts /qa_verify_personas.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
3.91 kB
"""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 ***')