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

  1. The paper becomes a render plan: question numbering, section headings, page breaks and answer space, worked out once for both languages.
  2. 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.
  3. 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=Strict and 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.