Technical details
How ICT Setter is built, for the IT staff who run it and anyone curious about what happens between typing a question and printing a paper.
The stack
| Part | Built with |
|---|---|
| Interfaces | React 19, TypeScript and Vite; Fluent UI and Base UI components in the suite's own teal theme |
| Server | Node.js, Express and Prisma over SQLite, with sessions stored in the database |
| Documents | Python fills approved Word masters; LibreOffice converts them to PDF |
| Diagrams | XeLaTeX with TikZ, then dvisvgm and Poppler for SVG and PNG |
| Answer checker | Python, with g++ and Free Pascal for C++ and Pascal answers |
One repository
packages/exam-core what an exam is: the paper model, codecs, validation packages/publication-contracts what a print or export request is packages/assessment-domain exam rules: lifecycle, the pin, results, analytics apps/assessment-server Assessment's use cases and API apps/assessment-client the Assessment interface shared/ editing, collaboration and the question bank server/ the server that hosts both apps, and the workers client/ the ICT Setter interface scripts/ the document renderer, TikZ compiler, answer checker site/ this site
Assessment never imports the authoring code; a boundary test and lint rule keep it that way, so the exam side depends only on the paper model and its own rules.
The paper model
A paper is a tree: sections, questions, parts and sub-parts, and the blocks inside them (text, lists, tables, code, pictures, diagrams, answer space, page breaks). Every piece of text is a pair, English and Chinese, in a small rich-text format with marks for bold, italics, code, underline, superscript and boxed blanks. The same codecs and validation run in the editor, as you type, and on the server, before anything is saved.
Each node has a stable identity, and papers serialise to canonical JSON, which is what gets hashed. Edits are sent as operations against a known version; one person edits at a time under a lease that renews while they work, and every saved version is kept.
From paper to print
- The paper becomes a render plan: question numbering, section headings, page breaks and answer space, worked out once for both languages.
- The Python renderer opens an approved Word master and fills its marked regions with the paper's content, so headers, footers, cover and page set-up come from the master untouched.
- LibreOffice converts the Word file to PDF for the preview and for printing.
Masters come in template packs, content-hashed and approved by an administrator before use. The work runs in a background worker; each job reports its stage, so the interface can show what it is doing.
Chinese typography follows fixed rules: PMingLiU for text, DFLiHei Bold for bold, and 1-point character spacing on Chinese text but not on runs of English letters and digits. The renderer measures text with the exam fonts' own metrics, to place marks at the end of the last line and to size tables and listings to their content.
Diagrams
Setters write only the drawing commands; the document set-up, fonts and libraries are fixed. Commands that could read or write files, define macros or reach the network are refused before compiling. Each diagram is compiled with time and output limits, and the SVG is cut down to plain shapes before it is stored. Results are cached by a hash of the source, the labels and the toolchain, so an unchanged diagram is never drawn twice.
Exams
- An exam pins exactly one locked paper version and one marking-scheme version, with a hash of each and of every picture. Everything the exam does, from delivery to reports, reads through the pin and checks it.
- Candidates receive an allow-list projection of the paper: prompts, content and options under stable IDs, never the scheme, notes or sources.
- Timing is kept by the server. Submitted answers are frozen by database triggers, and finalised marking is never edited: re-marking adds a new marking run.
- Totals, grades, ranks and statistics are derived from the marks whenever they are read, never stored, so they cannot drift from the marking.
- Each marked part carries an equivalence hash, so its statistics are pooled across exams only when it is truly the same part.
The answer checker
A marker's checking script is Python, whatever language the candidate wrote in. It sets inputs, runs the answer, calls functions and states what it expects; the same script works for a Python, Pascal or C++ answer, an SQL statement or a spreadsheet formula. Programs written as a fragment or with their data typed in are handled as written.
The candidate's program runs in a separate process with no inherited environment and its own empty folder, a 5-second time limit, a memory limit and caps on file and output size.
Security
- Staff accounts are by invitation; sessions are kept on the server.
- Requests that change data carry a CSRF token.
-
Candidate secrets are shown once and stored only as scrypt hashes. A candidate's session cookie
is
httpOnly,SameSite=Strictand limited to the exam API, and sign-in is rate-limited. - Staff without a role in an exam cannot see that it exists; a role without a permission is refused.
Testing
Each package has its own Vitest suite; the renderer, TikZ compiler and answer checker are tested with pytest; Playwright drives both apps end to end, including candidates sitting an exam. CI runs formatting, lint, types and every suite, builds documents on Linux, and tests the Windows deployment scripts on Windows.