Files
rootnode-academy/docs/COURSE_CONTENT_MODEL.md
T
ced4568andClaude Sonnet 5 2db136875f docs(phase6b): document curriculum expansion
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]>
2026-08-26 09:09:47 -05:00

163 lines
10 KiB
Markdown

# 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`** — reads `progress[scenario_id].status === 'completed'`, the exact same
`progress` table `curriculum-rules.js` already reads for stage completion (`platform/api/src/
progress.js`'s `loadProgress`). 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`** — reads `achievements` (`achievement_type IN ('assessment_completed',
'capstone_completed')` with `metadata->>'assessment_definition_id'` matching), the exact
mechanism `achievements.js`'s `recordAssessmentAchievement` already 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/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.
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`, changing
`completion_behavior`, adding/removing/reordering activities in a way that changes what
"complete" means) SHOULD bump `version` on the affected row(s) — see
`023_phase6b_curriculum.sql`'s own `version = version + 1` on every row it semantically
changes (resequencing, prerequisite changes, `is_required` changes).
- **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. A `manual_marker` completion
(`activity_completions`) is keyed by `activity_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 in `progress`.
- **Scenario references across versions**: a `lab` activity's `scenario_id` is just a string
reference into `scenarios/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 of
`curriculum.js`'s cache). If an activity's `scenario_id` itself is changed to point at a
DIFFERENT scenario, that's a semantic edit (bump the version) and — depending on intent — may
warrant a new `activity_id` instead 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.