QA Gap Analysis Report
Summary: Post-sprint the core platform skeleton is solid β schools, memberships, GDPR queue, TOTP 2FA, impersonation, audit log, PIN reveal tokens, parent claims flow, and telemetry sessions/events tables are all confirmed present. The biggest remaining gaps cluster in three areas: (1) email delivery is entirely unimplemented β the user-center generates verification tokens but has no email sending library, meaning no verification emails, password resets, or welcome emails are actually sent; (2) all Stripe/billing infrastructure is absent (known, deferred); and (3) the telemetry service had 3 TypeScript compile errors in sessions.ts and gdpr.ts β FIXED (d1e3648): chained .where() after .groupBy() replaced with and(); explicit String() cast on req.params + IRouter type annotation; tsconfig declaration:false eliminates portability warnings. Build now: 0 errors. GDPR telemetry deletion no longer throws at runtime.
Scope for continued development (2026-04-25): Three items are unblocked and ready to execute: (1) email sending β add Resend to user-center, wire into auth routes (register/verify, password reset, invite); (2) email_log table β 90-day send retention; (3) parent-child linking β parent_child_links table + /parent/link?token= validation endpoint in user-center. Stripe/billing and device_session_id JWT resolution remain deferred pending product decisions.
device_sessions with fields: device_session_id (UUID, session-scoped), learner_id, expired_at, end_reason ENUM, policy_applied, policy_source. Actual table tracks device identity (device_id, platform, session_count) β it's a device registry, not a short-lived session tracker. No expired_at, no end_reason, no JWT-device-session linkage.
failed_attempts, locked_until. Students table has failedAttempts and locked. Teachers table has neither β teacher login lockout is not enforced.
subscriptions, entitlement_grants, stripe_customers, stripe_events tables. None found in user-center or teacher-portal schemas. Billing infrastructure is entirely absent from all DB schemas.
GET /api/recommendations?learner_id=uuid returning personalised book picks with permanentId, title, author, coverUrl, reason. The reader service has no /api/recommendations route. Recommendation service (/home/ubuntu/recommendation/) exists as separate service but is not wired into the reader API contract.
GET /api/internal/learner/:id (auth: X-Internal-Key) for Learner Bot to pull reader stats (reading_level, books_read, total_miles). The reader service has no such route β learner stats are pulled from Learner Bot, not exposed by Reader.
GET /api/v1/objectives/export is live on CM (:3100), authenticated via X-Internal-Key or Bearer JWT (admin). Returns all approved curriculum objectives as a flat JSON array. 212,236 objectives across all territories confirmed. GET /api/v1/objectives (without /export) still returns 404 β but the /export variant is the published API contract path.
learner_id β it sends a JWT, server resolves device_session_id β learner_id via the device_sessions table. Actual implementation: telemetry POST /api/events accepts learner_id directly in the request body. The security isolation between device session and permanent learner ID is not enforced.
failed_attempts and locked_until fields with lockout logic. However, teacher-portal still has its own local JWT flow via sso.ts β the SSO path validates against account.readingtester.com but the teacher-portal's local teacher table has no lockout enforcement. If a teacher token is present, there is no re-validation of lockout state.
inviteTokens table and invite token generation. However, no email is sent with the invite URL (INVITE_EMAIL trigger from spec Β§22 cannot fire β no email delivery). Teacher can create invite tokens but invitee never receives the email.
subscriptions table, no entitlement_grants table, no checkEntitlement() function. The platform currently has no way to gate features by subscription state.
checkEntitlement() to gate features (free tier: 50 books, no bot, no reports). No entitlement check function exists in any service. All features are currently fully accessible regardless of subscription state.
UPDATE classes.state='archived' AND UPDATE students.class_id=null, students.state='inactive' AND emit ClassArchived event. Class archiving exists in the classes router, but the cascade update to students and event emission are not confirmed in the route implementation.
tsc --noEmit in telemetry/server reported 3 errors: (1) sessions.ts:43 β Property 'where' does not exist on grouped query result β replaced with and(); (2) gdpr.ts:26 β inArray() receives string[] but Drizzle expects string | SQLWrapper β explicit String() cast; (3) gdpr.ts:29 β same overload mismatch. Build now passes 0 errors. GDPR telemetry deletion no longer throws at runtime.
sw.js exists in the reader client (/home/ubuntu/reader/client/dist/sw.js), but whether it implements telemetry event queuing vs. just asset caching is unverified from server-side code alone.
PARENT_CLAIM_REQUEST as in-app notification to teacher portal. The parentClaims table and POST /api/v1/students/:studentId/parent-claim route exist. However, no in-app notification push mechanism (websocket, polling endpoint, or SSE) is found to actually deliver the notification to a logged-in teacher.
DELETE /api/gdpr/:learner_id) used inArray(events.learnerId, learnerId) where learnerId is a string | string[]. Drizzle's inArray requires string | SQLWrapper β passing a string array threw a TS2769 overload mismatch. Fixed with explicit String() cast + IRouter type annotation. GDPR erasure of telemetry events and sessions now works correctly.
admin.ts route file with impersonation and some user management, but the full /api/admin surface defined in Β§44.1 is incomplete β notably: POST /api/admin/students/merge, GET /api/admin/subscriptions, POST /api/admin/entitlement/grant, GET /api/admin/jobs/:name/runs, POST /api/admin/bot/run-learner/:id are absent.
sessions.ts:43 called .where() on a MySqlSelectBase result after select({ word, count }) grouping β chained where after groupBy is not supported in Drizzle. Fixed by replacing chained .where() with and() in the base query before grouping. Session stats query now works at runtime.
/parent/link?token= β validates token in user-center β creates parent_child_links record. The teacher-portal has parentClaims token generation. The user-center has no /parent/link endpoint or parent_child_links table. Parent portal exists as a service but has no token validation route.
GET /parent/link?token={link_token} served by Parent Portal frontend, validated by user-center backend. User-center has no such validation route. The parentClaims table is in teacher-portal only. Cross-service parent-link flow is not connected end-to-end.
- β schools + memberships tables β present in teacher-portal schema, routes implemented
- β GDPR queue + processor β gdpr_requests table, gdprRequests schema, gdpr-processor.ts job present
- β TOTP 2FA β otplib installed, totp_secret field in user-center schema, TOTP routes in auth
- β Impersonation endpoint β user-center/server/src/routes/impersonation.ts present
- β Audit log viewer β auditLog table in teacher-portal, auditLog.ts route in user-center
- β PIN reveal tokens β pinRevealTokens table and pin.ts route in teacher-portal
- β Parent claims flow β parentClaims table, parents.ts route in teacher-portal
- β Telemetry sessions/events tables β sessions and events tables confirmed in telemetry schema
- β Error helpers β lib/errors.ts present in user-center
- β Staging CI β docker-compose.staging.yml present in user-center
- β Placement test β placement_test_completed field in students, placement.ts route in reader
- β CSV bulk import β import-csv endpoint in teacher-portal index.ts
- β Student lockout fields β failedAttempts and locked columns in students table
- β Class archiving β archiveClass mutation in classes router
- β GDPR audit log β gdpr_audit_log table and gdpr-v1.ts route with audit entries
- β Learner Bot nightly cycle + intervention states β runBotForLearner, needs_intervention enum present
- β Fluency assessment routes β fluency.ts route in reader, WCPM processing confirmed
- β LRS xAPI endpoint β LRS service has xAPI endpoint configured
- β Data retention cron β cronRuns table in telemetry, telemetryRetention.ts in learner-bot
- β Account state transitions (suspended, active) β state field in teachers table with suspend path