Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ An independent open-source learning platform under development. It does **not co
| --- | ---: | --- |
| catalog-only | 0 | Catalog metadata only |
| outlined | 0 | Structured plan without developed teaching |
| partial | 25 | Authored readings, connected coursework and formative checks; depth and review remain limited |
| partial | 27 | Authored readings, connected coursework and formative checks; depth and review remain limited |
| beta | 0 | Developed course awaiting required review/gates |
| complete | 0 | All full-course gates satisfied |
| externally reviewed | 0 | Qualified named reviewer examined a specified version |

**No current package meets the complete-course standard.** The repository contains 25 partial packages with 634 lessons, 3,537 formative questions, 2,131 retrieval cards and 151 self-assessed case prompts. These counts measure authored material; they do not establish semester depth, measured workload, scientific review or university equivalence.
**No current package meets the complete-course standard.** The repository contains 27 partial packages with 674 lessons, 3,916 formative questions, 2,284 retrieval cards and 163 self-assessed case prompts (content validator inventory, 2026-10-31). These counts measure authored material; they do not establish semester depth, measured workload, scientific review or university equivalence.

The packages span life sciences, chemistry, mathematics, physics, statistics, computing and engineering. Original readings, worked examples, synthetic datasets and practice are available in each subject, with uneven coverage and depth. See the [package inventory](#package-inventory), [measured content gaps](docs/COURSE_GAP_REPORT.md), and [MIT OpenCourseWare scope audit](docs/COURSE_SCOPE_AUDIT.md) for the current boundaries and priorities. Detailed development history belongs in the [progress review](docs/PROGRESS_REVIEW.md).

Development now proceeds across the repository in bounded instructional batches. Follow-up priorities address demonstrated instructional gaps, broader course boundaries and learner evidence identified against official MIT OpenCourseWare syllabi and calendars. New material is original and attributed; references compare scope and expectations without claiming affiliation, copied course delivery, or equivalent credit. Metadata and review-report refreshes are bundled with substantive changes.

The curriculum map adds **37 catalog-only subject nodes** and **8 pathway maps** beside the 25 partial course packages. Catalog-only entries describe planned study areas and dependencies; they contain no authored lessons and cannot be enrolled in. Mapped prerequisite order is planning guidance and does not establish university equivalency.
The curriculum map adds **37 catalog-only subject nodes** and **8 pathway maps** beside the 27 partial course packages. Catalog-only entries describe planned study areas and dependencies; they contain no authored lessons and cannot be enrolled in. Mapped prerequisite order is planning guidance and does not establish university equivalency.

See [the audit](docs/CURRENT_STATE_AUDIT.md), [quality standard](docs/CONTENT_QUALITY_STANDARD.md), and [remaining work](docs/ROADMAP.md).

Expand All @@ -47,6 +47,8 @@ See [the audit](docs/CURRENT_STATE_AUDIT.md), [quality standard](docs/CONTENT_QU
| [General Chemistry II](content/courses/general-chemistry-2/course.json) | Chemistry | partial |
| [Genetics & Genomics](content/courses/genetics/course.json) | Life sciences | partial |
| [Geroscience & Regenerative Biology](content/courses/geroscience/course.json) | Life sciences | partial |
| [Laboratory Safety and Biosafety](content/courses/lab-biosafety/course.json) | Life sciences laboratory | partial |
| [Laboratory Measurement and Solution Preparation](content/courses/lab-measurement/course.json) | Life sciences laboratory | partial |
| [Linear Algebra for Modeling & Robotics](content/courses/linear-algebra/course.json) | Mathematics | partial |
| [Organic Chemistry I for Life Sciences](content/courses/organic-chemistry/course.json) | Chemistry | partial |
| [Physics: Electricity & Magnetism](content/courses/physics-em/course.json) | Physics | partial |
Expand Down Expand Up @@ -87,7 +89,9 @@ Single-choice, multiple-select, numeric, bounded symbolic-expression, narrow dat

The React reader now includes six scientific calculators under **Study tools**, available without enrollment: dilution, buffer pH, enzyme kinetics, RC charging, diffusion timescale, and Ohm's law. Each shows units and model assumptions and links to related courses. Calculator scratch work is ungraded.

The React **Case studio** also exposes the authored prompts and self-check criteria across all 25 partial courses. Enrolled learners can save a private written response and checklist marks; drafts stay bound to the course version and prompt, remain available in account exports, and do not affect grades. Archived cases are read-only.
**Bench practice** replays simulations of real bench procedures (micropipette gravimetric verification, decimal dilutions with the FDA BAM aerobic plate count, a Class II biosafety cabinet session with a spill, validating an autoclave waste cycle with biological indicators, and the first response to a needlestick, cut or eye splash) from the learner's own choices of action, with a checkpoint debrief at the end. Runs are formative, unrecorded and cannot train hand skill; see the [wet-lab practice plan](docs/WET_LAB_PRACTICE_PLAN.md).

The React **Case studio** also exposes the authored prompts and self-check criteria across all 27 partial courses. Enrolled learners can save a private written response and checklist marks; drafts stay bound to the course version and prompt, remain available in account exports, and do not affect grades. Archived cases are read-only.

The previous calculators, cards, case self-assessment and browser notebook remain preserved in `legacy/`; the React reader now has its own searchable notebook over server-saved lesson notes. To use the old reader separately:

Expand Down
162 changes: 162 additions & 0 deletions backend/courselab/bench/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Bench-procedure practice: protocol definitions, models and deterministic replay."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator

from .autoclave import AutoclaveValidation
from .bsc import BiosafetyCabinetSession
from .engine import ActionRejected, ProtocolInvalid, ProtocolModel, replay
from .exposure import ExposureResponse
from .pipette import PipetteVerification
from .plate_count import AerobicPlateCount

MODELS: dict[str, type[ProtocolModel]] = {
model.protocol_id: model for model in (PipetteVerification, AerobicPlateCount, BiosafetyCabinetSession, AutoclaveValidation, ExposureResponse)
}

__all__ = ["ActionRejected", "BenchLibrary", "ProtocolInvalid", "replay"]


class _Strict(BaseModel):
model_config = ConfigDict(extra="forbid")


class Source(_Strict):
kind: Literal["cited", "simulation-typical", "simulation-model"]
source_id: str | None = None
location: str | None = None
note: str | None = None

@model_validator(mode="after")
def cited_needs_source(self):
if self.kind == "cited" and not (self.source_id and self.location):
raise ValueError("A cited value needs a source_id and a location")
return self


class Option(_Strict):
value: str
label: str


class Param(_Strict):
name: str
label: str
type: Literal["number", "integer", "boolean", "choice"]
min: float | None = None
max: float | None = None
unit: str | None = None
options: list[Option] | None = None

@model_validator(mode="after")
def complete(self):
if self.type in ("number", "integer") and (self.min is None or self.max is None or self.min > self.max):
raise ValueError(f"{self.name}: numeric parameters need min <= max")
if self.type == "choice" and not self.options:
raise ValueError(f"{self.name}: choice parameters need options")
return self


class Action(_Strict):
name: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
label: str
params: list[Param]


class Parameter(_Strict):
name: str
value: float | int | str | list[float]
unit: str
label: str
source: Source


class Checkpoint(_Strict):
id: str = Field(pattern=r"^[a-z0-9-]+$")
severity: Literal["critical", "major", "minor"]
description: str
rationale: str
source: Source


class SourceUse(_Strict):
source_id: str
use: str


class ProtocolDefinition(_Strict):
schema_version: Literal["1.0"]
id: str = Field(pattern=r"^[a-z0-9-]+$")
version: str = Field(pattern=r"^\d+\.\d+\.\d+$")
title: str
layer: int = Field(ge=0, le=5)
summary: str
scenario: str
guided_steps: list[str] = Field(min_length=1)
parameters: list[Parameter]
actions: list[Action] = Field(min_length=1)
checkpoints: list[Checkpoint] = Field(min_length=1)
sources: list[SourceUse]
limitations: list[str] = Field(min_length=1)

@model_validator(mode="after")
def unique_names(self):
for label, names in (("parameter", [p.name for p in self.parameters]),
("action", [a.name for a in self.actions]),
("checkpoint", [c.id for c in self.checkpoints])):
if len(names) != len(set(names)):
raise ValueError(f"Duplicate {label} name in {self.id}")
return self


class BenchLibrary:
"""Loads every protocol under content/bench/protocols and pairs it with its model."""

def __init__(self, content_root: Path):
self.directory = Path(content_root) / "bench" / "protocols"
self._models: dict[str, ProtocolModel] | None = None

def models(self) -> dict[str, ProtocolModel]:
if self._models is None:
models = {}
if self.directory.is_dir():
for path in sorted(self.directory.glob("*.json")):
try:
raw = json.loads(path.read_text(encoding="utf-8"))
ProtocolDefinition.model_validate(raw)
except (ValueError, ValidationError) as exc:
raise ProtocolInvalid(f"{path.name}: {exc}") from exc
if raw["id"] != path.stem:
raise ProtocolInvalid(f"{path.name}: file name must match the protocol id")
model = MODELS.get(raw["id"])
if model is None:
raise ProtocolInvalid(f"{path.name}: no simulation model for {raw['id']}")
models[raw["id"]] = model(raw)
self._models = models
return self._models

def get(self, protocol_id: str) -> ProtocolModel | None:
return self.models().get(protocol_id)

def summaries(self) -> list[dict]:
return [public_summary(model.definition) for model in self.models().values()]


def public_summary(definition: dict) -> dict:
return {key: definition[key] for key in ("id", "version", "title", "layer", "summary")}


def public_protocol(definition: dict) -> dict:
"""What the browser may see before a run ends: no checkpoints, no hidden-model parameters."""
return {
**public_summary(definition),
"scenario": definition["scenario"],
"actions": definition["actions"],
"limitations": definition["limitations"],
"sources": definition["sources"],
}
Loading
Loading