fix(curriculum): enforce course/module lock on manual-marker completion

POST /api/student/activities/:activityId/complete previously checked only that the activity
exists and the student holds an active PROGRAM enrollment — never whether the activity's own
course/module was actually unlocked for that student. A direct POST could mark any
manual_marker activity (lesson/demonstration/knowledge_check/guided_exercise/reflection)
complete regardless of the real prerequisite chain, the exact class of gap Phase 6B already
fixed once for the capstone's own start route, just never closed for this more general one.

Found while proving the real ROOT 01->02->03->04->05 chain via direct API (this phase's own
explicit requirement) — and confirmed to predate Phase 6E entirely by reproducing it against an
unrelated, pre-existing ROOT 05/06 pair before touching any Phase 6E content.

Fix: the route now resolves the activity's own course/module through the exact same
buildCourseTree() lock computation every other route already trusts, and returns 403 { error:
'locked' } if either is locked — reusing the existing mechanism, not a second, duplicated check.
Verified live: both a ROOT 02 bypass attempt and a ROOT 06 bypass attempt are now correctly
rejected; legitimate unlocked completions are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 17:08:28 -05:00
co-authored by Claude Sonnet 5
parent 682ef2de47
commit a09b7aaa53
2 changed files with 25 additions and 1 deletions
+5 -1
View File
@@ -79,7 +79,11 @@ decided, and it never invents new state for anything that's independently verifi
from (a lesson has no "pass/fail," a reflection has no validator). `POST /api/student/
activities/:id/complete` refuses (409 `not_manually_completable`) for any activity whose
`completion_behavior` isn't `manual_marker` — a student can never fake a lab or assessment
completion through this route.
completion through this route. **(Phase 6E)** the route also resolves the activity's own
course/module through the same `buildCourseTree()` lock computation every other route trusts,
and refuses (403 `locked`) if either is locked for that student — a real, pre-existing gap
found and fixed while proving the ROOT 01-04 prerequisite chain; see
`docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md`.
Course and module completion are pure aggregates over their activities' completion (`total`/
`completed`/`pct`/`complete` — see `totals()`), never separately stored.