Files
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

34 KiB
Raw Permalink Blame History

RootNode Academy — Engineering SOP

Living doc. Covers the "how" of building this platform — the "why" and business/curriculum process live in the Business Pack (~/Documents/CDAA Technologies/07 RootNode academy/RootNode_Academy_Business_Pack/), this doc doesn't duplicate that. Update this file in the same commit as the change it describes.

1. Where this actually lives

  • Repo: ~/Dev/Gitea/ced4568/rootnode-academy — remote is Gitea (git.synthossystems.com/ced4568/rootnode-academy), not GitHub.
  • Gotcha: ~/Dev/GitHub/ced4568/kubekosh is a different clone of the same original upstream fork, still pointed at GitHub, and it's behind (missing the RootNode logo commit and the Real Ops enforce-limits scenario as of this writing). It's not deployed anywhere. Don't edit it by mistake — always confirm git remote -v shows gitea: before making changes.
  • Known-good baseline as of this doc: commit 6ddfb1c ("add probe-port-mismatch and enforce-limits to rootnode-real-ops bundle"). Everything below builds on top of that, clean working tree.

2. Architecture at a glance

backend/server.js      Express API + SQLite (better-sqlite3) + WebSocket PTY terminal — the
                        "Lab image," single-tenant, unmodified by the Platform work below
frontend/               Vite + React SPA, built to frontend/dist and served by nginx
scenarios/data/         One JSON file per scenario (task or MCQ)
scenarios/bundles/      Flat scenario_id lists grouped by cert track (basics/appdev/admin/security/real-ops)
scenarios/rootnode-path.json   The RootNode guided learning path (staged, see §4)
scripts/entrypoint.sh   Container boot: starts k3s, nginx, node backend, sets the shell banner/PS1
Dockerfile              ubuntu:22.04 + k3s + node20 + nginx, single image, one port (80)

platform/provisioner/   Phase 1 Platform Provisioner — disposable per-student Lab lifecycle,
                        PostgreSQL registry, egress firewall. See §8 and
                        platform/provisioner/README.md.
platform/egress-proxy/  Shared allowlisting egress proxy every Lab environment's traffic must
                        pass through.

Single Express process, single SQLite file at /data/progress.db (or $PROGRESS_DB), single embedded k3s cluster per Lab environment. The Lab image itself is still single-tenant by design — see §5 for identity, and §8 for how Phase 1 multiplies it into a real multi-student platform without modifying it.

3. Local dev / safe smoke-testing

Never test schema changes against the real /data/progress.db first. Point at a scratch file:

cd backend && npm install   # first time only
node --check server.js      # syntax check, catches typos fast
PROGRESS_DB=/tmp/scratch/progress.db node server.js &
curl -s http://localhost:4000/api/health
# ...exercise the endpoints you changed...
fuser -k 4000/tcp            # stop it

To test a migration against real prior data without touching prod: copy the actual /data/progress.db out of the running container/volume, point PROGRESS_DB at the copy, boot, and verify the data you expect is still there before ever pointing at the real file.

kubectl/k3s-backed endpoints (/setup, /validate, /context, the WS terminal) won't work outside the container — that's expected. API-and-DB-only endpoints (roster, login, progress, curriculum, bundles, scenarios list) all smoke-test fine on a bare host.

4. Curriculum content

