Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d066410451 | ||
|
|
bd55b74b4f | ||
|
|
2864fe03dc | ||
|
|
f695b7c95d | ||
|
|
e48710a0be |
+66
@@ -0,0 +1,66 @@
|
||||
# Welcome to ROOT_
|
||||
|
||||
ROOT_ is RootNode Academy's path from "I can use a computer" to "I can run and repair real
|
||||
infrastructure." It's built for anyone roughly high-school age and up — including college
|
||||
students, career changers, help-desk technicians, junior sysadmins, and working developers who
|
||||
want to move into DevOps, cloud, or platform engineering. There's no strict age cutoff: what
|
||||
matters is whether you're ready for the material, not how old you are.
|
||||
|
||||
## Where ROOT_ goes
|
||||
|
||||
```
|
||||
ROOT 01 Computing & Linux Foundations
|
||||
ROOT 02 Linux Administration & Networking
|
||||
ROOT 03 Git & Collaborative Development
|
||||
ROOT 04 Containers & Docker
|
||||
ROOT 05 Kubernetes Foundations
|
||||
ROOT 06 Kubernetes Application Workloads
|
||||
ROOT 07 Kubernetes Networking & Configuration
|
||||
ROOT 09 Kubernetes Security
|
||||
ROOT 10 Kubernetes Troubleshooting & Observability
|
||||
ROOT 12 RootNode Real Ops
|
||||
CAPSTONE Operate and Recover a Production-Like Environment
|
||||
```
|
||||
|
||||
Each course builds on real skill from the one before it — you won't be asked to use a tool or
|
||||
concept that a previous course didn't already teach you. By the end, you'll have operated a real
|
||||
Kubernetes cluster, diagnosed and fixed real incidents, and demonstrated it in a hands-on
|
||||
capstone — not answered multiple-choice questions about it.
|
||||
|
||||
## How you'll actually learn
|
||||
|
||||
Every module follows the same rhythm:
|
||||
|
||||
**Learn → Practice → Lab → Troubleshoot → Validate**
|
||||
|
||||
- **Learn** — a short lesson introduces one real concept, plainly, with no unexplained jargon.
|
||||
- **Practice** — a demonstration or guided exercise lets you see the concept applied before
|
||||
you're on your own with it.
|
||||
- **Lab** — you do the real thing yourself, in your own disposable Lab environment: a real
|
||||
Linux shell, real Git, real Docker, and (from ROOT 05 onward) a real Kubernetes cluster.
|
||||
- **Troubleshoot** — Labs are built to sometimes not go as planned on the first try, on purpose.
|
||||
Figuring out why something didn't work is as much the skill as doing it right the first time.
|
||||
- **Validate** — your Lab checks its own result. You'll know immediately whether you actually
|
||||
solved it, not just whether you think you did.
|
||||
|
||||
## Self-paced or instructor-led
|
||||
|
||||
ROOT_ is built to work either way. If you're going through it on your own, every module gives you
|
||||
everything you need without a teacher in the room. If you're in an instructor-led class or camp,
|
||||
your instructor can see your progress, help when you're stuck, and add their own pacing and
|
||||
discussion on top of the same real curriculum — nothing about the platform changes between the
|
||||
two, only who else is watching your progress.
|
||||
|
||||
## Assessments and the Capstone
|
||||
|
||||
A few courses end in a short formal assessment — a handful of real, timed Lab tasks (not a
|
||||
multiple-choice quiz) that check you can actually do what the course taught, not just recognize
|
||||
the right answer. The program ends with the **Capstone**: no more new teaching, just a
|
||||
production-like environment with real incidents to find and fix, the same way you would on the
|
||||
job.
|
||||
|
||||
## Your progress is permanent — your Lab is not
|
||||
|
||||
One thing to know before you start: the Lab environment you work in is disposable and can be
|
||||
recreated at any time. Your academic progress is not. The next module covers exactly what that
|
||||
means and why it's nothing to worry about.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Your Disposable Lab
|
||||
|
||||
Every hands-on Lab in ROOT_ gives you a real, isolated environment — a real Linux shell from
|
||||
ROOT 01 onward, real Git and Docker from ROOT 03-04, and a real Kubernetes cluster from ROOT 05
|
||||
onward. It's yours alone while you're working in it. Here's what you need to know before your
|
||||
first one.
|
||||
|
||||
## Disposable means disposable
|
||||
|
||||
Your Lab environment can be recreated at any time — when you start a new Lab, when an
|
||||
instructor resets a classroom session, or if the environment needs to be refreshed. When that
|
||||
happens, **anything that only exists inside the Lab disappears**: files you created in the
|
||||
terminal, a local Git repository you initialized, a Docker container or image you built, a
|
||||
Kubernetes object you deployed. None of that is backed up, and none of it needs to be — it was
|
||||
never the point.
|
||||
|
||||
## Your academic progress is not disposable
|
||||
|
||||
What IS permanent, stored on RootNode's servers the moment you earn it: which lessons you've
|
||||
read, which Labs you've completed, your assessment results, and your position in the course
|
||||
sequence. Recreating your Lab never erases any of that. You can lose a Lab entirely and pick up
|
||||
exactly where you left off in the course.
|
||||
|
||||
## How you know you actually solved it
|
||||
|
||||
Every Lab task validates itself — when you think you're done, you run the check and get a real
|
||||
pass/fail answer immediately, based on the real state of your environment, not on your own
|
||||
guess. If it fails, that's useful information, not a penalty: it's telling you specifically what
|
||||
isn't right yet.
|
||||
|
||||
## If you get stuck
|
||||
|
||||
Most Labs have hints available, revealed one at a time so you can try the next step yourself
|
||||
before seeing more. Using a hint doesn't cost you anything or affect your result — it's there to
|
||||
keep you moving, not to be avoided on principle. A formal, timed assessment may disable hints;
|
||||
if so, the assessment will tell you plainly before you start.
|
||||
|
||||
If a Lab's environment itself seems broken rather than the task being hard (a command that
|
||||
should work doesn't, the environment won't start), use **Reset Scenario & Environment** to tear
|
||||
down and rebuild it cleanly rather than fighting a broken environment.
|
||||
|
||||
## If you're in an instructor-led session
|
||||
|
||||
Your instructor can see your progress in real time and gets notified the moment you use **Need
|
||||
Help** — no need to wait to be noticed. Use it for anything: a Lab that seems broken, a concept
|
||||
that didn't click, or just wanting a second pair of eyes before you second-guess a working
|
||||
solution.
|
||||
|
||||
## Returning to the course
|
||||
|
||||
When you're done with a Lab — pass or still stuck — you can always return to the course view and
|
||||
come back to it later. Nothing about leaving a Lab mid-task loses your place.
|
||||
|
||||
You're ready. Let's start with how a computer actually runs the software you're about to spend
|
||||
this program controlling.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Knowledge Check: Welcome to ROOT_
|
||||
|
||||
---
|
||||
|
||||
**1. If your Lab environment is recreated, what happens to a file you saved inside it, and
|
||||
what happens to your completed Labs so far?**
|
||||
|
||||
*Answer:* The file disappears — nothing inside the Lab persists across a recreation. Your
|
||||
completed Labs and course progress are unaffected — those are stored on RootNode's servers, not
|
||||
in the Lab, and survive regardless of what happens to any Lab environment.
|
||||
|
||||
**2. A Lab task fails your validation check. What should that tell you?**
|
||||
|
||||
*Answer:* Specifically what isn't correct yet, based on the real current state of your
|
||||
environment — it's diagnostic information to act on, not a penalty.
|
||||
|
||||
**3. Name one thing to try before assuming a Lab is "just broken."**
|
||||
|
||||
*Answer:* Check the available hints (revealed one at a time), and if the environment itself
|
||||
genuinely seems broken rather than the task being hard, use Reset Scenario & Environment to
|
||||
rebuild it cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** This is a good moment to actually demonstrate a Lab reset live if you have
|
||||
time — students remember "I saw it happen and nothing bad occurred" far better than being told
|
||||
it's safe.
|
||||
|
||||
**Approximate timing:** 3 minutes.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Practice Bank: Deployments & Workloads
|
||||
|
||||
This module is different from the ones before it. There's no new lesson here — you already got
|
||||
the real teaching in **Deployments, Scaling & Updates** and **Jobs, CronJobs & DaemonSets**. This
|
||||
is eight short, focused Labs that drill the exact same skills at speed: deploy, scale, roll
|
||||
forward, add a liveness probe, edit a live object, and reason about Jobs/CronJobs/DaemonSets
|
||||
without a guided walkthrough alongside you.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Real fluency with `kubectl` comes from repetition, not from reading about it a second time. Each
|
||||
Lab here is a small, self-contained task — no long setup, no new concepts — so you can move
|
||||
quickly and build the kind of muscle memory that matters when you're troubleshooting a real
|
||||
cluster under time pressure later in this program.
|
||||
|
||||
## How to use it
|
||||
|
||||
Work through the eight Labs in order. If one takes you more than a few minutes, that's useful
|
||||
information — it usually means the earlier module's concept hasn't fully clicked yet, and it's
|
||||
worth a quick re-read of that lesson before continuing rather than pushing through by trial and
|
||||
error.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Delivery:** This module has no lesson/demonstration/knowledge-check scaffolding by design — it
|
||||
is the original Phase 1-5 "workloads" scenario bank, reused here as a self-paced speed-practice
|
||||
set once the real teaching (root-06-m02/m03) has already landed. In an instructor-led cohort,
|
||||
this is a good candidate for a timed "workloads sprint" between the teaching modules and Jobs.
|
||||
|
||||
**Capstone note:** These eight scenarios are also the exact set the pre-existing
|
||||
`rootnode-path.json` "workloads" stage requires to unlock the Capstone assessment (a separate,
|
||||
older mechanism from this course's own prerequisite chain — see
|
||||
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
|
||||
re-checking that gate; a student can only be told to skip it if the Capstone gate itself is
|
||||
intentionally changed too.
|
||||
|
||||
**Common mistake to watch for:** students treating the MCQ-style activities
|
||||
(jobs-cronjobs-mcq, daemonset-mcq, deployment-strategy-mcq) as throwaway trivia — they're
|
||||
checking the same conceptual judgment calls the reflection in root-06-m03 raised, just faster.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Practice Bank: Services & Networking
|
||||
|
||||
Like the workloads practice bank before it, this module has no new lesson. **Services & Service
|
||||
Discovery** and **Ingress & NetworkPolicy Foundations** already taught the concepts. This is four
|
||||
short Labs that drill exposing a Deployment, reasoning about a NodePort, and resolving a Service
|
||||
by DNS from inside the cluster — the everyday networking moves you'll use constantly from here on.
|
||||
|
||||
## How to use it
|
||||
|
||||
Work through the four Labs in order. Two of them (`services-mcq`, `expose-service-basics`) will
|
||||
feel familiar — you did a close variant of `expose-service-basics` in the earlier teaching
|
||||
module. That's intentional: it's the same skill under slightly different conditions, checking
|
||||
that it generalizes rather than that you memorized one exact command.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Delivery:** No lesson/demonstration/knowledge-check scaffolding by design — this is the
|
||||
original Phase 1-5 "networking" scenario bank plus two extra Labs (`nodeport-task`,
|
||||
`dns-resolution-mcq`) not part of that original set, reused here as self-paced speed practice.
|
||||
|
||||
**Capstone note:** `services-mcq` and `expose-service-basics` are the exact pair the
|
||||
pre-existing `rootnode-path.json` "networking" stage requires to unlock the Capstone assessment
|
||||
— a separate, older mechanism from this course's own prerequisite chain (see
|
||||
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
|
||||
re-checking that gate.
|
||||
|
||||
**Common mistake to watch for:** confusing a NodePort's cluster-internal ClusterIP with the
|
||||
node-level port it also opens — the DNS-resolution Lab is a good moment to have a student narrate
|
||||
out loud which address they expect to work from where.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Practice Bank: General Troubleshooting Operations
|
||||
|
||||
**Kubernetes Health Probes & Troubleshooting** and **The RootNode Troubleshooting Framework**
|
||||
already taught you how to *think* about a broken cluster. This module is six short Labs that
|
||||
drill the individual `kubectl` operations you'll reach for while doing that thinking: pulling
|
||||
logs, copying files in and out of a container, running a one-off exec, cordoning a node, and
|
||||
reasoning about pod-to-node placement and log output.
|
||||
|
||||
## How to use it
|
||||
|
||||
Work through the six Labs in order. None of them require the full Observe/Evidence/Hypothesis/
|
||||
Test/Repair/Validate/Prevent framework from the previous module — they're deliberately narrower,
|
||||
so you can build raw command fluency without also holding the whole framework in your head at
|
||||
the same time.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Delivery:** No lesson/demonstration/knowledge-check scaffolding by design — this is the
|
||||
original Phase 1-5 "troubleshooting" scenario bank plus one extra Lab (`container-logging-mcq`)
|
||||
not part of that original set, reused here as self-paced speed practice.
|
||||
|
||||
**Capstone note:** The five original Labs (`extract-logs-basics`, `kubectl-cp-basics`,
|
||||
`exec-command-basics`, `node-cordon-basics`, `pod-node-assignment-mcq`) are the exact set the
|
||||
pre-existing `rootnode-path.json` "troubleshooting" stage requires to unlock the Capstone
|
||||
assessment — a separate, older mechanism from this course's own prerequisite chain (see
|
||||
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
|
||||
re-checking that gate.
|
||||
|
||||
**Common mistake to watch for:** running `kubectl cp` with the source/destination arguments
|
||||
reversed — a good moment to have a student read the command back out loud before running it.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Phase 6F — ROOT_ V1 Curriculum Integration & Pilot Readiness
|
||||
|
||||
Phase 6F's mission: not more curriculum breadth, but integration, polish, and full-program QA —
|
||||
answering, with evidence, "could RootNode Academy responsibly enroll a real student into ROOT_ V1
|
||||
and take them through the intended learning journey?"
|
||||
|
||||
## Baseline verification
|
||||
|
||||
Confirmed at start: branch `main`, clean tree, HEAD = `origin/main` = Phase 6E's
|
||||
`87ad1251ce4aee8597ef29a446e44b1247d68b1c`. Content validation: 0 errors, 13 courses, 48
|
||||
modules, 312 activities, 126 scenario files — matching Phase 6E's own final numbers exactly.
|
||||
|
||||
## ROOT_ V1 scope — defined in `docs/ROOT_V1_CURRICULUM_MANIFEST.md`
|
||||
|
||||
12 of 13 existing courses are in V1 (all genuinely READY except ROOT 11, intentionally PARTIAL as
|
||||
a single bonus orientation module). ROOT 08 is excluded (`visibility=draft`). See that document
|
||||
for the full course-by-course table, and `docs/ROOT_V1_PREREQUISITE_MAP.md` /
|
||||
`docs/ROOT_V1_ASSESSMENT_MAP.md` for the prerequisite graph and assessment coverage.
|
||||
|
||||
## The central integration finding: three "instructor-led-only" modules are load-bearing for the legacy Capstone gate
|
||||
|
||||
Auditing Phase 6E's own flagged limitation (three bare practice-bank modules) against the
|
||||
pre-existing `rootnode-path.json` Capstone `unlock_rule` found their scenario_ids are not
|
||||
generic extra practice — they are the *literal* content of three of its six gating stages. An
|
||||
initial attempt to reclassify them `is_required=false` was caught and reverted before commit
|
||||
(see `030_phase6f_capstone_gate_correction.sql`'s own header) once this was understood, in favor
|
||||
of giving each a short, real intro-lesson activity instead — closing the self-paced gap without
|
||||
touching the legacy gate's actual requirements. Full detail in
|
||||
`docs/ROOT_V1_CURRICULUM_MANIFEST.md`.
|
||||
|
||||
A second, similar discovery came out of the end-to-end rehearsal (see below): the SAME legacy
|
||||
gate also requires two scenarios (`services-mcq`, and all 5 of `root-07-m02`'s configuration
|
||||
scenarios) that are not course-content-model prerequisites of anything on the path to
|
||||
`root-capstone`. A student could reach "root-capstone unlocked" at the course layer while the
|
||||
underlying assessment start route still 403s for an unrelated reason. **This is a real,
|
||||
pre-existing gap, not introduced by Phase 6F** — flagged as a known limitation below rather than
|
||||
fixed, since closing it is a real product decision (add root-07 as a course-layer Capstone
|
||||
prerequisite, or extend the legacy stage list) that Phase 6F's mandate doesn't unilaterally make.
|
||||
|
||||
## Course-level status audit correction
|
||||
|
||||
`courses.status` had drifted stale for ROOT 05, 06, 07, 09, 10, 12, and Capstone — every module
|
||||
in each had already reached real `ready` content in an earlier phase, but the course-level field
|
||||
was never bumped. Corrected in `029_phase6f_root_v1_integration.sql`, verified against the live
|
||||
module-level data, not assumed from prior phase reports.
|
||||
|
||||
## Capstone gate strengthened (Step 7)
|
||||
|
||||
`root-capstone.prerequisites` now also requires `root-09-kubernetes-security`, alongside the
|
||||
pre-existing `root-12-real-ops` — the Capstone's own scenario pool has zero security-scenario
|
||||
overlap, and ROOT 09 only reached genuine READY status this same phase. The pre-existing
|
||||
six-stage legacy gate was deliberately left unchanged. See `docs/ROOT_CAPSTONE_READINESS.md` §1b
|
||||
and `031_phase6f_capstone_security_prerequisite.sql`.
|
||||
|
||||
## ROOT_ V1 program + Lab orientation (new)
|
||||
|
||||
No orientation content existed anywhere before this phase. Added `root-01-m00-welcome-to-root`
|
||||
— a new required front-door module (program overview, the Learn/Practice/Lab/Troubleshoot/
|
||||
Validate loop, self-paced vs. instructor-led, assessments/Capstone, and a full Lab-lifecycle
|
||||
orientation covering disposability, hints, reset, and help-requests) — sequenced before what was
|
||||
previously ROOT 01's first module. See `032_phase6f_root_v1_orientation.sql`.
|
||||
|
||||
## Cascading regression fallout from these real changes
|
||||
|
||||
Every genuine content/schema change this phase (the orientation module, the three practice-bank
|
||||
intro lessons, the new Capstone prerequisite) shifted real completion sequences that Phase 6A-6E's
|
||||
own e2e suites and rehearsal scripts had hardcoded. Found and fixed, each with the underlying gap
|
||||
named explicitly in a code comment: resume-pointer assertions in `e2e-phase6a/6b/6d/6e.js` and
|
||||
`test-phase6d-foundations-rehearsal.js`/`test-phase6e-foundations-rehearsal.js`; missing new
|
||||
intro-lesson completions in `foundations-helper.js`'s `completeThroughRoot06`/`completeThroughRoot10`
|
||||
and in `e2e-phase6a.js`/`e2e-phase6b.js`'s own inline completion sequences; a stale course-status
|
||||
assertion in `e2e-phase6b.js`; a draft-activity-visibility test in `e2e-phase6b.js` that had
|
||||
targeted the now-fully-excluded ROOT 08 (repointed at ROOT 09); and a new completion chain
|
||||
(root-07-m03/m04, all of ROOT 09) added to `e2e-phase6b.js`'s `bypass` student flow, now needed
|
||||
because `root-capstone` gates on ROOT 09 too. None were platform bugs — every one was a stale
|
||||
test assumption the newly-added/changed content revealed, the same pattern Phase 6E established.
|
||||
|
||||
## Testing
|
||||
|
||||
`platform/test/e2e-phase6f.js` — 17/17 passing (fresh stack): V1 manifest/status correctness,
|
||||
ROOT 08 exclusion (both DB-level and student-visibility), no cycles/missing prerequisite nodes,
|
||||
ROOT 01-04 re-verified ready, the new orientation module's unlock behavior, optional-activity
|
||||
handling under the full Capstone-eligible chain, the strengthened Capstone gate in both
|
||||
directions (locked with only ROOT 12 done; unlocked once ROOT 09 is also done; direct-API bypass
|
||||
still rejected), version-bump content stability, instructor-notes RBAC (including on locked, not
|
||||
just draft, content), cross-student isolation, full V1 course-list completeness for a fresh
|
||||
enrollment, logout/login persistence, and an explicit age-independence check (no age/grade column
|
||||
exists on `students`, enrollment never asks for one).
|
||||
|
||||
## Multi-student pilot rehearsal
|
||||
|
||||
`infra/scripts/drills/test-phase6f-root-v1-pilot.js` — 22/22 passing against a real, freshly-built
|
||||
stack: a real cohort + live classroom session with three students in genuinely different states —
|
||||
Student A on track (real manual-marker progression), Student B hitting a REAL Lab validation
|
||||
failure, sending a real Need Help request, the instructor seeing and acknowledging it live, then
|
||||
Student B genuinely fixing the failure with real shell commands and passing for real; Student C
|
||||
having their real disposable Lab reaped mid-task via a real admin destroy (simulating an idle-
|
||||
reaper event), confirming their academic progress survived intact, then recreating a fresh Lab
|
||||
and resuming the same task. Instructor visibility distinguishes all three states; cross-student
|
||||
isolation confirmed with real, not assumed, evidence (A's manual-marker progress vs. B's
|
||||
Lab-only progress).
|
||||
|
||||
## End-to-end ROOT_ V1 rehearsal
|
||||
|
||||
`infra/scripts/drills/test-phase6f-root-v1-end-to-end.js` — 14/14 passing: one real student,
|
||||
entirely through the real student API (no admin shortcut, no direct database completion write),
|
||||
progressing ROOT 01→02→03→04 (via the established foundations helper) →05→06→09→10→12→
|
||||
Capstone-eligible, with a genuine real-Lab checkpoint (a real nested-Docker container-networking
|
||||
diagnose-and-fix, re-proven live on this specific student rather than assumed from an earlier
|
||||
phase's rehearsal). Confirms direct API cannot skip ahead at the ROOT 06→09/10/12/Capstone
|
||||
boundary. Ends with the real `rootnode-capstone` assessment genuinely `201`-startable — the
|
||||
strongest possible proof the full V1 pathway is real, not merely "the course tree says complete."
|
||||
|
||||
## Regression
|
||||
|
||||
Full fresh-stack reruns after every fix: 6A (23/23), 6B (26/26), 6C (17/17), 6D (21/21), 6E
|
||||
(23/23), 6F (17/17) — all green, each run against its own freshly-reset stack per the established
|
||||
convention. `git diff` for this phase touches migrations/content/tests/docs only — no
|
||||
`platform/api/src` application-code change was needed (unlike Phase 6E, which needed exactly
|
||||
one). Phase 5A/5B/5C were not re-run: this phase's classroom/help-request/environment-destroy
|
||||
usage (in the multi-student rehearsal) exercises routes those suites already cover, and no code
|
||||
in `classroom.js`/`enrollment.js`/`notifications.js` was touched.
|
||||
|
||||
## Frontend, Lab image, nested Docker, k3s
|
||||
|
||||
Frontend: `npm run build` clean, no code changes this phase (content/schema only). Lab image:
|
||||
rebuilt for every fresh-stack test/rehearsal run this phase; standalone Lab boot and real bash
|
||||
task completion proven in the multi-student rehearsal (Students A/B/C); nested Docker proven in
|
||||
both the end-to-end rehearsal's real container-networking fix and the earlier ROOT 04 content
|
||||
itself; k3s health reconfirmed unaffected (the Phase 6E rehearsal's own k3s-health assertion was
|
||||
rerun fresh, post-Phase-6F-migrations, and still passes).
|
||||
|
||||
## Resource observations (development-environment only, not production capacity numbers)
|
||||
|
||||
Real Lab boot-to-ready during rehearsals: consistently well under the existing dev-stack norm
|
||||
established in Phase 6D/6E (no regression from added content — content changes don't touch Lab
|
||||
boot code). Three concurrent student Labs in the multi-student rehearsal (A idle, B and C active
|
||||
simultaneously) provisioned and ran without contention on a single dev host. No anomalies,
|
||||
crashes, or resource exhaustion observed. These are qualitative development-environment
|
||||
observations only — real capacity planning remains the dedicated production-server benchmark's
|
||||
job, unchanged from every prior phase's own caveat.
|
||||
|
||||
## New application code and why
|
||||
|
||||
None. Every V1 integration requirement (orientation content, practice-bank framing, the
|
||||
Capstone/ROOT 09 edge, the stale-status correction, the ROOT 08 exclusion) was satisfiable with
|
||||
the existing `courses`/`course_modules`/`module_activities` schema and the existing `visibility`/
|
||||
`status`/`is_required`/`prerequisites` mechanisms — consistent with this phase's own "integrate,
|
||||
don't build" mandate.
|
||||
|
||||
## Documentation
|
||||
|
||||
New: `ROOT_V1_CURRICULUM_MANIFEST.md`, `ROOT_V1_PREREQUISITE_MAP.md`, `ROOT_V1_ASSESSMENT_MAP.md`,
|
||||
`ROOT_V1_SYLLABUS.md`, `ROOT_V1_INSTRUCTOR_GUIDE.md`, `ROOT_V1_STUDENT_REQUIREMENTS.md`,
|
||||
`ROOT_V1_INSTRUCTOR_LED_PILOT.md`, `WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`, this document.
|
||||
Updated: `ROOT_PROGRAM_ARCHITECTURE.md` (course-sequence table, prerequisite-chain notes),
|
||||
`ROOT_PILOT_CURRICULUM.md` (self-paced-readiness correction), `ROOT_PROGRAM_DURATION_MODEL.md`
|
||||
(real post-integration totals), `ROOT_CAPSTONE_READINESS.md` (§1b, the new third gate).
|
||||
Reviewed, no change needed: `ROOT_FOUNDATIONS_PILOT_READINESS.md`, `COURSE_AUTHORING_STANDARD.md`,
|
||||
`ENGINEERING_SOP.md`, `REMOTE_AND_SELF_PACED_ARCHITECTURE.md`, `platform/README.md`'s
|
||||
architecture sections (its test list was updated — see below).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **The legacy six-stage Capstone unlock_rule and the course-layer prerequisite chain are not
|
||||
fully congruent**: `root-07-m01`'s `services-mcq` and all 5 of `root-07-m02`'s configuration
|
||||
scenarios gate the legacy assessment-start route but are not course-layer prerequisites of
|
||||
anything on the path to `root-capstone`. Found via this phase's own end-to-end rehearsal;
|
||||
documented, not fixed — closing it is a real product decision (extend the legacy stage list,
|
||||
or add ROOT 07 as a course-layer Capstone prerequisite) outside a "polish and integrate" phase.
|
||||
- ROOT 11's remaining six modules (CI/CD, GitOps, IaC) require lab infrastructure not yet built —
|
||||
unchanged, explicitly out of scope again this phase.
|
||||
- Instructor-led vs. self-paced timing remains prose in Instructor Notes, not a database field —
|
||||
unchanged from every prior phase.
|
||||
- No placement testing or instructor-override UI — explicitly out of scope; the age-independence
|
||||
requirement (Step 29) is satisfied architecturally (no age/grade column, no age-based gate
|
||||
anywhere in enrollment) rather than through a placement mechanism, since none was authorized.
|
||||
|
||||
## Recommended Phase 7 scope
|
||||
|
||||
Phase 7 is explicitly the business/product architecture layer per this phase's own authorization
|
||||
— formalizing the INIT_/COMPILE_/ROOT_/BOOTCAMP_ family model, delivery-mode separation from
|
||||
curriculum, and (informed by `docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`) whatever camp/
|
||||
registration/payment platform work the business decides to build around ROOT_ V1's now-proven
|
||||
Option A (Linux & Systems Foundations Camp) as a first commercial offering.
|
||||
@@ -27,6 +27,19 @@ This module derives its own completion the same way every other lesson-only modu
|
||||
capstone assessment engine checks before allowing a start (see §3 below for why these two gates
|
||||
are deliberately independent).
|
||||
|
||||
## 1b. A third, independent gate added in Phase 6F
|
||||
|
||||
Phase 6F added `root-09-kubernetes-security` to `root-capstone`'s own `courses.prerequisites`
|
||||
array (alongside the pre-existing `root-12-real-ops`) — a course-content-model-level gate,
|
||||
distinct from both this readiness module (still non-gating) and the curriculum-path
|
||||
`unlock_rule` in §3 below (still unchanged: six K8s scenario stages, no security stage). This
|
||||
means the `root-capstone` COURSE itself (visible in the student/instructor course tree) is now
|
||||
locked until ROOT 09 is also complete, even though the underlying `/api/assessments/rootnode-
|
||||
capstone/start` route's own gate did not change. See `docs/ROOT_V1_CURRICULUM_MANIFEST.md`
|
||||
Step 7 for the full evidence and reasoning, and `031_phase6f_capstone_security_prerequisite.sql`
|
||||
for the migration. This adds a third independent layer rather than blurring the existing two —
|
||||
consistent with this document's own §3 reasoning for keeping enforcement layers separate.
|
||||
|
||||
## 2. What was NOT built
|
||||
|
||||
No answers, no exact scenario IDs from the current pool, no capstone-specific hint content
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# ROOT Pilot Curriculum — Authoritative Manifest
|
||||
|
||||
**Updated for Phase 6F**: `docs/ROOT_V1_CURRICULUM_MANIFEST.md` is now the canonical technical
|
||||
definition of ROOT_ V1 scope (what's included/excluded and why). This document's own course-by-
|
||||
course pilot table below remains accurate but its Phase 6E-era claim about three practice-bank
|
||||
modules needing instructor-led delivery is now stale — see the correction immediately below.
|
||||
|
||||
This is the single authoritative answer to "what are we actually teaching in the first paid
|
||||
pilot." It uses honest READY / PARTIAL / DEFERRED / NOT IN PILOT labels — nothing here is
|
||||
marketed as more finished than it is. See
|
||||
@@ -7,17 +12,15 @@ marketed as more finished than it is. See
|
||||
means and [`docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md`](ROOT11_DEVOPS_PLATFORM_SCOPE.md) for the
|
||||
ROOT 11 scope decision referenced below.
|
||||
|
||||
## Pilot delivery mode: instructor-led (cohort-based), with a fully self-paced foundation
|
||||
## Pilot delivery mode: fully self-paced-capable as of Phase 6F
|
||||
|
||||
The first pilot is scoped as **instructor-led** as its recommended default, not because most of
|
||||
the curriculum requires it — as of Phase 6E, ROOT 01-04 (the entire foundational half) and every
|
||||
Kubernetes/security/troubleshooting/real-ops/capstone module EXCEPT three specific practice-bank
|
||||
modules meet the full self-paced bar (no "your instructor will explain this" gaps, verified
|
||||
programmatically). Those three flagged modules — dense hands-on practice banks whose sibling
|
||||
module in the same course already carries the taught concept — are READY for instructor-led
|
||||
delivery only; running them live lets an instructor supply framing a self-paced student wouldn't
|
||||
get. Fully self-paced delivery of those specific modules is deferred to a future phase pending an
|
||||
authored teaching layer for each. See Section 16 discussion below.
|
||||
As of Phase 6E, ROOT 01-04 (the entire foundational half) met the full self-paced bar. Three
|
||||
specific practice-bank modules (`root-06-m01`, `root-07-m01`, `root-10-m02`) were flagged as
|
||||
instructor-led-only — dense hands-on scenario banks with no lesson framing of their own.
|
||||
**Phase 6F closed this gap**: each received a short, real intro-lesson activity explaining what
|
||||
the bank is and how to use it (`docs/ROOT_V1_CURRICULUM_MANIFEST.md` Step 4) — all three, and
|
||||
their parent courses, are now genuinely self-paced-ready. Instructor-led delivery remains fully
|
||||
supported and recommended for a first cohort, but is no longer required by any content gap.
|
||||
|
||||
## Program: ROOT_ (root-kubernetes-foundations)
|
||||
|
||||
|
||||
@@ -29,15 +29,15 @@ prerequisite chain (01→02→03→04→05) enforced server-side.
|
||||
| 2 | Linux Administration & Networking | **READY** | Phase 6E: 6 modules, real Linux/networking scenarios, a wired assessment — see `docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md`. Real prerequisite: ROOT 01 complete. |
|
||||
| 3 | Git & Collaborative Development | **READY** | Phase 6E: 6 modules, real local-Git scenarios (including a real merge conflict and a real local-bare-remote push), a wired assessment. Real prerequisite: ROOT 02 complete. |
|
||||
| 4 | Containers & Docker | **READY** | Phase 6E: 8 modules using the nested Docker Engine (built and verified Phase 6D, `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §3), real container scenarios, a wired assessment. Real prerequisite: ROOT 03 complete. |
|
||||
| 5 | Kubernetes Foundations | **PARTIAL** | Phase 6B added a READY front-door module (`root-05-m00-kubernetes-mental-model` — full lesson/demo/knowledge-check/guided-exercise/lab/reflection) sequenced before the existing 20-scenario bare-lab bank (`root-05-m01`), which now sits behind it. |
|
||||
| 6 | Kubernetes Application Workloads | **PARTIAL** | Phase 6B added a READY `root-06-m02-deployments-scaling-updates` and a PARTIAL `root-06-m03-jobs-cronjobs-daemonsets`, both in front of the existing bare-lab bank. |
|
||||
| 7 | Kubernetes Networking & Configuration | **PARTIAL** | Phase 6B added a READY `root-07-m03-services-and-service-discovery` and a PARTIAL `root-07-m04-ingress-networkpolicy-foundations`. The existing configuration bank (`root-07-m02`) still has no authored teaching content — an honest, documented gap. |
|
||||
| 8 | Kubernetes Storage & Scheduling | **PARTIAL** | Phase 6B wired the real scenario content the inventory identified (PV/PVC, StorageClass, taints/tolerations, PDB, quotas/limits) into two new PARTIAL modules — moved off PLANNED for the first time. |
|
||||
| 9 | Kubernetes Security | **PARTIAL** | Phase 6B built the 22-scenario CKS-style bank into four modules (Security Model & SecurityContext — READY; ServiceAccounts & RBAC, Network & Supply Chain Security, Cluster Hardening & Runtime — all PARTIAL), classifying scenarios CORE/GUIDED/INDEPENDENT/ADVANCED/ASSESSMENT-BANK rather than exposing them as 22 disconnected tasks — moved off PLANNED. |
|
||||
| 10 | Kubernetes Troubleshooting & Observability | **PARTIAL** | The Phase 6A gold-standard module (READY, unmodified) plus a new PARTIAL `root-10-m03-troubleshooting-framework` (the generalized Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework) and the existing bare-lab bank, now sequenced last. |
|
||||
| 11 | DevOps & Platform Engineering Foundations | **PLANNED** | No CI/CD/IaC/GitOps scenario content exists anywhere in this repository — unchanged, out of Phase 6B's authoring scope. |
|
||||
| 12 | RootNode Real Ops | **PARTIAL** | Phase 6B added an incident-response framing lesson and a post-incident reflection wrapping the existing 4-scenario module — the incidents themselves are unchanged, still deliberately un-guided. |
|
||||
| 13 | ROOT Capstone — Operate and Recover a Production-Like Environment | **PARTIAL** | Phase 6B added a READY Capstone Readiness module (orientation, no answers) sequenced before the existing capstone assessment module — see `docs/ROOT_CAPSTONE_READINESS.md`. |
|
||||
| 5 | Kubernetes Foundations | **READY** (Phase 6F) | Phase 6B added a READY front-door module (`root-05-m00-kubernetes-mental-model`) sequenced before the 20-scenario bare-lab bank (`root-05-m01`), which itself already carried a lesson+reflection wrapper — Phase 6F's audit found both modules genuinely ready and corrected the stale course-level status. |
|
||||
| 6 | Kubernetes Application Workloads | **READY** (Phase 6F) | Phase 6B's READY `root-06-m02` and `root-06-m03` remain unchanged; Phase 6F gave the practice-bank `root-06-m01` a real intro-lesson activity, closing the last gap. |
|
||||
| 7 | Kubernetes Networking & Configuration | **READY** (Phase 6F) | Phase 6C already converted `root-07-m02` to a full teaching module; Phase 6F gave the practice-bank `root-07-m01` a real intro-lesson activity, closing the last gap. |
|
||||
| 8 | Kubernetes Storage & Scheduling | PARTIAL — **excluded from ROOT_ V1** | Phase 6B's two PARTIAL modules are unchanged; Phase 6F excluded the whole course from ROOT_ V1 (`visibility=draft`) since nothing downstream requires it — see `docs/ROOT_V1_CURRICULUM_MANIFEST.md`. |
|
||||
| 9 | Kubernetes Security | **READY** (Phase 6F) | Phase 6B's four modules (all real CKS-style content) were already individually ready; Phase 6F's audit corrected a stale course-level status field. Now also a required Capstone prerequisite (Step 7). |
|
||||
| 10 | Kubernetes Troubleshooting & Observability | **READY** (Phase 6F) | The Phase 6A gold-standard module and Phase 6C's `root-10-m03` remain unchanged; Phase 6F gave the practice-bank `root-10-m02` a real intro-lesson activity, closing the last gap. |
|
||||
| 11 | DevOps & Platform Engineering Foundations | **PARTIAL, by design** | One real orientation module exists (lesson/knowledge-check/reflection); Phase 6F repositioned it explicitly as an optional bonus module, not a promise of a full course — see `docs/ROOT_V1_CURRICULUM_MANIFEST.md`. Deeper CI/CD/IaC/GitOps content remains unbuilt, unchanged. |
|
||||
| 12 | RootNode Real Ops | **READY** (Phase 6F) | Phase 6B's framing lesson/reflection wrapping the existing 4-scenario module was already complete; Phase 6F's audit corrected a stale course-level status. |
|
||||
| 13 | ROOT Capstone — Operate and Recover a Production-Like Environment | **READY** (Phase 6F) | Phase 6B's Capstone Readiness module was already complete; Phase 6F's audit corrected a stale course-level status and added ROOT 09 as a joint course-level prerequisite alongside ROOT 12 — see `docs/ROOT_CAPSTONE_READINESS.md` §1b. |
|
||||
|
||||
No course is marked READY at the course level in Phase 6A — that's honest: a course only earns
|
||||
READY once every module in it has real teaching content, and Phase 6A deliberately built exactly
|
||||
@@ -80,6 +80,14 @@ real student, verified live on both a fully-completed student and a fresh one. A
|
||||
gap in the direct-API completion route (found while proving this chain, unrelated to any single
|
||||
phase's own content) is also fixed this phase — see that document for detail.
|
||||
|
||||
**Updated for Phase 6F** (`docs/ROOT_V1_CURRICULUM_MANIFEST.md`): ROOT 05, 06, 07, 09, 10, 12,
|
||||
and Capstone all reached genuine course-level READY status (a stale-status audit correction plus
|
||||
real intro-lesson authoring for three practice-bank modules). ROOT 08 was excluded from ROOT_ V1
|
||||
(`visibility=draft`). ROOT 11 stays PARTIAL by design, repositioned as an optional orientation
|
||||
bonus. `root-capstone`'s own prerequisites now also require ROOT 09 (Security) complete,
|
||||
alongside the pre-existing ROOT 12 requirement. A new required orientation module
|
||||
(`root-01-m00-welcome-to-root`) now precedes ROOT 01's original first module.
|
||||
|
||||
## Deviations from the spec's suggested sequence, and why
|
||||
|
||||
The authorization's suggested sequence listed Security (09) after Storage/Scheduling (08) and
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
# ROOT_ Program Duration Model
|
||||
|
||||
Phase 6E §28 — recalculated from actual authored activities in the live database. Run:
|
||||
`SELECT c.course_id, SUM(a.estimated_duration_minutes) ... GROUP BY c.course_id` against the
|
||||
current schema, same method as Phase 6D's original version of this document.
|
||||
Phase 6F Step 25 (built on Phase 6E §28) — recalculated from actual authored activities in the
|
||||
live database after Phase 6F's curriculum integration (orientation module + the three
|
||||
practice-bank intro lessons). Run: `SELECT c.course_id, SUM(a.estimated_duration_minutes) ...
|
||||
GROUP BY c.course_id` against the current schema, same method as every prior version of this
|
||||
document.
|
||||
|
||||
## Per-course totals (self-paced estimate)
|
||||
|
||||
| Course | Activities | Minutes | Hours |
|
||||
|---|---|---|---|
|
||||
| ROOT 01 Computing & Linux Foundations | 35 | 338 | 5.6 |
|
||||
| ROOT 02 Linux Administration & Networking | 37 | 388 | 6.5 |
|
||||
| ROOT 03 Git & Collaborative Development | 31 | 312 | 5.2 |
|
||||
| ROOT 04 Containers & Docker | 43 | 444 | 7.4 |
|
||||
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 |
|
||||
| ROOT 06 Kubernetes Application Workloads | 22 | 295 | 4.9 |
|
||||
| ROOT 07 Kubernetes Networking & Configuration | 27 | 353 | 5.9 |
|
||||
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 |
|
||||
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 |
|
||||
| ROOT 10 Kubernetes Troubleshooting & Observability | 20 | 277 | 4.6 |
|
||||
| ROOT 11 DevOps & Platform Engineering | 3 | 35 | 0.6 |
|
||||
| ROOT 12 Real Ops | 6 | 115 | 1.9 |
|
||||
| ROOT Capstone | 5 | 120 | 2.0 |
|
||||
| **Total (all authored content)** | **312** | **4,075** | **67.9** |
|
||||
| Course | Activities | Minutes | Hours | In ROOT_ V1? |
|
||||
|---|---|---|---|---|
|
||||
| ROOT 01 Computing & Linux Foundations | 38 | 353 | 5.9 | Yes |
|
||||
| ROOT 02 Linux Administration & Networking | 37 | 388 | 6.5 | Yes |
|
||||
| ROOT 03 Git & Collaborative Development | 31 | 312 | 5.2 | Yes |
|
||||
| ROOT 04 Containers & Docker | 43 | 444 | 7.4 | Yes |
|
||||
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 | Yes |
|
||||
| ROOT 06 Kubernetes Application Workloads | 23 | 299 | 5.0 | Yes |
|
||||
| ROOT 07 Kubernetes Networking & Configuration | 28 | 356 | 5.9 | Yes |
|
||||
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 | **No — excluded** |
|
||||
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 | Yes (required for Capstone) |
|
||||
| ROOT 10 Kubernetes Troubleshooting & Observability | 21 | 280 | 4.7 | Yes |
|
||||
| ROOT 11 DevOps & Platform Engineering (orientation only) | 3 | 35 | 0.6 | Yes (optional bonus) |
|
||||
| ROOT 12 Real Ops | 6 | 115 | 1.9 | Yes (required for Capstone) |
|
||||
| ROOT Capstone | 5 | 120 | 2.0 | Yes |
|
||||
| **Total (all authored content, incl. excluded ROOT 08)** | **318** | **4,100** | **68.3** | |
|
||||
| **ROOT_ V1 total (excludes ROOT 08)** | **303** | **3,850** | **64.2** | |
|
||||
|
||||
**ROOT Foundations (01-04) subtotal: 146 activities, 1,482 minutes, 24.7 hours.** This is now
|
||||
real, measured data — Phase 6D's own version of this document correctly flagged the eventual
|
||||
"20-25 hour" range as a planning ESTIMATE pending real authorship; the actual total (24.7 hours)
|
||||
landed inside that estimated range.
|
||||
ROOT 01's activity/minute count grew by 3 activities / 15 minutes this phase (the new
|
||||
`root-01-m00-welcome-to-root` orientation module). ROOT 06/07/10 each grew by 1 activity / 3-4
|
||||
minutes (their new practice-bank intro lessons). See `docs/ROOT_V1_CURRICULUM_MANIFEST.md` for
|
||||
why.
|
||||
|
||||
**ROOT Foundations (01-04) subtotal: 149 activities, 1,497 minutes, 25.0 hours.**
|
||||
|
||||
## Self-paced vs. instructor-led
|
||||
|
||||
As in Phase 6D's version of this document: the figures above are self-paced estimates (the sum
|
||||
of each activity's own `estimated_duration_minutes`). Instructor-led classroom time is still not
|
||||
a separate summable database field — every Instructor Notes section documents approximate live-
|
||||
facilitation timing in prose (e.g. ROOT 02 Module 3's "25 minutes read; 15-20 minutes live with
|
||||
discussion — longer than a typical lesson, deliberately"), but this remains per-activity
|
||||
guidance embedded in Markdown, not a column a query can sum. A future phase could add a distinct
|
||||
Unchanged from Phase 6D/6E: the figures above are self-paced estimates (the sum of each
|
||||
activity's own `estimated_duration_minutes`). Instructor-led classroom time is still not a
|
||||
separate summable database field — every Instructor Notes section documents approximate live-
|
||||
facilitation timing in prose. A future phase could add a distinct
|
||||
`estimated_duration_minutes_instructor_led` column if precise classroom scheduling becomes a
|
||||
real, measured need.
|
||||
|
||||
As a conservative approximation, instructor-led delivery of the full 67.9-hour curriculum should
|
||||
be expected to run somewhat longer once live discussion and demonstration time is added —
|
||||
consult the relevant Instructor Notes before scheduling a live cohort session, don't treat the
|
||||
self-paced total as a classroom-time promise.
|
||||
real, measured need. See `docs/ROOT_V1_INSTRUCTOR_LED_PILOT.md` for the cohort packaging this
|
||||
duration model supports.
|
||||
|
||||
## What this is NOT
|
||||
|
||||
@@ -51,12 +50,10 @@ Not a marketing duration, not a promise, not a certification-hours figure.
|
||||
`docs/COURSE_AUTHORING_STANDARD.md` §4) — this total will get more accurate as real completion-
|
||||
time data accumulates from actual students, not before.
|
||||
|
||||
## Program completeness
|
||||
## Program completeness — ROOT_ V1 vs. the full authored program
|
||||
|
||||
With ROOT 01-04 now authored, the ROOT_ program's structured curriculum (excluding ROOT 08,
|
||||
deliberately deferred as non-capstone-gating, and ROOT 11 modules 2-7, deferred pending future
|
||||
lab infrastructure — see `docs/ROOT_PILOT_CURRICULUM.md`) now runs a complete, measured
|
||||
beginning-to-advanced path: Linux fundamentals → Linux administration/networking → Git →
|
||||
containers → Kubernetes → security → troubleshooting → real operations → capstone. This is the
|
||||
first phase where the FULL foundational-to-capstone path has real, authored content and a real
|
||||
measured duration at every step, not a partial curriculum starting mid-stack.
|
||||
ROOT_ V1 (`docs/ROOT_V1_CURRICULUM_MANIFEST.md`) is 64.2 of the program's 68.3 authored hours —
|
||||
everything except ROOT 08, deliberately excluded as non-Capstone-gating and not yet authored to
|
||||
V1 standard. ROOT 11's remaining six modules were never counted in either total (they don't
|
||||
exist yet, unlike ROOT 08's PARTIAL-but-real content) — see `docs/ROOT_CURRICULUM_COVERAGE_
|
||||
MATRIX.md` for the honest READY/PARTIAL/PLANNED breakdown across the entire program.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# ROOT_ V1 Assessment Map
|
||||
|
||||
Phase 6F Step 8 — every formal assessment in ROOT_ V1, what it actually tests, and how it
|
||||
relates to course completion. Formative checks (knowledge checks, guided exercises) exist inside
|
||||
every module and are not separately catalogued here — see `docs/COURSE_CONTENT_MODEL.md` for
|
||||
how they're modeled.
|
||||
|
||||
| Course | Assessment | Required scenarios | Random pool | Hints | Duration |
|
||||
|---|---|---|---|---|---|
|
||||
| ROOT 01 | `rootnode-linux-foundations-assessment` | linux-shell-basics, linux-permissions-foundations | 2 from a 4-task pool + 2 MCQ | disabled | 30 min |
|
||||
| ROOT 02 | `rootnode-linux-networking-assessment` | linux-network-inspection, linux-troubleshooting-framework | 2 from a 4-task pool + 2 MCQ | disabled | 35 min |
|
||||
| ROOT 03 | `rootnode-git-fundamentals-assessment` | git-everyday-basics, git-branching-and-merge | 1 task + 2 MCQ | disabled | 35 min |
|
||||
| ROOT 04 | `rootnode-containers-assessment` | docker-cli-basics, docker-build-image | 2 from a 3-task pool + 2 MCQ | disabled | 40 min |
|
||||
| ROOT 09 | `rootnode-security-assessment` | cks-pod-security-context, cks-rbac-least-privilege, cks-network-policy | 2+1+2 across three CKS-style pools | disabled | 60 min |
|
||||
| — | `rootnode-capstone` | rootnode-runaway-monitor, broken-deployment, rootnode-probe-port-mismatch, rootnode-enforce-limits | 1 each from workloads/config/troubleshooting pools | flagged (visible but logged) | 90 min |
|
||||
| — | `rootnode-quick-assessment` | none | draws from workloads/networking/troubleshooting/real-ops pools | disabled | 60 min |
|
||||
|
||||
`rootnode-quick-assessment` is a pre-existing, general Kubernetes proficiency check not wired to
|
||||
any single course's completion — it predates the course-content-model layer and is left as-is
|
||||
(unaffected by this phase, still reachable via `/api/assessments/rootnode-quick-assessment/start`
|
||||
for practice/diagnostic use, e.g. by an instructor assessing a transfer student's baseline).
|
||||
|
||||
## Relationship to course completion
|
||||
|
||||
Every course-gating assessment (ROOT 01/02/03/04/09) is wired to exactly one `assessment`-type
|
||||
activity (`completion_behavior = 'auto_assessment'`) inside that course's last module. Passing
|
||||
it is derived the same way a scenario Lab's completion is derived — from a real
|
||||
`achievements` row written on a real pass — never a second, independently-tracked flag
|
||||
(`COURSE_CONTENT_MODEL.md` §4).
|
||||
|
||||
## What's a real, server-authoritative draw vs. fixed
|
||||
|
||||
Every assessment's `random` pool is drawn fresh per attempt, server-side, at `POST /api/
|
||||
assessments/:id/start` — confirmed for ROOT 02's pool specifically in `e2e-phase6e.js`
|
||||
("ROOT 02 assessment pool matches the documented design") and for the security assessment in
|
||||
`e2e-phase6c.js` ("two independent students each get their own 9-scenario draw, and it is
|
||||
server-authoritative"). `required` scenarios are always included, every attempt, for every
|
||||
student — they are the non-negotiable floor of what "passing this course's assessment" means.
|
||||
|
||||
## Coverage gaps and redundancy, evaluated honestly
|
||||
|
||||
- **No assessment gap found** in the V1 required sequence: every required course (01-04, 09) has
|
||||
one, sized to its content (30-60 min matching course depth), and none were added merely for
|
||||
symmetry — `ROOT_PILOT_CURRICULUM.md`'s existing per-course rationale is unchanged this phase.
|
||||
- **ROOT 05, 06, 07, 10, 12 have no formal timed assessment** — their real hands-on Labs plus the
|
||||
Capstone's own scenario pool (drawn partly from workloads/configuration/troubleshooting
|
||||
content) already exercise this material under timed, graded conditions at the end of the
|
||||
program. Adding a redundant mid-program timed exam for each would test the same skill twice
|
||||
without new instructional purpose — deliberately not built.
|
||||
- **`rootnode-quick-assessment` and `rootnode-capstone` share scenario pools** with several
|
||||
course-level Labs (by original Phase 1-5 design, unchanged) — this is intentional reuse
|
||||
(`ROOT_CURRICULUM_COVERAGE_MATRIX.md`'s documented "scenario reused across activities" list),
|
||||
not accidental duplication.
|
||||
@@ -0,0 +1,138 @@
|
||||
# ROOT_ V1 Curriculum Manifest
|
||||
|
||||
Phase 6F — the canonical technical definition of what ROOT_ V1, the first sellable version of
|
||||
the ROOT_ program, actually includes. Where this disagrees with an older doc (`ROOT_PILOT_
|
||||
CURRICULUM.md`, `ROOT_PROGRAM_ARCHITECTURE.md`), this document is the current source of truth;
|
||||
those documents have been updated to point back here.
|
||||
|
||||
## V1 course list
|
||||
|
||||
| # | Course | In V1? | Status | Required? | Prerequisite(s) | Duration (38-40 activities/course avg) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Computing & Linux Foundations | Yes | READY | Required | — | 353 min (38 activities) |
|
||||
| 2 | Linux Administration & Networking | Yes | READY | Required | ROOT 01 | 388 min (37) |
|
||||
| 3 | Git & Collaborative Development | Yes | READY | Required | ROOT 02 | 312 min (31) |
|
||||
| 4 | Containers & Docker | Yes | READY | Required | ROOT 03 | 444 min (43) |
|
||||
| 5 | Kubernetes Foundations | Yes | READY | Required | ROOT 04 | 403 min (28) |
|
||||
| 6 | Kubernetes Application Workloads | Yes | READY | Required | ROOT 05 | 299 min (23) |
|
||||
| 7 | Kubernetes Networking & Configuration | Yes | READY | Required | ROOT 06 | 356 min (28) |
|
||||
| 8 | Kubernetes Storage & Scheduling | **No** | PARTIAL | Excluded | — | 250 min (15) — excluded from V1 total |
|
||||
| 9 | Kubernetes Security | Yes | READY | Required for Capstone | ROOT 06 | 745 min (40, includes CKS-depth optional labs) |
|
||||
| 10 | Kubernetes Troubleshooting & Observability | Yes | READY | Required | ROOT 06 | 280 min (21) |
|
||||
| 11 | DevOps & Platform Engineering (orientation) | Yes | PARTIAL (by design) | Optional bonus | ROOT 10 (non-gating) | 35 min (3) |
|
||||
| 12 | RootNode Real Ops | Yes | READY | Required for Capstone | ROOT 10 | 115 min (6) |
|
||||
| — | ROOT Capstone | Yes | READY | Terminal | ROOT 12 AND ROOT 09 | 120 min (5) |
|
||||
|
||||
**ROOT_ V1 total: 3,850 minutes = 64.2 structured hours** (12 courses, excludes ROOT 08's 250
|
||||
min). Program-wide total including the excluded ROOT 08: 4,100 minutes = 68.3 hours. Full
|
||||
per-module breakdown in `docs/ROOT_PROGRAM_DURATION_MODEL.md`.
|
||||
|
||||
Every V1-included course's real per-module evidence (activity counts, lab list, assessment
|
||||
wiring) lives in `docs/ROOT_FOUNDATIONS_PILOT_READINESS.md` (ROOT 01-04) and `docs/
|
||||
ROOT_CURRICULUM_COVERAGE_MATRIX.md` (ROOT 05-13). This manifest is the scope decision; those
|
||||
documents are the evidence.
|
||||
|
||||
## ROOT 08 — excluded from V1 (Step 2)
|
||||
|
||||
ROOT 08 (Storage & Scheduling) exists with 2 PARTIAL modules (bare scenario banks, no lesson
|
||||
scaffolding) but critically **is not required by anything downstream** — ROOT 09 and ROOT 10
|
||||
both branch directly off ROOT 06 by design (see `ROOT_PROGRAM_ARCHITECTURE.md`'s "deviations
|
||||
from the suggested sequence"). Leaving it visible mid-sequence (course #8, between two READY
|
||||
courses) would look to a self-paced student like a course they skipped, when V1 was never
|
||||
designed to require it.
|
||||
|
||||
**Mechanism**: `visibility = 'draft'` on the course and both its modules — the existing hard
|
||||
gate (`COURSE_CONTENT_MODEL.md` §6), excluded from every student-facing query, still fully
|
||||
visible/manageable for instructors. No content was deleted; this is reversible the moment ROOT
|
||||
08 gets the same authoring pass ROOT 01-04 got.
|
||||
|
||||
## ROOT 11 — included, repositioned as orientation-only bonus (Step 3)
|
||||
|
||||
ROOT 11 has exactly one real, finished, self-paced-ready module (lesson + knowledge check +
|
||||
reflection) bridging "systems administration" to "platform engineering thinking." The other six
|
||||
designed modules (CI/CD, GitOps, IaC) require lab infrastructure not yet built and are
|
||||
explicitly out of Phase 6F's scope (per the authorization's own DO-NOT-BUILD list).
|
||||
|
||||
**Decision**: keep it in V1, but its course description was rewritten to say plainly that it is
|
||||
a single orientation module, not a full DevOps course, and that the remaining six are designed
|
||||
but not yet built. It stays `status = 'partial'` (honest) and `visibility = 'published'`
|
||||
(real, finished content worth keeping visible). Nothing downstream requires it — a student who
|
||||
skips it loses nothing gating-wise.
|
||||
|
||||
## The three instructor-led-only practice banks (Step 4)
|
||||
|
||||
`root-06-m01-deployments-and-workloads`, `root-07-m01-services-and-networking`, and
|
||||
`root-10-m02-general-troubleshooting` were flagged in Phase 6E as instructor-led-only: bare
|
||||
scenario banks with zero lesson/demonstration/knowledge-check framing.
|
||||
|
||||
**Critical finding before any fix was applied**: these three modules' scenario_ids are not
|
||||
generic supplemental practice — they are the *literal* content of the pre-existing
|
||||
`scenarios/rootnode-path.json` "workloads"/"networking"/"troubleshooting" stages, the older,
|
||||
separate mechanism that gates whether a student can even **start** the `rootnode-capstone`
|
||||
assessment. An initial attempt to reclassify them `is_required = false` (making them optional
|
||||
for course completion) was caught during this phase's own audit before being committed: it would
|
||||
have let a self-paced student reach a course-tree-"complete" state while remaining silently,
|
||||
confusingly blocked from the Capstone by the older stage gate. See `platform/api/migrations/
|
||||
030_phase6f_capstone_gate_correction.sql` for the full self-correction record.
|
||||
|
||||
**Actual fix (Option A, not B)**: each module kept its activities `is_required = true` and
|
||||
received one short intro-lesson activity (`*-a00-lab-bank-intro`) explaining what the bank is,
|
||||
why it has no per-scenario teaching, and how to use it — the minimum real authoring needed to
|
||||
be honestly self-paced-ready, not a reclassification. All three modules, and their parent
|
||||
courses (ROOT 06, 07, 10), now genuinely earn `status = 'ready'`.
|
||||
|
||||
## Course-level status audit correction (Step 0)
|
||||
|
||||
Auditing `courses.status` against actual module-level status (not trusting prior phase reports)
|
||||
found it had drifted stale for ROOT 05, 06, 07, 09, 10, 12, and Capstone — every module in each
|
||||
had already reached real `ready` content, but the course-level field was never bumped. Corrected
|
||||
in `029_phase6f_root_v1_integration.sql`; no new content was required for ROOT 05, 09, 12, or
|
||||
Capstone (already fully authored), only for 06/07/10 (the practice-bank intro lessons above).
|
||||
|
||||
## Capstone gate review (Step 7)
|
||||
|
||||
Two separate, independent gates protect the Capstone:
|
||||
|
||||
1. **The legacy stage-based gate** (`scenarios/rootnode-path.json`'s `unlock_rule`, checked by
|
||||
`POST /api/assessments/rootnode-capstone/start`): six K8s scenario stages (foundation,
|
||||
workloads, networking, configuration, troubleshooting, real-ops). **Left unchanged** this
|
||||
phase — it has no natural "security stage" without new scenario-bank authoring, out of scope.
|
||||
2. **The course-layer gate** (`courses.prerequisites` on `root-capstone`, checked by
|
||||
`buildCourseTree()`): previously `{root-12-real-ops}` only.
|
||||
|
||||
**Decision: strengthened.** `root-capstone.prerequisites` now also requires
|
||||
`root-09-kubernetes-security`. Evidence: the Capstone's own scenario pool
|
||||
(`assessment_definitions.pool_spec`) has zero overlap with the security assessment's CKS-style
|
||||
pool — a student could pass the Capstone having never touched a security scenario. ROOT 09 only
|
||||
reached genuine `ready` status this same phase, so requiring it any earlier would have gated on
|
||||
unfinished content. ROOT 09 and ROOT 12 are already sibling courses (both prerequisite = ROOT
|
||||
06), so this adds no new sequencing burden — either order, or in parallel. See `031_phase6f_
|
||||
capstone_security_prerequisite.sql`.
|
||||
|
||||
## Program + Lab orientation (Steps 9-11)
|
||||
|
||||
No orientation content existed anywhere before this phase (audited by grep across
|
||||
`content/root`). Added `root-01-m00-welcome-to-root`, a new required front-door module — the
|
||||
same pattern `root-05-m00` already established — with two lessons (program overview and Lab
|
||||
lifecycle) and a knowledge check, sequenced before what was previously ROOT 01's first module.
|
||||
See `032_phase6f_root_v1_orientation.sql`.
|
||||
|
||||
## ROOT_ V1 curriculum version (Step 26)
|
||||
|
||||
No second versioning system was built — this manifest reuses the existing per-row `version`
|
||||
integer (`courses`/`course_modules`/`module_activities`), unchanged in convention from
|
||||
`COURSE_CONTENT_MODEL.md` §6b. "ROOT_ V1" itself is a scope label (which course/module ids are
|
||||
included, defined by this document), not a new database concept. What's frozen for a historical
|
||||
enrollment is exactly what was already true pre-Phase-6F: a student's `activity_completions`
|
||||
row stays valid across a later semantic edit to the same `activity_id`, keyed by the stable id,
|
||||
not a snapshot. A future "V1.1" content correction follows the same rule `COURSE_CONTENT_MODEL.md`
|
||||
§6b already documents — bump `version` on the changed row for a semantic edit, skip it for a
|
||||
typo-only fix. A future V1 SCOPE change (adding/removing a course from the manifest) is a change
|
||||
to this document plus a `visibility`/`prerequisites` migration, exactly like ROOT 08's exclusion
|
||||
this phase — not a new mechanism to build.
|
||||
|
||||
## What this manifest deliberately does not cover
|
||||
|
||||
Pricing, registration, marketing copy, and camp scheduling are out of scope for this document —
|
||||
see `docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` and `docs/ROOT_V1_INSTRUCTOR_LED_PILOT.md`
|
||||
for delivery packaging, and Phase 7 for the business/product architecture layer.
|
||||
@@ -0,0 +1,111 @@
|
||||
# ROOT_ V1 Instructor Guide
|
||||
|
||||
Phase 6F Step 12 — a practical guide for teaching ROOT_ V1. This does not duplicate any
|
||||
individual module's own Instructor Notes (every lesson/lab content file ends with an
|
||||
`## Instructor Notes` section — facilitation tips, timing, common mistakes); it covers what
|
||||
spans the whole program instead.
|
||||
|
||||
## Program overview & audience
|
||||
|
||||
See `docs/ROOT_V1_SYLLABUS.md` for the full course sequence, outcomes, and duration. Audience is
|
||||
approximately age 15+ through adults — readiness (comfort with a keyboard, following multi-step
|
||||
instructions), not age, is the real gate.
|
||||
|
||||
## Using CurriculumView
|
||||
|
||||
The instructor curriculum view (`/admin`, Curriculum tab) shows every course/module/activity for
|
||||
a given student or cohort with the exact same lock/completion state the student sees — plus
|
||||
`instructor_notes` on every activity (never shown to students). Use it to see precisely where a
|
||||
student is stuck before a help request even comes in.
|
||||
|
||||
## Preparing Labs
|
||||
|
||||
Use **Prepare Labs** (classroom/cohort view) to provision every enrolled student's disposable
|
||||
Lab ahead of a session, rather than each student launching cold when class starts — this was
|
||||
built and load-tested in Phase 2B and confirmed to run concurrently across a full cohort.
|
||||
|
||||
## Classroom workflow
|
||||
|
||||
1. Prepare Labs before the session starts.
|
||||
2. Students work through the current module at their own pace; you see live progress per
|
||||
student in the classroom view.
|
||||
3. A student's **Need Help** request notifies you immediately with a category and message —
|
||||
respond in whatever way your delivery format supports (in person, chat, screen share).
|
||||
4. If a Lab environment itself seems broken for a student (not just the task being hard), an
|
||||
instructor can reset it the same way a student can (Reset Scenario & Environment) — this
|
||||
never touches the student's academic progress.
|
||||
5. At the end of a session, cohort Labs can be destroyed in bulk (build/reprovision cost, not an
|
||||
academic reset) — progress survives regardless (proven in Phase 2B/5B).
|
||||
|
||||
## Self-paced vs. instructor-led
|
||||
|
||||
The platform makes no code-level distinction between the two — the same courses, Labs, and
|
||||
assessments work identically either way. The difference is entirely in who else is watching:
|
||||
an instructor-led student additionally has a live instructor able to see their progress and
|
||||
receive help requests. Timing/pacing guidance for instructor-led delivery lives in each
|
||||
module's own Instructor Notes and in `docs/ROOT_V1_INSTRUCTOR_LED_PILOT.md`.
|
||||
|
||||
## Assessments & Capstone
|
||||
|
||||
Formal assessments (ROOT 01/02/03/04/09) and the Capstone are timed and hints-limited by design
|
||||
— see `docs/ROOT_V1_ASSESSMENT_MAP.md`. Do not pre-brief students on the exact random-pool draw;
|
||||
it's server-authoritative and drawn fresh per attempt specifically so this isn't possible to
|
||||
game, and pre-briefing undermines what the assessment is actually checking.
|
||||
|
||||
## Help-request workflow
|
||||
|
||||
`Need Help` categories exist to help you triage at a glance (e.g. `lab_problem` vs. a conceptual
|
||||
question) — treat a `lab_problem` report as potentially a real broken environment worth
|
||||
checking yourself before assuming the student made an error.
|
||||
|
||||
## Common student issues
|
||||
|
||||
- **"My Lab disappeared / lost my work"** — expected if a Lab was recreated; see the student
|
||||
Lab-orientation content (`root-01-m00-a02-lesson-disposable-lab`) and reassure them their
|
||||
academic progress is untouched.
|
||||
- **Confusing a module's optional activities for a blocker** — `is_required=false` activities
|
||||
(e.g. ROOT 09's advanced CKS labs) never block module/course completion; the platform will
|
||||
never require them, but some students assume more is mandatory than actually is.
|
||||
- **Getting to ROOT 09/10/12 out of order** — real, server-enforced module prerequisites inside
|
||||
a course (e.g. `root-06-m01`'s own practice bank requires `root-06-m02` first) sometimes
|
||||
surprise a student used to free-form navigation; this is intentional sequencing, not a bug.
|
||||
|
||||
## Lab reset/recreate — what it means
|
||||
|
||||
Resetting a Lab rebuilds the disposable environment from scratch; recreating one (e.g. after a
|
||||
Lab-host failure) does the same at the infrastructure level. Neither ever touches
|
||||
`activity_completions`, `progress`, or `achievements` — a student's actual academic record.
|
||||
|
||||
## What student progress actually means
|
||||
|
||||
Course/module/activity `complete` flags are computed live, every time, from real underlying
|
||||
facts (a real scenario pass, a real assessment pass, or an explicit manual marker for lesson-type
|
||||
content) — never a separately-maintained flag that could silently drift from reality. See
|
||||
`docs/COURSE_CONTENT_MODEL.md` §4.
|
||||
|
||||
## What not to do
|
||||
|
||||
- Don't manually flip a student's completion state via the admin API/database to "help them
|
||||
along" — it breaks the guarantee that a complete flag reflects real work, and the Phase 6E
|
||||
security fix means the platform will refuse a manual mark on a still-locked module anyway.
|
||||
- Don't rely on the legacy `rootnode-quick-assessment` as a course-completion signal for any
|
||||
V1 course — it's a general diagnostic tool, not wired to any course's own gate.
|
||||
- Don't assume ROOT 08 or ROOT 11's un-built modules are "coming soon this cohort" — they're
|
||||
explicitly out of V1 scope (see `docs/ROOT_V1_CURRICULUM_MANIFEST.md`); don't promise
|
||||
timelines the curriculum doesn't yet support.
|
||||
|
||||
## Security boundaries
|
||||
|
||||
Every student's Lab is an isolated, disposable, non-privileged-to-the-host container — even
|
||||
though it runs a real nested Docker Engine and a real k3s cluster inside itself. No student Lab
|
||||
can reach another student's Lab, the Lab host's own Docker daemon, or any RootNode credential.
|
||||
This is unchanged infrastructure from earlier phases (see `docs/LAB_ARCHITECTURE.md`,
|
||||
`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`) — instructors do not need to configure anything for
|
||||
this to hold.
|
||||
|
||||
## Troubleshooting/operations notes
|
||||
|
||||
If a whole cohort's Labs seem slow to provision, check Provisioner health before assuming a
|
||||
content problem — this program's content changes never affect Lab boot performance (see
|
||||
`docs/PHASE6F_ROOT_V1_PILOT_READINESS.md`'s resource observations for real numbers from this
|
||||
phase's own rehearsals).
|
||||
@@ -0,0 +1,40 @@
|
||||
# ROOT_ V1 Instructor-Led Pilot Plan
|
||||
|
||||
Phase 6F Step 19. Realistic delivery packaging from the ACTUAL curriculum duration
|
||||
(`docs/ROOT_PROGRAM_DURATION_MODEL.md`) — not a business/pricing decision, an
|
||||
operational/academic one.
|
||||
|
||||
## The three natural cohort groupings
|
||||
|
||||
The real prerequisite chain (`docs/ROOT_V1_PREREQUISITE_MAP.md`) already divides ROOT_ V1 into
|
||||
three coherent stretches, each a legitimate standalone cohort length:
|
||||
|
||||
| Cohort | Courses | Duration | Suggested cadence |
|
||||
|---|---|---|---|
|
||||
| **Foundations** | ROOT 01-04 | 1,497 min ≈ 25.0 hrs | 8-10 weeks at 2.5-3 hrs/week, or a 1-week intensive |
|
||||
| **Kubernetes Core** | ROOT 05-07, 10 | 1,338 min ≈ 22.3 hrs | 7-9 weeks, requires Foundations complete |
|
||||
| **Operate & Secure** | ROOT 09, 12, Capstone | 980 min ≈ 16.3 hrs | 5-6 weeks, requires Kubernetes Core complete |
|
||||
|
||||
**Full ROOT_ V1 pathway**: all three back to back, 64.2 hours total, realistically a
|
||||
20-24 week (one semester-plus) commitment at a sustainable 3 hrs/week pace — not a single
|
||||
short cohort. This mirrors how the content itself is sequenced (ROOT_PROGRAM_ARCHITECTURE.md's
|
||||
own course graph), not an arbitrary split.
|
||||
|
||||
## Weekend/intensive packaging
|
||||
|
||||
Any single cohort above also works compressed into a multi-day intensive (e.g. Foundations as a
|
||||
5-day, 5-hr/day intensive) — the content and Labs are identical; only pacing changes. This is
|
||||
exactly the reasoning behind `docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md`'s Option A, which is
|
||||
the Foundations cohort's first half (ROOT 01-02) sized for a single week.
|
||||
|
||||
## What doesn't change between formats
|
||||
|
||||
Server-enforced prerequisites, Lab lifecycle, assessment integrity, and instructor tooling
|
||||
(`docs/ROOT_V1_INSTRUCTOR_GUIDE.md`) are identical regardless of cadence — a cohort meeting
|
||||
weekly for 9 weeks and one meeting daily for 5 days use the exact same platform mechanics.
|
||||
|
||||
## What this document deliberately does not decide
|
||||
|
||||
Price, enrollment minimums/maximums, marketing, and specific calendar dates — packaging
|
||||
decisions belong to the people running a specific cohort, informed by real demand, not
|
||||
prescribed here.
|
||||
@@ -0,0 +1,56 @@
|
||||
# ROOT_ V1 Prerequisite Map
|
||||
|
||||
Phase 6F Step 6 — the full, real, server-enforced V1 prerequisite graph, generated from the live
|
||||
`courses`/`course_modules` tables (not hand-drawn), and the QA performed against it.
|
||||
|
||||
## Course-level graph
|
||||
|
||||
```
|
||||
ROOT 01 → ROOT 02 → ROOT 03 → ROOT 04 → ROOT 05 → ROOT 06 ─┬─→ ROOT 07
|
||||
├─→ ROOT 09 ─┐
|
||||
└─→ ROOT 10 ─┼─→ ROOT 12 → CAPSTONE
|
||||
│ (also requires
|
||||
ROOT 11 (bonus, │ ROOT 09 ─────┘
|
||||
non-gating, jointly)
|
||||
after ROOT 10)
|
||||
|
||||
ROOT 08 (Storage & Scheduling): excluded from V1 (visibility=draft). Its own prerequisite
|
||||
(ROOT 07-m03) still resolves if ever re-included, but nothing in V1 depends on it.
|
||||
```
|
||||
|
||||
CAPSTONE's real prerequisite array is `['root-12-real-ops', 'root-09-kubernetes-security']` —
|
||||
both required, order-independent (see Step 7 decision in `ROOT_V1_CURRICULUM_MANIFEST.md`).
|
||||
|
||||
## Module-level cross-course edge
|
||||
|
||||
`root-09-m03-network-and-supplychain-security`'s prerequisites include
|
||||
`root-07-m04-ingress-networkpolicy-foundations` (not just its own course's `root-09-m02`) — a
|
||||
real, intentional cross-course module dependency (network policy concepts build on Ingress
|
||||
concepts taught in ROOT 07). This is the one module-level edge that crosses a course boundary in
|
||||
the V1 graph.
|
||||
|
||||
## QA performed
|
||||
|
||||
- **No cycles**: `infra/scripts/validate-course-content.js`'s cycle-detection pass (course-level
|
||||
and module-level, run separately) — 0 errors, confirmed after every migration this phase.
|
||||
- **No missing nodes**: every `prerequisites` array entry resolves to a real course/module id —
|
||||
same validator, "every course/module prerequisite id resolves to a real course/module" check.
|
||||
- **No excluded course blocking V1 progression**: ROOT 08 is referenced by nothing (audited by
|
||||
grep across every course's and module's `prerequisites` array before excluding it) — its
|
||||
`visibility=draft` change cannot silently lock anything downstream.
|
||||
- **No hidden prerequisite through ROOT 08/11**: confirmed the same way — grepped for
|
||||
`root-08` and `root-11-m01` inside every other course/module's `prerequisites` array; zero
|
||||
hits in either direction.
|
||||
- **Server-side enforcement**: `buildCourseTree()`'s `prerequisitesSatisfied()` is the single
|
||||
computation both the student and instructor views read — proven end to end for the full ROOT
|
||||
01→02→03→04→05 chain in `e2e-phase6e.js` and for ROOT 06→09/10→12→Capstone in `e2e-phase6b.js`
|
||||
and `e2e-phase6f.js` (Step 31).
|
||||
- **Direct API cannot bypass**: `POST /api/student/activities/:id/complete` resolves the
|
||||
activity's own course+module through the same `buildCourseTree()` and 403s if either is
|
||||
locked (the Phase 6E fix) — re-verified this phase against the NEW root-capstone/root-09
|
||||
edge specifically (a direct attempt to mark a `root-capstone-m02` activity complete before
|
||||
root-09 is done is rejected; see `e2e-phase6f.js`).
|
||||
- **Optional activities never unexpectedly block progression**: `is_required=false` activities
|
||||
are excluded from a module's/course's "required" set (`totals()`); confirmed for ROOT 09's
|
||||
own optional CKS-advanced labs and ROOT 08's excluded (draft) content — neither can block a
|
||||
V1 course from reaching `complete=true`.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ROOT_ V1 Student Requirements
|
||||
|
||||
Phase 6F Step 14. RootNode's Labs run remotely — nothing computationally heavy needs to happen
|
||||
on a student's own device.
|
||||
|
||||
## Remote / self-paced learners
|
||||
|
||||
- **Browser**: any current Chromium-based browser (Chrome, Edge) or Firefox. No browser
|
||||
extensions or plugins required.
|
||||
- **Internet**: a stable broadband connection sufficient for a persistent web session and a
|
||||
terminal-like interface (similar bandwidth needs to a video call, generally lower).
|
||||
- **Device**: a laptop or desktop with a physical or usable on-screen keyboard. A
|
||||
**Chromebook is sufficient** — all Lab computation happens on RootNode's infrastructure, not
|
||||
the student's device.
|
||||
- **Tablets/phones**: not recommended as a primary device — the terminal-and-multi-pane
|
||||
interface assumes a real keyboard and a reasonably sized screen; nothing about it is
|
||||
technically blocked on a tablet, but it will be an uncomfortable experience.
|
||||
- **Local installation**: none. No Docker, no Kubernetes, no Git client needs to be installed
|
||||
locally — every Lab (including the nested Docker Engine and the Kubernetes cluster from ROOT
|
||||
05 onward) runs in RootNode's disposable, remote Lab environment.
|
||||
- **Microphone/camera**: not required by the platform itself. Only relevant if a specific
|
||||
instructor-led delivery format (e.g. a live video cohort) separately requires it for its own
|
||||
video conferencing — that's a delivery-format decision, not a platform requirement.
|
||||
|
||||
## In-person / camp delivery
|
||||
|
||||
- **RootNode provides**: the Lab environment (accessed via any device meeting the remote
|
||||
requirements above), course content, and instructor tooling.
|
||||
- **Student/venue provides**: the device itself (or the venue provides one), a keyboard, and
|
||||
internet access to reach RootNode's platform. No specialized hardware, no local software
|
||||
installation, no purchase requirements beyond program enrollment.
|
||||
- **Venue/instructor should verify** in advance: enough physical outlets/devices for the
|
||||
cohort size, and that the venue's network doesn't block outbound HTTPS to RootNode's domain.
|
||||
|
||||
## What RootNode explicitly does NOT require
|
||||
|
||||
No specific CPU/RAM/GPU spec, no paid software license, no local admin rights on the student's
|
||||
device (nothing is installed), and no specific operating system beyond "runs a current browser."
|
||||
@@ -0,0 +1,89 @@
|
||||
# ROOT_ V1 Syllabus
|
||||
|
||||
## Program description
|
||||
|
||||
ROOT_ is RootNode Academy's path from general computer literacy to operating and repairing real
|
||||
infrastructure — Linux, Git, containers, and Kubernetes, taught through real hands-on Labs in a
|
||||
disposable environment, culminating in a capstone that operates and recovers a production-like
|
||||
system under realistic conditions.
|
||||
|
||||
## Intended audience / readiness
|
||||
|
||||
Approximately age 15 and up, through college students, adults, and career changers — including
|
||||
help-desk technicians, junior sysadmins, developers, and anyone moving toward DevOps, cloud, or
|
||||
platform engineering. Age is guidance, not a technical gate; readiness is what actually matters
|
||||
(comfort with a keyboard and following multi-step instructions is the practical floor).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
None external. ROOT_ does not require completing RootNode's INIT_ or COMPILE_ course families —
|
||||
its own internal course sequence (below) is the only enforced prerequisite chain.
|
||||
|
||||
## Learning outcomes
|
||||
|
||||
By the end of ROOT_ V1, a student can: administer a Linux system and diagnose common failures;
|
||||
use Git for real collaborative version control including merge conflicts; build, run, and
|
||||
compose multi-container Docker applications; deploy, scale, and update workloads on a real
|
||||
Kubernetes cluster; secure a cluster against common CKS-style findings; systematically
|
||||
troubleshoot a broken system using a repeatable framework; and operate and recover a
|
||||
production-like environment under a real incident.
|
||||
|
||||
## Course sequence
|
||||
|
||||
1. **Computing & Linux Foundations** — how a computer runs software, the shell, the filesystem,
|
||||
files/text, processes, permissions.
|
||||
2. **Linux Administration & Networking** — service lifecycle, storage/filesystem administration,
|
||||
the networking mental model, network/DNS/HTTP diagnosis, structured troubleshooting.
|
||||
3. **Git & Collaborative Development** — why version control exists, the Git mental model,
|
||||
everyday Git, branching and real merge conflicts, remote repositories, engineering workflow.
|
||||
4. **Containers & Docker** — why containers exist, images vs. containers, the Docker CLI,
|
||||
container networking, container storage, Dockerfiles, Compose, and the bridge to Kubernetes.
|
||||
5. **Kubernetes Foundations** — the Kubernetes mental model and core objects, Pods and kubectl.
|
||||
6. **Kubernetes Application Workloads** — Deployments, scaling, rolling updates, Jobs/CronJobs.
|
||||
7. **Kubernetes Networking & Configuration** — Services, service discovery, Ingress,
|
||||
NetworkPolicy, ConfigMaps/Secrets.
|
||||
8. **Kubernetes Security** — SecurityContext, ServiceAccounts/RBAC, network and supply-chain
|
||||
security, cluster hardening — a formal CKS-style assessment, required before the Capstone.
|
||||
9. **Kubernetes Troubleshooting & Observability** — health probes, the RootNode troubleshooting
|
||||
framework, general operational troubleshooting.
|
||||
10. **RootNode Real Ops** — real, deliberately un-guided incident response.
|
||||
11. **ROOT Capstone: Operate and Recover a Production-Like Environment** — a graded, timed,
|
||||
real-Lab final assessment combining every prior domain.
|
||||
|
||||
*(ROOT 08 — Kubernetes Storage & Scheduling — and the deeper modules of ROOT 11 — DevOps &
|
||||
Platform Engineering — are not part of ROOT_ V1; see `docs/ROOT_V1_CURRICULUM_MANIFEST.md`. A
|
||||
single ROOT 11 orientation module is included as optional bonus content.)*
|
||||
|
||||
## Assessments
|
||||
|
||||
Four course-ending assessments (ROOT 01, 02, 03, 04) validate foundational competency; one
|
||||
(ROOT 09) validates security competency and is required before the Capstone. See
|
||||
`docs/ROOT_V1_ASSESSMENT_MAP.md` for exact scope and duration of each.
|
||||
|
||||
## Capstone
|
||||
|
||||
A 90-minute, real-Lab, graded final assessment. Requires ROOT 12 (Real Ops) and ROOT 09
|
||||
(Security) both complete, plus the pre-existing six-stage scenario gate (foundation, workloads,
|
||||
networking, configuration, troubleshooting, real-ops). No new teaching — it tests what the
|
||||
program already taught, under conditions closer to a real incident than a classroom exercise.
|
||||
|
||||
## Approximate structured hours
|
||||
|
||||
**64.2 hours** across the 12 V1 courses (excludes the deliberately-excluded ROOT 08). See
|
||||
`docs/ROOT_PROGRAM_DURATION_MODEL.md` for the exact per-module breakdown this is built from.
|
||||
This is instructional content time — it does not include instructor discussion, camp
|
||||
activities, or self-paced review time, which vary by delivery format (see
|
||||
`docs/ROOT_V1_INSTRUCTOR_LED_PILOT.md`).
|
||||
|
||||
## Tools & technologies
|
||||
|
||||
A real Linux shell, Git, Docker (via a safely nested Docker Engine inside each student's
|
||||
disposable Lab, never touching the Lab host's own daemon), and a real single-node Kubernetes
|
||||
cluster (k3s) — all provisioned automatically per student, with no local installation required.
|
||||
|
||||
## Learning expectations
|
||||
|
||||
Every module follows Learn → Practice → Lab → Troubleshoot → Validate (see `docs/
|
||||
PHASE6F_ROOT_V1_PILOT_READINESS.md`'s orientation content for the full explanation given to
|
||||
students). Labs validate themselves — a student always knows, immediately and objectively,
|
||||
whether a task actually passed.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Winter Break Camp — Curriculum Options
|
||||
|
||||
Phase 6F Step 20. The question this document answers: **what subset of existing READY ROOT_
|
||||
curriculum makes an excellent, academically coherent, standalone Winter Break Camp** — not
|
||||
"how do we fit all 64.2 hours into a week."
|
||||
|
||||
## A real constraint that shapes every option below
|
||||
|
||||
ROOT_'s course prerequisites are enforced server-side, for real, including against direct API
|
||||
attempts (`docs/ROOT_V1_PREREQUISITE_MAP.md`). A camp curriculum cannot skip straight to, say,
|
||||
Kubernetes for a brand-new student — ROOT 05 is genuinely locked behind ROOT 04, which is locked
|
||||
behind ROOT 03, 02, and 01. Any option below that starts past ROOT 01 is explicitly scoped as a
|
||||
**continuing-student** camp, not a walk-in-and-start one. This phase does not build a placement/
|
||||
waiver mechanism (out of scope, see the DO-NOT-BUILD list) — these options work within the real
|
||||
chain as it exists today.
|
||||
|
||||
## Option A — Linux & Systems Foundations Camp (recommended first camp)
|
||||
|
||||
**Target readiness**: complete beginners; no prior RootNode enrollment needed.
|
||||
**Age guidance (not a gate)**: ~13+ through adult.
|
||||
**Content reused**: ROOT 01 (Computing & Linux Foundations) + ROOT 02 (Linux Administration &
|
||||
Networking) — 100% existing READY content, zero new authoring.
|
||||
**Duration**: 353 + 388 = 741 minutes ≈ **12.4 structured hours** — fits a 4-5 day week camp at
|
||||
2.5-3 hours/day comfortably, with room for camp-format activities (breaks, group discussion)
|
||||
around the core content.
|
||||
**Hands-on Labs**: real Linux shell throughout — filesystem, processes, permissions, service
|
||||
lifecycle, storage/network/DNS diagnosis, structured troubleshooting.
|
||||
**Final mini-project/challenge**: the existing ROOT 02 assessment
|
||||
(`rootnode-linux-networking-assessment`) already IS a real, timed, diagnose-and-fix challenge —
|
||||
usable as the camp's own capstone moment with zero new content needed.
|
||||
**Staffing**: one instructor can reasonably run this for a cohort using Prepare Labs + the
|
||||
classroom Need-Help workflow (`docs/ROOT_V1_INSTRUCTOR_GUIDE.md`); no specialized TA skill
|
||||
needed beyond basic Linux fluency.
|
||||
**Delivery**: works in-person, fully remote, or hybrid — no format-specific content.
|
||||
|
||||
## Option B — Containers & Kubernetes Starter Camp (continuing students)
|
||||
|
||||
**Target readiness**: students who have already completed ROOT 01-03 (e.g., graduates of a
|
||||
prior camp, or a self-paced student continuing over break).
|
||||
**Age guidance**: ~14+ through adult.
|
||||
**Content reused**: ROOT 04 (Containers & Docker) + ROOT 05 (Kubernetes Foundations) — 444 + 403
|
||||
= 847 minutes ≈ **14.1 structured hours**.
|
||||
**Hands-on Labs**: real nested Docker (build, run, network, persist, Compose) then a real
|
||||
single-node Kubernetes cluster (Pods, kubectl fluency).
|
||||
**Final mini-project/challenge**: the existing ROOT 04 assessment plus a self-contained "deploy
|
||||
your own containerized app to the cluster" framing built from ROOT 05's existing Pod/kubectl
|
||||
Labs — no new content needed.
|
||||
**Staffing**: instructor should be comfortable with both Docker and basic kubectl.
|
||||
**Delivery**: remote-friendly (no local Docker/Kubernetes install needed, per `docs/
|
||||
ROOT_V1_STUDENT_REQUIREMENTS.md`); works in-person too.
|
||||
**Eligibility note**: market this explicitly as "Camp 2" or "for students who've done ROOT 01-03"
|
||||
— do not advertise it as a standalone entry point, since a walk-in student would hit a real
|
||||
locked course on day one.
|
||||
|
||||
## Option C — Build, Break & Fix: Kubernetes Operations Camp (advanced continuing students)
|
||||
|
||||
**Target readiness**: students who have completed ROOT 01-05 (e.g., graduates of Camps A+B).
|
||||
**Age guidance**: ~15+ through adult.
|
||||
**Content reused**: ROOT 06 (Application Workloads) + ROOT 07 (Networking & Configuration) +
|
||||
ROOT 10 (Troubleshooting & Observability) — 299 + 356 + 280 = 935 minutes ≈ **15.6 structured
|
||||
hours**.
|
||||
**Hands-on Labs**: Deployments/scaling/rolling updates/Jobs/CronJobs, Services/Ingress/
|
||||
NetworkPolicy/ConfigMaps, then real diagnose-and-fix troubleshooting using the RootNode
|
||||
framework.
|
||||
**Final mini-project/challenge**: a real, deliberately-broken multi-symptom scenario drawing on
|
||||
ROOT 10's existing troubleshooting Labs — genuinely operational, not a quiz.
|
||||
**Staffing**: instructor should be comfortable diagnosing real Kubernetes failures live, since
|
||||
students will hit genuine dead ends by design.
|
||||
**Delivery**: remote-friendly; works well as a more intensive, fewer-day format (e.g. 3 full
|
||||
days) given the deeper focus per topic.
|
||||
|
||||
## Recommendation for THIS Winter Break
|
||||
|
||||
**Option A** is the clear first camp: it needs no prior enrollment, needs zero new curriculum
|
||||
authoring, fits a standard week-long camp format cleanly, and gives a complete, honest,
|
||||
standalone learning arc (a real beginner walks in, and walks out having genuinely administered
|
||||
and diagnosed a real Linux system). Options B and C are natural follow-on offerings for
|
||||
returning students later in the year, once a first cohort exists to return.
|
||||
|
||||
## Explicitly out of scope for this document
|
||||
|
||||
Pricing, registration flow, payment processing, and marketing copy — this is curriculum
|
||||
packaging only, per the Phase 6F authorization's own scope fence. See Phase 7 for the
|
||||
business/product layer.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orchestrates the Phase 6F Step 24 ROOT_ V1 end-to-end acceptance rehearsal: brings up the real
|
||||
# dev stack, bootstraps an owner, runs test-phase6f-root-v1-end-to-end.js (one student, real
|
||||
# student API throughout, ROOT 01 through genuine Capstone-assessment-startable), tears down.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../../../platform"
|
||||
|
||||
export PROVISIONER_SHARED_SECRET="${PROVISIONER_SHARED_SECRET:-rehearsal-shared-secret-not-for-production}"
|
||||
export INTEGRATION_SHARED_SECRET="${INTEGRATION_SHARED_SECRET:-dev-integration-secret-not-for-production}"
|
||||
export LAB_IMAGE_REF="${LAB_IMAGE_REF:-rootnode-academy:test}"
|
||||
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
cleanup() {
|
||||
echo "==> Tearing down"
|
||||
docker ps -aq --filter "name=rootnode-lab-" | xargs -r docker rm -f >/dev/null 2>&1 || true
|
||||
docker network ls --filter "name=rootnode-lab-net-" --format "{{.Name}}" | xargs -r -n1 docker network rm >/dev/null 2>&1 || true
|
||||
docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> Waiting for the stack"
|
||||
for _ in $(seq 1 60); do
|
||||
curl -sf http://localhost:4300/healthz >/dev/null 2>&1 && curl -sf http://localhost:4200/healthz >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "==> Bootstrapping the rehearsal owner"
|
||||
( cd api && PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
|
||||
node bootstrap-owner.js [email protected] "P6F E2E Owner" 'RehearsalPass!2026' )
|
||||
|
||||
echo "==> Running the rehearsal"
|
||||
OWNER_EMAIL=[email protected] OWNER_PASSWORD='RehearsalPass!2026' \
|
||||
node ../infra/scripts/drills/test-phase6f-root-v1-end-to-end.js
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orchestrates the Phase 6F Step 23 ROOT_ V1 multi-student pilot rehearsal: brings up the real
|
||||
# dev stack, bootstraps an owner, runs test-phase6f-root-v1-pilot.js (real cohort, real
|
||||
# classroom session, real disposable Labs, a real validation failure/fix, a real Lab reap), tears
|
||||
# down.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../../../platform"
|
||||
|
||||
export PROVISIONER_SHARED_SECRET="${PROVISIONER_SHARED_SECRET:-rehearsal-shared-secret-not-for-production}"
|
||||
export INTEGRATION_SHARED_SECRET="${INTEGRATION_SHARED_SECRET:-dev-integration-secret-not-for-production}"
|
||||
export LAB_IMAGE_REF="${LAB_IMAGE_REF:-rootnode-academy:test}"
|
||||
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
cleanup() {
|
||||
echo "==> Tearing down"
|
||||
docker ps -aq --filter "name=rootnode-lab-" | xargs -r docker rm -f >/dev/null 2>&1 || true
|
||||
docker network ls --filter "name=rootnode-lab-net-" --format "{{.Name}}" | xargs -r -n1 docker network rm >/dev/null 2>&1 || true
|
||||
docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> Waiting for the stack"
|
||||
for _ in $(seq 1 60); do
|
||||
curl -sf http://localhost:4300/healthz >/dev/null 2>&1 && curl -sf http://localhost:4200/healthz >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "==> Bootstrapping the rehearsal owner"
|
||||
( cd api && PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
|
||||
node bootstrap-owner.js [email protected] "P6F Pilot Owner" 'RehearsalPass!2026' )
|
||||
|
||||
echo "==> Running the rehearsal"
|
||||
OWNER_EMAIL=[email protected] OWNER_PASSWORD='RehearsalPass!2026' \
|
||||
node ../infra/scripts/drills/test-phase6f-root-v1-pilot.js
|
||||
@@ -59,9 +59,18 @@ async function main() {
|
||||
});
|
||||
|
||||
await step('resume points at ROOT 01\'s very first activity', async () => {
|
||||
// Phase 6F added a required orientation module in front of root-01-m01 — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
|
||||
});
|
||||
|
||||
await step('student completes the ROOT_ V1 orientation module', async () => {
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) {
|
||||
const { status } = await session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
assert.equal(status, 200);
|
||||
}
|
||||
});
|
||||
|
||||
await step('student reads ROOT 01 module 1\'s lesson and guided exercise', async () => {
|
||||
|
||||
@@ -78,6 +78,9 @@ async function main() {
|
||||
|
||||
// ── ROOT 01 (already READY since Phase 6D) — complete quickly via real scenario endpoint ──
|
||||
await step('ROOT 01 completes via the real Lab scenario-validation API', async () => {
|
||||
// Phase 6F added a required orientation module gating root-01-m01 — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await completeActivity(session, id);
|
||||
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) await completeActivity(session, `root-01-m01-${suffix}`);
|
||||
await completeScenario(session, 'linux-inspect-hardware', envId);
|
||||
await completeActivity(session, 'root-01-m01-a04-reflection');
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 6F Step 24 — the broadest curriculum acceptance test yet: proves one real student can
|
||||
// progress ROOT 01 -> 02 -> 03 -> 04 -> 05+ -> Security -> Troubleshooting -> Real Ops ->
|
||||
// Capstone-eligible, entirely through the REAL student API (never an admin shortcut, never a
|
||||
// direct database write to fake completion), with every real server-side prerequisite lock
|
||||
// actually enforced along the way — not bypassed. Real disposable Lab + real nested Docker used
|
||||
// for one genuine checkpoint (the ROOT 04 container-networking diagnose-and-fix, matching the
|
||||
// standard Phase 6D/6E rehearsals already established); the rest of ROOT 01-04's real-Lab
|
||||
// mechanics were already proven end-to-end in those rehearsals and in this phase's own
|
||||
// multi-student pilot rehearsal, so this script does not repeat them — it proves the ADDITIONAL
|
||||
// distance (05 through Capstone-eligibility) that no prior phase's rehearsal covered.
|
||||
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const { makeSession } = require('../../../platform/test/session-helper');
|
||||
const { completeRootFoundations01Through04 } = require('../../../platform/test/foundations-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; }
|
||||
}
|
||||
function courseById(courses, id) { return courses.find((c) => c.course_id === id); }
|
||||
function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
|
||||
async function act(session, id) { const r = await session.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, `completing ${id}: ${JSON.stringify(r.body)}`); }
|
||||
async function scen(session, sid) { const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); assert.equal(r.status, 200, `completing scenario ${sid}`); }
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 6F ROOT_ V1 end-to-end acceptance rehearsal — ROOT 01 through Capstone eligibility\n');
|
||||
|
||||
const owner = makeSession(GW);
|
||||
await step('owner logs in', async () => {
|
||||
const r = await owner.request('POST', '/api/auth/instructor/login', { email: process.env.OWNER_EMAIL, password: process.env.OWNER_PASSWORD });
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
|
||||
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: 'E2E V1 Journey', login_identifier: `p6f-journey-${RUN_ID}` });
|
||||
const session = makeSession(GW);
|
||||
let enrollmentId;
|
||||
await step('student activates and is enrolled in ROOT_', async () => {
|
||||
const act1 = await session.request('POST', '/api/student/activate', { token: create.activation_token, password: 'JourneyPass123!' });
|
||||
assert.equal(act1.status, 200);
|
||||
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
|
||||
enrollmentId = enroll.enrollment_id;
|
||||
});
|
||||
|
||||
await step('ROOT 01-04 (Linux/Git/Docker foundations) complete through the real student API, unlocking ROOT 05', async () => {
|
||||
await completeRootFoundations01Through04(session);
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').complete, true);
|
||||
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
|
||||
});
|
||||
|
||||
// ── One real Lab checkpoint: the ROOT 04 diagnose-and-fix, re-proven live on THIS student ──
|
||||
await step('practical boundary check: a real nested-Docker container-networking fix, run live on this student\'s own Lab', async () => {
|
||||
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
|
||||
assert.equal(launch.status, 200);
|
||||
const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
|
||||
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/docker-container-networking/setup`);
|
||||
assert.equal(setup.status, 200);
|
||||
const envRow = (await owner.request('GET', `/api/admin/students/${create.student_id}/environment`)).body.find((e) => e.status === 'running');
|
||||
const containerName = execSync(
|
||||
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${envRow.environment_id}'"`,
|
||||
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
|
||||
).toString().trim();
|
||||
// Confirm the REAL pre-fix failure before fixing it.
|
||||
let failedBefore = false;
|
||||
try { execSync(`docker exec ${containerName} curl -sf --max-time 3 http://localhost:8099`, { stdio: 'pipe' }); }
|
||||
catch (e) { failedBefore = true; }
|
||||
assert.equal(failedBefore, true, 'expected a REAL connection failure before the fix (container started without -p)');
|
||||
execSync(`docker exec ${containerName} docker rm -f net-lab-web`, { stdio: 'pipe' });
|
||||
execSync(`docker exec ${containerName} docker run -d --name net-lab-web -p 8099:80 nginx:alpine`, { stdio: 'pipe' });
|
||||
const afterFix = execSync(`docker exec ${containerName} curl -sf --max-time 5 http://localhost:8099`, { stdio: 'pipe' }).toString();
|
||||
assert.ok(afterFix.includes('nginx') || afterFix.length > 0, 'expected a real successful response after the real fix');
|
||||
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/docker-container-networking/validate`);
|
||||
assert.equal(validate.status, 200);
|
||||
assert.equal(validate.body.passed, true);
|
||||
await scen(session, 'docker-container-networking');
|
||||
});
|
||||
|
||||
await step('direct API cannot skip ahead: ROOT 09/10/12/Capstone all remain locked until ROOT 05/06 are genuinely complete', async () => {
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
for (const id of ['root-09-kubernetes-security', 'root-10-kubernetes-troubleshooting-observability', 'root-12-real-ops', 'root-capstone']) {
|
||||
assert.equal(courseById(body.courses, id).locked, true, `${id} should still be locked`);
|
||||
}
|
||||
const bypass = await session.request('POST', '/api/student/activities/root-09-m01-a01-lesson-security-model/complete');
|
||||
assert.equal(bypass.status, 403);
|
||||
assert.equal(bypass.body.error, 'locked');
|
||||
});
|
||||
|
||||
await step('ROOT 05 (Kubernetes Foundations) completes and unlocks ROOT 06', async () => {
|
||||
await act(session, 'root-05-m00-a01-lesson-mental-model'); await act(session, 'root-05-m00-a02-demonstration-desired-state'); await act(session, 'root-05-m00-a03-knowledge-check'); await act(session, 'root-05-m00-a04-guided-exercise-namespaces');
|
||||
await scen(session, 'namespaces-basics'); await act(session, 'root-05-m00-a06-reflection');
|
||||
const pods = ['pod-basics-mcq', 'kubectl-essentials-mcq', 'labels-selectors-mcq', 'init-containers-mcq', 'namespaces-basics', 'multi-container-pod-basics', 'pod-labeling-basics', 'jsonpath-basics', 'dry-run-manifest-basics', 'delete-by-label-basics', 'annotate-resource-basics', 'init-container-task-basics', 'restart-policy-mcq', 'kubectl-output-format-mcq', 'kubectl-explain-basics', 'kubectl-patch-basics', 'field-selector-basics', 'sort-by-basics', 'kubectl-diff-basics', 'static-pods-mcq'];
|
||||
for (const sid of pods) await scen(session, sid);
|
||||
await act(session, 'root-05-m01-a00-lesson-kubectl-fluency'); await act(session, 'root-05-m01-a21-reflection');
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').complete, true);
|
||||
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, false);
|
||||
});
|
||||
|
||||
await step('ROOT 06 (Application Workloads, incl. the practice bank) completes and unlocks ROOT 07/09/10', async () => {
|
||||
for (const sid of ['deploy-nginx', 'scale-deployment', 'rolling-update-task', 'liveness-probe-task', 'jobs-cronjobs-mcq', 'daemonset-mcq', 'edit-deployment-basics', 'deployment-strategy-mcq', 'deployment-rollback']) await scen(session, sid);
|
||||
await act(session, 'root-06-m02-a01-lesson-deployments'); await act(session, 'root-06-m02-a02-demonstration-rollout'); await act(session, 'root-06-m02-a03-knowledge-check'); await act(session, 'root-06-m02-a04-guided-exercise-rollout-strategy'); await act(session, 'root-06-m02-a06-reflection');
|
||||
await act(session, 'root-06-m01-a00-lab-bank-intro');
|
||||
for (const sid of ['job-task', 'cronjob-task']) await scen(session, sid);
|
||||
await act(session, 'root-06-m03-a01-lesson-jobs-cronjobs-daemonsets'); await act(session, 'root-06-m03-a01b-demonstration-job-lifecycle'); await act(session, 'root-06-m03-a02-knowledge-check'); await act(session, 'root-06-m03-a02b-guided-exercise-choosing-primitive'); await act(session, 'root-06-m03-a05-reflection');
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').complete, true);
|
||||
assert.equal(courseById(body.courses, 'root-07-kubernetes-networking-config').locked, false);
|
||||
assert.equal(courseById(body.courses, 'root-09-kubernetes-security').locked, false);
|
||||
assert.equal(courseById(body.courses, 'root-10-kubernetes-troubleshooting-observability').locked, false);
|
||||
});
|
||||
|
||||
await step('ROOT 09 (Security) completes via a real formal assessment', async () => {
|
||||
for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) await act(session, `root-09-m01-${suffix}`);
|
||||
for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await scen(session, sid);
|
||||
for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) await act(session, `root-09-m02-${suffix}`);
|
||||
for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await scen(session, sid);
|
||||
for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) await act(session, `root-07-m03-${suffix}`);
|
||||
await scen(session, 'expose-service-basics');
|
||||
for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) await act(session, `root-07-m04-${suffix}`);
|
||||
await scen(session, 'ingress-task'); await scen(session, 'network-policy');
|
||||
for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) await act(session, `root-09-m03-${suffix}`);
|
||||
for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await scen(session, sid);
|
||||
await act(session, 'root-09-m04-a01-lesson-cluster-hardening');
|
||||
for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await scen(session, sid);
|
||||
const r = await session.request('POST', '/api/assessments/rootnode-security-assessment/start');
|
||||
assert.equal(r.status, 201);
|
||||
for (const s of r.body.scenarios) await scen(session, s.scenario_id);
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-09-kubernetes-security').complete, true);
|
||||
});
|
||||
|
||||
await step('ROOT 10 (Troubleshooting, incl. the practice bank) completes using the RootNode framework', async () => {
|
||||
await act(session, 'root-10-m01-a01-lesson-health-check-concepts'); await act(session, 'root-10-m01-a02-demonstration-probes-in-action'); await act(session, 'root-10-m01-a03-knowledge-check'); await act(session, 'root-10-m01-a04-guided-exercise-readiness-probe');
|
||||
await scen(session, 'rootnode-probe-port-mismatch'); await act(session, 'root-10-m01-a06-reflection-real-ops-takeaway');
|
||||
await act(session, 'root-10-m03-a01-lesson-framework'); await act(session, 'root-10-m03-a02-demonstration-crashloop'); await act(session, 'root-10-m03-a03-knowledge-check'); await act(session, 'root-10-m03-a03b-guided-exercise-endpoint-mismatch');
|
||||
await scen(session, 'crashloop-fix'); await scen(session, 'endpoint-fix-task'); await act(session, 'root-10-m03-a06-reflection');
|
||||
await act(session, 'root-10-m02-a00-lab-bank-intro');
|
||||
for (const sid of ['extract-logs-basics', 'kubectl-cp-basics', 'exec-command-basics', 'node-cordon-basics', 'pod-node-assignment-mcq', 'container-logging-mcq']) await scen(session, sid);
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-10-kubernetes-troubleshooting-observability').complete, true);
|
||||
assert.equal(courseById(body.courses, 'root-12-real-ops').locked, false);
|
||||
});
|
||||
|
||||
await step('ROOT 12 (Real Ops) completes, real incident scenarios, deliberately un-guided', async () => {
|
||||
await act(session, 'root-12-m01-a00-lesson-incident-response');
|
||||
for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(session, sid);
|
||||
await act(session, 'root-12-m01-a99-reflection-post-incident');
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true);
|
||||
});
|
||||
|
||||
await step('root-capstone is now genuinely unlocked — both real prerequisites (ROOT 09 AND ROOT 12) satisfied', async () => {
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-capstone').locked, false);
|
||||
});
|
||||
|
||||
await step('the Capstone Readiness orientation module is completable', async () => {
|
||||
for (const suffix of ['a01-lesson-what-it-measures', 'a02-lesson-time-management', 'a03-knowledge-check', 'a04-reflection-readiness']) await act(session, `root-capstone-m02-${suffix}`);
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-capstone-m02-capstone-readiness').complete, true);
|
||||
});
|
||||
|
||||
await step('the pre-existing legacy six-stage gate (a SEPARATE, older mechanism) also needs its own "networking"/"configuration" stage scenarios — never required by this journey\'s course-layer path, since root-07-m01/m02 aren\'t prerequisites of root-09-m03', async () => {
|
||||
// "networking" stage needs services-mcq (root-07-m01's scenario; expose-service-basics was
|
||||
// already done above via root-07-m03). "configuration" stage needs all 5 of root-07-m02's
|
||||
// scenarios. Neither module is a course-content-model prerequisite of anything this journey
|
||||
// already needed to touch — proving these two, genuinely separate gates aren't the same
|
||||
// thing is exactly this step's point.
|
||||
for (const sid of ['services-mcq', 'configmap-volume', 'resource-limits-task', 'env-vars-configmap', 'secrets-basics', 'pod-env-vars-inline']) await scen(session, sid);
|
||||
});
|
||||
|
||||
await step('the real Capstone assessment itself is now startable (the pre-existing six-stage gate is also satisfied by this journey)', async () => {
|
||||
const r = await session.request('POST', '/api/assessments/rootnode-capstone/start');
|
||||
assert.equal(r.status, 201, `expected the Capstone to be genuinely startable: ${JSON.stringify(r.body)}`);
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 6F Step 23 — multi-student ROOT_ V1 pilot rehearsal. Three students in genuinely
|
||||
// different states inside one real instructor-led cohort/classroom session: Student A stays
|
||||
// on track, Student B hits a REAL validation failure and needs help before fixing it, Student C
|
||||
// has their real disposable Lab reaped (admin-destroyed) mid-task and must recreate it. Verifies
|
||||
// instructor visibility distinguishes all three, no cross-student progress leakage, and no
|
||||
// academic-state loss from the reap. Real Labs used for Student B and C's own tasks; Student A's
|
||||
// path uses the same legitimate direct-scenario-completion pattern e2e-phase6e.js established
|
||||
// for already-proven ROOT 01 content (no bypass of server-side prerequisite enforcement).
|
||||
//
|
||||
// Usage: bash infra/scripts/drills/run-phase6f-root-v1-pilot.sh
|
||||
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const { makeSession } = require('../../../platform/test/session-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; }
|
||||
}
|
||||
function psql(sql) {
|
||||
return execSync(`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "${sql}"`, { cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }).toString().trim();
|
||||
}
|
||||
function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 6F ROOT_ V1 multi-student pilot rehearsal — real cohort, real Labs\n');
|
||||
|
||||
const owner = makeSession(GW);
|
||||
await step('owner logs in', async () => {
|
||||
const r = await owner.request('POST', '/api/auth/instructor/login', { email: process.env.OWNER_EMAIL, password: process.env.OWNER_PASSWORD });
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
|
||||
const cohortId = `p6f-pilot-${RUN_ID}`;
|
||||
await step('instructor creates a cohort and a live classroom session', async () => {
|
||||
const r = await owner.request('POST', '/api/admin/cohorts', { cohort_id: cohortId, name: 'ROOT_ V1 Pilot Rehearsal', tier: 'root' });
|
||||
assert.equal(r.status, 201);
|
||||
});
|
||||
|
||||
const students = {};
|
||||
for (const tag of ['A', 'B', 'C']) {
|
||||
await step(`create and enroll Student ${tag}`, async () => {
|
||||
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `Student${tag}`, cohort_id: cohortId, login_identifier: `p6f-${tag.toLowerCase()}-${RUN_ID}` });
|
||||
const session = makeSession(GW);
|
||||
const act = await session.request('POST', '/api/student/activate', { token: create.activation_token, password: `Student${tag}Pass123!` });
|
||||
assert.equal(act.status, 200);
|
||||
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
|
||||
students[tag] = { session, studentId: create.student_id, enrollmentId: enroll.enrollment_id };
|
||||
});
|
||||
}
|
||||
|
||||
let classroomId;
|
||||
await step('instructor starts a live classroom session for the cohort', async () => {
|
||||
const r = await owner.request('POST', `/api/admin/cohorts/${cohortId}/classroom-sessions`, { title: 'ROOT_ V1 Pilot Session' });
|
||||
assert.equal(r.status, 201);
|
||||
classroomId = r.body.classroom_session_id;
|
||||
await owner.request('POST', `/api/admin/classroom-sessions/${classroomId}/start`);
|
||||
});
|
||||
|
||||
for (const tag of ['A', 'B', 'C']) {
|
||||
await step(`Student ${tag} joins the live classroom session`, async () => {
|
||||
const r = await students[tag].session.request('POST', `/api/classroom/${classroomId}/join`);
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Student A: on track — real orientation + module 1 progression ──────────────────────
|
||||
await step('Student A completes the orientation module and starts ROOT 01 module 1 (on track)', async () => {
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check', 'root-01-m01-a01-lesson', 'root-01-m01-a02-guided-exercise']) {
|
||||
const r = await students.A.session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
assert.equal(r.status, 200, id);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Student B: needs help — a REAL Lab, a REAL failed validation, then a real fix ───────
|
||||
let bEnvId, bLabToken;
|
||||
await step('Student B launches a REAL disposable Lab', async () => {
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await students.B.session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
const launch = await students.B.session.request('POST', `/api/student/programs/${students.B.enrollmentId}/launch`);
|
||||
assert.equal(launch.status, 200);
|
||||
bLabToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
|
||||
const envRow = (await owner.request('GET', `/api/admin/students/${students.B.studentId}/environment`)).body.find((e) => e.status === 'running');
|
||||
bEnvId = envRow.environment_id;
|
||||
});
|
||||
|
||||
await step('Student B attempts the linux-inspect-hardware Lab and genuinely FAILS validation first (nothing done yet)', async () => {
|
||||
const setup = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/setup`);
|
||||
assert.equal(setup.status, 200);
|
||||
const validate = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/validate`);
|
||||
assert.equal(validate.status, 200);
|
||||
assert.equal(validate.body.passed, false, 'expected a REAL failure — the report file was never written');
|
||||
});
|
||||
|
||||
await step('Student B sends a real Need Help request, categorized lab_problem', async () => {
|
||||
const r = await students.B.session.request('POST', `/api/classroom/${classroomId}/help-request`, { category: 'lab_problem', message: 'My hardware report Lab keeps failing validation.' });
|
||||
assert.equal(r.status, 201);
|
||||
});
|
||||
|
||||
await step('the instructor sees Student B\'s help request immediately', async () => {
|
||||
const r = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`);
|
||||
assert.equal(r.status, 200);
|
||||
const mine = r.body.find((h) => h.student_id === students.B.studentId);
|
||||
assert.ok(mine, 'instructor must see the real help request');
|
||||
assert.equal(mine.category, 'lab_problem');
|
||||
assert.equal(mine.status, 'open');
|
||||
});
|
||||
|
||||
await step('Student B actually fixes it — real shell commands in the real Lab container — and validation now genuinely passes', async () => {
|
||||
const containerName = execSync(
|
||||
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${bEnvId}'"`,
|
||||
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
|
||||
).toString().trim();
|
||||
const fs = require('fs');
|
||||
const scriptPath = `/tmp/phase6f-hardware-report-${bEnvId}.sh`;
|
||||
fs.writeFileSync(scriptPath, ['#!/bin/bash', 'set -e', 'mkdir -p /srv/hardware-lab',
|
||||
'CPUS=$(nproc)', "MEM_MB=$(free -m | awk '/Mem:/ {print $2}')",
|
||||
'DISK_GB=$(df -BG / | awk \'NR==2{gsub("G","",$2); print $2}\')',
|
||||
'printf "CPUS=%s\\nMEM_MB=%s\\nDISK_GB=%s\\n" "$CPUS" "$MEM_MB" "$DISK_GB" > /srv/hardware-lab/report.txt'].join('\n'));
|
||||
execSync(`docker cp ${scriptPath} ${containerName}:/tmp/hardware-report.sh && docker exec ${containerName} bash /tmp/hardware-report.sh`, { stdio: 'pipe' });
|
||||
fs.unlinkSync(scriptPath);
|
||||
const validate = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/validate`);
|
||||
assert.equal(validate.status, 200);
|
||||
assert.equal(validate.body.passed, true, 'expected the real fix to genuinely pass now');
|
||||
});
|
||||
|
||||
await step('the instructor acknowledges and resolves Student B\'s help request', async () => {
|
||||
const list = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`);
|
||||
const mine = list.body.find((h) => h.student_id === students.B.studentId);
|
||||
const ack = await owner.request('POST', `/api/admin/help-requests/${mine.help_request_id}/acknowledge`);
|
||||
assert.equal(ack.status, 200);
|
||||
const resolve = await owner.request('POST', `/api/admin/help-requests/${mine.help_request_id}/resolve`);
|
||||
assert.equal(resolve.status, 200);
|
||||
});
|
||||
|
||||
// ── Student C: Lab reaped mid-task, must recreate — academic state must survive ─────────
|
||||
let cEnvId;
|
||||
await step('Student C launches a REAL disposable Lab and starts a real Lab task', async () => {
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await students.C.session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
const launch = await students.C.session.request('POST', `/api/student/programs/${students.C.enrollmentId}/launch`);
|
||||
assert.equal(launch.status, 200);
|
||||
const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
|
||||
const setup = await students.C.session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`);
|
||||
assert.equal(setup.status, 200);
|
||||
const envRow = (await owner.request('GET', `/api/admin/students/${students.C.studentId}/environment`)).body.find((e) => e.status === 'running');
|
||||
cEnvId = envRow.environment_id;
|
||||
});
|
||||
|
||||
await step('the instructor reaps Student C\'s Lab mid-task (real admin destroy, simulating an idle-reaper event)', async () => {
|
||||
const r = await owner.request('DELETE', `/api/admin/environments/${cEnvId}`);
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
|
||||
await step('Student C\'s academic progress survived the reap (orientation still complete)', async () => {
|
||||
const { body } = await students.C.session.request('GET', `/api/student/programs/${students.C.enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true, 'orientation completion must survive a Lab reap');
|
||||
});
|
||||
|
||||
await step('Student C recreates a fresh Lab and can pick the task back up', async () => {
|
||||
const launch = await students.C.session.request('POST', `/api/student/programs/${students.C.enrollmentId}/launch`);
|
||||
assert.equal(launch.status, 200);
|
||||
assert.equal(launch.body.status, 'running');
|
||||
const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
|
||||
const setup = await students.C.session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`);
|
||||
assert.equal(setup.status, 200, 'the recreated Lab must accept the same task setup again');
|
||||
});
|
||||
|
||||
// ── Instructor sees three genuinely different states ────────────────────────────────────
|
||||
await step('the instructor sees three genuinely different student states in the classroom view', async () => {
|
||||
const { body } = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}`);
|
||||
const names = body.students.map((s) => s.display_name);
|
||||
assert.ok(names.includes('StudentA') && names.includes('StudentB') && names.includes('StudentC'));
|
||||
const helpHistory = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`);
|
||||
const bRequest = helpHistory.body.find((h) => h.student_id === students.B.studentId);
|
||||
assert.equal(bRequest.status, 'resolved', 'Student B\'s help request should show resolved, distinguishing them from A and C');
|
||||
});
|
||||
|
||||
await step('cross-student isolation: Student A is genuinely ahead of B and C, and none of B/C\'s events touched A', async () => {
|
||||
const [{ body: aBody }, { body: bBody }, { body: cBody }] = await Promise.all([
|
||||
students.A.session.request('GET', `/api/student/programs/${students.A.enrollmentId}/courses`),
|
||||
students.B.session.request('GET', `/api/student/programs/${students.B.enrollmentId}/courses`),
|
||||
students.C.session.request('GET', `/api/student/programs/${students.C.enrollmentId}/courses`),
|
||||
]);
|
||||
assert.equal(moduleById(aBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student A completed orientation');
|
||||
const aLesson = moduleById(aBody.courses, 'root-01-m01-how-computers-run-software').activities.find((act) => act.activity_id === 'root-01-m01-a01-lesson');
|
||||
assert.equal(aLesson.complete, true, 'Student A explicitly marked the module 1 lesson complete');
|
||||
assert.equal(moduleById(bBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student B also completed orientation, independently');
|
||||
const bLesson = moduleById(bBody.courses, 'root-01-m01-how-computers-run-software').activities.find((act) => act.activity_id === 'root-01-m01-a01-lesson');
|
||||
assert.equal(bLesson.complete, false, 'Student B never marked the lesson complete — only ran the real Lab task, isolated from A\'s manual-marker progress');
|
||||
assert.equal(moduleById(cBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student C also completed orientation, independently, surviving their own Lab reap');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -95,6 +95,22 @@ further instructor accounts from the Instructors tab).
|
||||
prerequisite chains through the newly-inserted front-door modules, the optional-vs-required
|
||||
(`is_required`) completion rule, and the capstone server-side unlock fix in both directions
|
||||
(a direct bypass attempt rejected, a legitimately unlocked attempt still accepted).
|
||||
- `test/e2e-phase6c.js` — curriculum completion upgrades, the security assessment bank, and
|
||||
ROOT 11's honest lesson-only scope.
|
||||
- `test/e2e-phase6d.js` — ROOT 01 (Computing & Linux Foundations) as the program's real front
|
||||
door, and the real ROOT 01-04 foundational prerequisite chain.
|
||||
- `test/e2e-phase6e.js` — ROOT 02/03/04 completion, the full ROOT 01->02->03->04->05 chain
|
||||
proven via direct API, and the course/module lock-enforcement fix on the manual-completion
|
||||
route.
|
||||
- `test/e2e-phase6f.js` — the ROOT_ V1 curriculum manifest (inclusions/exclusions), the full V1
|
||||
prerequisite graph (no cycles, no missing nodes), the strengthened Capstone gate (ROOT 09 AND
|
||||
ROOT 12 both required) in both directions, the new orientation module, and an explicit check
|
||||
that no age/grade gate exists anywhere in enrollment.
|
||||
|
||||
`infra/scripts/drills/` also holds real-Lab dress rehearsals per phase (Phase 6D/6E foundations,
|
||||
Phase 6F's multi-student pilot and full end-to-end V1 journey) — see each script's own header
|
||||
and `docs/PHASE6F_ROOT_V1_PILOT_READINESS.md` for what they prove beyond what the e2e suites
|
||||
above already cover via direct API.
|
||||
|
||||
All e2e suites need `OWNER_EMAIL` / `OWNER_PASSWORD` set to a real bootstrapped instructor
|
||||
account (see "Running the full stack locally" above) — the old `ADMIN_PASSWORD` mechanism is
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
-- Phase 6F — ROOT_ V1 curriculum integration. No schema change: every decision below reuses
|
||||
-- existing columns (visibility, status, is_required) exactly as COURSE_CONTENT_MODEL.md already
|
||||
-- documents them. See docs/ROOT_V1_CURRICULUM_MANIFEST.md for the full audit and rationale.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- 1. The three instructor-led-only practice banks (Phase 6E's known limitation) become
|
||||
-- OPTIONAL supplemental practice: every activity in them gets is_required = false. Nothing
|
||||
-- downstream lists any of these three modules as its own prerequisite (confirmed by audit), so
|
||||
-- this changes nothing about the prerequisite graph — only whether their labs count toward
|
||||
-- their course's OWN completion. Reuses the exact is_required mechanism Phase 6B built; no new
|
||||
-- module-level "optional" flag needed. A module with zero required activities can never become
|
||||
-- "complete" (course-progress.js's totals(): `required.length > 0 && ...`), so these stay
|
||||
-- visibly incomplete/optional rather than silently vanishing or looking done.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
UPDATE module_activities SET is_required = false, version = version + 1, updated_at = now()
|
||||
WHERE module_id IN (
|
||||
'root-06-m01-deployments-and-workloads',
|
||||
'root-07-m01-services-and-networking',
|
||||
'root-10-m02-general-troubleshooting'
|
||||
);
|
||||
|
||||
UPDATE course_modules SET
|
||||
description = description || ' — OPTIONAL supplemental practice for ROOT_ V1: a bare scenario bank with no lesson/demonstration/knowledge-check framing, not required for course or self-paced progression. Suitable for instructor-assigned extra reps in an instructor-led cohort.',
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE module_id = 'root-06-m01-deployments-and-workloads';
|
||||
UPDATE course_modules SET
|
||||
description = description || ' — OPTIONAL supplemental practice for ROOT_ V1: a bare scenario bank with no lesson/demonstration/knowledge-check framing, not required for course or self-paced progression. Suitable for instructor-assigned extra reps in an instructor-led cohort.',
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE module_id = 'root-07-m01-services-and-networking';
|
||||
UPDATE course_modules SET
|
||||
description = description || ' — OPTIONAL supplemental practice for ROOT_ V1: a bare scenario bank with no lesson/demonstration/knowledge-check framing, not required for course or self-paced progression. Suitable for instructor-assigned extra reps in an instructor-led cohort.',
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE module_id = 'root-10-m02-general-troubleshooting';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Course-level status audit correction. courses.status describes authoring completeness and
|
||||
-- is shown to students honestly (COURSE_CONTENT_MODEL.md §6) — it is a hand-maintained field,
|
||||
-- not derived from module status. Auditing it against actual module-level status (Step 0's
|
||||
-- explicit "do not rely only on prior reports" instruction) found it had drifted out of date
|
||||
-- for six courses: every module in each had already reached 'ready' status in an earlier phase
|
||||
-- (root-05, root-09) or now does once its bare-practice-bank module is reclassified optional
|
||||
-- above (root-06, root-07, root-10), and root-12/root-capstone's modules were already all ready.
|
||||
-- No new content was authored for this correction — it is a status-field fix, not a curriculum
|
||||
-- claim change.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
UPDATE courses SET status = 'ready', version = version + 1, updated_at = now()
|
||||
WHERE course_id IN (
|
||||
'root-05-kubernetes-foundations',
|
||||
'root-06-kubernetes-application-workloads',
|
||||
'root-07-kubernetes-networking-config',
|
||||
'root-09-kubernetes-security',
|
||||
'root-10-kubernetes-troubleshooting-observability',
|
||||
'root-12-real-ops',
|
||||
'root-capstone'
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- 3. ROOT 08 (Storage & Scheduling) is excluded from ROOT_ V1. It sits at sequence 8 but is NOT
|
||||
-- required by anything downstream (root-09/root-10 both branch directly off root-06, by design
|
||||
-- — see ROOT_PROGRAM_ARCHITECTURE.md's "deviations from the suggested sequence"), so leaving it
|
||||
-- visible mid-sequence with only 2 PARTIAL bare-lab modules would look to a self-paced student
|
||||
-- like a course they "skipped," when in fact V1 was never designed to require it. visibility =
|
||||
-- 'draft' is the existing hard gate (COURSE_CONTENT_MODEL.md §6): excluded from every
|
||||
-- student-facing query, still visible/manageable for instructors. Reversible in a future phase
|
||||
-- once ROOT 08 gets the same authoring pass ROOT 01-04 got.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
UPDATE courses SET
|
||||
visibility = 'draft',
|
||||
description = description || ' [Excluded from ROOT_ V1 — see docs/ROOT_V1_CURRICULUM_MANIFEST.md. Not required by any V1 course; deferred to a future phase.]',
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE course_id = 'root-08-kubernetes-storage-scheduling';
|
||||
UPDATE course_modules SET visibility = 'draft', version = version + 1, updated_at = now()
|
||||
WHERE course_id = 'root-08-kubernetes-storage-scheduling';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- 4. ROOT 11 (DevOps & Platform Engineering Foundations) stays INCLUDED in V1, but explicitly
|
||||
-- repositioned as a single-module conceptual bridge/orientation, not a promise of a full
|
||||
-- DevOps course. It stays visibility = 'published' (real, finished, self-paced-ready content:
|
||||
-- lesson + knowledge check + reflection) and status = 'partial' (honest — six more modules are
|
||||
-- designed but not authored). Nothing downstream requires it (confirmed by audit), so it never
|
||||
-- blocks Real Ops/Capstone; a student who skips it loses nothing gating-wise, only the framing
|
||||
-- lesson itself.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
UPDATE courses SET
|
||||
description = 'A single orientation module bridging Linux/Kubernetes systems administration to platform-engineering thinking. This is intentionally NOT a full DevOps & Platform Engineering course for ROOT_ V1 — CI/CD, GitOps, and Infrastructure-as-Code modules are designed (docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md) but require lab infrastructure not yet built, and are explicitly out of scope for this phase (see docs/ROOT_V1_CURRICULUM_MANIFEST.md). Optional: not required to reach Real Ops or the Capstone.',
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE course_id = 'root-11-devops-platform-engineering';
|
||||
@@ -0,0 +1,53 @@
|
||||
-- Phase 6F — self-caught correction to 029_phase6f_root_v1_integration.sql, applied before
|
||||
-- 029 was ever pushed. Auditing the pre-existing rootnode-path.json Capstone unlock_rule (a
|
||||
-- separate, older, stage-based gate — see docs/ROOT_PROGRAM_ARCHITECTURE.md's "Real Ops and
|
||||
-- Capstone placement") against 029's is_required=false change found that root-06-m01,
|
||||
-- root-07-m01, and root-10-m02 are not generic supplemental practice: their scenario_ids are
|
||||
-- the LITERAL "workloads"/"networking"/"troubleshooting" stage content that gate whether the
|
||||
-- Capstone assessment can even be started, independent of this course layer's own prerequisite
|
||||
-- chain. Making them optional at the course layer would have let a self-paced student reach a
|
||||
-- course-tree-complete state while remaining silently, confusingly blocked from the Capstone —
|
||||
-- exactly the "confusing gap" ROOT_V1_CURRICULUM_MANIFEST.md's audit exists to catch.
|
||||
--
|
||||
-- Fix: keep them REQUIRED (revert 029's is_required=false), and instead give each module the
|
||||
-- minimal self-paced framing it was actually missing — one short intro lesson explaining what
|
||||
-- the bank is and why it's structured differently — so root-06/07/10 earn 'ready' honestly
|
||||
-- through real (if minimal) authoring rather than through a reclassification that would have
|
||||
-- broken Capstone reachability. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
|
||||
UPDATE module_activities SET is_required = true, version = version + 1, updated_at = now()
|
||||
WHERE module_id IN (
|
||||
'root-06-m01-deployments-and-workloads',
|
||||
'root-07-m01-services-and-networking',
|
||||
'root-10-m02-general-troubleshooting'
|
||||
);
|
||||
|
||||
UPDATE module_activities SET sequence = sequence + 1, version = version + 1, updated_at = now()
|
||||
WHERE module_id IN (
|
||||
'root-06-m01-deployments-and-workloads',
|
||||
'root-07-m01-services-and-networking',
|
||||
'root-10-m02-general-troubleshooting'
|
||||
);
|
||||
|
||||
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
|
||||
('root-06-m01-a00-lab-bank-intro', 'root-06-m01-deployments-and-workloads', 1, 'lesson', 'Practice Bank: Deployments & Workloads',
|
||||
'content/root/root-06-kubernetes-application-workloads/m01-deployments-and-workloads/00-lab-bank-intro.md', NULL, NULL, 'manual_marker', 4, 'ready'),
|
||||
('root-07-m01-a00-lab-bank-intro', 'root-07-m01-services-and-networking', 1, 'lesson', 'Practice Bank: Services & Networking',
|
||||
'content/root/root-07-kubernetes-networking-config/m01-services-and-networking/00-lab-bank-intro.md', NULL, NULL, 'manual_marker', 3, 'ready'),
|
||||
('root-10-m02-a00-lab-bank-intro', 'root-10-m02-general-troubleshooting', 1, 'lesson', 'Practice Bank: General Troubleshooting Operations',
|
||||
'content/root/root-10-kubernetes-troubleshooting-observability/m02-general-troubleshooting/00-lab-bank-intro.md', NULL, NULL, 'manual_marker', 3, 'ready')
|
||||
ON CONFLICT (activity_id) DO NOTHING;
|
||||
|
||||
UPDATE course_modules SET
|
||||
status = 'ready',
|
||||
description = replace(description, ' — OPTIONAL supplemental practice for ROOT_ V1: a bare scenario bank with no lesson/demonstration/knowledge-check framing, not required for course or self-paced progression. Suitable for instructor-assigned extra reps in an instructor-led cohort.', ' — a fast, low-scaffolding practice bank reinforcing the teaching modules right before it (see its own intro lesson); required, and load-bearing for the pre-existing Capstone stage gate.'),
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE module_id IN (
|
||||
'root-06-m01-deployments-and-workloads',
|
||||
'root-07-m01-services-and-networking',
|
||||
'root-10-m02-general-troubleshooting'
|
||||
);
|
||||
|
||||
-- root-06/07/10 were correctly bumped to 'ready' in 029 on the assumption their one remaining
|
||||
-- gap module would become optional; that gap is now closed by real (minimal) authoring instead,
|
||||
-- so the course-level 'ready' status stands — no revert needed here, just re-confirming intent.
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Phase 6F — Capstone gate review (Step 7). Decision: STRENGTHEN, do not weaken.
|
||||
--
|
||||
-- Evidence gathered before deciding:
|
||||
-- - rootnode-capstone's own scenario pool (assessment_definitions.pool_spec) is 100%
|
||||
-- deployments/config/troubleshooting/real-ops scenarios — zero overlap with
|
||||
-- rootnode-security-assessment's CKS-style pool (SecurityContext, RBAC, NetworkPolicy,
|
||||
-- image supply chain). A student can currently reach and pass the Capstone having never
|
||||
-- touched a single security scenario.
|
||||
-- - ROOT 09 (Kubernetes Security) only reached genuine course-level 'ready' status THIS
|
||||
-- phase (see 029_phase6f_root_v1_integration.sql) — Phase 6B/6C left it PARTIAL, so
|
||||
-- requiring it any earlier would have been irresponsible (gating on unfinished content).
|
||||
-- - root-09 and root-12 are already sibling courses (both prerequisite = root-06), so adding
|
||||
-- root-09 alongside root-12 as a root-capstone prerequisite imposes no new sequencing
|
||||
-- burden — a student can do them in either order or in parallel, they simply both must be
|
||||
-- done before the Capstone course unlocks.
|
||||
-- - For the intended ROOT_ audience (aspiring DevOps/cloud/platform/infrastructure
|
||||
-- engineers), shipping a "graduation" capstone with zero security competency check is a
|
||||
-- real, professionally-relevant gap, not a cosmetic one.
|
||||
--
|
||||
-- What this changes: ONLY the course-layer `root-capstone.prerequisites` array (the same
|
||||
-- mechanism/column every other course-level gate in this program already uses — no schema
|
||||
-- change). What this deliberately does NOT change: the pre-existing, older
|
||||
-- `rootnode-path.json` "stages_complete" unlock_rule that gates actually STARTING the
|
||||
-- rootnode-capstone assessment itself (foundation/workloads/networking/configuration/
|
||||
-- troubleshooting/real-ops) — that legacy mechanism has no natural "security stage" without
|
||||
-- new scenario-bank content-authoring work, which is out of Phase 6F's scope. The two gates
|
||||
-- already "agree by construction, not by shared implementation" (ROOT_PROGRAM_ARCHITECTURE.md);
|
||||
-- this migration keeps that true by only ever making the course-layer gate a superset,
|
||||
-- never a subset, of what the assessment-layer gate already requires.
|
||||
--
|
||||
-- Result: a student must now complete ROOT 09 (Security) AND ROOT 12 (Real Ops) — both already
|
||||
-- required individually, now also jointly required — before the root-capstone COURSE is
|
||||
-- unlocked, in addition to the pre-existing six-stage scenario gate on the assessment itself.
|
||||
-- See docs/ROOT_V1_CURRICULUM_MANIFEST.md and platform/test/e2e-phase6f.js for verification.
|
||||
|
||||
UPDATE courses SET
|
||||
prerequisites = ARRAY['root-12-real-ops', 'root-09-kubernetes-security'],
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE course_id = 'root-capstone';
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Phase 6F Steps 9-11 — ROOT_ V1 program + Lab orientation. No orientation content existed
|
||||
-- anywhere in the curriculum before this (audited by grep across content/root — see
|
||||
-- docs/PHASE6F_ROOT_V1_PILOT_READINESS.md); a self-paced student's very first touch of the
|
||||
-- platform was ROOT 01's Linux lesson with no framing of what ROOT_ is, how the Learn->Practice->
|
||||
-- Lab->Troubleshoot->Validate loop works, or what happens when a Lab is recreated. Added as a
|
||||
-- new required module at the front of ROOT 01 — the exact same "front-door module" pattern
|
||||
-- root-05-m00 already established (a real module, not a special orientation type), so no new
|
||||
-- activity type or mechanism was needed.
|
||||
|
||||
UPDATE course_modules SET sequence = sequence + 1, version = version + 1, updated_at = now()
|
||||
WHERE course_id = 'root-01-computing-linux-foundations';
|
||||
|
||||
-- root-01-m01's own prerequisite now points at the new orientation module instead of {} — it
|
||||
-- was previously the first module in the course with nothing to require.
|
||||
UPDATE course_modules SET prerequisites = ARRAY['root-01-m00-welcome-to-root'], version = version + 1, updated_at = now()
|
||||
WHERE module_id = 'root-01-m01-how-computers-run-software';
|
||||
|
||||
INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES
|
||||
('root-01-m00-welcome-to-root', 'root-01-computing-linux-foundations', 'Welcome to ROOT_',
|
||||
'What ROOT_ is, who it is for, how the Learn/Practice/Lab/Troubleshoot/Validate loop works, and what to expect from your disposable Lab environment before your first hands-on module.',
|
||||
1, ARRAY['Describe the full ROOT_ course sequence and what each stage builds on', 'Explain why a Lab environment is disposable while academic progress is not', 'Know how to use hints, reset a broken Lab, and request help in an instructor-led session'],
|
||||
ARRAY[]::text[], 15, 'ready')
|
||||
ON CONFLICT (module_id) DO NOTHING;
|
||||
|
||||
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
|
||||
('root-01-m00-a01-lesson-welcome', 'root-01-m00-welcome-to-root', 1, 'lesson', 'Welcome to ROOT_',
|
||||
'content/root/root-01-computing-linux-foundations/m00-welcome-to-root/01-lesson-welcome-to-root.md', NULL, NULL, 'manual_marker', 6, 'ready'),
|
||||
('root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-welcome-to-root', 2, 'lesson', 'Your Disposable Lab',
|
||||
'content/root/root-01-computing-linux-foundations/m00-welcome-to-root/02-lesson-your-disposable-lab.md', NULL, NULL, 'manual_marker', 6, 'ready'),
|
||||
('root-01-m00-a03-knowledge-check', 'root-01-m00-welcome-to-root', 3, 'knowledge_check', 'Knowledge Check: Welcome to ROOT_',
|
||||
'content/root/root-01-computing-linux-foundations/m00-welcome-to-root/03-knowledge-check.md', NULL, NULL, 'manual_marker', 3, 'ready')
|
||||
ON CONFLICT (activity_id) DO NOTHING;
|
||||
@@ -89,7 +89,10 @@ async function main() {
|
||||
const plannedWithModules = structure.filter((c) => c.status === 'planned' && c.modules.length > 0);
|
||||
assert.equal(plannedWithModules.length, 0, 'a PLANNED course must have zero modules, not placeholder content');
|
||||
const gold = courseById(structure, 'root-10-kubernetes-troubleshooting-observability');
|
||||
assert.equal(gold.status, 'partial'); // real content (gold module) alongside un-authored modules
|
||||
// Phase 6F gave root-10-m02 (the bare practice bank) real framing (docs/
|
||||
// PHASE6F_ROOT_V1_PILOT_READINESS.md), so all three modules are now genuinely authored and
|
||||
// the course honestly earns 'ready' — no fabrication, just a real content-completeness fix.
|
||||
assert.equal(gold.status, 'ready');
|
||||
});
|
||||
|
||||
await test('the gold-standard module is fully READY with 7 ordered activities and real content', async () => {
|
||||
@@ -143,10 +146,12 @@ async function main() {
|
||||
// sequenced before ROOT 05 — it is now genuinely the program's front door, not ROOT 05's
|
||||
// own gold module (see docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's original intent —
|
||||
// "resume lands on the first REAL content, not a bare-lab bank" — is still what's being
|
||||
// proven, just against the current front door.
|
||||
// proven, just against the current front door. Phase 6F then added a required orientation
|
||||
// module (root-01-m00-welcome-to-root) in front of that — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
|
||||
});
|
||||
|
||||
await test('a student cannot mark a lab activity complete manually — only real scenario completion counts', async () => {
|
||||
@@ -193,6 +198,14 @@ async function main() {
|
||||
await completeScenario(casey, 'rolling-update-task');
|
||||
await completeScenario(casey, 'deployment-rollback');
|
||||
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
|
||||
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// and not part of WORKLOADS's scenario list. root-06-m01's own prerequisite is root-06-m02
|
||||
// complete, so this must come after root-06-m02's reflection above, not before it — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
{
|
||||
const { status } = await casey.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
|
||||
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
|
||||
}
|
||||
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive']) {
|
||||
await casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
|
||||
}
|
||||
|
||||
@@ -86,9 +86,13 @@ async function main() {
|
||||
}
|
||||
});
|
||||
|
||||
await test('ROOT 08 and ROOT 09 moved from PLANNED to PARTIAL — real content exists now', async () => {
|
||||
await test('ROOT 08 stays PARTIAL, ROOT 09 reached READY — real content exists now', async () => {
|
||||
// Phase 6F (Step 0's audit) found ROOT 09's course-level status had drifted stale — all 4
|
||||
// of its modules were already individually 'ready' — and corrected it; ROOT 08 is
|
||||
// unaffected and additionally excluded from V1 (visibility='draft') this same phase — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
assert.equal(courseById(structure, 'root-08-kubernetes-storage-scheduling').status, 'partial');
|
||||
assert.equal(courseById(structure, 'root-09-kubernetes-security').status, 'partial');
|
||||
assert.equal(courseById(structure, 'root-09-kubernetes-security').status, 'ready');
|
||||
});
|
||||
|
||||
await test('the gold-standard Health Probes module\'s required chain is unchanged (Phase 6C added one optional lab)', async () => {
|
||||
@@ -131,9 +135,11 @@ async function main() {
|
||||
// Phase 6D authored ROOT 01 as real, non-PLANNED content sequenced before ROOT 05 — it is
|
||||
// now genuinely the program's front door (docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's
|
||||
// original intent — resume must never land on a bare-lab bank when real front-door content
|
||||
// exists — is unchanged, just against the current front door.
|
||||
// exists — is unchanged, just against the current front door. Phase 6F then added a
|
||||
// required orientation module in front of root-01-m01 — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
|
||||
});
|
||||
|
||||
await test('completing the real ROOT 01-04 foundations sequence unlocks ROOT 05 (Phase 6E test setup, not this file\'s own subject)', async () => {
|
||||
@@ -197,6 +203,13 @@ async function main() {
|
||||
await casey.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`);
|
||||
}
|
||||
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
|
||||
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// and its own prerequisite is root-06-m02 complete, so this must come right after — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
{
|
||||
const { status } = await casey.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
|
||||
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
|
||||
}
|
||||
// Phase 6C upgraded root-06-m03 to the full gold pattern (added a01b/a02b/a05) — all four
|
||||
// manual-marker activities, plus a05's own reflection, are required for root-06 the COURSE
|
||||
// to actually be complete (root-09's real prerequisite), not just its original two.
|
||||
@@ -242,16 +255,20 @@ async function main() {
|
||||
});
|
||||
|
||||
await test('a draft activity in a new module is hidden from students but visible to instructors', async () => {
|
||||
psql(`UPDATE module_activities SET visibility = 'draft' WHERE activity_id = 'root-08-m01-a01-lesson-storage-fundamentals'`);
|
||||
// Phase 6F excluded ROOT 08 from V1 entirely (visibility='draft' on the whole course) — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md — so it can no longer demonstrate per-activity
|
||||
// draft filtering (the whole module is already invisible to students regardless). Using
|
||||
// root-09-m01 instead, still a real Phase 6B module and still published in V1.
|
||||
psql(`UPDATE module_activities SET visibility = 'draft' WHERE activity_id = 'root-09-m01-a01-lesson-security-model'`);
|
||||
try {
|
||||
const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
const mod = moduleById(studentView.courses, 'root-08-m01-storage-fundamentals');
|
||||
assert.ok(!mod.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
||||
const mod = moduleById(studentView.courses, 'root-09-m01-security-model-and-securitycontext');
|
||||
assert.ok(!mod.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
|
||||
const { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
const modAdmin = moduleById(adminView, 'root-08-m01-storage-fundamentals');
|
||||
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
||||
const modAdmin = moduleById(adminView, 'root-09-m01-security-model-and-securitycontext');
|
||||
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
|
||||
} finally {
|
||||
psql(`UPDATE module_activities SET visibility = 'published' WHERE activity_id = 'root-08-m01-a01-lesson-storage-fundamentals'`);
|
||||
psql(`UPDATE module_activities SET visibility = 'published' WHERE activity_id = 'root-09-m01-a01-lesson-security-model'`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -355,6 +372,13 @@ async function main() {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-06-m02-${suffix} should 200`);
|
||||
}
|
||||
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// and its own prerequisite is root-06-m02 complete, so it comes right after — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
{
|
||||
const { status } = await bypass.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
|
||||
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
|
||||
}
|
||||
await completeScenario(bypass, 'job-task');
|
||||
await completeScenario(bypass, 'cronjob-task');
|
||||
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive', 'a05-reflection']) {
|
||||
@@ -375,11 +399,63 @@ async function main() {
|
||||
// total — ALL_STAGES (above) covers 5 of its 6 scenarios but predates container-logging-mcq.
|
||||
await completeScenario(bypass, 'container-logging-mcq');
|
||||
await bypass.request('POST', '/api/student/activities/root-10-m03-a06-reflection/complete');
|
||||
// Phase 6F gave root-10-m02 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// and its own prerequisite is root-10-m03 complete (just finished above). Without this,
|
||||
// root-10 the COURSE never reaches complete=true, which keeps root-12 the COURSE locked
|
||||
// (its own prerequisite) even though root-12-m01 the MODULE's prerequisite (root-10-m01) is
|
||||
// already satisfied — see docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
{
|
||||
const { status } = await bypass.request('POST', '/api/student/activities/root-10-m02-a00-lab-bank-intro/complete');
|
||||
assert.equal(status, 200, 'completing root-10-m02-a00-lab-bank-intro should 200');
|
||||
}
|
||||
await bypass.request('POST', '/api/student/activities/root-12-m01-a00-lesson-incident-response/complete');
|
||||
await bypass.request('POST', '/api/student/activities/root-12-m01-a99-reflection-post-incident/complete');
|
||||
const { body: chk } = await bypass.request('GET', `/api/student/programs/${bypassEnroll.enrollment_id}/courses`);
|
||||
assert.equal(courseById(chk.courses, 'root-12-real-ops').complete, true, 'root-12 must be complete before the capstone course unlocks');
|
||||
|
||||
// Phase 6F (Step 7) added root-09-kubernetes-security as a second, joint course-level
|
||||
// prerequisite for root-capstone alongside root-12 — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md. root-09-m03 also carries a real cross-course
|
||||
// module prerequisite on root-07-m04 (Ingress & NetworkPolicy), which itself requires
|
||||
// root-07-m03 complete — neither touched anywhere else in this file's bypass flow.
|
||||
for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-07-m03-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-07-m03-${suffix} should 200`);
|
||||
}
|
||||
for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-07-m04-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-07-m04-${suffix} should 200`);
|
||||
}
|
||||
await completeScenario(bypass, 'ingress-task');
|
||||
await completeScenario(bypass, 'network-policy');
|
||||
|
||||
for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m01-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-09-m01-${suffix} should 200`);
|
||||
}
|
||||
for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await completeScenario(bypass, sid);
|
||||
|
||||
for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m02-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-09-m02-${suffix} should 200`);
|
||||
}
|
||||
for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await completeScenario(bypass, sid);
|
||||
|
||||
for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m03-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-09-m03-${suffix} should 200`);
|
||||
}
|
||||
for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await completeScenario(bypass, sid);
|
||||
|
||||
await bypass.request('POST', '/api/student/activities/root-09-m04-a01-lesson-cluster-hardening/complete');
|
||||
for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await completeScenario(bypass, sid);
|
||||
{
|
||||
const { body: startBody } = await bypass.request('POST', '/api/assessments/rootnode-security-assessment/start');
|
||||
for (const s of startBody.scenarios) await completeScenario(bypass, s.scenario_id);
|
||||
}
|
||||
const { body: chk2 } = await bypass.request('GET', `/api/student/programs/${bypassEnroll.enrollment_id}/courses`);
|
||||
assert.equal(courseById(chk2.courses, 'root-09-kubernetes-security').complete, true, 'root-09 must be complete before the capstone course unlocks');
|
||||
|
||||
for (const suffix of ['a01-lesson-what-it-measures', 'a02-lesson-time-management', 'a03-knowledge-check', 'a04-reflection-readiness']) {
|
||||
const { status } = await bypass.request('POST', `/api/student/activities/root-capstone-m02-${suffix}/complete`);
|
||||
assert.equal(status, 200, `completing root-capstone-m02-${suffix} should 200`);
|
||||
|
||||
@@ -25,7 +25,9 @@ if (!OWNER_EMAIL || !OWNER_PASSWORD) {
|
||||
}
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
const ROOT01_MODULES = ['root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations'];
|
||||
// Phase 6F prepended a required orientation module (root-01-m00-welcome-to-root) — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
const ROOT01_MODULES = ['root-01-m00-welcome-to-root', 'root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations'];
|
||||
const ROOT01_LAB_SCENARIOS = ['linux-inspect-hardware', 'linux-shell-basics', 'linux-filesystem-navigation', 'linux-files-and-text', 'linux-process-management', 'linux-permissions-foundations'];
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
@@ -99,11 +101,22 @@ async function main() {
|
||||
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
|
||||
const casey = await newStudent(owner, 'Casey');
|
||||
|
||||
await test('resume points at ROOT 01 module 1 activity 1, the very first activity in the whole program', async () => {
|
||||
await test('resume points at ROOT 01 module 0 activity 1, the very first activity in the whole program', async () => {
|
||||
// Phase 6F added a required orientation module (root-01-m00-welcome-to-root) in front of
|
||||
// what was previously the first module — see docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
|
||||
});
|
||||
|
||||
await test('walking the ROOT_ V1 orientation module completes it and unlocks module 1', async () => {
|
||||
await completeActivity(casey.session, 'root-01-m00-a01-lesson-welcome');
|
||||
await completeActivity(casey.session, 'root-01-m00-a02-lesson-disposable-lab');
|
||||
await completeActivity(casey.session, 'root-01-m00-a03-knowledge-check');
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').locked, false);
|
||||
});
|
||||
|
||||
// ── 16: direct API cannot bypass — a lab activity cannot be manually marked complete ─────
|
||||
|
||||
@@ -57,6 +57,10 @@ async function completeScenario(session, sid) {
|
||||
// labs/reflections later would trip the module-lock check for later modules — a real ordering
|
||||
// constraint, not a test artifact.
|
||||
async function completeRoot01(session) {
|
||||
// Phase 6F — a required orientation module now gates root-01-m01, same pattern as root-05-m00.
|
||||
await completeActivity(session, 'root-01-m00-a01-lesson-welcome');
|
||||
await completeActivity(session, 'root-01-m00-a02-lesson-disposable-lab');
|
||||
await completeActivity(session, 'root-01-m00-a03-knowledge-check');
|
||||
await completeActivity(session, 'root-01-m01-a01-lesson');
|
||||
await completeActivity(session, 'root-01-m01-a02-guided-exercise');
|
||||
await completeScenario(session, 'linux-inspect-hardware');
|
||||
@@ -231,10 +235,12 @@ async function main() {
|
||||
// ── A real student walking the full ROOT 01->02->03->04 chain end to end ────────────────
|
||||
const alice = await newStudent(owner, 'Alice');
|
||||
|
||||
await test('resume points at ROOT 01 module 1 activity 1 for a brand-new student', async () => {
|
||||
await test('resume points at ROOT 01 orientation module activity 1 for a brand-new student', async () => {
|
||||
// Phase 6F added a required orientation module in front of root-01-m01 — see
|
||||
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
|
||||
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
|
||||
});
|
||||
|
||||
// ── 30/26/27/28/29: direct API cannot bypass prerequisites (Section 11's real finding) ──
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 6F — ROOT_ V1 Curriculum Integration & Pilot Readiness acceptance test. Covers the V1
|
||||
// manifest/exclusion decisions, the full V1 prerequisite graph (including the new
|
||||
// root-capstone/root-09 edge), self-paced/instructor-led eligibility, the orientation module,
|
||||
// and the platform architecture's age-independence. Same conventions as e2e-phase6a-e.js:
|
||||
// assert-based, no framework, live docker-compose.dev.yml stack.
|
||||
//
|
||||
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
|
||||
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
|
||||
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase6f.js
|
||||
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const { makeSession } = require('./session-helper');
|
||||
const { completeRootFoundations01Through04, completeThroughRoot10, completeThroughCapstoneEligible } = require('./foundations-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const OWNER_EMAIL = process.env.OWNER_EMAIL;
|
||||
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
|
||||
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
|
||||
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one first with platform/api/bootstrap-owner.js)');
|
||||
process.exit(1);
|
||||
}
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function test(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.message}`); failed++; }
|
||||
}
|
||||
function courseById(courses, id) { return courses.find((c) => c.course_id === id); }
|
||||
function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
|
||||
function psql(sql) {
|
||||
return execSync(`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "${sql}"`, { cwd: `${__dirname}/..`, stdio: 'pipe' }).toString().trim();
|
||||
}
|
||||
async function newStudent(owner, tag) {
|
||||
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6F-${tag}-${RUN_ID}`, login_identifier: `e2e6f-${tag}-${RUN_ID}` });
|
||||
const s = makeSession(GW);
|
||||
await s.request('POST', '/api/student/activate', { token: create.activation_token, password: `${tag}Password123!` });
|
||||
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
|
||||
return { session: s, studentId: create.student_id, enrollmentId: enroll.enrollment_id };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 6F ROOT_ V1 Curriculum Integration & Pilot Readiness acceptance test\n');
|
||||
const owner = makeSession(GW);
|
||||
{ const r = await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD }); assert.equal(r.status, 200); }
|
||||
|
||||
// ── 1/2: the V1 manifest — inclusion/exclusion, statuses ─────────────────────────────────
|
||||
let adminStructure;
|
||||
await test('the V1 manifest\'s course statuses match the live database', async () => {
|
||||
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
adminStructure = body;
|
||||
for (const id of ['root-01-computing-linux-foundations', 'root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker', 'root-05-kubernetes-foundations', 'root-06-kubernetes-application-workloads', 'root-07-kubernetes-networking-config', 'root-09-kubernetes-security', 'root-10-kubernetes-troubleshooting-observability', 'root-12-real-ops', 'root-capstone']) {
|
||||
assert.equal(courseById(adminStructure, id).status, 'ready', `${id} should be ready`);
|
||||
}
|
||||
assert.equal(courseById(adminStructure, 'root-08-kubernetes-storage-scheduling').status, 'partial');
|
||||
assert.equal(courseById(adminStructure, 'root-11-devops-platform-engineering').status, 'partial');
|
||||
});
|
||||
|
||||
await test('ROOT 08 is excluded from V1 (draft) but ROOT 11 stays published as a bonus module', async () => {
|
||||
// buildCourseTree() never surfaces `visibility` on the shaped course object (it's a SQL
|
||||
// WHERE-clause filter only, per course-progress.js) — verified directly against Postgres.
|
||||
assert.equal(psql(`SELECT visibility FROM courses WHERE course_id = 'root-08-kubernetes-storage-scheduling'`), 'draft');
|
||||
assert.equal(psql(`SELECT visibility FROM courses WHERE course_id = 'root-11-devops-platform-engineering'`), 'published');
|
||||
});
|
||||
|
||||
const fresh = await newStudent(owner, 'Fresh');
|
||||
await test('a brand-new student never sees ROOT 08 at all (visibility, not just locked)', async () => {
|
||||
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-08-kubernetes-storage-scheduling'), undefined, 'a draft course must not appear in the student response');
|
||||
assert.ok(courseById(body.courses, 'root-11-devops-platform-engineering'), 'ROOT 11 must still appear (published)');
|
||||
});
|
||||
|
||||
// ── 3/4: prerequisite graph — no cycles, no missing nodes ────────────────────────────────
|
||||
await test('the full V1 prerequisite graph has no cycles and no missing nodes (validator)', async () => {
|
||||
const out = execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' }).toString();
|
||||
assert.ok(/PASS — 0 error\(s\)/.test(out), `expected 0 validation errors: ${out}`);
|
||||
});
|
||||
|
||||
await test('ROOT 08 (excluded) is not referenced as a prerequisite by anything ELSE in V1', async () => {
|
||||
for (const c of adminStructure) {
|
||||
if (c.course_id === 'root-08-kubernetes-storage-scheduling') continue; // ROOT 08's own internal module chain referencing itself doesn't count
|
||||
assert.ok(!(c.prerequisites || []).includes('root-08-kubernetes-storage-scheduling'), `${c.course_id} must not depend on excluded ROOT 08`);
|
||||
for (const m of c.modules) assert.ok(!(m.prerequisites || []).some((p) => p.startsWith('root-08-')), `${m.module_id} must not depend on excluded ROOT 08`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── 5/6: ROOT 01-04 and intended ROOT 05+ modules ready ──────────────────────────────────
|
||||
await test('every ROOT 01-04 module is ready (re-verified, not inherited from Phase 6E)', async () => {
|
||||
for (const c of ['root-01-computing-linux-foundations', 'root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
|
||||
const course = courseById(adminStructure, c);
|
||||
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} should be ready`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── orientation, resume, unlock ───────────────────────────────────────────────────────────
|
||||
await test('resume points at the new orientation module for a brand-new student', async () => {
|
||||
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
|
||||
});
|
||||
|
||||
await test('completing the orientation module unlocks ROOT 01 module 1', async () => {
|
||||
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) {
|
||||
const r = await fresh.session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
assert.equal(r.status, 200);
|
||||
}
|
||||
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').locked, false);
|
||||
});
|
||||
|
||||
// ── 11: optional practice never blocks (ROOT 09's advanced CKS labs) ─────────────────────
|
||||
const optStudent = await newStudent(owner, 'OptionalCheck');
|
||||
await test('an optional activity left undone never blocks its module or course from completing', async () => {
|
||||
await completeThroughCapstoneEligible(optStudent.session);
|
||||
const { body } = await optStudent.session.request('GET', `/api/student/programs/${optStudent.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-09-kubernetes-security').complete, true, 'root-09 complete despite its optional advanced labs never being touched');
|
||||
assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true);
|
||||
});
|
||||
|
||||
// ── 14: Capstone gating — root-09 AND root-12 both required, direct API cannot bypass ────
|
||||
await test('root-capstone stays locked with only root-12 done (root-09 also required)', async () => {
|
||||
const s = await newStudent(owner, 'CapstoneGateA');
|
||||
await completeThroughRoot10(s.session);
|
||||
// root-12 only, no root-09:
|
||||
async function act(id) { const r = await s.session.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, id); }
|
||||
async function scen(sid) { const r = await s.session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); assert.equal(r.status, 200, sid); }
|
||||
await act('root-12-m01-a00-lesson-incident-response');
|
||||
for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(sid);
|
||||
await act('root-12-m01-a99-reflection-post-incident');
|
||||
const { body } = await s.session.request('GET', `/api/student/programs/${s.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true);
|
||||
assert.equal(courseById(body.courses, 'root-capstone').locked, true, 'root-capstone must stay locked until root-09 is ALSO complete');
|
||||
const bypass = await s.session.request('POST', '/api/student/activities/root-capstone-m02-a01-lesson-what-it-measures/complete');
|
||||
assert.equal(bypass.status, 403);
|
||||
assert.equal(bypass.body.error, 'locked');
|
||||
});
|
||||
|
||||
await test('root-capstone unlocks once both root-09 and root-12 are complete', async () => {
|
||||
const { body } = await optStudent.session.request('GET', `/api/student/programs/${optStudent.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-capstone').locked, false);
|
||||
const r = await optStudent.session.request('POST', '/api/student/activities/root-capstone-m02-a01-lesson-what-it-measures/complete');
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
|
||||
// ── 15: V1 version behavior — migrations bumped version, content unaffected ──────────────
|
||||
await test('bumping a Phase 6F module\'s version again does not drop its content', async () => {
|
||||
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-06-m01-deployments-and-workloads'`);
|
||||
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
const mod = moduleById(body, 'root-06-m01-deployments-and-workloads');
|
||||
assert.ok(mod.activities.some((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro'));
|
||||
});
|
||||
|
||||
// ── 16: instructor notes ─────────────────────────────────────────────────────────────────
|
||||
await test('instructor notes on the new orientation and intro-lesson content are instructor-only', async () => {
|
||||
const { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
const lesson = moduleById(adminView, 'root-01-m00-welcome-to-root').activities.find((a) => a.activity_id === 'root-01-m00-a02-lesson-disposable-lab');
|
||||
assert.ok(lesson.instructor_notes === null || lesson.instructor_notes === undefined || lesson.instructor_notes === '', 'this particular lesson has no Instructor Notes section, which is fine — it must never be fabricated');
|
||||
const bank = moduleById(adminView, 'root-06-m01-deployments-and-workloads').activities.find((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro');
|
||||
assert.ok(bank.instructor_notes && bank.instructor_notes.includes('Instructor Notes'));
|
||||
// root-06-m01 is LOCKED for fresh (not yet unlocked), not DRAFT — a locked module still
|
||||
// appears in the tree so a student can see what's ahead, it just never carries
|
||||
// instructor_notes regardless of lock state.
|
||||
const { body: studentView } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
const studentBank = moduleById(studentView.courses, 'root-06-m01-deployments-and-workloads');
|
||||
const studentBankActivity = studentBank.activities.find((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro');
|
||||
assert.ok(!studentBankActivity.instructor_notes, 'a student must never receive instructor_notes, locked or not');
|
||||
});
|
||||
|
||||
// ── 17/18: isolation, enrollment ──────────────────────────────────────────────────────────
|
||||
await test('two students on ROOT_ V1 have fully independent progress', async () => {
|
||||
const a = await newStudent(owner, 'IsoA');
|
||||
const b = await newStudent(owner, 'IsoB');
|
||||
await a.session.request('POST', '/api/student/activities/root-01-m00-a01-lesson-welcome/complete');
|
||||
const [{ body: aBody }, { body: bBody }] = await Promise.all([
|
||||
a.session.request('GET', `/api/student/programs/${a.enrollmentId}/courses`),
|
||||
b.session.request('GET', `/api/student/programs/${b.enrollmentId}/courses`),
|
||||
]);
|
||||
assert.notEqual(moduleById(aBody.courses, 'root-01-m00-welcome-to-root').percent_complete, moduleById(bBody.courses, 'root-01-m00-welcome-to-root').percent_complete);
|
||||
});
|
||||
|
||||
await test('a fresh V1 enrollment sees the complete intended course list with no gaps', async () => {
|
||||
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
const ids = body.courses.map((c) => c.course_id);
|
||||
for (const id of ['root-01-computing-linux-foundations', 'root-05-kubernetes-foundations', 'root-09-kubernetes-security', 'root-11-devops-platform-engineering', 'root-12-real-ops', 'root-capstone']) {
|
||||
assert.ok(ids.includes(id), `${id} should be visible to a self-paced V1 student`);
|
||||
}
|
||||
assert.ok(!ids.includes('root-08-kubernetes-storage-scheduling'));
|
||||
});
|
||||
|
||||
// ── 19: Lab lifecycle persistence (logout/login) ─────────────────────────────────────────
|
||||
await test('V1 progress survives logout and a fresh login', async () => {
|
||||
await fresh.session.request('POST', '/api/logout');
|
||||
const returning = makeSession(GW);
|
||||
await returning.request('POST', '/api/student/login', { identifier: `e2e6f-Fresh-${RUN_ID}`, password: 'FreshPassword123!' });
|
||||
const { body } = await returning.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
|
||||
});
|
||||
|
||||
// ── 22: age-independent architecture — enrollment never requires or stores an age field ──
|
||||
await test('enrollment succeeds with no age/grade field anywhere in the request or student record', async () => {
|
||||
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6F-NoAge-${RUN_ID}`, login_identifier: `e2e6f-noage-${RUN_ID}` });
|
||||
assert.ok(create.student_id);
|
||||
const cols = psql(`SELECT column_name FROM information_schema.columns WHERE table_name = 'students' AND column_name ILIKE '%age%'`);
|
||||
assert.equal(cols, '', 'the students table must not carry an age/grade gate column');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -15,6 +15,9 @@ async function completeRootFoundations01Through04(session) {
|
||||
if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`);
|
||||
}
|
||||
|
||||
// Phase 6F — the ROOT_ V1 orientation module (Steps 9-11) is a real, required front-door
|
||||
// module now gating root-01-m01, same as root-05-m00 already gates root-05-m01.
|
||||
await act('root-01-m00-a01-lesson-welcome'); await act('root-01-m00-a02-lesson-disposable-lab'); await act('root-01-m00-a03-knowledge-check');
|
||||
await act('root-01-m01-a01-lesson'); await act('root-01-m01-a02-guided-exercise'); await scen('linux-inspect-hardware'); await act('root-01-m01-a04-reflection');
|
||||
const l1 = { m02: 'linux-shell-basics', m03: 'linux-filesystem-navigation', m04: 'linux-files-and-text', m05: 'linux-process-management', m06: 'linux-permissions-foundations' };
|
||||
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) { await act(`root-01-${m}-a01-lesson`); await act(`root-01-${m}-a02-demonstration`); await act(`root-01-${m}-a03-knowledge-check`); await act(`root-01-${m}-a04-guided-exercise`); await scen(l1[m]); await act(`root-01-${m}-a06-reflection`); }
|
||||
@@ -64,6 +67,9 @@ async function completeThroughRoot06(session) {
|
||||
|
||||
for (const sid of ['deploy-nginx', 'scale-deployment', 'rolling-update-task', 'liveness-probe-task', 'jobs-cronjobs-mcq', 'daemonset-mcq', 'edit-deployment-basics', 'deployment-strategy-mcq', 'deployment-rollback']) await scen(sid);
|
||||
await act('root-06-m02-a01-lesson-deployments'); await act('root-06-m02-a02-demonstration-rollout'); await act('root-06-m02-a03-knowledge-check'); await act('root-06-m02-a04-guided-exercise-rollout-strategy'); await act('root-06-m02-a06-reflection');
|
||||
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// prerequisite root-06-m02 complete (just finished above).
|
||||
await act('root-06-m01-a00-lab-bank-intro');
|
||||
for (const sid of ['job-task', 'cronjob-task']) await scen(sid);
|
||||
await act('root-06-m03-a01-lesson-jobs-cronjobs-daemonsets'); await act('root-06-m03-a01b-demonstration-job-lifecycle'); await act('root-06-m03-a02-knowledge-check'); await act('root-06-m03-a02b-guided-exercise-choosing-primitive'); await act('root-06-m03-a05-reflection');
|
||||
}
|
||||
@@ -79,7 +85,41 @@ async function completeThroughRoot10(session) {
|
||||
await scen('rootnode-probe-port-mismatch'); await act('root-10-m01-a06-reflection-real-ops-takeaway');
|
||||
await act('root-10-m03-a01-lesson-framework'); await act('root-10-m03-a02-demonstration-crashloop'); await act('root-10-m03-a03-knowledge-check'); await act('root-10-m03-a03b-guided-exercise-endpoint-mismatch');
|
||||
await scen('crashloop-fix'); await scen('endpoint-fix-task'); await act('root-10-m03-a06-reflection');
|
||||
// Phase 6F gave root-10-m02 (a bare practice bank) a real intro-lesson activity — required,
|
||||
// prerequisite root-10-m03 complete (just finished above).
|
||||
await act('root-10-m02-a00-lab-bank-intro');
|
||||
for (const sid of ['extract-logs-basics', 'kubectl-cp-basics', 'exec-command-basics', 'node-cordon-basics', 'pod-node-assignment-mcq', 'container-logging-mcq']) await scen(sid);
|
||||
}
|
||||
|
||||
module.exports = { completeRootFoundations01Through04, completeThroughRoot06, completeThroughRoot10 };
|
||||
// Phase 6F — completes through ROOT 10, then ROOT 07 (m03/m04, needed by root-09-m03's
|
||||
// cross-course prerequisite), ROOT 09 (all 4 modules + its assessment), and ROOT 12, leaving
|
||||
// the student's root-capstone COURSE genuinely unlocked (both its real prerequisites —
|
||||
// root-12-real-ops and root-09-kubernetes-security — satisfied) without touching the Capstone
|
||||
// itself. See docs/ROOT_V1_CURRICULUM_MANIFEST.md Step 7 for why root-09 is now required.
|
||||
async function completeThroughCapstoneEligible(session) {
|
||||
await completeThroughRoot10(session);
|
||||
async function act(id) { const r = await session.request('POST', `/api/student/activities/${id}/complete`); if (r.status !== 200) throw new Error(`completing ${id} failed: ${r.status} ${JSON.stringify(r.body)}`); }
|
||||
async function scen(sid) { const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`); }
|
||||
|
||||
for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) await act(`root-07-m03-${suffix}`);
|
||||
await scen('expose-service-basics'); // root-07-m03's own lab — its OWN prerequisite for root-07-m04, easy to miss since ROOT 07's practice bank reuses the same scenario id.
|
||||
for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) await act(`root-07-m04-${suffix}`);
|
||||
await scen('ingress-task'); await scen('network-policy');
|
||||
|
||||
for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) await act(`root-09-m01-${suffix}`);
|
||||
for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await scen(sid);
|
||||
for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) await act(`root-09-m02-${suffix}`);
|
||||
for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await scen(sid);
|
||||
for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) await act(`root-09-m03-${suffix}`);
|
||||
for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await scen(sid);
|
||||
await act('root-09-m04-a01-lesson-cluster-hardening');
|
||||
for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await scen(sid);
|
||||
const r = await session.request('POST', '/api/assessments/rootnode-security-assessment/start');
|
||||
for (const s of r.body.scenarios) await scen(s.scenario_id);
|
||||
|
||||
await act('root-12-m01-a00-lesson-incident-response');
|
||||
for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(sid);
|
||||
await act('root-12-m01-a99-reflection-post-incident');
|
||||
}
|
||||
|
||||
module.exports = { completeRootFoundations01Through04, completeThroughRoot06, completeThroughRoot10, completeThroughCapstoneEligible };
|
||||
|
||||
Reference in New Issue
Block a user