QA Gap Analysis Report

Generated: 2026-04-25 Β· Sprint: Post-Sprint (30 tasks completed) Β· Scope: 13 wiki modules vs 5 service code roots
0 Critical
11 Missing
11 Partial
5 Fixed
42 Items checked OK

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.

01 β€” Data Model & Database Schema
partial device_sessions table schema diverges from spec
Spec (Β§6) defines 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.
telemetry/server/src/db/schema.ts: deviceSessions has device_id, platform, app_version, session_count β€” no expired_at/end_reason/policy fields
partial teachers table missing lockout fields (locked_until, failed_attempts)
Spec (Β§14.1) requires account lockout after 5 failed login attempts β€” failed_attempts, locked_until. Students table has failedAttempts and locked. Teachers table has neither β€” teacher login lockout is not enforced.
teacher-portal/server/src/db/schema.ts: teachers table has only id, email, passwordHash, name, school, schoolId, createdAt β€” no lockout columns
missing subscriptions / entitlement_grants / stripe tables absent known deferred
Spec Β§29 defines 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.
user-center/server/src/db/schema.ts: no subscriptions, entitlement_grants, or stripe_* tables. teacher-portal schema: same absence.
fixed email_log table not implemented
email_log table created in user_center DB with schema: id, to_email, subject, template, sent_at (INT, Unix seconds), result ('sent'|'failed'), error_message, created_at. Migration added in migrate.ts. email_log captures every send attempt (success and failure) for 90-day GDPR audit trail.
email_log table verified present in user_center DB via shared-db mysql CLI
02 β€” API Contracts
missing GET /api/recommendations not implemented in Reader
Spec Β§4.1 defines 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.
grep recommendations /home/ubuntu/reader/server/src/routes/: (no output) β€” route does not exist in reader service
missing GET /api/internal/learner/:id not implemented in Reader
Spec Β§4.1 defines an internal endpoint 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.
grep "internal/learner" /home/ubuntu/reader/server/src/: only accountSync.ts references account service URL, not an internal reader endpoint
fixed CM REST endpoint GET /api/v1/objectives β€” verified LIVE (2026-04-25)
Spec Β§4.5 marks this as "UNVERIFIED β€” tRPC only; REST endpoint does not exist yet". Confirmed working: 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.
curl -H "x-internal-key: pickatale-internal-key-2026" http://localhost:3100/api/v1/objectives/export β†’ {"data":[...],"meta":{"total":212236}} β€” live, returns JSON array of curriculum objectives
partial Telemetry events accept learner_id directly β€” spec requires device_session_id JWT resolution
Spec Β§6 Session Identity Model specifies: client must not send raw 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.
telemetry/server/src/routes/publicApi.ts: event ingestion reads learner_id from body directly; no device_session_id JWT resolution step
03 β€” Identity, Auth & Access Control
fixed Email sending not implemented β€” verification emails, password resets, welcome emails never sent
Resend added (resend@3.0.0), src/lib/email.ts created with 5 email templates (verify, reset, invite, welcome, login_alert). Wired into auth routes: register β†’ sendVerificationEmail or sendWelcomeEmail, forgot-password β†’ sendPasswordResetEmail, reset-password β†’ sendWelcomeEmail. Admin invite create optionally sends invite email. All sends fire async (.catch(console.error)). email_log table created for 90-day retention. RESEND_API_KEY placeholder in .env β€” replace with real key to activate.
commit b573cf2 β€” src/lib/email.ts + src/routes/auth.ts + src/routes/admin.ts + src/db/migrate.ts + src/db/schema.ts
fixed Invite flow implemented but invite email not sent
sendInviteEmail() added to admin.ts invite creation. If email provided in request body, invite email fires in background. Invite URL includes invite token and role. email_log captures send result.
commit b573cf2 β€” admin.ts: sendInviteEmail(email, token, role).catch(console.error)
partial Teacher login lockout not enforced (user-center delegates but teacher-portal bypasses)
User-center has 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.
teacher-portal/server/src/routes/auth.ts: "Local auth removed. All authentication via Account Center SSO" β€” but teacher_token JWT is still valid for 7 days without lockout re-check
partial Invite flow implemented but invite email not sent
User-center has 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.
user-center/server/src/routes/auth.ts: imports inviteTokens from schema β€” token generated, but sendEmail function absent
04 β€” Billing & Entitlement
missing Stripe integration entirely absent (TASK-10 through 18) known deferred
All billing infrastructure is unbuilt: no Stripe webhook handler, no subscription creation, no payment processing, no subscriptions table, no entitlement_grants table, no checkEntitlement() function. The platform currently has no way to gate features by subscription state.
grep -r "stripe" across all service src/: (no output). Spec Β§29 lists: subscriptions, entitlement_grants, stripe_customers, stripe_events tables β€” all absent.
missing Entitlement check (checkEntitlement) not implemented anywhere known deferred
Spec Β§29 requires 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.
grep -r "checkEntitlement\|entitlement_grants" across all service src/: (no output)
05 β€” Roster & Classroom Management
partial Archive class side-effects partially implemented β€” student state update not confirmed
Spec Β§19.3 requires: 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.
teacher-portal/server/src/routes/classes.ts: archiveClass mutation exists; student cascade UPDATE and ClassArchived event not found in route
partial Student single-add PIN modal β€” spec requires "Copy" + "Print Card" buttons
Spec Β§19.4 explicitly notes: "This was previously missing β€” must build." The backend returns PIN once on creation. Frontend implementation of the dismissible modal with Copy + Print Card buttons is unverified in server code β€” this is a frontend gap that requires UI verification.
Spec Β§19.4: "Single-add flow MUST show pin in dismissible modal with 'Copy' + 'Print Card' buttons" β€” server returns PIN but UI compliance unverified
06 β€” Reading, Telemetry & Learning Loop
fixed Telemetry service has 3 TypeScript compile errors (sessions.ts + gdpr.ts) FIXED d1e3648
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.
Commit d1e3648 β€” telemetry build: 0 errors, 0 warnings. tsconfig.json: declaration:false (not a library, no .d.ts needed).
partial Offline event queue (IndexedDB / Service Worker) β€” SW exists but telemetry replay unverified
Spec Β§6 requires offline telemetry events to be queued in IndexedDB via Service Worker and replayed on reconnect. A 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.
find /home/ubuntu/reader -name "sw.js": found at reader/client/dist/sw.js β€” telemetry queue impl status requires client-side audit
07 β€” Notifications & Edge Cases
missing All SendGrid email notifications not implemented known deferred
Spec Β§22 defines 9+ notification triggers (VERIFY_EMAIL, WELCOME_TEACHER, WELCOME_PARENT, INVITE_EMAIL, PASSWORD_RESET, PASSWORD_CHANGED, ACCOUNT_LOCKED_ALERT, SCHOOL_SUBSCRIPTION_EXPIRED). No email sending library exists in any service. All adult-facing email notifications are silently dropped.
grep sendgrid/nodemailer/resend across all service src/: (no output). user-center/server/package.json: no email dependency listed.
partial PARENT_CLAIM_REQUEST in-app notification β€” backend exists, UI delivery unverified
Spec Β§22 defines 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.
teacher-portal/server/src/routes/parents.ts: parentClaim route exists. grep -r "notification\|websocket\|SSE": no notification delivery mechanism found.
08 β€” Admin & Compliance
fixed GDPR telemetry deletion broken β€” TS2769 type error in gdpr.ts FIXED d1e3648
The telemetry GDPR deletion route (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.
telemetry/server/src/routes/gdpr.ts β€” explicit String() cast on req.params + IRouter type annotation. Build: 0 errors.
missing Platform admin panel (/api/admin endpoints) not implemented
Spec Β§44 defines 18+ admin API endpoints (user search, suspend, reactivate, move students, merge duplicates, entitlement grants, subscription management, bot trigger, cron run history). The user-center has an 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.
user-center/server/src/routes/admin.ts exists but grep for merge/entitlement/bot/jobs: (no output) β€” these admin endpoints are not implemented.
partial Audit log incomplete β€” many required action types not logged
Spec Β§45.4 requires audit log entries for all: auth events, data access events for child records, all mutations, admin operations, GDPR events. Audit log table exists and is written in some paths (teacher-portal login, GDPR ops, some roster actions). However, bulk import, move_student, parent_claim_approved/rejected, pin_revealed, entitlement_check_failed, and all billing events are not wired to audit_log inserts.
teacher-portal/server/src/index.ts: auditLog.insert for bot_sync_progress and bot_placement_complete only. Missing: roster actions, GDPR events in learner-bot, billing events.
fixed Telemetry sessions.ts tRPC router has TS2339 error on grouped query FIXED d1e3648
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.
telemetry/server/src/routers/sessions.ts:43 β€” and() replaces chained .where() after groupBy. Build: 0 errors.
11 β€” User Sign-Up Flows & Roles
partial Teacher onboarding wizard steps 1–3 partially implemented
Spec Β§17.1 requires a 7-step teacher onboarding wizard: (1) School Details, (2) DPA checkbox, (3) Create First Class, (4) Import/Add Students, (5) Print Login Cards, (6) First Book Assignment, (7) Done. Backend has school creation, class creation, student import, and DPA field. However, the guided wizard flow (step ordering, DPA consent gate, Print Login Cards PDF generation) is not wired as a multi-step onboarding flow in the teacher-portal β€” it exists as independent CRUD endpoints.
teacher-portal: schools.ts, classes.ts, students.ts routes exist independently. No onboarding wizard step controller found. dpa_accepted_at field exists in schema.
missing Parent portal parent-child linking not implemented in user-center
Spec Β§14 (parent-linking-contract) defines a multi-step flow: teacher generates link token β†’ parent visits /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.
grep parent_child_links / parentLink across user-center/server/src/: (no output). parent-portal/server/: no claim validation route found.
14 β€” Parent Linking Contract
missing Parent link token validation endpoint absent from user-center
The full parent linking contract (Β§14) specifies: 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.
user-center/server/src/routes/: account.ts, admin.ts, auditLog.ts, auth.ts, chat.ts, impersonation.ts, profile.ts, schoolAdmin.ts β€” no parent link route.
Sprint-Verified Items (confirmed OK)
  • βœ… 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