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
|
capstone assessment engine checks before allowing a start (see §3 below for why these two gates
|
||||||
are deliberately independent).
|
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
|
## 2. What was NOT built
|
||||||
|
|
||||||
No answers, no exact scenario IDs from the current pool, no capstone-specific hint content
|
No answers, no exact scenario IDs from the current pool, no capstone-specific hint content
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# ROOT Pilot Curriculum — Authoritative Manifest
|
# 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
|
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
|
pilot." It uses honest READY / PARTIAL / DEFERRED / NOT IN PILOT labels — nothing here is
|
||||||
marketed as more finished than it is. See
|
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
|
means and [`docs/ROOT11_DEVOPS_PLATFORM_SCOPE.md`](ROOT11_DEVOPS_PLATFORM_SCOPE.md) for the
|
||||||
ROOT 11 scope decision referenced below.
|
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
|
As of Phase 6E, ROOT 01-04 (the entire foundational half) met the full self-paced bar. Three
|
||||||
the curriculum requires it — as of Phase 6E, ROOT 01-04 (the entire foundational half) and every
|
specific practice-bank modules (`root-06-m01`, `root-07-m01`, `root-10-m02`) were flagged as
|
||||||
Kubernetes/security/troubleshooting/real-ops/capstone module EXCEPT three specific practice-bank
|
instructor-led-only — dense hands-on scenario banks with no lesson framing of their own.
|
||||||
modules meet the full self-paced bar (no "your instructor will explain this" gaps, verified
|
**Phase 6F closed this gap**: each received a short, real intro-lesson activity explaining what
|
||||||
programmatically). Those three flagged modules — dense hands-on practice banks whose sibling
|
the bank is and how to use it (`docs/ROOT_V1_CURRICULUM_MANIFEST.md` Step 4) — all three, and
|
||||||
module in the same course already carries the taught concept — are READY for instructor-led
|
their parent courses, are now genuinely self-paced-ready. Instructor-led delivery remains fully
|
||||||
delivery only; running them live lets an instructor supply framing a self-paced student wouldn't
|
supported and recommended for a first cohort, but is no longer required by any content gap.
|
||||||
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.
|
|
||||||
|
|
||||||
## Program: ROOT_ (root-kubernetes-foundations)
|
## 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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 | **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. |
|
| 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 | **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. |
|
| 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** | 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. |
|
| 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 | **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. |
|
| 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 | **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. |
|
| 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 | **PLANNED** | No CI/CD/IaC/GitOps scenario content exists anywhere in this repository — unchanged, out of Phase 6B's authoring scope. |
|
| 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 | **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. |
|
| 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 | **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`. |
|
| 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
|
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
|
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
|
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.
|
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
|
## Deviations from the spec's suggested sequence, and why
|
||||||
|
|
||||||
The authorization's suggested sequence listed Security (09) after Storage/Scheduling (08) and
|
The authorization's suggested sequence listed Security (09) after Storage/Scheduling (08) and
|
||||||
|
|||||||
@@ -1,48 +1,47 @@
|
|||||||
# ROOT_ Program Duration Model
|
# ROOT_ Program Duration Model
|
||||||
|
|
||||||
Phase 6E §28 — recalculated from actual authored activities in the live database. Run:
|
Phase 6F Step 25 (built on Phase 6E §28) — recalculated from actual authored activities in the
|
||||||
`SELECT c.course_id, SUM(a.estimated_duration_minutes) ... GROUP BY c.course_id` against the
|
live database after Phase 6F's curriculum integration (orientation module + the three
|
||||||
current schema, same method as Phase 6D's original version of this document.
|
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)
|
## Per-course totals (self-paced estimate)
|
||||||
|
|
||||||
| Course | Activities | Minutes | Hours |
|
| Course | Activities | Minutes | Hours | In ROOT_ V1? |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| ROOT 01 Computing & Linux Foundations | 35 | 338 | 5.6 |
|
| ROOT 01 Computing & Linux Foundations | 38 | 353 | 5.9 | Yes |
|
||||||
| ROOT 02 Linux Administration & Networking | 37 | 388 | 6.5 |
|
| ROOT 02 Linux Administration & Networking | 37 | 388 | 6.5 | Yes |
|
||||||
| ROOT 03 Git & Collaborative Development | 31 | 312 | 5.2 |
|
| ROOT 03 Git & Collaborative Development | 31 | 312 | 5.2 | Yes |
|
||||||
| ROOT 04 Containers & Docker | 43 | 444 | 7.4 |
|
| ROOT 04 Containers & Docker | 43 | 444 | 7.4 | Yes |
|
||||||
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 |
|
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 | Yes |
|
||||||
| ROOT 06 Kubernetes Application Workloads | 22 | 295 | 4.9 |
|
| ROOT 06 Kubernetes Application Workloads | 23 | 299 | 5.0 | Yes |
|
||||||
| ROOT 07 Kubernetes Networking & Configuration | 27 | 353 | 5.9 |
|
| ROOT 07 Kubernetes Networking & Configuration | 28 | 356 | 5.9 | Yes |
|
||||||
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 |
|
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 | **No — excluded** |
|
||||||
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 |
|
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 | Yes (required for Capstone) |
|
||||||
| ROOT 10 Kubernetes Troubleshooting & Observability | 20 | 277 | 4.6 |
|
| ROOT 10 Kubernetes Troubleshooting & Observability | 21 | 280 | 4.7 | Yes |
|
||||||
| ROOT 11 DevOps & Platform Engineering | 3 | 35 | 0.6 |
|
| ROOT 11 DevOps & Platform Engineering (orientation only) | 3 | 35 | 0.6 | Yes (optional bonus) |
|
||||||
| ROOT 12 Real Ops | 6 | 115 | 1.9 |
|
| ROOT 12 Real Ops | 6 | 115 | 1.9 | Yes (required for Capstone) |
|
||||||
| ROOT Capstone | 5 | 120 | 2.0 |
|
| ROOT Capstone | 5 | 120 | 2.0 | Yes |
|
||||||
| **Total (all authored content)** | **312** | **4,075** | **67.9** |
|
| **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
|
ROOT 01's activity/minute count grew by 3 activities / 15 minutes this phase (the new
|
||||||
real, measured data — Phase 6D's own version of this document correctly flagged the eventual
|
`root-01-m00-welcome-to-root` orientation module). ROOT 06/07/10 each grew by 1 activity / 3-4
|
||||||
"20-25 hour" range as a planning ESTIMATE pending real authorship; the actual total (24.7 hours)
|
minutes (their new practice-bank intro lessons). See `docs/ROOT_V1_CURRICULUM_MANIFEST.md` for
|
||||||
landed inside that estimated range.
|
why.
|
||||||
|
|
||||||
|
**ROOT Foundations (01-04) subtotal: 149 activities, 1,497 minutes, 25.0 hours.**
|
||||||
|
|
||||||
## Self-paced vs. instructor-led
|
## Self-paced vs. instructor-led
|
||||||
|
|
||||||
As in Phase 6D's version of this document: the figures above are self-paced estimates (the sum
|
Unchanged from Phase 6D/6E: the figures above are self-paced estimates (the sum of each
|
||||||
of each activity's own `estimated_duration_minutes`). Instructor-led classroom time is still not
|
activity's own `estimated_duration_minutes`). Instructor-led classroom time is still not a
|
||||||
a separate summable database field — every Instructor Notes section documents approximate live-
|
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
|
facilitation timing in prose. A future phase could add a distinct
|
||||||
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
|
|
||||||
`estimated_duration_minutes_instructor_led` column if precise classroom scheduling becomes a
|
`estimated_duration_minutes_instructor_led` column if precise classroom scheduling becomes a
|
||||||
real, measured need.
|
real, measured need. See `docs/ROOT_V1_INSTRUCTOR_LED_PILOT.md` for the cohort packaging this
|
||||||
|
duration model supports.
|
||||||
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.
|
|
||||||
|
|
||||||
## What this is NOT
|
## 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-
|
`docs/COURSE_AUTHORING_STANDARD.md` §4) — this total will get more accurate as real completion-
|
||||||
time data accumulates from actual students, not before.
|
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,
|
ROOT_ V1 (`docs/ROOT_V1_CURRICULUM_MANIFEST.md`) is 64.2 of the program's 68.3 authored hours —
|
||||||
deliberately deferred as non-capstone-gating, and ROOT 11 modules 2-7, deferred pending future
|
everything except ROOT 08, deliberately excluded as non-Capstone-gating and not yet authored to
|
||||||
lab infrastructure — see `docs/ROOT_PILOT_CURRICULUM.md`) now runs a complete, measured
|
V1 standard. ROOT 11's remaining six modules were never counted in either total (they don't
|
||||||
beginning-to-advanced path: Linux fundamentals → Linux administration/networking → Git →
|
exist yet, unlike ROOT 08's PARTIAL-but-real content) — see `docs/ROOT_CURRICULUM_COVERAGE_
|
||||||
containers → Kubernetes → security → troubleshooting → real operations → capstone. This is the
|
MATRIX.md` for the honest READY/PARTIAL/PLANNED breakdown across the entire program.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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 () => {
|
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`);
|
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.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 () => {
|
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 ──
|
// ── 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 () => {
|
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}`);
|
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) await completeActivity(session, `root-01-m01-${suffix}`);
|
||||||
await completeScenario(session, 'linux-inspect-hardware', envId);
|
await completeScenario(session, 'linux-inspect-hardware', envId);
|
||||||
await completeActivity(session, 'root-01-m01-a04-reflection');
|
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
|
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
|
(`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).
|
(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
|
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
|
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);
|
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');
|
assert.equal(plannedWithModules.length, 0, 'a PLANNED course must have zero modules, not placeholder content');
|
||||||
const gold = courseById(structure, 'root-10-kubernetes-troubleshooting-observability');
|
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 () => {
|
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
|
// 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 —
|
// 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
|
// "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.course_id, 'root-01-computing-linux-foundations');
|
||||||
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');
|
||||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
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 () => {
|
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, 'rolling-update-task');
|
||||||
await completeScenario(casey, 'deployment-rollback');
|
await completeScenario(casey, 'deployment-rollback');
|
||||||
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/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 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']) {
|
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`);
|
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-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 () => {
|
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
|
// 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
|
// 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
|
// 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`);
|
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 () => {
|
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-${suffix}/complete`);
|
||||||
}
|
}
|
||||||
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/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
|
// 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
|
// 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.
|
// 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 () => {
|
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 {
|
try {
|
||||||
const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||||
const mod = moduleById(studentView.courses, 'root-08-m01-storage-fundamentals');
|
const mod = moduleById(studentView.courses, 'root-09-m01-security-model-and-securitycontext');
|
||||||
assert.ok(!mod.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
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 { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||||
const modAdmin = moduleById(adminView, 'root-08-m01-storage-fundamentals');
|
const modAdmin = moduleById(adminView, 'root-09-m01-security-model-and-securitycontext');
|
||||||
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
|
||||||
} finally {
|
} 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`);
|
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`);
|
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, 'job-task');
|
||||||
await completeScenario(bypass, 'cronjob-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']) {
|
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.
|
// total — ALL_STAGES (above) covers 5 of its 6 scenarios but predates container-logging-mcq.
|
||||||
await completeScenario(bypass, 'container-logging-mcq');
|
await completeScenario(bypass, 'container-logging-mcq');
|
||||||
await bypass.request('POST', '/api/student/activities/root-10-m03-a06-reflection/complete');
|
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-a00-lesson-incident-response/complete');
|
||||||
await bypass.request('POST', '/api/student/activities/root-12-m01-a99-reflection-post-incident/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`);
|
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');
|
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']) {
|
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`);
|
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`);
|
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 RUN_ID = Date.now();
|
||||||
const PROGRAM = 'root-kubernetes-foundations';
|
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'];
|
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;
|
let passed = 0, failed = 0;
|
||||||
@@ -99,11 +101,22 @@ async function main() {
|
|||||||
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
|
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
|
||||||
const casey = await newStudent(owner, 'Casey');
|
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`);
|
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.course_id, 'root-01-computing-linux-foundations');
|
||||||
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');
|
||||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
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 ─────
|
// ── 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
|
// labs/reflections later would trip the module-lock check for later modules — a real ordering
|
||||||
// constraint, not a test artifact.
|
// constraint, not a test artifact.
|
||||||
async function completeRoot01(session) {
|
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-a01-lesson');
|
||||||
await completeActivity(session, 'root-01-m01-a02-guided-exercise');
|
await completeActivity(session, 'root-01-m01-a02-guided-exercise');
|
||||||
await completeScenario(session, 'linux-inspect-hardware');
|
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 ────────────────
|
// ── A real student walking the full ROOT 01->02->03->04 chain end to end ────────────────
|
||||||
const alice = await newStudent(owner, 'Alice');
|
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`);
|
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.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) ──
|
// ── 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}`);
|
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');
|
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' };
|
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`); }
|
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);
|
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');
|
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);
|
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');
|
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 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 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');
|
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);
|
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