Repository navigation
Work Based Learning Internals
Technical companion to the Work-Based Learning Guide. Canonical design
docs live in the main repo: WBL_Assessment_Framework.md (philosophy), WBL_Schema_Design.md
(data model + numbered decisions), WBL_API_Design.md (routes). Code:
server/routes/wbl.js + server/wbl/logic.js. Tables: Database Schema.
A program (wbl_programs) owns curriculum and progression — skills, credentials, QC criteria,
phase, awards — and ownership (teacher_key) lives there and nowhere else. A class owns roster
and gradebook. wbl_class_programs links them many-to-many. Student history keys to
(program_id, student_id), so credentials and phase survive a section change or a new school year.
Every assessment attaches to a Work Event (wbl_work_events) via a per-student participant
row (wbl_work_event_participants).
| # | Decision | Mechanism |
|---|---|---|
| 7 | Skill versions pinned on assessments; evidence carries forward |
wbl_skill_assessments.skill_version_id is fixed at assessment time; wbl_credential_skills references the skill lineage (wbl_skills.id), not a version. |
| 8 | Program = progression, class = roster/gradebook | history on (program_id, student_id). |
| 9 | Invariants enforced by triggers, not convention | Do Now daily cap and Exit Slip UPDATE block are DB triggers (they fire despite foreign_keys = OFF). |
| 10 / 15 | Exit slips are voided, never edited | append-only wbl_exit_slip_voids; trigger blocks UPDATE, permits DELETE. |
| 11 | QC floor alerts, doesn't gate | denominator is active weeks, not calendar weeks. |
| 13 | Transfer claims may cite across programs |
wbl_transfer_claims.cited_program_id may differ from program_id; verification searches the student's whole record. |
| 16 | Program enrollment is explicit |
wbl_program_enrollments, not derived from class rosters (but Sync Roster populates it from them). |
| 17 |
student_id canonical form |
normalizeStudentId on ingest; WBL tables store normalized, class_students stores raw — dual lookup in code. |
computed_phase = 2 iff the student has a wbl_credential_award (not revoked) for EVERY credential
in wbl_phase2_prereqs for the program; else 1
effectivePhase = override_phase ?? computed_phase # wbl_student_phase
recomputePhase runs after attainment changes. An instructor override
(wbl_student_phase.override_phase) always wins. Transfer-claim scoring and the Habits of Work
transfer sync are gated on effectivePhase == 2.
A demonstration of a required skill counts when all hold:
-
wbl_skill_assessments.result = 'mastered', for this(program, student, skill). - The same participant has a
wbl_holistic_callsrow whose tierrank ≥the credential'smin_holistic_tierrank. (Grading a job to spec can therefore complete a credential with no new assessment —recomputeAttainmentis called after every Holistic Call for exactly this reason.) - Demonstrations are counted across distinct
work_event_ids — one job contributes at most one.
Plus: a wbl_skill_checks row (roster-wide standing credit, UNIQUE(program, student, skill)) is
one unconditional demonstration — crediting it is the instructor attesting mastery, so no
Holistic gate.
A skill is satisfied when demos ≥ required_demonstrations. A credential is earned when
every required skill is satisfied. recomputeAttainment writes a wbl_credential_awards row
(source = 'auto') and snapshots the satisfying evidence into wbl_award_evidence /
wbl_award_skill_check_evidence so the trail survives later catalog edits.
-
Weekly cap:
UNIQUE(program_id, student_id, iso_week)onwbl_qc_checksis the cap. -
Rotation (
rotationQueue): students on an active job with no check this ISO week, longest-unchecked first. -
Floor (
qcFloorReport): flags students belowwbl_programs.qc_max_weeks_unchecked, measured against weeks the student actually had an active job — gaps between jobs and absences don't count as missed coverage.
attendanceRatio(program, class, student, from, to) over wbl_attendance — see
Grading Algorithms. A
wbl_called_outs row for a date drops that day's UNV from the ratio. Returns null when no
attendance rows exist in range (callers treat that as "not pulled").
Cumulative rating average over the marking period, one value per class meeting day
(meetingDates), clamped to the student's enrollment span. Per day: no Do Now → 0; code not picked →
excluded; picked + Exit Slip voided → 0; picked + unverified → excluded; picked + verified with
rating (0–4) → rating × 25; picked + no slip → 0. Score = average of what counted; no countable
days ⇒ blank. Full rules: Grading Algorithms.
wbl_credential_sync.state / wbl_credential_skill_sync.state ∈ not_started | in_progress | due,
per class.
-
GET /api/wbl/sync/progress?class_id=returns the sync sets and, for everynot_startedrow, lazily auto-promotes it toin_progressifhasSkillEvidencefinds anymasteredassessment or skill check for a roster student. No write-side hooks — it's evaluated fresh each time the panel loads. -
PATCH /api/wbl/credentials/:id/statecascades the new state to all the credential's skill rows once (creating rows viaINSERT OR IGNOREfirst); skills are independently editable afterward. -
POST /api/wbl/sync/idsupserts the PowerSchool assignment IDs without touchingstate(named-columnON CONFLICT DO UPDATE, notINSERT OR REPLACE).
The old sync_enabled column and wbl_transfer_sync table are retained but unread — superseded by
state and wbl_habits_sync respectively. wbl_habits_sync is keyed by soft_skill_code (not the
CHECK(kind IN ('application','extension')) of wbl_transfer_sync, which can't be widened in
place); a migration copied existing transfer PS-assignment IDs across.
Resolves user_key → student_id and exposes, for programs the student is enrolled in via a linked
class: current credentials and progress, skill checks, active/'past work events, work history, the
day's Do Now (GET/POST, one per day), Exit Slip submission, and transfer-claim filing. The student
dashboard's WBL section is built entirely from these.