Two separate structures, don't confuse them:

  • Bundles (scenarios/bundles/*.json) — flat scenario_id lists, one per cert track. Loaded by loadBundles()/GET /api/bundles. Adding a bundle file here = adding a new cert-track library. Every scenario referenced must have a matching file in scenarios/data/.
  • RootNode path (scenarios/rootnode-path.json) — the guided onboarding sequence, separate from bundles on purpose (it's staged, bundles are flat, and the bundle loader would choke on a staged file if it were dropped in scenarios/bundles/). Loaded by loadCurriculumPath()/GET /api/curriculum/path (raw content) and, as of Phase 3B, computeCurriculumState()/GET /api/curriculum/state (rule-driven lock/complete/next-scenario state — see docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §4). Stages are Foundation → Workloads → Networking → Configuration → Troubleshooting → Real Ops → Capstone, built by reusing existing 1-k8s-basics and rootnode-real-ops scenarios grouped by their existing category field — no new scenario content was written for this. Each stage carries a completion_rule (all_required/threshold/prerequisites). Capstone is no longer a hardcoded placeholder — it's "type": "capstone" with an "unlock_rule" and a "capstone_assessment_id" pointing at the rootnode-capstone assessment definition (Postgres, assessment_definitions table), which is where its actual scenario draw lives — see the Phase 3B doc §7. Don't put real scenarios directly into the JSON stage's scenario_ids for capstone; that field stays empty by design, the draw is defined in the assessment definition instead.

To add/reorder a stage: edit rootnode-path.json directly, referencing existing scenarios/data/*.json ids. No backend code change needed — it's read fresh via POST /api/cache/reload or on server restart.

As of Phase 6A, root_ also has a course/module/activity layer ON TOP of this (lessons, demonstrations, guided exercises around the same scenario ids) — see §17 and docs/COURSE_CONTENT_MODEL.md. rootnode-path.json itself is unmodified and still the thing curriculum-rules.js computes stage lock/completion from; the course layer is a parallel, additive structure that references the same scenario ids, not a replacement.

5. Student identity — what's built, what isn't

Current state (Phase 5A — see docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md for the full design):

  • students.student_id (student-NNN, sequential) is still the FK every other table hangs off of and never changes across an account's lifecycle, including conversion.
  • Real credentials: email and login_identifier (independently optional/unique — email for older/self-paced students, an instructor-assigned login ID for younger ones), password_hash reusing instructors' exact scrypt machinery (src/auth/password.js). A student with neither field set is a plain pilot account, unchanged since Phase 2A: POST /api/login { student_id }, no password, instructor-roster-only.
  • First login/reset is one shared, single-use, hashed, time-limited token mechanism (students-auth.js) — instructors never see or set a student's actual password.
  • Sessions are the same Postgres-backed sessions table instructors use (opaque, HttpOnly, hashed, revocable, CSRF-protected); login is rate-limited the same Postgres-backed way as instructor login (Phase 4.1), now also applied to students.
  • Per-student progress/scenario_attempts/exam sessions isolation (below) and real concurrent multi-student Lab isolation (Phase 1's Provisioner/k3s-per-student model) were both already in place well before this — Phase 5A only changed how a student proves who they are, not how their data or Labs are isolated.

Not built — deliberate non-goals, not gaps (Phase 5A §37):

  • No public self-registration — accounts are still instructor-created/converted/imported only.
  • No email delivery infrastructure — password reset works via an instructor-issued token today; self-service reset returns a real token only in non-production, by design.
  • No Parent/Guardian portal — the schema doesn't block adding one later, but none exists.

Historical note, kept for context: earlier drafts of this section described a pre-Phase-2A state (X-Student-Id header, unauthenticated roster POST, one shared cluster) that no longer reflects any deployed version of this codebase; that content has been superseded by the above.

6. Schema migration pattern

SQLite can't ALTER a primary key. The codebase's existing convention (see progress table history) is:

  1. PRAGMA table_info(<table>) → check if the new column/shape is already there (idempotent, safe to run on every boot).
  2. Simple new nullable column → ALTER TABLE ... ADD COLUMN.
  3. Primary-key or structural change → build <table>_new with the new shape, INSERT ... SELECT the old data across with sane defaults for new required columns, DROP TABLE, RENAME TO. Never drop data — give it a legacy/default value ('unassigned', etc.) instead of discarding rows.
  4. Test the migration against a copy of real data (§3) before it ever runs against /data/progress.db in the container.

7. Roadmap status

Order agreed 2026-08-17/18. Status as of this doc:

  1. RootNode identity/branding pass — partial. Shell banner fixed (⎈ RootNode Academy Lab — node: k8s-lab). Upstream KubeKosh attribution left untouched in footer/README/LICENSE. Broader "make Real Ops first-class" UI work not started.
  2. RootNode curriculum mode — done. Rule-driven stage completion/unlock as of Phase 3B (§12), student-facing tabs (My Learning/Practice/Assessments/Capstone/Achievements) at frontend/src/platform/.
  3. Student accounts / persistent progress — done, Postgres-authoritative as of Phase 2B (§9), sessions persistent as of Phase 3A (§10). Real student credentials (email or login-identifier + password, first-login/reset token flow) closed the remaining "pick your name" gap as of Phase 5A — see §5 above and docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md. Pilot (no-login) accounts still work unchanged for anyone not yet converted.
  4. Instructor dashboard — done (§9/§10). Full admin portal at /admin: dashboard, cohort and student management, classroom Prepare/Destroy Labs, per-instructor accounts with owner/admin/instructor roles, audit log.
  5. Per-student isolation — Phase 1 + Phase 2A + Phase 2B + Phase 3A done, see §8/§9/§10. Ephemeral per-student Lab environments, Gateway-mediated routing with persistent server-controlled sessions, a full instructor/admin control plane with real per-person accounts and RBAC, and a working entry→dashboard→launch→lab→progress flow are implemented and end-to-end tested on a single dev machine, including a 3-student cohort classroom workflow and a real Platform API restart mid-session. Still not cleared for minors — the production two-VM Proxmox split this depends on doesn't exist yet, and OIDC/SSO is a documented adapter, not a working integration (docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §16).
  6. Scenarios → teaching modules (incident context/investigation/prevention) — not started.
  7. Capstone/exam engine — done as of Phase 3B (§12). RootNode Capstone (Real Ops-weighted, unlockable by curriculum-completion rule, not a permanent placeholder) plus a general exam engine (definitions, cohort assignment, random scenario draw, server-authoritative timer).
  8. Progressive hint levels — done as of Phase 3B (§12). Server-enforced sequential reveal, tracked per attempt, policy-aware under assessment mode (free/disabled/flagged).
  9. Production observability — not started.
  10. CI scenario testing — not started.
  11. Container/build optimization — explicitly deprioritized by design (platform works, not urgent).

Next slice: production OIDC/SSO, the production two-VM Proxmox deployment, and teaching-module scenario content are the biggest remaining items. See docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §21 for the full Phase 4 boundary.

8. Platform Provisioner (Phase 1)

Full design: docs/LAB_ARCHITECTURE.md + docs/PHASE1_PLATFORM_FOUNDATION.md. Operational reference (config, local dev, known limitations): platform/provisioner/README.md. This section is just the map.

  • What it is: an internal-only service that provisions a disposable, fully isolated Lab environment per student — its own Docker network, its own container running the existing, unmodified Lab image, its own firewall-enforced egress. Postgres-backed registry (environments, environment_events, lab_hosts), idle reaper, startup reconciliation.
  • What it is not: directly reachable by a student or the Gateway anymore — as of Phase 2B, the Platform API (§9) is the only caller (student launch/reset AND admin prepare/destroy/ reset both go through it). Still exercised directly via its internal API (shared-secret auth) or platform/provisioner/test/run.js for isolated Provisioner testing.
  • Where identity lives: as of Phase 2B, Postgres is authoritative — students/cohorts/progress/scenario_attempts live in the same rootnode_platform database as environments, owned by the Platform API's own migrations. See §9. As of Phase 3A, instructors/sessions/cohort_instructors/audit_events live there too. See §10. environments.student_id is still a plain opaque reference, not a real FK — that's a deliberate, documented choice (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §2), not an oversight.
  • Local dev: platform/provisioner/docker-compose.dev.yml runs Postgres + the egress proxy + the Provisioner on one machine — the documented Phase 1 dev exception. Production is a two-VM Proxmox split (Platform VM / Lab Host VM); the dev compose file is explicitly not that.
  • Test procedure: platform/provisioner/test/run.js — 12 automated integration tests against a live dev stack (isolation, firewall ordering, reset/destroy cleanup, audit events). Some properties (idle reaper timing, remote TLS lab-host, reconciliation-on-crash, real-image k3s behavior) were validated manually during implementation rather than automated — see the Phase 1 final report for what and why.

9. Platform API (Phase 2B)

Full design: docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md. Operational reference: platform/api/README.md.

  • What it is: platform/api/ — a lightweight Node/Express service (no k3s, no --privileged, no Lab terminal runtime) owning students, cohorts, enrollments, curriculum assignments, progress, scenario attempts, and — as of Phase 3A — instructor accounts, persistent sessions, and the audit log (§10). The sole caller of the Provisioner for both the student launch/reset path and the instructor/admin control plane.
  • What it is not: reachable directly by a browser — always through the Gateway (platform/gateway), same trust boundary the Provisioner has relative to this service.
  • Local dev: part of the same platform/docker-compose.dev.yml stack as the Provisioner — no separate compose file.
  • Test procedure: platform/test/e2e-phase2a.js (student flow) and platform/test/e2e-phase2b.js (classroom acceptance workflow) — both need a bootstrapped instructor account as of Phase 3A, see §10.

10. Identity and instructor access (Phase 3A)

Full design: docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md. Operational reference: platform/api/README.md (bootstrap, config).

  • What it is: persistent Postgres-backed sessions (student + instructor, survive a Platform API restart), real per-instructor accounts (owner/admin/instructor roles, scrypt password hashing, per-instructor salt), server-enforced RBAC on every /api/admin/* route, cohort-scoped access for the plain instructor role, CSRF double-submit protection, login rate limiting, and a platform-level audit_events log.
  • What it replaced: Phase 2B's in-memory session Maps (gone on every restart) and the single shared ADMIN_PASSWORD env var (no accountability, no roles, no revocation).
  • Bootstrap: node platform/api/bootstrap-owner.js <email> <name> <password> — the only supported way to create the first instructor account; no manual database editing.
  • Test procedure: platform/test/e2e-phase3a.js — 21 automated tests covering session persistence (including a real docker compose restart api), instructor auth, RBAC, cohort scoping, CSRF, rate limiting, password change, and audit logging.
  • Known limitations, future OIDC path, production HTTPS expectations: see docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §15–17.

11. Dependency lock convention

Every first-party Node service commits its package-lock.json: backend/, frontend/, platform/api/, platform/gateway/, platform/provisioner/. This wasn't consistently true before Phase 3B — platform/api/'s lockfile existed locally but was never committed, and platform/gateway//platform/provisioner/ never had one generated at all. Neither was a deliberate policy; both were oversights, fixed in Phase 3B (chore(deps) commits).

Going forward: after any npm install that changes a service's dependencies, the updated package-lock.json is part of the same commit as the package.json change — never left untracked, never regenerated-and-ignored. Validate a lockfile actually matches its package.json with npm ci (fails loudly on drift) before committing either.

12. Learning & Assessment Engine (Phase 3B)

Full design: docs/PHASE3B_LEARNING_AND_ASSESSMENT.md.

  • What it is: the teaching/assessment layer on top of everything Phases 1–3A built — detailed per-attempt telemetry (scenario_attempts, evolved from Phase 2B's one-row-per-click shape into a real attempt lifecycle), server-enforced progressive hints, rule-driven curriculum stage/capstone unlock logic, a full exam + capstone assessment engine (assessment_definitions/assessment_sessions/assessment_session_scenarios), and persistent achievements records.
  • Where the boundary moved: the Gateway now rewrites one Lab response (GET /lab/:token/api/scenarios/:scenarioId, stripping hints) so progressive hints can be genuinely server-gated rather than just UI-collapsed — the Lab image's own code/bytes are still untouched; this is a response rewrite at the Gateway boundary, same pattern as every other Lab-mediation decision made in earlier phases. The Lab's own frontend bundle did change this phase (ScenarioPanel.jsx, new ProgressiveHints/AssessmentBanner components) — Phase 3B explicitly authorized enhancing Lab UX, unlike prior phases.
  • Test procedure: platform/test/e2e-phase3b.js — covers attempt telemetry, hint sequencing/persistence-through-Lab-reset, curriculum/capstone lock-unlock, the exam engine (random draw, timer, expiry, restart persistence), two-student isolation, and instructor analytics/RBAC.
  • Known limitations: see the Phase 3B doc §20 — no automatic abandoned-attempt detection, no per-cohort needs-attention threshold configuration, raw-JSON pool_spec authoring in the admin UI (correct, not yet a guided form).

13. Production Infrastructure (Phase 4)

Full design: docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md, plus detail docs PRODUCTION_DEPLOYMENT.md, PRODUCTION_NETWORK_SECURITY.md, BACKUP_DR.md, OBSERVABILITY.md, CAPACITY_PLANNING.md, PRODUCTION_VALIDATION_CHECKLIST.md, and 12 RUNBOOK_*.md files, all under docs/. Everything lives in infra/ (Ansible, Terraform, Compose, firewall, monitoring, backup, CI, load-test tooling).

  • What it is: the infrastructure-as-code needed to run rn-platform-01/rn-lab-host-01 on a real Hetzner/Proxmox host — buildable and locally testable now, even though the dedicated server hasn't been purchased. Status is explicitly PHASE 4 BUILD COMPLETE / PRODUCTION DEPLOYMENT PENDING HARDWARE, not "deployed."
  • Where the boundary held: the Platform VM / Lab Host VM security split from docs/LAB_ARCHITECTURE.md is codified in firewall rulesets and mutual-TLS Docker config, not weakened anywhere.
  • Known gap surfaced, then resolved in Phase 4.1: the Gateway's Lab-reachability mechanism didn't survive the move to two real separate VMs — flagged explicitly rather than silently fixed, then actually built and simulation-tested (a Lab Host Router, platform/lab-router/) in Phase 4.1. See §13 below.
  • Validated without the hardware: real tofu validate, ansible-playbook --syntax-check, docker compose config, caddy validate, promtool/amtool, and a genuine end-to-end backup→encrypt→restore→verify cycle against disposable local Postgres with the real production migrations applied. None of this proves the real Hetzner network boundary — that needs docs/PRODUCTION_VALIDATION_CHECKLIST.md walked against the real server.

13. Pre-Production Hardening (Phase 4.1)

Full design: docs/PHASE4_1_PREPRODUCTION_HARDENING.md, plus docs/PRODUCTION_NETWORK_SECURITY.md (routing resolution), docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md, docs/SECURITY_SCANNING.md.

  • What it is: closes the Gateway-to-Lab cross-VM gap Phase 4 flagged (a Lab Host Router, platform/lab-router/, plus a routing abstraction in the Gateway, platform/gateway/src/lab-endpoint.js), then hardens the application layer found along the way: abandoned-attempt handling (scenario_attempts.status = 'abandoned', never a punitive failure), a Postgres-backed login rate limiter (survives a restart, ready to be shared/multi-instance), real pagination on audit/attempt/assessment-session history, a structured assessment authoring UI (owner-only raw JSON remains as a fallback), a minimal role_in_cohort enforcement (teacher/viewer), a /api/version endpoint, and admission control (a clean 503 "capacity full" instead of a 500 once a configured cap is reached).
  • Tested, not just designed: a real two-Docker-network routing simulation (infra/scripts/simulate-two-host/, 13/13), a destructive Postgres restore drill (infra/backup/test-destructive-restore-drill.sh), a Lab Host loss/rebuild drill (infra/scripts/drills/test-lab-host-loss.js), a deployment rollback drill, a CI-fails-closed drill (which found and fixed a real pre-existing set -e bug in both smoke-test scripts — see that drill's own output), and a full fake-cohort dress rehearsal (infra/scripts/drills/test-fake-cohort-rehearsal.js) — all passing, all in infra/scripts/drills/ or infra/backup/.
  • Known limitation carried forward: real remote/self-paced delivery needs actual student authentication (today's student_id-only login relies on instructors controlling who gets a valid id) — see docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md for the full accounting of what already works vs. what's genuinely Phase 5 scope. Closed in Phase 5A, below.

14. Student Identity & Remote Classroom (Phase 5A)

Full design: docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md, plus docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md and docs/RUNBOOK_REMOTE_CLASSROOM.md.

  • What it is: two deliverables built on the Phase 4.1 milestone. (A) Real student accounts — email-or-login-identifier + password, reusing instructors' exact credential machinery, a single first-login/reset token mechanism, deactivation enforced per-request — replacing pilot "pick your name" identification without touching a single existing pilot student's progress. (B) A remote classroom control center — scheduled/live/ended sessions, derived (not stored) presence via heartbeat, a live instructor dashboard combining real Provisioner Lab state and real academic records, a help-request queue, guidance-only announcements/current-activity, and a generic external meeting-URL field. Explicitly not video/voice/chat/shell-takeover — RootNode owns identity and coordination, an external service owns the call.
  • Tested, not just designed: platform/test/e2e-phase5a.js (21 checks: the two-student RBAC matrix and full password-security checklist, including a real Platform API restart proving rate-limit state survives it) and infra/scripts/drills/test-phase5a-rehearsal.js (15 checks: 5 pilot students with seeded real history, a live classroom, help requests, a real mid-rehearsal API restart proving classroom/session/presence recovery, then converting two students and confirming zero orphaned data) — both against a live docker-compose.dev.yml stack, not mocks. The full pre-existing regression suite was re-run given how much shared code this phase touched (server.js, admin-routes.js, sessions.js, rbac.js).

15. Self-Paced Learning & Enrollment Foundation (Phase 5B)

Full design: docs/PHASE5B_SELF_PACED_ENROLLMENT.md, plus docs/RUNBOOK_SELF_PACED_ENROLLMENT.md and docs/WEBSITE_ENROLLMENT_INTEGRATION.md.

  • What it is: programs (a first-class learning product, distinct from curriculum JSON — curriculum_path is just what it points at) and enrollments (a student's academic access to a program, independent of cohort membership — a self-paced enrollment has no cohort at all). Server-enforced entitlement (enrollment.deriveEnrollmentAccess, mirroring classroom.js's derived-not-stored derivePresence pattern from Phase 5A) gates a new POST /api/student/programs/:id/launch in front of the exact same disposable-Lab sequence /api/environment/launch already used (extracted into environment-view.js so both share one implementation) — pending/future/expired/cancelled enrollments cannot launch, server-side, not just hidden in the UI. Completion is entirely server-derived from the existing capstone achievement mechanism (Phase 3B, unmodified) and idempotent. A new, separate POST /api/integrations/enrollments boundary (its own pre-shared-secret auth, no session cookie involved — same trust-boundary pattern as Gateway↔Provisioner's PROVISIONER_SHARED_SECRET) is what the separate public website will eventually call; idempotent via a DB unique constraint on the caller's own event id, proven safe under real concurrent duplicate requests, not just sequential retries.
  • A real bug found and fixed: POST /api/progress/record predated the wrap() async-error-handling convention — an unexpected internal error there (a malformed environment_id) crashed the entire Platform API process, not just that one request. Fixed (wrapped + input-validated) and regression-tested.
  • Tested, not just designed: platform/test/e2e-phase5b.js (32 checks) and infra/scripts/drills/test-phase5b-selfpaced-rehearsal.js (15 checks) — both against a live stack, including real Lab destroy/reaper-reconciliation/recreate persistence proofs, a full real capstone completion, integration-endpoint concurrency, and a real Platform API restart. Migration safety proven explicitly against a fresh Postgres seeded with realistic pre-5B data (see that doc §14).

16. Enrollment Operations & Communications (Phase 5C)

Full design: docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md, plus docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md and docs/RUNBOOK_STUDENT_ACTIVATION.md.

  • What it is: a durable, provider-neutral notification pipeline (notification_events, a FOR UPDATE SKIP LOCKED worker polling on a plain setInterval — same pattern as the existing session/login-attempt sweepers, no new infrastructure) that finally sends real email for activation, enrollment confirmation, password reset, and password-changed — closing the gap Phase 5A explicitly left open. Google Workspace adapter: Gmail API via a service account with domain-wide delegation (no human password, no SMTP basic auth — see that doc §2 for why), behind the same provider interface a deterministic dev provider implements, so no test or dev run ever reaches a real inbox. Reuses Phase 5A's activation/reset token mechanism entirely — no competing invitation-token system.
  • A real bug found and fixed: request-password-reset would mint a reset token for a never-activated student, which doesn't make sense (there's no password yet to reset) and would have sent a confusing email. Fixed to fall through to the same anti-enumeration response as a nonexistent account.
  • Tested, not just designed: platform/test/e2e-phase5c.js (28 checks: activation vs. confirmation routing, duplicate/concurrent webhook idempotency, temporary/permanent/exhausted retry classification, resend/regenerate token invalidation, production token-secrecy gating via a real container recreate with COOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit hygiene, a real restart) and infra/scripts/drills/test-phase5c-enrollment-activation.js (10 checks: one continuous story from website enrollment through a real rendered/extracted activation email, a real Lab launch and scenario completion, a second enrollment's confirmation email, and a full password-reset round trip) — both against a live stack, using only the dev provider (no real network calls). Live Google Workspace delivery has since been validated for real, end to end, via [email protected] — see docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md §14.

17. ROOT_ Course Architecture + Content Delivery Foundation (Phase 6A)

Full design: docs/PHASE6A_ROOT_COURSE_FOUNDATION.md, plus docs/ROOT_CURRICULUM_INVENTORY.md, docs/ROOT_PROGRAM_ARCHITECTURE.md, docs/COURSE_CONTENT_MODEL.md, and docs/COURSE_AUTHORING_STANDARD.md.

  • What it is: a course → module → activity content model (022_course_content.sql) layered over the existing scenario/assessment/achievement systems for root_ only, so a lesson can be taught BEFORE a lab is presented instead of the platform only ever showing a bare scenario list. A lab activity references an existing scenario_id; an assessment/capstone activity references an existing assessment_definition_id — neither duplicates content. Long-form lesson prose lives in git-tracked Markdown (content/root/), split into shared/instructor-only sections by a ## Instructor Notes heading convention (content.js). One buildCourseTree() (course-progress.js) serves both the self-paced student route and the instructor curriculum view — no forked curriculum for the two delivery modes.
  • Completion, never a second source of truth: a lab's completion reads the existing progress table; an assessment/capstone activity's completion reads the existing achievements table; only lesson-only activities (nothing else to derive completion from) get the one genuinely new table, activity_completions.
  • A real bug found and fixed during testing: computing a module/course's prerequisite lock and then stripping its working totals in the SAME map iteration made the result depend on Map iteration order — a dependent entry could see its prerequisite's totals already deleted. Fixed by splitting into two explicit passes (course-progress.js).
  • One gold-standard module fully authored: ROOT 10's Kubernetes Health Probes & Troubleshooting (lesson → demonstration → knowledge check → guided exercise → real Lab rootnode-probe-port-mismatch → reflection), the template docs/COURSE_AUTHORING_STANDARD.md documents for all future modules. Every other course is honestly partial (real labs, no lessons yet) or planned (no content exists) — nothing was fabricated to look finished.
  • Tested, not just designed: platform/test/e2e-phase6a.js (21 checks: hierarchy/ordering, all three completion behaviors, prerequisite locking, resume, cross-student isolation, RBAC for self-paced no-cohort students, draft-content hiding, version stability) and infra/scripts/drills/test-phase6a-course-rehearsal.js (12 checks, real k3s: a student reads the gold module's real lesson content, launches a real Lab, the Lab's own setup_commands stand up the broken deployment, a real kubectl patch fixes it, and the module completes and unlocks the next one) — both against a live stack.

18. ROOT_ Curriculum Authoring & Coverage Expansion (Phase 6B)

Full design: docs/PHASE6B_ROOT_CURRICULUM_EXPANSION.md, plus docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md and docs/ROOT_CAPSTONE_READINESS.md.

  • What it is: 13 new teaching modules across ROOT 05-10/12/Capstone (023_phase6b_curriculum.sql), built entirely from the 98-scenario inventory Phase 6A already catalogued — no new scenarios needed. ROOT 08/09 moved from PLANNED to PARTIAL; scenario coverage in structured curriculum went from 44/98 to 88/98 (89.8%), verified by direct query, not estimated. 3 modules (plus the pre-existing gold standard) reach the full READY bar; every other new module is honestly PARTIAL, missing a demonstration or guided exercise, documented per-module rather than glossed over.
  • is_required (new module_activities column, defaults true): distinguishes CORE teaching from GUIDED/INDEPENDENT-practice duplicates and ADVANCED/optional extras — a module is complete once every REQUIRED activity is done; an incomplete optional one never blocks it. Every Phase 6A activity is unaffected (defaults to required).
  • The capstone server-side unlock fix: POST /api/assessments/:id/start never checked unlock_rule before this phase — a direct API call could start the capstone before its prerequisite stages were complete, even though the UI never exposed that path. Fixed by binding to the same computeCurriculumState() gate GET /api/capstone already used. See docs/ROOT_CAPSTONE_READINESS.md §4 for the full story, including a pre-existing gap this fix exposed in e2e-phase5b.js's own capstone test (fixed alongside).
  • Tested, not just designed: platform/test/e2e-phase6b.js (24 checks: new-module hierarchy, prerequisite chains through the newly-inserted front-door modules, the optional-vs-required completion rule, the capstone bypass fix in both directions, persistence/ isolation/RBAC/draft-hiding proven against the new content) and infra/scripts/drills/test-phase6b-root-course-rehearsal.js (11 checks, real k3s: a student walks a brand-new module — ROOT 07 Services & Service Discovery — end to end including a real kubectl expose against a live cluster, plus an instructor narrative proving Curriculum view, Prepare Labs, and classroom tooling all still work against the expanded curriculum).