This guide is for anyone changing the lab: adding a simulator, extending a lesson, writing a challenge bank, or fixing a bug. Read architecture.md first if you have not — it explains the layers that the conventions below assume.
Requirements: Node 20.19 or newer (package.json declares engines.node) and npm.
npm install
npm run dev # Vite dev server, http://localhost:5173/Operating-Systems-Visual-Lab/The app is a static single-page application with hash routing, so there is no backend to start and no environment variables to set.
| Command | What it does |
|---|---|
npm run dev |
Start the dev server with hot reload |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint over the whole repo |
npm run lint:fix |
ESLint with --fix |
npm run test |
Run the Vitest suite once |
npm run test:watch |
Run Vitest in watch mode |
npm run build |
Typecheck, then a production build into dist/ |
npm run preview |
Serve the built output |
npm run check |
typecheck → lint → test → build. Run this before you finish. |
These are the conventions the codebase already follows; following them keeps it coherent.
- The dependency direction is one-way.
features → engine / content / components. Engines and content importtypesonly. Nothing imports upwards. - Engines are pure and React-free. No timers, no randomness, no I/O, no
useState. Same input, same steps. If your engine needs to be decided by a clock, it belongs in the playback hook instead. - Every step explains itself. A
SimulationStepwithout an explanation is a step a student cannot learn from. - Content is data, not code. Cross-reference by id (
algorithmIds,topicId,labId). Never reach into a visualisation from content. - Nothing is hand-typed that an engine can compute. This applies most sharply to challenge answers.
- No new dependencies without a reason. The runtime dependency list is deliberately just React and React DOM. Check whether the platform already provides what you need.
- Match the existing style. Prettier-ish formatting is manual, but the shape is consistent: 2-space
indent, single quotes, semicolons, trailing commas,
readonlyon content/interface fields, explicit return types on exported functions.
strict, noUncheckedIndexedAccess and verbatimModuleSyntax are all on. Two consequences you will
meet often:
- Indexing an array yields
T | undefined. Handle it — do not reach for a non-null assertion unless the invariant is genuinely local and obvious. - Type-only imports must be marked:
import type { Lab } from '@/types'orimport { thing, type Other } from '...'.
One rule catches people out: react-refresh/only-export-components requires that a file exporting a
React component exports only components. If you need to export a helper or a constant from a
component file, move it to its own module. This is why the codebase has small files like
buttonStyles.ts and speedOptions.ts next to their components.
Style layers are imported in a fixed order in src/styles/index.css:
tokens → base → layout → components → lab → process → synchronization → deadlock
→ memory → paging → replacement → disk → fs → features → challenges → quiz → learning
Put a new area's rules in its own file and add it to that list — never into components.css.
Use tokens (var(--space-4), var(--text-sm), var(--border)) rather than literal values, so both
themes keep working. Check any new colour in light and dark.
- Append an
Algorithmobject tosrc/content/algorithms.ts:id,name,topicId,summary,description,difficulty,status,parameters,properties,tags. - Set
status: 'planned'if there is no simulator yet — the UI is honest about it and the curriculum stays browsable. - If the algorithm has knobs, describe them in
parameters(kind: 'number' | 'boolean' | 'select') with bounds and a default. The parameter panel renders them for you. - Add teaching
pseudocodeif you have it. Omit it rather than inventing a listing. - Run
npm run test—tests/content/content.test.tsasserts content integrity and will catch a dangling reference.
Follow src/engine/scheduling/fcfs.ts as the reference.
- Create the engine in the area folder (
src/engine/<area>/), implementingSimulationEngine<TInput, TData>. - Shape the run:
validate()reports issues as{ path, message };run()returns{ algorithmId, labId, input, steps, state, summary }. - Register a descriptor in
src/engine/registry.ts(implemented: true). The registry stores descriptors, not run functions — do not change that; it is what avoids circular imports. - Export the engine and its ids from the area's
index.ts, then fromsrc/engine/index.ts. - Write tests: determinism (same input twice), the algorithm's documented behaviour, edge cases
(empty input, single item, ties), validation failures, and the metrics in
summary.
- Add the
Labtosrc/content/labs.ts. Keep the invariants the tests check: at least three objectives and at least onealgorithmId. - Build the workstation under
src/features/<area>/. - Register it in the
SIMULATORSmap insrc/pages/LabPage.tsx(lab id → component). A lab with no entry rendersPlannedLab, which is the correct fallback, not a bug. - Compose it inside
LabShell, and use the shared components (ConceptBlock,MetricsPanel,Timeline,EventLog,ExplanationPanel,SimulationControls). Do not hand-roll controls — a learner should not have to relearn them. - Add styles to the area's stylesheet.
- Create a builder in
src/engine/challenge/builders/that returnsreadonly Challenge[]. - Compute every expected answer by running the engine. Never type a number that a simulator could produce. Use the builders' shared helpers.
- Register the bank in
CHALLENGE_BANKS(id,topic,title,labId) and add the builder tobuildAll()inbank.ts. tests/engine/challenge.test.tswill check the catalogue: unique ids, valid per-kind fields, a resolvablerelatedLab, and that every answer is resolvable. Make it pass, do not weaken it.
- Add or edit the
GuidedLessoninsrc/content/lessons.ts. Every lesson has the same six stages in the same order:concept → visualization → experiment → walkthrough → challenge → reflection. - Stages are a discriminated union on
kind. If you add a new kind, extendLessonStage, the stage order array, and the lesson view — the compiler will list every place you must handle. - Reference real labs and real challenge banks by id;
tests/content/lessons.test.tschecks that they resolve to the right area. - Keep numbers exact. If a walkthrough quotes a result, reproduce it in the lab first. (A past version of this content quoted two averages and a service order that did not match the engines; the docs and the lesson now use the engine's values.)
npm run test runs 635 tests across 36 files. Tests live in tests/ and are organised by layer:
| Folder | What it covers |
|---|---|
tests/engine/* |
One file per engine: schedules, metrics, edge cases, validation, determinism |
tests/features/*Drafts.test.ts |
The draft ↔ engine-input adapters that back each lab's editors |
tests/content/* |
Content integrity and guided-lesson structure |
tests/utils/* |
Formatting, arrays, routing, theme resolution, learning helpers |
tests/types/model.test.ts |
Domain-model contracts |
What to test, in order of value:
- Determinism — run the same input twice and deep-equal the steps.
- Documented behaviour — the specific schedule, order or victim the algorithm promises.
- Edge cases — empty input, one element, ties, boundary values, zero-size requests.
- Validation — that bad input is rejected with the right
path. - Derived answers — anything the challenge builders read.
Engines are pure, so their tests need no DOM and no mocks. Keep it that way.
- Every doc starts with a one-line purpose and lists prerequisites.
- Quote numbers only if you reproduced them from the app. If you cannot, describe the shape of the result instead of inventing a figure.
- Prefer a table for comparisons and a numbered list for procedures.
- Link between docs rather than repeating the same explanation twice.
The document set lives in docs/ and is indexed from README.md. If you add a doc, add it to that
index.
New UI must not regress these, all of which currently hold:
- Semantic landmarks, one
h1per page, and a working skip link. - Keyboard reachability and operation for every control; no click-only handlers on non-interactive elements.
- A visible focus state (inherit the global
:focus-visiblering; do not remove it). - An accessible name for every control; decorative glyphs hidden with
aria-hidden. - A text equivalent for anything a chart conveys: a table, a metrics panel, an event log or a caption.
- Correct behaviour under
prefers-reduced-motionandprefers-contrast: more. - Sufficient contrast in both themes.
.github/workflows/deploy.yml runs on pushes to main and on manual dispatch. It installs with
npm ci, runs typecheck → lint → tests, builds with Vite, and publishes dist/ to GitHub Pages. Enable
Settings → Pages → Source: GitHub Actions once in the repository.
Three details make GitHub Pages work under the repository path, and you should not break them:
vite.config.tssetsbase: '/Operating-Systems-Visual-Lab/', so every emitted asset URL is prefixed with the project path.- Routing is hash-based, so no server-side rewrite is needed for deep links and a refresh always re-requests the same document from the Pages root.
public/.nojekyllstops Jekyll from touching the bundle, andpublic/404.htmlrecovers non-hash deep links such as/Operating-Systems-Visual-Lab/labs/lab-fcfs.
Use the helpers in src/utils/base.ts / src/utils/router.ts (assetUrl, hrefFor) rather than
building URLs by hand.
npm run check
That is typecheck, lint, tests and a production build — the same sequence the deployment workflow runs. Then check your own change in the browser at desktop, tablet and phone widths, in both themes, and with the keyboard only. Confirm there is no horizontal overflow and that the console is clean.
The project is released under the MIT License (declared in package.json). Contributions are
accepted under the same terms.