Real Stripe TEST traffic (Phase 9.1) exposed a genuine defect the deterministic TestProvider
never could: payment_intent.payment_failed and charge.refunded events reference Stripe's
PaymentIntent/Charge object, never the Checkout Session, but payment_transactions.
provider_reference only ever stored the Session id. A real failure or refund webhook would have
silently failed to match any transaction (UNKNOWN_TRANSACTION).
- payment-provider.js: session creation now also sets payment_intent_data.metadata (the same
metadata already carrying our transaction_id), so it propagates onto the PaymentIntent Stripe
creates. normalizeEvent() (now async) reads transaction_id back from the PaymentIntent's own
metadata directly for payment_intent.payment_failed, and via one extra paymentIntents.retrieve
call for charge.refunded (Charges don't inherit PaymentIntent metadata automatically).
- commerce.js: the transaction lookup now matches provider_reference OR transaction_id — one
additional OR clause, no schema/migration change.
- webhook-routes.js: added the now-required await on verifyAndParseWebhook (TestProvider's
synchronous return is unaffected).
- docker-compose.dev.yml: wires PAYMENT_PROVIDER/STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET through
to the api container (previously entirely absent — Phase 9 never needed it against the
deterministic provider), empty/unset by default matching EMAIL_PROVIDER's own convention.
Verified fixed against a real Stripe TEST refund (Phase 9.1); e2e-phase9.js 20/20 and the Winter
Break commerce rehearsal 13/13 rerun clean after the fix.
RUN_STRIPE_INTEGRATION_TEST=true (default: no-op, zero Stripe network traffic). Refuses to run
against a live-mode secret key. Automates what's provable via pure API alone — real Checkout
Session creation verified against Stripe's own retrieve API, server-authoritative pricing proven
against Stripe's own recorded amount even under a tampered caller amount, invalid-signature
webhook rejection with no domain side effect — then polls RootNode's status endpoint for a
human/Stripe-CLI-completed real payment rather than fabricating a signed success event. Never
runs as part of normal regression/CI.
19/19: pricing in the public serializer, checkout gated on agreements, server-authoritative
amount, webhook signature verification and idempotency, payment success/failure/abandonment,
last-seat checkout race, reservation reaper, manual refund with no cascade, minor guardian
routing, admin/RBAC payment-data restrictions, audit/metric hygiene, the status-lookup endpoint,
and confirmation that ROOT_ V1/free-offering/classroom flows are unaffected.
- webhook-routes.js: POST /api/webhooks/payment — signature-verified, idempotent, always 200
once verified (a forged/invalid signature is a clean quiet 400)
- integration-routes.js: POST /api/integrations/registrations/:id/agreements (closes Phase 8's
gap — agreement acceptance now on the public registration contract), POST .../:id/checkout
(server-to-server, amount always server-side), GET .../:id/status (minimal safe status lookup
for the website's success page — no PII, no amount, no provider reference)
- public-offerings-routes.js: price_amount/price_currency/requires_payment in the public serializer
- metrics.js: checkout/payment/webhook/conversion/seat-released counters, all low-cardinality
- notifications.js: listForRegistration now unions by the registration's own enrollment_id too,
so the post-conversion activation notification shows up in the registration's communication
history (previously only pre-conversion notifications were visible)
- migration 037: offerings.price_amount/price_currency, offering_registrations.reservation_expires_at,
payment_transactions, payment_webhook_events (webhook_event_id as real PK — the idempotency mechanism)
- payment-provider.js: StripeProvider (real SDK, HMAC webhook verification) + TestProvider
(deterministic, in-process, no network) behind one interface, selected via PAYMENT_PROVIDER
- commerce.js: createCheckoutSession (server-side price, never caller-supplied), processWebhookEvent
(idempotent), handlePaymentSuccess/Failure/CheckoutAbandoned/Refund, reservation reaper support
- offerings.js: computeReservationExpiry, updateOffering (allowlisted PATCH), readiness gained
price/payment_provider checks, convertRegistrationToEnrollment now fires the activation email
every other enrollment path already triggers, and links the registration's guardian_id into
student_guardians on conversion (real bug: a converted minor had no guardian link, so the new
activation email had nowhere to resolve a recipient to)
- server.js: raw-body middleware scoped to /api/webhooks/payment, reservation-reaper poller
New: PHASE8_ENROLLMENT_OPERATIONS.md (phase summary), WINTER_BREAK_CAMP_OPERATIONS.md
(the real operational walkthrough), REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md,
OFFERING_ADMIN_GUIDE.md.
Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT
table), OFFERING_AND_REGISTRATION_MODEL.md (both ordering bugs, launch readiness,
registration enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now
wired), WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with
worked examples, replacing the old speculative future-flow section), platform/README.md
(test list).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- e2e-phase8.js (16/16): offering CRUD/transitions/readiness, registration open/closed
enforcement, capacity + concurrent final-seat race through the real registration
endpoints, the public serializer's safety, website integration idempotency (sequential +
concurrent), adult/minor paths, required-agreement conversion blocking with version
snapshotting, offering->cohort membership, guardian recipient resolution, RBAC, and
confirmation ROOT_ V1/self-paced/classroom flows are unaffected.
- test-phase8-winter-camp-registration.js (12/12): the full commercial-path story with no
payment code — website submits a real minor registration (real guardian created/matched),
guardian agreement acceptance, a simulated admin-side payment confirmation, conversion to
real enrollment/cohort, real account activation, an independent adult path, two students
genuinely competing for the final seat, and a REAL Lab task completed by the minor
student with the instructor then seeing the real progress.
- e2e-phase7.js: fixed two now-stale test assumptions the new server-authoritative
registration-window enforcement broke (an offering left in draft that Phase 7's own test
never transitioned before registering against it) and widened a file-allowlist check for
the new, legitimate public-offerings-routes.js.
Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32, e2e-phase5c.js 29/29, all fresh-stack.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- Registration is now server-authoritative: draft/cancelled/completed/full/window-closed
offerings reject registration attempts with distinct error codes (assertRegistrationOpen).
- offering_agreement_requirements + conversion-time blocking on missing required
agreements; accepted_by_type gained 'registrant' for an adult self-accepting before
their student_id exists.
- computeOfferingReadiness() — a server-derived launch checklist (curriculum, instructor,
schedule, location, capacity, registration window, agreements, curriculum package).
- Guardian communications wired into the existing Phase 5C notification system: a guardian
email fallback in resolveRecipient(), a parallel enqueueForRegistration()/
resolveRegistrationRecipient() pair for pre-account registration-stage notifications
(registration_received/confirmed/needs_action, agreement_required, camp_operational_update),
five new RootNode-branded templates. Same table, same worker, same masked admin view —
no second notification system.
- public-offerings-routes.js: the unauthenticated, explicitly-allowlisted offering read API.
- POST /api/integrations/registrations: the website's idempotent registration submission
contract, same shared-secret boundary as the existing enrollment integration endpoint.
Two real bugs found and fixed during this phase's own testing before any commit:
(1) the five new notification_type values were wired into the worker before the DB's own
CHECK constraint was widened to accept them — every registration-stage notification was
silently failing until a comprehensive smoke test caught it (036 fixes the constraint;
every previously-silent notification catch now at least logs).
(2) firing a notification right after a registration's insert, before its seat reservation
could still fail and roll the row back, created a real foreign-key violation on rollback —
fixed by reordering createRegistration() to validate -> reserve -> insert -> notify.
See docs/OFFERING_AND_REGISTRATION_MODEL.md and docs/PHASE8_ENROLLMENT_OPERATIONS.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- e2e-phase7.js (17/17): organizations/locations, guardians, curriculum packages
(reference-only), offering capacity/transitions, registration lifecycle, agreements,
registration-to-enrollment conversion, RBAC, age-guidance-independence, and confirmation
that existing ROOT_ V1 self-paced/classroom flows are unaffected.
- test-phase7-winter-camp.js (18/18): the full Winter Break Camp operational path with NO
payment code invoked — organization/location/curriculum-package/cohort/offering setup,
guardian + adult registration, agreement acceptance, a simulated admin-side commerce
confirmation, conversion to real enrollments and real cohort membership, real account
activation via real one-time tokens, a real classroom session, and a REAL Lab task
(linux-inspect-hardware) completed by the minor student inside a real disposable Lab
container, confirmed to survive a real logout/login.
Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32 (including its own embedded Phase 5A
checks) on independent fresh stacks.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- organizations, locations, guardians, student_guardians (033) — additive, no existing
table's meaning changes. Widens programs.family to accept 'bootcamp'.
- curriculum_packages/curriculum_package_items, offerings, instructor_offerings,
offering_registrations, agreement_definitions/agreement_acceptances (034) — the full
purchasable-delivery model. Server-defined status transitions only (no client-controlled
offering/registration status). Capacity uses a single atomic UPDATE (reserved_seats),
never a SELECT-count-then-INSERT race.
- offerings.js — business logic: reserveSeat/releaseSeat, transitionOffering/
transitionRegistration (fixed transition tables), convertRegistrationToEnrollment
(reuses enrollment.js's existing resolveOrCreateStudent/createManualEnrollment rather
than duplicating student-matching logic).
- enrollment.js — resolveOrCreateStudent gains an optional, default-off returnToken param
(used only by offerings.js's admin-only conversion route; the public website integration
endpoint's contract is completely unchanged, per docs/WEBSITE_ENROLLMENT_INTEGRATION.md).
- admin-offerings-routes.js — the full admin/owner API surface. Registration/guardian
detail is admin/owner only; offering/location listing is any instructor (access_notes
stripped for a plain instructor).
- A real bug found and fixed during this phase's own smoke testing before any commit: the
registration route originally reserved a seat BEFORE validating the registration's own
fields, letting a validation failure consume a real seat and get masked behind a false
OFFERING_FULL under a full offering. Fixed: validate (create) first, reserve second, roll
back the registration row if reservation then genuinely fails.
See docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md and
docs/OFFERING_AND_REGISTRATION_MODEL.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- New platform/test/e2e-phase6f.js (17 tests): V1 manifest/exclusion correctness, the full
V1 prerequisite graph, ROOT 01-04 re-verification, the orientation module's unlock
behavior, optional-activity handling, the strengthened Capstone gate in both directions,
instructor-notes RBAC on locked (not just draft) content, isolation, and an explicit
age-independence check.
- foundations-helper.js: completeThroughRoot06/completeThroughRoot10 updated for the new
practice-bank intro-lesson activities; new completeThroughCapstoneEligible() helper
(ROOT 07-m03/m04 + full ROOT 09 + ROOT 12) shared by e2e-phase6f.js and both new drills.
- e2e-phase6a/6b/6d/6e.js: resume-pointer assertions updated for the new orientation
module; e2e-phase6a.js's and e2e-phase6b.js's own inline ROOT 06/07/09/10/12 completion
sequences updated for the new intro-lesson activities and the new root-09 Capstone
dependency; e2e-phase6b.js's stale ROOT 09 status assertion and its draft-activity test
(previously targeting the now fully-excluded ROOT 08) corrected.
- test-phase6d/6e-foundations-rehearsal.js: same orientation-module fix applied to the
real-Lab dress rehearsals.
All of 6A (23/23) / 6B (26/26) / 6C (17/17) / 6D (21/21) / 6E (23/23) / 6F (17/17) verified
green on independent fresh stacks. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- Correct stale courses.status for ROOT 05/06/07/09/10/12/Capstone (all modules were
already ready; the course-level field was never bumped in an earlier phase).
- Exclude ROOT 08 (Storage & Scheduling) from ROOT_ V1 via visibility=draft — nothing
downstream requires it, and leaving it visible mid-sequence would look like a skipped course.
- Self-correct a same-phase mistake: reclassifying the three instructor-led-only practice
banks (root-06-m01/root-07-m01/root-10-m02) as optional would have broken Capstone
reachability, since their scenarios are the literal legacy stage-gate content. Kept them
required and gave each a real intro-lesson activity instead.
- Strengthen root-capstone's prerequisites to require ROOT 09 (Security) alongside ROOT 12
(Real Ops) — the Capstone's own scenario pool has zero security overlap, and ROOT 09 only
reached genuine READY status this phase.
- Add a required orientation module (root-01-m00-welcome-to-root) explaining the program,
the Learn/Practice/Lab/Troubleshoot/Validate loop, and Lab-lifecycle expectations — no
orientation content existed anywhere before this phase.
See docs/ROOT_V1_CURRICULUM_MANIFEST.md and docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.
Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.
Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.
Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.
Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
POST /api/student/activities/:activityId/complete previously checked only that the activity
exists and the student holds an active PROGRAM enrollment — never whether the activity's own
course/module was actually unlocked for that student. A direct POST could mark any
manual_marker activity (lesson/demonstration/knowledge_check/guided_exercise/reflection)
complete regardless of the real prerequisite chain, the exact class of gap Phase 6B already
fixed once for the capstone's own start route, just never closed for this more general one.
Found while proving the real ROOT 01->02->03->04->05 chain via direct API (this phase's own
explicit requirement) — and confirmed to predate Phase 6E entirely by reproducing it against an
unrelated, pre-existing ROOT 05/06 pair before touching any Phase 6E content.
Fix: the route now resolves the activity's own course/module through the exact same
buildCourseTree() lock computation every other route already trusts, and returns 403 { error:
'locked' } if either is locked — reusing the existing mechanism, not a second, duplicated check.
Verified live: both a ROOT 02 bypass attempt and a ROOT 06 bypass attempt are now correctly
rejected; legitimate unlocked completions are unaffected.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds all 8 ROOT 04 modules (why containers exist, images & containers, Docker CLI, container
networking, container storage, Dockerfile & image build, Docker Compose, containers →
Kubernetes) — 43 activities, using the nested Docker Engine Phase 6D built and tested. Modules
1, 2, and 8 are conceptual (no lab by design) — the honest bridge module (M8) explicitly names
what Kubernetes adds beyond what this course already taught, without re-teaching Kubernetes
itself.
Adds 5 new hands-on Docker scenarios plus 2 MCQs, all against the real nested Docker Engine:
docker-cli-basics (run/exec/inspect a real container), docker-container-networking (a genuine
"works internally, unreachable from host" port-publishing failure, diagnosed and fixed by
recreating the container with the correct `-p` flag), docker-container-storage (a real
destroy/recreate cycle proving named-volume persistence, not just described), docker-build-image
(a real Dockerfile, a real build, a real custom image run), and docker-compose-multiservice (a
real two-service Compose application with a real published port). All tested end-to-end via the
real scenario-validation API, including negative tests.
Wires rootnode-containers-assessment (2 required + 4 random-draw scenarios) using the existing
Phase 3B assessment engine.
This completes the entire ROOT_ Foundations sequence (ROOT 01-04) — see
docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds all 6 ROOT 03 modules (why version control exists, the Git mental model, everyday Git,
branching & merging, remote repositories, engineering Git workflow) — 31 activities. Uses only
real LOCAL Git repositories throughout — no Gitea account, no network access, and no
RootNode-canonical-repo credential is ever needed or exposed in a student Lab
(docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §4). Modules 1, 2, and 6 are conceptual bookends with no
lab, matching the flexible readiness standard already used in ROOT 01/02.
Adds 3 new hands-on Git scenarios plus 2 MCQs: git-everyday-basics (init/add/commit, validated
by inspecting real `git log`/`git show` state, not just command output strings — Step 6's
explicit requirement), git-branching-and-merge (a REAL merge conflict caused on purpose by two
branches editing the same line, resolved by the student, validated by confirming the conflict
markers are gone and a real two-parent merge commit exists), and git-remote-clone-push (a real
local bare repository standing in for a remote — clone/commit/push validated by inspecting the
REMOTE side directly, not just the student's own clone). All tested end-to-end via the real
scenario-validation API, including negative tests.
Wires rootnode-git-fundamentals-assessment (2 required + 3 random-draw scenarios) using the
existing Phase 3B assessment engine.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds all 6 ROOT 02 modules (Linux system administration, storage & filesystems, the
Networking Mental Model, Linux network inspection, DNS & HTTP, Linux troubleshooting) — 37
activities, each with a real lesson, demonstration/guided exercise, knowledge check, hands-on
Lab, and reflection. The Networking Mental Model module (M3) gets deliberately extra depth per
its "critical module" designation, with no lab by design — conceptual mastery via knowledge
check and guided exercise, matching root-05-m00's own precedent.
Adds 6 new hands-on Linux/networking scenarios plus 2 MCQs: linux-service-inspection (inspects
the platform's own real nginx without touching it — a genuinely safe, real service-inspection
target that avoids ever risking the Lab's own connectivity), linux-service-lifecycle (start/
verify/stop a fully controllable service), linux-storage-inspection, linux-network-inspection
(ip/ss/getent/curl, each framed as answering one specific diagnostic question),
linux-dns-http-diagnosis (a genuine DNS-resolves-but-wrong-port failure, diagnosed and fixed),
and linux-troubleshooting-framework (a real missing-config-file failure, resolved via the
RootNode Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework). All tested
end-to-end via the real scenario-validation API, including negative tests confirming they fail
when unsolved.
Wires rootnode-linux-networking-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds platform/test/e2e-phase6d.js (20 checks): ROOT 01 hierarchy, ROOT 02-04 honestly empty
(not padded), activity ordering, lab-cannot-be-manually-completed, full course completion via
lesson/lab/assessment, the prerequisite chain proven on two students (one who completed ROOT 01,
one fresh), existing ROOT 05+/Capstone state unaffected, is_required unaffected, self-paced
no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/login persistence,
cross-student isolation, version-bump stability, and content validation. 20/20 pass against both
a warm and a freshly-reset dev stack.
Adds infra/scripts/drills/{run,test}-phase6d-foundations-rehearsal — the real disposable-Lab
dress rehearsal: login -> enroll -> lesson/guided-exercise -> real Lab launch -> real bash
hardware inspection (nproc/free/df, written to a real file) -> real validation -> a genuine
permission denial (a second Linux user actually denied via `su -`) then a real chmod fix ->
remaining modules -> assessment -> course completion -> ROOT 02 unlock -> logout/login
persistence -> instructor visibility of the resulting learning state. 11/11 pass against a real
Lab container, no mocking. (First run caught a real triple-escaping bug in the hardware-report
step's inline heredoc — fixed by writing the script to a file and docker-cp-ing it in instead.)
Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js that this phase's own
ROOT 01 addition broke: both suites asserted `resume` landed on ROOT 05's gold module as "the
first real content," which was true before this phase and is now ROOT 01. Both suites' original
intent (resume must never land on a bare-lab bank) is preserved, just against the current front
door. Both suites re-verified green (21/21 and 24/24) against a fresh stack.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds all 6 ROOT 01 modules (how computers run software, the shell, filesystem navigation,
files & text, processes, users & permissions foundations), each with a real lesson,
demonstration/guided exercise, knowledge check where warranted, a hands-on Lab, and a
reflection — 35 activities, 29 new content files.
Adds 6 new hands-on Linux scenarios (linux-inspect-hardware, linux-shell-basics,
linux-filesystem-navigation, linux-files-and-text, linux-process-management,
linux-permissions-foundations) plus 2 MCQ scenarios, using the existing generic scenario/
validation engine (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1) — every one tested end-to-end via
the real scenario setup/validate API, including negative tests confirming they correctly FAIL
when not actually solved (e.g. the process-management scenario fails while the target process
is still running; the permissions scenario is tested against a real `su - labuser` denial).
Wires rootnode-linux-foundations-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.
Adds a real, server-side ROOT 01->02->03->04->05 course prerequisite chain
(courses.prerequisites) — verified live on two students: one who completed ROOT 01 sees ROOT 02
unlock; a fresh student sees ROOT 02 correctly locked while 03/04/05 stay vacuously satisfied
(their own prerequisite courses are still empty). ROOT 06+ and the Capstone are unaffected.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds platform/test/e2e-phase6c.js (17 checks): upgraded-module activity
chains, documented-exception promotions, optional-lab non-blocking behavior,
no-fake-completion-from-viewing, idempotent completion, instructor-notes
exclusion and RBAC on new content, security-assessment pool/draw/cross-student
isolation/completion, the capstone gate's documented indifference to ROOT 09,
ROOT 11's honest partial/lesson-only rendering, and version-bump content
stability — plus running validate-course-content.js as part of the suite.
All 17 pass against both a warm and a freshly-reset dev stack.
Adds infra/scripts/drills/{run,test}-phase6c-security-config-rehearsal —
Section 22's real-k3s dress rehearsal: login -> program -> course -> lesson ->
demonstration -> knowledge-check -> guided-exercise -> real Lab -> real
kubectl (create Secret, apply Pod, wait for Ready) -> validation -> progress
persistence across logout/login -> instructor visibility of the resulting
learning state, walking root-07-m02-configuration-and-secrets (upgraded from
PARTIAL this phase). 10/10 pass against a real k3s Lab, no mocking.
Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js
that Phase 6C's own legitimate content changes broke: the gold module
(root-10-m01) now has 7 activities (6 required + 1 new optional lab, not 6),
and root-05-m01 now has 2 required manual-marker activities the course-
completion path must also complete (its new bookending lesson/reflection).
Both suites are back to green (21/21 and 24/24) against a fresh stack.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Upgrades six PARTIAL modules to the gold-module standard (lesson/demonstration/
knowledge-check/guided-exercise/lab(s)/reflection), preserving every existing lab
activity_id and scenario_id: root-06-m03 (jobs/cronjobs/daemonsets), root-07-m04
(ingress/networkpolicy), root-07-m02 (configuration/secrets, converted in place
from a bare 5-lab bank), root-09-m02 (serviceaccounts/rbac), root-09-m03
(network/supply-chain security), root-10-m03 (troubleshooting framework).
root-05-m01 (20-task kubectl-fluency bank) gets a minimal bookending lesson +
reflection rather than a full redesign, since each task already carries its own
explanation. root-12-m01, root-capstone-m01, and root-09-m04 are promoted to
READY via documented exceptions (no new activities) where adding one would
misrepresent their intentional design.
Wires a new rootnode-security-assessment (exam, 60min, hint_policy disabled)
using the existing Phase 3B assessment engine: 3 required hands-on CORE
scenarios + 4 random-draw pools covering SecurityContext, RBAC, network
policy, and the 7 previously-unwired CKS MCQs. Wires the previously-unwired
readiness-probe-task scenario as an OPTIONAL practice lab in root-10-m01,
using the existing is_required model.
Adds root-11-m01-devops-platform-thinking — an honest, lesson-only ROOT 11
orientation module naming DevOps/platform engineering practices already
exercised in ROOT 05-10/12, explicit that GitOps/CI-CD/IaC/observability need
lab infrastructure this platform doesn't yet provide. No fake labs.
Adds infra/scripts/validate-course-content.js: cross-checks the DB against
scenario/content files for duplicate ids, orphans, invalid references, cycles,
sequence collisions, instructor-notes leakage, and (new) self-paced content
deferring explanation to a live instructor. 0 errors against the resulting
22 courses/modules, 163 activities, 98 scenario files.
No application code changed — every gap was satisfiable with existing tables
(course_modules, module_activities, assessment_definitions) and the existing
content-delivery pipeline.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
PHASE6B_ROOT_CURRICULUM_EXPANSION.md — the phase summary: what was
authored per course, the is_required extension, the capstone unlock
fix, verified scenario coverage before/after (44 -> 88 of 98, 89.8%,
by direct query not estimate), and honest known limitations/Phase 6C
recommendations.
ROOT_CAPSTONE_READINESS.md — the readiness module and the server-side
unlock fix in full detail, including why the module's completion and
the real assessment's unlock gate are deliberately independent systems.
Updated ROOT_CURRICULUM_INVENTORY.md (Phase 6B coverage numbers),
ROOT_PROGRAM_ARCHITECTURE.md (ROOT 08/09 PARTIAL, new module lists per
course), COURSE_CONTENT_MODEL.md (is_required completion rule, a new
content-versioning section per this phase's own requirement),
COURSE_AUTHORING_STANDARD.md (classifying near-duplicate/advanced
scenarios), ENGINEERING_SOP.md (new Phase 6B section),
REMOTE_AND_SELF_PACED_ARCHITECTURE.md (cross-reference), and
platform/README.md (e2e-phase6b.js, the Phase 6B rehearsal, isolation-
per-suite regression guidance).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
e2e-phase6b.js (24 checks): hierarchy for the 13 new modules across
ROOT 05-10/12/Capstone, prerequisite chains through the newly-inserted
front-door modules, lesson/knowledge-check completion in brand-new
modules, a newly-wired scenario (job-task) completing correctly, the
optional-vs-required (is_required) completion rule, persistence/
isolation/RBAC/draft-hiding proven against the new content, and the
capstone server-side unlock fix in both directions (a direct bypass
attempt rejected with blocking_stage_ids, an ordinary exam unaffected,
a legitimately unlocked attempt still accepted and its completion
reflected in the course content model).
test-phase6b-root-course-rehearsal.js + wrapper (11 checks, real k3s):
one continuous student narrative through a brand-new module (ROOT 07
Services & Service Discovery, not the Phase 6A gold module) — real Lab
launch, the Lab's own setup_commands, a real 'kubectl expose' against
the live cluster, real validate, logout/login/Resume Learning, Lab
destroy/recreate with academic progress intact — plus an instructor
narrative proving Curriculum view, Prepare Labs, and classroom/learning
tooling all still work against the expanded curriculum.
e2e-phase6a.js updated for two of this phase's own intentional,
documented behavior changes (not defects): resume now points at
root-05's new mental-model module instead of the old bare-lab bank, and
root-10's gold module now unlocks the new troubleshooting-framework
module rather than the bare-lab bank directly.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
POST /api/assessments/:definitionId/start only ever checked that the
definition existed and was active — it never checked unlock_rule. GET
/api/capstone correctly computed and displayed lock state, and the
student UI never exposed a start action while locked, but a direct POST
to this route bypassed the check entirely (documented as a known gap in
docs/PHASE6A_ROOT_COURSE_FOUNDATION.md §10). Student UI state is not the
enforcement boundary.
Fixed by binding to the exact same computeCurriculumState() gate
/api/capstone already relies on, rather than re-parsing
definition.unlock_rule independently — one computation of 'is this
unlocked,' not two that could drift. Only a definition the curriculum
path actually names as ITS capstone is gated this way; an ordinary exam
is completely unaffected. Returns a clean 403 { error: 'locked',
blocking_stage_ids } — the same shape /api/capstone already exposes.
Found while fixing this: e2e-phase5b.js's own capstone-completion test
had been passing only because of this exact gap — it never included the
real-ops stage in its completion loop before attempting to start the
capstone, and the missing unlock check let it succeed anyway. Fixed
alongside the platform code (REAL_OPS added to ALL_NON_CAPSTONE), not
routed around.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
023_phase6b_curriculum.sql — additive on top of 022_course_content.sql:
13 new teaching modules (ROOT 08/09 move from PLANNED to PARTIAL; ROOT
05-07/10 gain a new front-door module sequenced ahead of the existing
bare-lab bank; ROOT 12 gains an incident-response framing lesson +
reflection; Capstone gains a readiness module). No existing row is
deleted; a small number of existing rows have their sequence/
prerequisites updated so a new teaching module supersedes the bare-lab
bank as the front door — never their scenario_id/activity_type/
completion_behavior, so no student's derived completion changes meaning.
is_required (new module_activities column, defaults true — every Phase
6A activity unaffected) lets a module distinguish CORE teaching from
GUIDED/INDEPENDENT-practice duplicates and ADVANCED/optional extras.
course-progress.js's totals() now considers a module complete once
every REQUIRED activity is done, not literally every activity — an
incomplete optional one no longer blocks completion, though it still
counts toward the displayed percentage.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
COURSE_AUTHORING_STANDARD.md — the standard every future lesson/module
must follow (target learner, prerequisites, measurable objectives,
concept-before-lab, instructor notes, terminology, versioning, review
requirements), grounded in the gold-standard module as the reference
implementation.
PHASE6A_ROOT_COURSE_FOUNDATION.md — the phase summary: youth-ladder
context, the Google Workspace status already recorded in Phase 5C's own
docs, architecture/build summary, honest content states, test results,
explicit non-goals, and known limitations/Phase 6B recommendations.
Updated ENGINEERING_SOP.md (new Phase 6A section + a curriculum-content
cross-reference), REMOTE_AND_SELF_PACED_ARCHITECTURE.md (a 7th
closed gap: self-paced enrollment now resolves to a real course tree,
not just a scenario list), LAB_ARCHITECTURE.md (Phase 6A reuses the
disposable Lab unmodified, verified by a real rehearsal), and
platform/README.md (course model in the api/ summary, new e2e suite
descriptions for 5A/5B/5C/6A which were missing entirely, and the
real-Lab rehearsal script).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
e2e-phase6a.js (21 checks): hierarchy/ordering, honest READY/PARTIAL/
PLANNED states, all three completion behaviors (manual marker/
scenario-derived/assessment-derived), prerequisite locking (including
the zero-content-is-vacuously-satisfied rule), resume, persistence
across logout/login, cross-student isolation, RBAC for self-paced
no-cohort students, draft-content hiding, and content-version
stability.
test-phase6a-course-rehearsal.js + run-phase6a-course-rehearsal.sh: a
real-k3s dress rehearsal of the gold-standard module end to end — a
student reads the real lesson content, launches a real disposable Lab,
the Lab's own POST /api/scenarios/:id/setup (called through the
Gateway's existing /lab/:token/* proxy exactly as the browser UI does)
stands up the broken api-gateway deployment, a real kubectl patch fixes
the readiness probe, and the module completes and unlocks the next one.
No mocked KubeKosh anywhere in this suite.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
course-routes.js — student-facing: the full course tree through an
enrollment (GET /api/student/programs/:id/courses) and the lesson
completion marker (POST /api/student/activities/:id/complete, refused
for anything but a manual_marker activity — a lab/assessment can never
be faked complete through this route).
admin-course-routes.js — instructor-facing, read-only (no WYSIWYG CMS
this phase): program structure with instructor notes
(GET /api/admin/programs/:id/courses) and per-student progress
(GET /api/admin/students/:id/courses), scoped via
requireStudentRelationshipAccess — the same self-paced-no-cohort rule
Phase 5C's notification routes already established, not
requireStudentCohortAccess (which would 403 a plain instructor out of
every self-paced student).
Dockerfile now also COPYs content/ alongside the existing scenarios/ —
same 'read-only, never executed' reasoning as that line.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
New tables (courses, course_modules, module_activities,
activity_completions) layered over the existing scenario/assessment/
achievement systems — a lab activity references an existing scenario_id,
an assessment/capstone activity references an existing
assessment_definition_id, neither duplicated. Long-form lesson prose
lives in git-tracked Markdown (content/root/), split into shared vs.
instructor-only sections by a '## Instructor Notes' heading convention
(content.js) rather than a second file or a CMS.
course-progress.js derives completion from EXISTING sources only:
auto_scenario reads the progress table, auto_assessment reads
achievements by assessment_definition_id, manual_marker is the one
genuinely new piece of state (for lesson-only activities with nothing
else to derive completion from). Prerequisite locking treats a
zero-content (PLANNED) prerequisite as vacuously satisfied, so an
honestly-unbuilt course never permanently locks everything after it.
Found by testing: computing a level's prerequisite lock and stripping
its working totals in the same map iteration made the result depend on
iteration order. Fixed with two explicit passes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Live Google Workspace delivery validation surfaced a real, self-resolving
Google-side condition: freshly-authorized domain-wide delegation can take
up to about a minute to propagate across Google's auth infrastructure,
producing intermittent unauthorized_client on the OAuth token exchange
even with correct configuration. gws-provider.js now classifies
unauthorized_client as a temporary (retryable) error instead of permanent,
so the existing bounded-backoff worker absorbs it automatically.
Also documents the alias-vs-delegated-user env passthrough in
docker-compose.dev.yml, and records both real findings (this one and the
GOOGLE_DELEGATED_USER alias distinction) plus the full live-delivery
validation outcome in the runbook and Phase 5C doc.
Validated live: isolated gws-email-test.js send (DKIM/SPF passing,
delivered) and a full real lifecycle test (create student, enrollment,
real activation email, activate, login, see program, real password-reset
email, reset, login with new password) via [email protected].
Real-world case found during setup: the sending address ([email protected]) is an
alias of an existing Workspace user, not its own paid seat (a deliberate cost-saving choice).
Domain-wide delegation authorizes a real Workspace ACCOUNT, not an arbitrary alias string, so
using the alias directly as the delegated 'sub' claim would have been wrong. Added
GOOGLE_DELEGATED_USER (the account that actually owns the alias and was granted delegation) as
an optional override, falling back to EMAIL_FROM_ADDRESS unchanged when the sending address is
itself a primary account. EMAIL_FROM_ADDRESS remains the visible From: header in both cases.
Documented in docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md with a concrete example.
- PHASE5C_ENROLLMENT_COMMUNICATIONS.md: the full design doc.
- RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md: every REQUIRES GOOGLE WORKSPACE ADMIN ACTION step
(mailbox, service account, domain-wide delegation scoped to gmail.send only, SPF/DKIM/DMARC),
environment variables, testing procedure, production secret handling, rotation, revocation,
troubleshooting, quotas.
- RUNBOOK_STUDENT_ACTIVATION.md: day-to-day operation of the pipeline from /admin.
- WEBSITE_ENROLLMENT_INTEGRATION.md: explicit website-vs-RootNode responsibility boundary,
the new contact_email field, and closes the 'RootNode doesn't send activation email yet'
known limitation this doc previously documented.
- RUNBOOK_SELF_PACED_ENROLLMENT.md, RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md: point at the new
automatic-email behavior and the Resend activation control.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md, ENGINEERING_SOP.md: close the enrollment-
communication gap; new ENGINEERING_SOP §16.
- PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a
Phase 5C production-validation section.
- platform/test/e2e-phase5c.js (29 checks): activation-vs-confirmation routing,
duplicate/concurrent webhook idempotency, temporary/permanent/exhausted retry
classification, resend/regenerate token invalidation, activation/reset single-use and
expiration, HTML-injection regression, production token-secrecy gating via a real container
recreate with COOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit
hygiene, and a real Platform API restart.
- infra/scripts/drills/test-phase5c-enrollment-activation.js (10 checks): one continuous
story — website enrollment through a real rendered/extracted activation email, a real Lab
launch and scenario completion, a second enrollment's confirmation email, a full
password-reset round trip, and the minor/no-email contact-email path.
- infra/scripts/gws-email-test.js: the opt-in (RUN_GWS_EMAIL_TEST=true) live Google Workspace
delivery test — does nothing without the explicit flag, sends exactly one message to one
operator-configured recipient when enabled.
- rootnode_notifications_total{notification_type,status} (computed fresh from the DB, like
the existing enrollment_breakdown gauge — never drifts from what's actually stored),
rootnode_notification_queue_depth, rootnode_notification_oldest_queued_seconds,
rootnode_notification_delivery_attempts_total{provider,result} (a real in-process counter —
per-attempt outcomes aren't reconstructable from the DB after the fact),
rootnode_student_activations_{success,invalid}_total. Every label is a bounded enum —
never student_id/email/token/notification_id/external_event_id.
- notification-routes.js: global list (owner/admin only), per-student list (scoped via
requireStudentRelationshipAccess), resend-activation (mechanically the same as
regenerate — Phase 5A's token model already guarantees only the latest is ever valid, so one
action covers both), a dev-only preview route (unconditionally refuses when
COOKIE_SECURE=1, mirroring student-auth-routes.js's existing dev_reset_token gate).
- NotificationsView.jsx: owner/admin global notification list, paginated, matching
AuditView.jsx's exact convention.
- StudentsView.jsx: student detail gained a Resend activation control and a Notifications
history card; Credentials card now explains the automatic-email behavior.
- AdminPortal.jsx: new Notifications nav entry (owner/admin only).
- integration-routes.js: accepts student.contact_email, calls notifyForEnrollment on a
genuinely new enrollment only (never on an idempotent replay — inherits Phase 5B's proven
enrollment-level idempotency, no separate notification dedup needed).
- admin-routes.js: create-student/convert/reset-password now enqueue the corresponding
notification using the SAME token already minted synchronously (never mints twice, which
would silently invalidate the token just handed back in the response). Fixed a real bug:
request-password-reset would mint a reset token for a never-activated student, which makes
no sense and would confuse them — now falls through to the generic anti-enumeration response.
- admin-programs-routes.js: manual enrollment triggers the same notification decision as the
website path.
- student-auth-routes.js: /activate records a metric; /change-password and /reset-password
both queue a password_changed security notice; /request-password-reset now actually delivers
email (production still never returns the token — unchanged anti-enumeration/isProduction
gating, just now with a real send behind it).
- rbac.js: requireStudentRelationshipAccess/checkStudentRelationshipAccess — the notification
visibility rule (cohort-shared or an enrollment they created), reusable beyond just
enrollments.
- server.js: wires notification-routes.js and starts the notification worker in main().
- docker-compose.dev.yml: PUBLIC_APP_ORIGIN, EMAIL_FROM_NAME, and a parametrized COOKIE_SECURE
(was hard-commented-out; needed so e2e-phase5c.js can actually prove production gating).
- email/provider.js: the seam. EMAIL_PROVIDER unset always defaults to the dev provider,
regardless of any other setting — misconfiguration means email silently doesn't send for
real, never that a dev/test run accidentally reaches a real student.
- email/gws-provider.js: Gmail API users.messages.send via a service account with Workspace
domain-wide delegation, impersonating one dedicated mailbox. No human password, no SMTP
basic auth, no googleapis/google-auth-library dependency — a hand-rolled RS256 JWT-bearer
OAuth exchange + one REST call (~120 lines), matching this codebase's established
hand-roll-before-adding-a-dependency convention.
- email/dev-provider.js: never makes a network call, returns the rendered message back to the
caller for dev/test inspection. Supports magic +forcetempfail/+forcepermfail recipient
markers so the permanent test suite can deterministically exercise retry/failure
classification without needing a real flaky provider.
- email/templates.js: activation/enrollment-confirmation/password-reset/password-changed,
HTML + plain-text, no external images/tracking/JS. Every interpolated value is HTML-escaped
in the HTML body (a real injection finding — display_name can come straight from a website
integration event).
- notification-worker.js: a setInterval poller (same pattern as sessions.js's sweeper), claims
via notifications.js, renders via templates.js, builds activation/reset URLs from
PUBLIC_APP_ORIGIN, stores a dev-only preview (never in production) so tests can extract the
real URL without a mailbox.
- migrations/021: notification_events (queued/sending/sent/failed/cancelled, bounded
exponential backoff via attempt_count/next_attempt_at, a classified last_error_code, never a
raw stack trace or provider credential).
- notifications.js: enqueue (records a no-recipient case as already-failed rather than
silently dropping it), claimNext (FOR UPDATE SKIP LOCKED — safe if two worker processes ever
run), markSent/markFailedOrRetry (bounded retry, terminal-state token scrubbing),
notifyForEnrollment (the shared activation-vs-confirmation decision every enrollment-creation
path calls), maskEmail/toAdminView (the only shapes any admin API ever returns).
- enrollment.js: enrollmentContactEmail() — reads back the minor/no-email transitional
contact-email convention (stored in the existing enrollments.metadata JSONB, never written to
students.email) that createFromIntegrationEvent/createManualEnrollment now accept.
- PHASE5B_SELF_PACED_ENROLLMENT.md: the full design doc.
- RUNBOOK_SELF_PACED_ENROLLMENT.md: operational runbook for /admin -> Programs.
- WEBSITE_ENROLLMENT_INTEGRATION.md: the complete contract for the separate website team —
endpoint, auth, request/response schema, idempotency/retry rules, student matching, minor/
no-email behavior, error responses, what not to send, local testing, production-secret
handling. Written to be usable without reading the Platform API source.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the self-registration/curriculum-assignment/
delivery-mode/enrollment-model gaps this doc previously flagged as Phase 5+ scope.
- ENGINEERING_SOP.md: new §15. LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md,
README.md, platform/README.md: cross-references and a Phase 5B production-validation section.
- platform/test/e2e-phase5b.js (32 checks): program seed, entitlement gating
(pending/future/expired/cancelled), cross-student isolation, real Lab launch/destroy/
reaper-reconciliation/recreate persistence, server-derived idempotent completion via a full
real capstone completion, the integration boundary (auth, idempotency, real concurrency,
unknown/inactive program rejection, minor/no-email path, no card data), RBAC scoping, a
regression test for the process-crash bug this suite found, Phase 5A smoke, and a real
Platform API restart.
- infra/scripts/drills/test-phase5b-selfpaced-rehearsal.js (15 checks): one continuous story —
activation, a simulated website enrollment event, real Lab work, Lab destroy, a new session,
Continue Learning, a fresh Lab, full real capstone completion, Completed transition, and a
second uninvolved student proven unable to reach any of it.
- metrics.js: rootnode_enrollments_total{status,delivery_mode}, rootnode_selfpaced_active_
enrollments, rootnode_program_completions_total{program_id}, rootnode_enrollment_events_
{created,duplicate,rejected}_total. Every label is a bounded enum or an admin-curated slug —
never student_id/email/enrollment_id/external_reference.
- integration-routes.js: POST /api/integrations/enrollments — separate from student/instructor
session auth entirely (no cookie read), pre-shared-secret header auth matching the existing
Gateway<->Provisioner trust-boundary pattern. Idempotent via a DB unique constraint on the
caller's own event_id (not app-level check-then-insert) — proven safe under real concurrent
duplicate requests. Student resolution is create-or-match by email OR login_identifier
(minor/no-email support), reusing Phase 5A's exact credential-issuing path. A small,
provider-neutral status vocabulary (paid/active/confirmed/pending/awaiting_payment) — never
a raw payment-provider object model.
- docker-compose.dev.yml: INTEGRATION_SHARED_SECRET, optional in dev (fails closed if unset).
- enrollment.js: server-only status transitions (pending->active->completed, cancel),
deriveEnrollmentAccess (pure, derived-at-read-time like classroom.js's derivePresence —
'expired' is never stored, computed from access_ends_at), maybeCompleteEnrollment
(idempotent, server-derived from the existing capstone achievement).
- environment-view.js: publicEnvView/launchEnvironmentForStudent extracted out of server.js's
original /api/environment/launch route body so the new entitlement-gated self-paced launch
reuses the identical disposable-Lab sequence instead of a second copy — behavior for the
existing cohort/pilot route is completely unchanged.
- program-status.js: combines an enrollment with real curriculum/progress state into one view
(access, percent-complete, current stage, deterministic next-activity/resume target) — shared
by the student and admin surfaces so both show identical information.
- selfpaced-routes.js: GET /api/student/programs(/:id), POST .../launch — entitlement enforced
server-side before ever touching the Provisioner, never just hidden in the UI.
- rbac.js: requireEnrollmentAccess — a plain instructor may manage an enrollment they created
or that belongs to a student sharing one of their cohorts; a self-paced enrollment can have
no cohort at all, so the existing cohort-only checks can't cover it.
- server.js: found and fixed a real bug — POST /api/progress/record's handler predated the
wrap() convention, so an unexpected internal error (a malformed environment_id) crashed the
entire process instead of just 500ing that request. Now wrapped + input-validated.
- migrations/019: programs table (slug PK, matching assessment_definitions/cohorts
convention), family (init/compile/root) and delivery_mode (self_paced/cohort/hybrid) as real
CHECK-constrained enums, versioned like assessment_definitions so an edit to
curriculum_path/delivery_mode never silently changes what an existing enrollment means. Seeds
exactly one program wrapping the existing RootNode/Kubernetes curriculum and capstone — no
new curriculum content invented.
- programs.js: list/get/create/update, with automatic version bump only when a
snapshot-relevant field actually changes.
- PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md: the full design doc.
- RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md, RUNBOOK_REMOTE_CLASSROOM.md: operational runbooks.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the 'no real student authentication' gap this
doc previously flagged as the one item blocking self-paced delivery.
- ENGINEERING_SOP.md §5 rewritten (was describing a pre-Phase-2A state no longer true of any
deployed version of this codebase) plus a new §14; roadmap item 3 updated.
- LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md:
cross-references and a Phase 5A production-validation section.
- platform/test/e2e-phase5a.js (21 checks): the two-student RBAC matrix (student can't reach
any /api/admin/* route, assigned/unassigned/viewer instructor access) and the full password
security checklist (correct/wrong/unknown-identifier/deactivated/immediate-deactivation/
rate-limit/rate-limit-survives-a-real-restart/change-password/session-revocation/
forgot-password anti-enumeration/admin-issued reset/CSRF), plus a classroom lifecycle smoke
pass. Replaces the throwaway smoke5a/smoke5b scripts used during development.
- infra/scripts/drills/test-phase5a-rehearsal.js (15 checks): 5 pilot students with real
seeded pre-existing progress/attempt/achievement history, a live classroom, help requests,
announcements, a REAL Platform API restart mid-rehearsal proving classroom/session/presence
recovery, then converting two students mid-rehearsal and confirming zero data was orphaned.
- Both run against a live docker-compose.dev.yml stack over real HTTP, not mocks.