Problem
agent/profiler.py does a read-JSON → mutate-in-memory → write-JSON cycle with no locking (load_profile / save_profile). Two concurrent /attempt requests for the same student (very plausible — a game client retrying a timed-out request, or multiple devices) can race: the second write silently clobbers the first's update, dropping SM-2 review state.
Scope
- Introduce a real datastore (SQLite is acceptable for this stage, Postgres if you want to go further) with a schema covering: students, per-word learning state, phonics struggles, theme preferences, session history.
- Make attempt-recording atomic per student (row-level lock, transaction, or optimistic concurrency with retry).
- Write a migration path: a one-time script that imports existing
data/student_profiles/*.json into the new store.
- Preserve the existing function signatures in
agent/profiler.py where reasonable so recommender.py and dashboard/report.py don't need rewrites — or update all callers if the interface has to change, and say why in the PR.
Acceptance criteria
Problem
agent/profiler.pydoes a read-JSON → mutate-in-memory → write-JSON cycle with no locking (load_profile/save_profile). Two concurrent/attemptrequests for the same student (very plausible — a game client retrying a timed-out request, or multiple devices) can race: the second write silently clobbers the first's update, dropping SM-2 review state.Scope
data/student_profiles/*.jsoninto the new store.agent/profiler.pywhere reasonable sorecommender.pyanddashboard/report.pydon't need rewrites — or update all callers if the interface has to change, and say why in the PR.Acceptance criteria
record_attemptcalls for the same student/word and assert the finalattemptscount equals N (this test should fail against the current implementation and pass after the fix).