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]>
34 KiB
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/kubekoshis 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 confirmgit remote -vshowsgitea: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 byloadBundles()/GET /api/bundles. Adding a bundle file here = adding a new cert-track library. Every scenario referenced must have a matching file inscenarios/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 inscenarios/bundles/). Loaded byloadCurriculumPath()/GET /api/curriculum/path(raw content) and, as of Phase 3B,computeCurriculumState()/GET /api/curriculum/state(rule-driven lock/complete/next-scenario state — seedocs/PHASE3B_LEARNING_AND_ASSESSMENT.md§4). Stages are Foundation → Workloads → Networking → Configuration → Troubleshooting → Real Ops → Capstone, built by reusing existing1-k8s-basicsandrootnode-real-opsscenarios grouped by their existingcategoryfield — no new scenario content was written for this. Each stage carries acompletion_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 therootnode-capstoneassessment definition (Postgres,assessment_definitionstable), which is where its actual scenario draw lives — see the Phase 3B doc §7. Don't put real scenarios directly into the JSON stage'sscenario_idsfor 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:
emailandlogin_identifier(independently optional/unique — email for older/self-paced students, an instructor-assigned login ID for younger ones),password_hashreusinginstructors' exactscryptmachinery (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
sessionstable 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/examsessionsisolation (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:
PRAGMA table_info(<table>)→ check if the new column/shape is already there (idempotent, safe to run on every boot).- Simple new nullable column →
ALTER TABLE ... ADD COLUMN. - Primary-key or structural change → build
<table>_newwith the new shape,INSERT ... SELECTthe 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. - Test the migration against a copy of real data (§3) before it ever runs against
/data/progress.dbin the container.
7. Roadmap status
Order agreed 2026-08-17/18. Status as of this doc:
- 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. - 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/. - 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.
- 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. - 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).
- Scenarios → teaching modules (incident context/investigation/prevention) — not started.
- 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).
- Progressive hint levels — done as of Phase 3B (§12). Server-enforced sequential reveal, tracked per attempt, policy-aware under assessment mode (free/disabled/flagged).
- Production observability — not started.
- CI scenario testing — not started.
- 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.jsfor isolated Provisioner testing. - Where identity lives: as of Phase 2B, Postgres is authoritative —
students/cohorts/progress/scenario_attemptslive in the samerootnode_platformdatabase asenvironments, owned by the Platform API's own migrations. See §9. As of Phase 3A,instructors/sessions/cohort_instructors/audit_eventslive there too. See §10.environments.student_idis 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.ymlruns 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.ymlstack as the Provisioner — no separate compose file. - Test procedure:
platform/test/e2e-phase2a.js(student flow) andplatform/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/instructorroles, scrypt password hashing, per-instructor salt), server-enforced RBAC on every/api/admin/*route, cohort-scoped access for the plaininstructorrole, CSRF double-submit protection, login rate limiting, and a platform-levelaudit_eventslog. - What it replaced: Phase 2B's in-memory session
Maps (gone on every restart) and the single sharedADMIN_PASSWORDenv 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 realdocker 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 persistentachievementsrecords. - Where the boundary moved: the Gateway now rewrites one Lab response
(
GET /lab/:token/api/scenarios/:scenarioId, strippinghints) 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, newProgressiveHints/AssessmentBannercomponents) — 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.mdis 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 needsdocs/PRODUCTION_VALIDATION_CHECKLIST.mdwalked 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 minimalrole_in_cohortenforcement (teacher/viewer), a/api/versionendpoint, 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-existingset -ebug 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 ininfra/scripts/drills/orinfra/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) — seedocs/REMOTE_AND_SELF_PACED_ARCHITECTURE.mdfor 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) andinfra/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 livedocker-compose.dev.ymlstack, 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_pathis just what it points at) andenrollments(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, mirroringclassroom.js's derived-not-storedderivePresencepattern from Phase 5A) gates a newPOST /api/student/programs/:id/launchin front of the exact same disposable-Lab sequence/api/environment/launchalready used (extracted intoenvironment-view.jsso 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, separatePOST /api/integrations/enrollmentsboundary (its own pre-shared-secret auth, no session cookie involved — same trust-boundary pattern as Gateway↔Provisioner'sPROVISIONER_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/recordpredated thewrap()async-error-handling convention — an unexpected internal error there (a malformedenvironment_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) andinfra/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, aFOR UPDATE SKIP LOCKEDworker polling on a plainsetInterval— 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-resetwould 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 withCOOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit hygiene, a real restart) andinfra/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]— seedocs/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 forroot_only, so a lesson can be taught BEFORE a lab is presented instead of the platform only ever showing a bare scenario list. Alabactivity references an existingscenario_id; anassessment/capstoneactivity references an existingassessment_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 Notesheading convention (content.js). OnebuildCourseTree()(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
progresstable; an assessment/capstone activity's completion reads the existingachievementstable; 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 templatedocs/COURSE_AUTHORING_STANDARD.mddocuments for all future modules. Every other course is honestlypartial(real labs, no lessons yet) orplanned(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) andinfra/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 ownsetup_commandsstand up the broken deployment, a realkubectl patchfixes 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(newmodule_activitiescolumn, defaultstrue): 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/startnever checkedunlock_rulebefore 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 samecomputeCurriculumState()gateGET /api/capstonealready used. Seedocs/ROOT_CAPSTONE_READINESS.md§4 for the full story, including a pre-existing gap this fix exposed ine2e-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) andinfra/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 realkubectl exposeagainst a live cluster, plus an instructor narrative proving Curriculum view, Prepare Labs, and classroom tooling all still work against the expanded curriculum).