PHASE6B_ROOT_CURRICULUM_EXPANSION.md — the phase summary: what was authored per course, the is_required extension, the capstone unlock fix, verified scenario coverage before/after (44 -> 88 of 98, 89.8%, by direct query not estimate), and honest known limitations/Phase 6C recommendations. ROOT_CAPSTONE_READINESS.md — the readiness module and the server-side unlock fix in full detail, including why the module's completion and the real assessment's unlock gate are deliberately independent systems. Updated ROOT_CURRICULUM_INVENTORY.md (Phase 6B coverage numbers), ROOT_PROGRAM_ARCHITECTURE.md (ROOT 08/09 PARTIAL, new module lists per course), COURSE_CONTENT_MODEL.md (is_required completion rule, a new content-versioning section per this phase's own requirement), COURSE_AUTHORING_STANDARD.md (classifying near-duplicate/advanced scenarios), ENGINEERING_SOP.md (new Phase 6B section), REMOTE_AND_SELF_PACED_ARCHITECTURE.md (cross-reference), and platform/README.md (e2e-phase6b.js, the Phase 6B rehearsal, isolation- per-suite regression guidance). Co-Authored-By: Claude Sonnet 5 <[email protected]>
10 KiB
Course Content Model
Phase 6A §3 — how RootNode represents actual teaching content (lessons, demonstrations,
knowledge checks, guided exercises, reflections) around the existing scenario/assessment
systems, without duplicating either. See docs/ROOT_PROGRAM_ARCHITECTURE.md for what this
model is populated with, and docs/COURSE_AUTHORING_STANDARD.md for how to author new content
against it.
1. Shape
program (existing, 019_programs.sql — unchanged)
→ course (new: courses)
→ module (new: course_modules)
→ activity (new: module_activities)
platform/api/migrations/022_course_content.sql is the schema. Every table follows the same
conventions already established by programs/assessment_definitions: a TEXT slug primary key,
a plain incrementing version integer bumped by hand on a semantic edit, status/visibility
CHECK constraints, created_at/updated_at.
2. Activity types and what they reference
| Type | References | Completion behavior |
|---|---|---|
lesson, demonstration, knowledge_check, guided_exercise, reflection |
a Markdown file (content_path) |
manual_marker |
lab |
an EXISTING scenario_id (scenarios/data/*.json) |
auto_scenario |
assessment, capstone |
an EXISTING assessment_definition_id (assessment_definitions) |
auto_assessment |
A lab activity never duplicates scenario content — it stores a bare scenario_id string, the
same id the existing curriculum/attempt/progress system already uses. An assessment/capstone
activity never duplicates an assessment definition — it stores the existing
assessment_definition_id and a foreign key enforces it actually exists. Neither activity type
is prevented from also having its own content_path (e.g. the gold module's lab activity has a
short Markdown intro at 05-lab-intro.md) — that's framing prose around the reference, not a
copy of the referenced thing.
3. Where the prose lives
Long-form lesson content is git-tracked Markdown under content/root/<course>/<module>/, not
Postgres. Reasoning, weighed explicitly per the authorization's instruction:
- Reviewable like code. A lesson edit is a normal diff/PR, not an opaque row update.
- No CMS needed. The authorization explicitly ruled one out ("Do not build a complex CMS"). A Postgres TEXT column holding Markdown would technically work but loses git history/diffing for the one thing (prose) that benefits from it most.
- Structural metadata still belongs in Postgres (ordering, type, references, status, objectives, prerequisites) because it's queried, joined, and mutated (locking/completion) — exactly the kind of relational state Postgres is for, and exactly what the authorization asked for ("Postgres preferred for metadata/state").
The Instructor Notes convention
One Markdown file per activity, not two. Everything before a ## Instructor Notes heading is
shared (self-paced-visible) content; that heading and everything after it is instructor-only.
platform/api/src/content.js does the split (splitContent()) — a plain string search on that
exact heading, no frontmatter parser, no template engine. The API layer (course-progress.js's
shapeActivity()) only ever attaches instructor_notes to the response when the caller is
resolved (server-side, from session identity) to be an instructor — never from a request
parameter. A self-paced student and an instructor read the identical file; they just see
different slices of it.
4. Completion rules — one source of truth per behavior
platform/api/src/course-progress.js's isActivityComplete() is the only place completion is
decided, and it never invents new state for anything that's independently verifiable:
auto_scenario— readsprogress[scenario_id].status === 'completed', the exact sameprogresstablecurriculum-rules.jsalready reads for stage completion (platform/api/src/ progress.js'sloadProgress). A lab activity's completion is a real fact about whether the student actually solved the scenario, not a flag a course layer could get out of sync with.auto_assessment— readsachievements(achievement_type IN ('assessment_completed', 'capstone_completed')withmetadata->>'assessment_definition_id'matching), the exact mechanismachievements.js'srecordAssessmentAchievementalready writes on a real pass. No new achievement type, no new table.manual_marker— the ONE new piece of state this phase adds:activity_completions(student_id, activity_id). Used ONLY for activity types with nothing else to derive completion from (a lesson has no "pass/fail," a reflection has no validator).POST /api/student/ activities/:id/completerefuses (409not_manually_completable) for any activity whosecompletion_behaviorisn'tmanual_marker— a student can never fake a lab or assessment completion through this route.
Course and module completion are pure aggregates over their activities' completion (total/
completed/pct/complete — see totals()), never separately stored.
Optional vs. required activities (Phase 6B)
module_activities.is_required (defaults true) lets a module distinguish CORE teaching from
GUIDED/INDEPENDENT-practice duplicates and ADVANCED/optional extras — see
docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md's classification scheme, used throughout ROOT 09. A
module's complete flag is true once every REQUIRED activity is done; an incomplete optional
activity never blocks it, though total/pct still count every activity, required or not.
Every Phase 6A activity has is_required = true (the column's default) — this is purely
additive, nothing that was complete before Phase 6B becomes incomplete now.
5. Prerequisite/locking rule
A course or module lists prerequisites (an array of other course/module ids). It's locked if
ANY prerequisite is not satisfied, where satisfied means: the prerequisite has zero total
activities (a PLANNED entry — vacuously satisfied, since there's nothing to have completed) OR
the prerequisite is fully complete. This is deliberate: without the "zero activities is vacuously
satisfied" clause, an honestly-PLANNED course with no content yet would permanently lock every
real course that lists it as a prerequisite, which would perversely punish honesty about what
isn't built yet. See course-progress.js's prerequisitesSatisfied().
Locking is computed in two passes over each level (compute every entry's totals/prerequisite-
check, THEN strip working state) rather than one — mutating and reading totals in the same pass
made the result depend on Map iteration order, a real bug caught by e2e-phase6a.js during
development (see that file's "found by testing" comment in course-progress.js).
6. Draft vs. published — separate from READY/PARTIAL/PLANNED
status (ready/partial/planned) describes authoring completeness and is shown to
everyone, including students, honestly ("PLANNED — coming soon" is useful information, not a
leak). visibility (published/draft) is a hard gate: a draft course/module/activity is
excluded from every query a student-facing route runs (buildCourseTree's visClause), full
stop, regardless of its status. Use draft for genuinely unfinished authoring work you don't
want a student to see at all yet; use status = planned for an honest "not built yet" that's
fine to be visible.
6b. Content versioning (Phase 6B §21)
Follows the exact same convention assessment_definitions/programs already established — a
plain incrementing version integer, bumped by hand, informational rather than enforced:
- A typo-only edit (fixing a spelling mistake, a broken link, rewording a sentence without changing its meaning) does NOT require a version bump — nothing about what a student is being taught or held to changed.
- A semantic edit (changing a learning objective, swapping a lab's
scenario_id, changingcompletion_behavior, adding/removing/reordering activities in a way that changes what "complete" means) SHOULD bumpversionon the affected row(s) — see023_phase6b_curriculum.sql's ownversion = version + 1on every row it semantically changes (resequencing, prerequisite changes,is_requiredchanges). - What happens to existing completions: nothing, automatically. Unlike an assessment
session (which snapshots the definition version it was drawn against, because a session is a
point-in-time timed attempt), a course/module/activity's completion is derived LIVE every time
it's read (
course-progress.js) from the CURRENT row plus the student's progress/achievements/ activity_completions — there is no snapshot to invalidate. Amanual_markercompletion (activity_completions) is keyed byactivity_id, which is stable across a version bump (the id doesn't change, only its content/metadata does) — so a student's prior "I did this" record stays valid even after a semantic edit, exactly like editing a scenario's own Markdown description doesn't retroactively un-complete it inprogress. - Scenario references across versions: a
labactivity'sscenario_idis just a string reference intoscenarios/data/*.json— if that scenario file itself changes, the activity automatically reflects the new content next time it's loaded (same as every other consumer ofcurriculum.js's cache). If an activity'sscenario_iditself is changed to point at a DIFFERENT scenario, that's a semantic edit (bump the version) and — depending on intent — may warrant a newactivity_idinstead of mutating the existing one, so a student's prior completion of the OLD scenario isn't silently reinterpreted as completion of the new one. - What this deliberately does NOT build: no content-revision history table, no diffing, no "students who saw v1 vs v2" cohort tracking. If a future phase needs that level of rigor (e.g. for compliance/audit reasons), it's a real, separate feature — not something to bolt onto a plain integer column.
7. Why this doesn't touch Phase 1-5 systems
No column was added to scenarios/*.json, assessment_definitions, achievements,
progress, or scenario_attempts. No existing API route's behavior changed (the /api/ progress/record, /api/assessments/*, /api/capstone routes are byte-for-byte unchanged).
The course layer is purely additive: new tables, new routes, new frontend views, reading
existing tables through their existing shapes.