OuRi Corp · open source · MIT · BETA

QKC Engine — dependency-free quiz engine for the web

Vanilla JS. No build step. One <script> tag. QKC.init() embeds a full quiz widget in any page; QKC.engine() gives you the same brain headless — seeded assembly, scoring, validation.

v1.2.0 0 dependencies UMD + ESM 8.8 KB gzip RU / EN 3 modes × 3 themes MIT
42 823questions in the largest bank running on it
811JavaScript questions in the QKC curriculum
8.8 KBgzip: engine + widget + injected CSS
0runtime dependencies, network calls, trackers
live widget · rendered right now by QKC.init()
Start here

What can you do with QKC Engine?

QKC Engine is a JavaScript quiz engine for developers. Use the demo to see the widget, the quick start to embed it, or the API reference to inspect available methods.

01 · TRY

Run the live demo

Answer a sample quiz and inspect the embedded widget.

Open demo →
02 · BUILD

Embed a quiz

Follow the setup steps and see the minimum integration code.

Quick start →
03 · REFERENCE

Explore the API

Review methods, question-bank format, examples and implementation notes.

API reference →

Project status: beta. Verify behavior against the live demo and current API reference before relying on it in production.

01 · quick start

Two tags and one call.

Drop the script into any page and point QKC.init() at a question bank. The widget brings its own CSS — nothing else to install, nothing to build, nothing to configure.

index.html

<!-- 1. the engine (UMD, 8.8 KB gzip) --> <script src="https://spaj7468-cell.github.io/qkc-engine/engine.js"></script> <!-- CDN mirror: https://cdn.jsdelivr.net/gh/spaj7468-cell/qkc-engine@main/dist/qkc-engine.js --> <!-- after the runs-on.dev claim merges this also works: https://engine.qkc.runs-on.dev/engine.js --> <!-- 2. your bank — any array of questions --> <script src="bank-7.js"></script> <div id="quiz"></div> <script> QKC.init({ bank: window.OURI_BANKS[7].algebra[1] }); </script>

result — live, on this page

The snippet is real: demo-bank.js on this page defines OURI_BANKS[7].algebra[1] — an actual excerpt (9 questions) from the grade-7 algebra bank of the QKC trainer. That exact call renders this:

LoadOne UMD file defines the global QKC (or import it as ESM/CJS). No CSS file, no fonts, no network calls.
Call QKC.init({ bank, el })The bank is flattened, validated, and a seeded PRNG picks count questions and shuffles their options.
PlayThe widget renders questions, checks answers, shows explanations — mode decides the flow (normal/exam/sprint).
Get resultsonFinish(result) hands you { pct, correct, wrong, skip, perTopic, seed, mode }. Store it, chart it, ignore it — the engine never phones home.
02 · live demo

The widget, wired to a live event console.

Not a knowledge test for you — a capability demo. Flip the switches like in a dev-tools panel: the code on the left and the widget on the right stay in sync, because the widget is that exact call re-run. Every update(), onAnswer and onFinish event is printed to the console below — finish the quiz and watch the result object arrive.

mode
theme
lang

code — exactly what runs

result

engine events · onAnswer / onFinish / update
  • // events will appear here — answer a question in the widget
03 · api reference

Every function, every option, every field.

The whole public surface is one global object QKC (default export in ESM/CJS). Two layers: QKC.init() — the embeddable widget; QKC.engine() — the headless core. Everything is synchronous and pure: same input + same seed → same output.

QKC.init(options) → instance

Mounts a quiz widget. Injects its scoped CSS (<style id="qkc-engine-css">) once per document, clears the mount point, builds a seeded test from the bank and renders question one.

optiontypedefaultdescription
bankarray | objectrequiredQuestions: a flat array of items, { items: [...] }, or the nested container { grade: { subject: { quarter: [...] } } } — anything QKC.toArray() accepts.
elstring | Elementappended to bodyMount point: CSS selector or DOM node. Its content is cleared. If omitted (or not found), a new <div> is appended to body.
countnumber10How many questions to pick (min 1, capped at bank length).
seedstring | numbertime-basedPRNG seed. Strings are hashed with FNV-1a. Same seed + bank + count → identical quiz down to option order.
mode'normal' | 'exam' | 'sprint''normal'Flow of the quiz — see §06 Modes. Invalid values fall back to 'normal'.
theme'bw' | 'light' | 'dark''bw'Widget color scheme via data-theme + CSS variables — see §07 Themes.
lang'ru' | 'en'autodetectUI language. Autodetect: <html lang> → navigator.language → 'en'.
timePerQuestionnumber (sec)30Sprint countdown per question. On timeout the question is auto-advanced and counted as skipped.
titlestring'QKC'Header title inside the widget.
shuffleOptionsbooleantrueShuffle choice options (deterministically, from the seed). Set false to keep bank order.
onFinish(result) => voidnullFires when the quiz finishes, with the full result object (below). Exceptions inside are caught — a broken callback can't break the widget.
onAnswer(ev) => voidnullNew in 1.2.0. Fires whenever an answer is graded: normal → on “check”; sprint → on select/timeout; exam → once per item at finish. ev = { id, value, correct, skipped, item: { t, ty, tp } }.
documentDocumentglobalDocument to render into — for jsdom/tests or multi-window setups.

instance — what init() returns

membertypedescription
rootHTMLElementThe .qkc-root node — style it, measure it, query inside it.
mountHTMLElementThe resolved mount point (el or the created div).
restart(o?)() → instanceApply options (optional) and start over: question 1, empty answers, fresh seeded build.
update(o?)() → instanceApply options live — bank, mode, theme, lang, title, count, seed, timePerQuestion, onFinish, onAnswer — and re-render. This is how the demo switches above work.
destroy()() → voidStop timers; clear the mount (or remove it if the engine created it).
results()() → result | nullThe result of the current/last finished run — or a live score of the current attempt; null before any question is built.

result object — onFinish(r) / instance.results()

shape

{ correct: 7, // graded right wrong: 2, // graded wrong skip: 1, // left empty total: 10, pct: 70, // Math.round(correct/total*100) perTopic: { // per-topic breakdown 'Многочлены': { correct: 3, total: 4 }, 'Одночлены': { correct: 2, total: 3 }, … }, seed: 'spring-2026', // the seed this run was built with mode: 'normal' }

grading rule

An answer is compared to c through QKC.norm(): trim → lowercase → collapse whitespace → comma becomes dot, then exact string equality. So ' Paris ' = 'paris', and '3,5' = '3.5'. Nothing fuzzier than that — grading is deterministic and auditable. Empty/null → skipped.

Per-topic stats come from the tp field; items without a topic land under '—' internally and are omitted from the widget's topic table.

QKC.engine(bank) → eng — the headless core

No DOM, no widget — build tests and score answers yourself (custom UI, server-side grading, Node scripts, generators).

methodreturnsdescription
eng.build(opts?)testAssemble a test: filter the bank, seed the PRNG, shuffle, pick count items, shuffle each choice's options. opts = { grade, subject, quarter, topics[], count=10, seed } — quarter accepts a number or 'all'.
eng.score(test, answers)resultGrade answers = { [itemId]: value } against the test. Same norm() rule and result shape as the widget.
eng.validate(){ ok, errors[] }Structural audit of the whole bank (see rules in §04).
eng.count(filter?)numberHow many items match the filter.
eng.topics(filter?)[{ topic, count }]Topic histogram, sorted by topic name.
eng.bankbankThe bank it was created with.

test object — what build() returns

{ seed: 'spring-2026', filter: { grade: 7, subject: 'algebra', quarter: 1, topics: [] }, items: [{ id: 0, // index — the key in answers t: 'Раскрой скобки: (x+10)(x+7)', ty: 'choice', o: ['x²+17x+70', …], // shuffled c: 'x² + 17x + 70', // ⚠ strip before clients e: 'По формуле (x+p)(x+q)…', tp: 'Многочлены', q: 1, grade: 7, subject: 'algebra' }, …] }

typical headless flow

const eng = QKC.engine(OURI_BANKS); const v = eng.validate(); // { ok, errors[] } const test = eng.build({ grade: 7, subject: 'algebra', quarter: 1, count: 10, seed: 'spring-2026' }); // send to a client WITHOUT answers: const safe = test.items.map(({ c, ...i }) => i); // …collect answers {0:'x²+17x+70', 1:'20x³', …} const res = eng.score(test, answers); res.pct; // 0..100 res.perTopic; // { 'Многочлены': {correct, total}, … }

standalone helpers

functionsignaturedescription
QKC.validate(bank) → { ok, errors[] }Works on arrays and containers; errors are human-readable strings with item indexes.
QKC.toArray(bank) → item[]Flatten array | {items} | container into a plain list (container items get grade/subject/quarter merged in).
QKC.items(container, filter?) → item[]Low-level traversal with the same filter object as build().
QKC.rng(seed:number) → { next, int, pick }mulberry32 PRNG. next()→[0,1), int(n)→0..n-1, pick(arr).
QKC.hashSeed(str) → uint32FNV-1a string hash — turns 'spring-2026' into a numeric seed.
QKC.norm(v) → stringThe answer normalizer (trim, lowercase, collapse spaces, ,→.).
QKC.STR{ en: {…}, ru: {…} }The widget UI dictionaries (25 keys each) — reuse or fork them for your own UI.
QKC.VERSIONstringSemver of the build, e.g. '1.2.0'.

Try it — real REPL, runs the real build

The box below evaluates JavaScript against this page's QKC and the real OURI_BANKS excerpt (grade 7 · algebra · quarter 1). End with an expression to see its value, or use console.log(). Nothing leaves your browser.

// output appears here
04 · bank format

A question is six fields. A bank is an array.

No DSL, no schema registry, no server. Plain JSON-serializable objects — the same format the QKC trainer ships its 42 823 questions in. If you can JSON.parse it, the engine can play it.

one item, annotated

{ "t": "Раскрой скобки: (x + 10)(x + 7)", // question text "ty": "choice", // 'choice' | 'bool' | 'input' "o": [ // options — choice only, ≥ 2 "x² + 17x + 70", "x² + 10x + 7", "x² + 70x + 17", "x² − 17x + 70" ], "c": "x² + 17x + 70", // correct answer (must be in o) "e": "По формуле (x+p)(x+q) = x² + (p+q)x + pq.", // explanation "tp": "Многочлены", // topic — filters + per-topic stats "q": 1 // optional: quarter inside a container }

Real item #0 of OURI_BANKS[7].algebra[1] — the bank loaded on this page.

fields

fieldtypemeaning
tstringQuestion text. Shown as-is (no HTML).
ty'choice' | 'bool' | 'input'Answer type — see §05.
ostring[]Options. Required for choice (≥ 2, must contain c). Ignored otherwise; bool auto-renders True/False.
cstringCorrect answer, compared via norm() (trim / case / whitespace / ,→.). For bool: 'true' or 'false'.
estringExplanation shown after checking (normal mode) — optional but your users will thank you.
tpstringTopic. Drives topics(), the topics filter and per-topic stats.
qnumber?Quarter/term index when items live in a container.

Three shapes of a bank

flat array

The simplest: [item, item, …]. Exactly what the playground and most embeds use.

QKC.init({ bank: [ … ] })

{ items: [...] }

An object wrapper — handy when a bank file also carries meta (title, version, …); the engine reads .items.

QKC.init({ bank: { items: [ … ] } })

nested container

{ grade: { subject: { quarter: [items] } } } — the QKC trainer shape. build() filters slice it by grade/subject/quarter/topic.

QKC.init({ bank: OURI_BANKS }) eng.build({ grade:7, subject:'algebra', quarter:1 })
fig. 1 — container anatomy: strict grade → subject → quarter partition
OURI_BANKS container object grade 7 keys: 0…11 algebra 20 subjects geometry … informatics 811 JS questions q1 q2 q3 [ item, item, item, … ] { t, ty, o, c, e, tp, q } ≥ 36 questions per quarter no item belongs to two quarters

Validation rules — exactly what QKC.validate() checks

  • Non-empty. bank is empty if there are zero items.
  • t — required, non-empty string.
  • ty — one of choice / bool / input.
  • c — required, non-empty.
  • choice — o required with ≥ 2 options, and c must be one of them.
  • bool — c must be 'true' or 'false'.

errors are actionable

QKC.validate([ { t: '', ty: 'nope', c: '' }, { t: 'Capital of France?', ty: 'choice', o: ['Paris'], c: 'Lyon' } ]); // → { ok: false, errors: [ // 'item#0: missing text (t)', // 'item#0: bad type (ty)', // 'item#0: missing answer (c)', // 'item#1: choice needs options (o)', // 'item#1: correct answer not in options' ] }

csv → bank in 10 lines

// t,ty,o,c,e,tp — options separated by "|" function csvToBank(csv) { const [head, ...rows] = csv.trim().split(/\r?\n/); return rows.map(line => { const [t, ty, o, c, e, tp] = line.split(';'); const item = { t, ty, c, e, tp }; if (ty === 'choice') item.o = o.split('|'); return item; }); } QKC.validate(csvToBank(text)); // audit first!
05 · question types

Three types. Each one live.

Every item declares its type in ty. Below — three real widgets, one question each, rendered by three QKC.init() calls on this page. Answer them: the behavior described is the behavior you get.

ty: 'choice' — radio options, keys A/B/C, deterministic shuffle
ty: 'bool' — auto options true/false, localized Верно/Неверно
ty: 'input' — free text, graded via norm(), Enter = check
typerequired fieldsrendered asgraded as
choicet, o (≥2), c ∈ orole="radiogroup" of buttons with letter keys; order shuffled from seed unless shuffleOptions:falseselected option string, norm()-compared to c
boolt, c: 'true'|'false'two options; labels localized (True/False · Верно/Неверно)'true'/'false' string
inputt, ctext input, monospace, placeholder from lang; Enter triggers the primary actiontyped text, norm()-compared — case/whitespace-insensitive, , = .
06 · modes

normal · exam · sprint — three flows, one widget.

The mode changes interaction, grading timing and what onAnswer does — not the API. Switch it live with instance.update({ mode }) or the demo above.

fig. 2 — mode flows
normal question shownanswer → [check] ✓/✗ + right answer+ explanation (e) [next] → q+1…repeat… [finish] → result + onFinishonAnswer fires on every check exam question shownanswer saved silently [next] / [prev] — free nav"3 / 10 answers" counter [finish] — gradeall items at once result + onFinishonAnswer × N fires at finish sprint question + countdowntimePerQuestion (30s) tap → flash → auto-nextno reveal, graded silently timeout →skipped, auto-next result + onFinishonAnswer on select and timeout the progress bar is the countdown: it drains in real time and the header shows “Time left Ns”
normalexamsprint
instant feedback✓ after check— none until finish— never (anti-cheat pace)
navigationforward onlynext + prev, any orderauto-advance
timer——per question (timePerQuestion)
explanationsafter each checknot shownnot shown
onAnswer timingon each checkbatch at finishon select / timeout
typical uselearning, practicemock tests, control worksfluency drills, tables, facts
07 · themes & css

Three themes, eight CSS variables, zero !important.

The widget injects one scoped stylesheet (.qkc-* classes, data-theme attribute) and paints itself entirely through CSS variables — so a theme is just a variable set, and yours can be too. All three, live, same question:

theme: 'bw' — strict black/white, square corners, 2px borders
theme: 'light' — paper
theme: 'dark' — ink
variablepaintsbwlightdark
--qkc-bgwidget background#fff#fafafa#131317
--qkc-fgtext, primary buttons, progress fill#000#15151a#f2f2f5
--qkc-lineborders#000#d7d7de#2d2d36
--qkc-mutedsecondary text, chips#3d3d3d#6b6b76#90909b
--qkc-softprogress track#f2f2f2#efeff2#1b1b21
--qkc-sel / --qkc-selfgselected option bg / fg#000 / #fff#15151a / #fff#f2f2f5 / #131317
--qkc-rcorner radius (bw = 0)0px14px14px

custom theme — 6 lines of css

/* add a fourth theme next to bw/light/dark */ <style> .qkc-root[data-theme="paper"]{ --qkc-bg:#f7f3ea; --qkc-fg:#201c14; --qkc-line:#c9c0ad; --qkc-muted:#7a715d; --qkc-soft:#ece5d4; --qkc-sel:#201c14; --qkc-selfg:#f7f3ea; --qkc-r:6px; } </style> <script> QKC.init({ bank, el:'#quiz', theme:'paper' }); // unknown theme → falls back to 'bw'; // but data-theme="paper" is still set, // so your variables win. </script>

why it never clashes with your page

All rules are prefixed with .qkc-root and injected once per document (<style id="qkc-engine-css">) — the widget neither reads nor writes your stylesheets. Host-page inheritance is cut at the root: it sets its own font-family, text-align, line-height and box-sizing. Ten widgets on one page share one stylesheet; each paints itself from its own data-theme.

Sizing: the root is max-width:600px; drop it in a flex/grid cell and it fills it. The site theme switch in the header of this page (dark/light/b/w) is the same idea applied to the docs.

08 · playground

Paste your bank. Watch the engine render it.

The most honest way to try a library: no install, no account — an array of {t, ty, o, c, e, tp} items on the left, the real engine on the right. Everything runs locally in your browser; nothing is uploaded. Ctrl+Enter runs.

bank (JSON or JS array literal)

rendered widget

mode
theme
lang
09 · embedding

Any page, any framework, any bundler. Or none.

The engine is UMD + ESM with committed dist/ — a <script> tag, an import, a require and a CDN link all load byte-identical code.

<!-- no bundler, no npm, no config --> <script src="https://spaj7468-cell.github.io/qkc-engine/engine.js"></script> <div id="quiz"></div> <script> const quiz = QKC.init({ bank: myBank, // [{ t, ty, o, c, e, tp }, …] el: '#quiz', mode: 'normal', // 'normal' | 'exam' | 'sprint' theme: 'bw', // 'bw' | 'light' | 'dark' lang: 'ru', // 'ru' | 'en' (default: autodetect) count: 10, seed: 'spring-2026', // same seed → same quiz onAnswer: a => console.log('answered', a.id, a.correct), onFinish: r => console.log(r.pct, r.perTopic) }); </script>
10 · architecture

The whole engine, taken apart.

One ES5-compatible file, four layers, zero dependencies. This section is the guided tour: what each layer does, what literally happens between QKC.init() and the first rendered question, and why the same seed always produces the same quiz.

fig. 3 — module map: src/core.js, bottom to top
exports — the QKC object init · engine · validate · items · toArray · rng · hashSeed · norm · STR · VERSION widget layer — QKC.init() ≈ 250 lines of DOM state st{mode,theme,lang,seed,count,…} renderers: question,result, error card one delegated clicklistener + keydown lifecycle: start,update, destroy i18n STR{ru,en} +injected .qkc-* CSS engine layer — QKC.engine(bank), pure functions build({filter, count, seed}) → test · score(test, answers) → result · count · topics · validate foundation rng: mulberry32 · hashSeed: FNV-1a · shuffle: Fisher–Yates · norm: answer comparator · items: container traversal
fig. 4 — data flow: bank in, result out
bankarray | {items} | container toArray()→ flat list validate(){ok, errors[]} rng(seed)FNV-1a → mulberry32 build() / internal buildshuffle → pick count → shuffle options test.items + answersanswers: { [id]: value } score(test, answers)norm(v) === norm(c) result { correct, wrong, skip, total, pct,perTopic, seed, mode } → onFinish

What literally happens, call by call

QKC.init(opts) resolves the environmentPicks document (option or global), resolves el (selector/node) or creates a mount div; unknown mode/theme/lang values fall back to safe defaults instead of throwing.
toArray(bank) flattens the bankArray → copy; {items} → .items; container → full traversal merging grade/subject/quarter into every item.
CSS is injected once<style id="qkc-engine-css"> with the whole .qkc-* sheet (~4 KB of the gzip budget). Tenth widget on the page reuses it.
validate(list) runs before anything rendersInvalid bank → the widget renders an error card listing up to 6 problems instead of a broken quiz. Empty bank → “Bank is empty”.
The seeded build runsseed (string → FNV-1a hash → uint32) feeds mulberry32; Fisher–Yates shuffle picks min(count, len) items; each choice's options are shuffled from the same stream. Same seed ⇒ byte-identical quiz.
render() draws one questionHead (title · mode chip · “Question i/N”), bar (progress, or a live countdown in sprint), topic chip, the control per ty (radiogroup / true-false / input), footer buttons per mode. Sprint starts a 100 ms setInterval.
One listener handles everythingA single delegated click handler on the root reads data-act/data-val; Enter in the input maps to the primary action. No per-button listeners, no virtual DOM — direct createElement calls.
Grading is norm() equalitytrim → lowercase → collapse whitespace → ,→., then ===. Fires onAnswer (timing depends on mode).
finish() assembles the resultCounts correct/wrong/skip, builds perTopic from tp, calls onFinish(result) inside try/catch, renders the result screen: big pct, ✓/✗/– counters, per-question dots, topic table, seed chip, restart button.
update()/restart()/destroy()update applies new opts and rebuilds (this is how live theme/mode/lang switching works); destroy stops the timer and clears or removes the mount — safe in SPA cleanup hooks.
fig. 5 — widget anatomy (normal mode, after a wrong check)
Quick Knowledge Check NORMAL Question 2 / 4 CHOICE · АРИФМЕТИКА 2 + 2 = ? A 3 B 4 C 5 ✓ correct Explanation: Базовая арифметика: 2 + 2 = 4. Next head — title + mode chip + progress bar — progress (or countdown) chip — ty · topic question — t options — role="radiogroup",letter keys, shuffled by seed;selected = inverted (aria-checked) after check: right option solid,wrong pick dashed, all disabled feedback + explanation (e) foot — mode-dependent buttons;Enter = primary action result screen replaces this: pct · ✓✗– counters · dots per question · per-topic table · seed chip · [Restart]

Design decisions, honestly

Determinism

mulberry32 PRNG + FNV-1a hashing + Fisher–Yates: same seed → same quiz down to option order. Tests are reproducible, grading is auditable, bugs are replayable.

build({ seed:'x' }) ≡ build({ seed:'x' })

No dependencies, on purpose

Every “why not X” has the same answer: a quiz engine needs ~600 lines, not a dependency tree. Nothing to audit, nothing to break on major releases, 8.8 KB total.

package.json → "dependencies": {}

ES5-compatible source

No arrow functions, no classes, no transpiler. The only ES6 API used is Object.assign (one-line polyfill for IE11, if anyone still needs it). The source is the build.

src/core.js → dist/ (wrapper only)

Direct DOM, no vdom

One question is ~20 createElement calls and one delegated listener. Re-render per state change is instantaneous at this scale — React would cost 40× the bytes for the same 60 fps.

render() → root.textContent='' → build → append

Client-only, zero telemetry

No network calls anywhere in the engine — banks are your files, results are your callbacks, storage is your choice. Works from file://, offline, in a kiosk.

fetch: 0 · XHR: 0 · beacons: 0

a11y as a feature

role="radiogroup"/"radio" + aria-checked options, aria-live result announcements, visible focus, keyboard-complete flow, prefers-reduced-motion respected by the host docs.

Tab / Enter / Space — everywhere

Repository layout & size budget

files

src/core.js the engine — single source of truth build.mjs wraps src → UMD + ESM (no transpile) dist/qkc-engine.js UMD build, committed dist/qkc-engine.mjs ESM build, committed engine.js root UMD copy → https://spaj7468-cell.github.io/qkc-engine/engine.js index.html this docs site style.css docs styles (3 themes) site.js docs wiring — runs the real dist demo-bank.js real bank excerpt (7·algebra·q1) test/test.mjs headless core tests (node) test/dom.mjs widget tests (jsdom)

where the 8.8 KB go

partsharenotes
headless core~35%rng, traversal, validate, build/score
widget JS~40%renderers, events, lifecycle, i18n dicts
widget CSS~25%injected once as a string
dependencies0 KBthere are none

Raw UMD: 31 KB. Brotli/gzip: ~8.8 KB — less than most fonts load. Browser support: evergreen Chrome/Firefox/Safari/Edge; jsdom for tests; Node ≥ 14 for the headless core.

11 · recipes

Copied-from-production patterns.

Short, complete, honest — each snippet runs against the documented API, no pseudo-code.

hide answers from the client

// server / build time: const test = eng.build({ quarter: 1, seed: req.session.seed }); res.json(test.items.map(({ c, ...safe }) => safe)); // server, on submit — answers never lived on the client: const result = eng.score(test, req.body.answers);

persist progress in localStorage

QKC.init({ bank, el: '#quiz', seed: todaySeed(), onFinish: r => localStorage.setItem( 'quiz:' + todaySeed(), JSON.stringify({ pct: r.pct, perTopic: r.perTopic, at: Date.now() }) ) }); // replay history → chart perTopic over time

load a bank at runtime (fetch)

const res = await fetch('/banks/algebra-7.json'); const bank = await res.json(); const v = QKC.validate(bank); if (!v.ok) throw new Error(v.errors[0]); QKC.init({ bank, el: '#quiz', count: 15 });

same test for everyone (exam day)

// a date-string seed = one canonical variant QKC.init({ bank, el: '#quiz', mode: 'exam', seed: 'control-work-2026-10-15', count: 20 }); // variant B for the other row — one character apart: QKC.init({ bank, el: '#quiz2', mode: 'exam', seed: 'control-work-2026-10-15-B', count: 20 });

telemetry via onAnswer (yours, not ours)

QKC.init({ bank, el: '#quiz', onAnswer: a => { navigator.sendBeacon?.('/analytics', JSON.stringify({ q: a.item.t, topic: a.item.tp, ok: a.correct, skipped: a.skipped, ts: Date.now() })); }}); // the engine itself sends nothing, ever

headless grading in CI (Node)

import QKC from 'qkc-engine'; import bank from './banks/informatics-9.json' assert { type: 'json' }; const v = QKC.engine(bank).validate(); if (!v.ok) { console.error(v.errors); process.exit(1); } console.log('bank OK:', QKC.toArray(bank).length, 'items');
12 · testing & quality

Small engine, serious tests.

Two suites run on every change — headless logic in pure Node, widget behavior in jsdom. No mocks of our own code: the DOM suite drives the real dist/ build with real clicks.

run them

npm test # = node build.mjs && node test/test.mjs && node test/dom.mjs node test/test.mjs # headless core, zero deps node test/dom.mjs # widget in jsdom

what is covered

  • core: filters & counts, topics histogram, validation errors, seeded reproducibility (same seed → same test), scoring (correct/wrong/skip/pct/perTopic), option shuffle keeps c inside o, flat-array and container banks.
  • widget: full pass to 100% in normal mode, wrong-answer reveal, exam navigation + partial score, sprint attributes & countdown, invalid/empty bank error cards, container auto-flatten, update()/destroy()/restart(), RU/EN strings, theme attributes, onFinish and onAnswer semantics per mode.

Semver, kept

Public surface = the tables in §03. Additive changes → minor; breaking → major + migration note in CHANGELOG. QKC.VERSION always tells the truth.

1.2.0 — onAnswer added, nothing removed

dist/ is committed

Consumers never need a build step: clone, link or CDN the repo and the exact tested bytes are there. build.mjs only wraps src/core.js — no transpilation, no minifier eating stack traces.

src == dist, byte-auditable

Battle-tested at 42 823

The QKC trainer runs this engine's rules over 147 grade×subject banks daily — flattening, filtering and seeding at that scale is the performance baseline.

O(n) traversal · O(1) per answer
13 · install & npm

Four ways in, same bytes.

dist/ is committed to the repo, so git and CDN installs work today and are byte-identical to the npm one. The package ships UMD (main) + ESM (module/exports), unpkg and jsdelivr fields included.

entry pointformatstatusurl
GitHub Pages mirrorUMD (<script>)works nowhttps://spaj7468-cell.github.io/qkc-engine/engine.js
GitHub Pages mirrorESM (import)works nowhttps://spaj7468-cell.github.io/qkc-engine/dist/qkc-engine.mjs
jsDelivr CDNUMDworks nowhttps://cdn.jsdelivr.net/gh/spaj7468-cell/qkc-engine@main/dist/qkc-engine.js
git installUMD + ESMworks nownpm i github:spaj7468-cell/qkc-engine
engine.qkc.runs-on.devUMDafter the runs-on.dev claim merges (PR #310)https://engine.qkc.runs-on.dev/engine.js
# npm registry npm install qkc-engine # or straight from git — works today npm install github:spaj7468-cell/qkc-engine # WORKING TODAY — two entry points, zero npm: # 1) UMD, classic script tag (GitHub Pages mirror): <script src="https://spaj7468-cell.github.io/qkc-engine/engine.js"></script> # 2) ESM, module import: import QKC from 'https://spaj7468-cell.github.io/qkc-engine/dist/qkc-engine.mjs'; # CDN mirrors (same bytes): <script src="https://cdn.jsdelivr.net/gh/spaj7468-cell/qkc-engine@main/dist/qkc-engine.js"></script> <script src="https://unpkg.com/qkc-engine@1.2.0/dist/qkc-engine.js"></script> # future short domain (runs-on.dev claim, PR #310 — pending merge): # https://engine.qkc.runs-on.dev/engine.js # mirror that works right now: <script src="https://spaj7468-cell.github.io/qkc-engine/dist/qkc-engine.js"></script>
// CommonJS const QKC = require('qkc-engine'); // ESM / bundlers (Vite, esbuild, webpack, Rollup…) import QKC from 'qkc-engine'; // named exports work too: import { engine, validate, rng, init } from 'qkc-engine'; // everything is there: QKC.init({ bank, el: '#quiz' }); // embeddable widget QKC.engine(bank).build({ … }); // headless test builder QKC.engine(bank).score(test, ans); // scoring + per-topic stats QKC.validate(bank); // structural audit QKC.rng(seed); // seeded PRNG

Package contents (files): dist/, src/, engine.js, README, LICENSE, CHANGELOG. No postinstall scripts, no telemetry, no surprises.

14 · faq

Straight answers.

Do I need a build step, bundler or framework?

No. dist/ is committed; a <script> tag is enough. Bundlers work too (UMD + ESM + exports map), but nothing requires them. The docs site you are reading is itself plain HTML/CSS/JS with no build.

I want my own UI. Is the widget optional?

Completely. QKC.engine(bank) is pure logic: build() gives you a test object, score() grades answers, validate()/count()/topics() introspect the bank. QKC.init() is a convenience layer you can ignore — it shares the same core.

Where do correct answers live? Can students cheat?

Client-side, inside your bank — like every offline-first quiz library. For graded exams, build the test server-side (the core runs in Node), strip c before sending items, and score on the server with eng.score() — see the recipe in §11. For practice widgets, cheating only cheats yourself.

How exactly are answers compared?

QKC.norm(): trim → lowercase → collapse whitespace runs → replace , with ., then strict string equality. ' Paris ' = 'paris'; '3,5' = '3.5'. No fuzzy matching, no regex per question — deterministic and explainable.

Same quiz every launch, or different every time?

Your choice via seed: a fixed string/number reproduces the exact quiz (questions and option order); omit it and the engine seeds from the clock. Reproducibility is the point — an exam variant must be replayable for auditing.

Multiple widgets on one page?

Yes — every init() is independent; the stylesheet is injected once and shared. Ten demos on this page prove it (hero, quick start, live demo, three type cards, three theme cards, playground).

React / Vue / Svelte / Angular / web components?

The widget is plain DOM with a destroy() — it fits any lifecycle. §09 has copy-paste wrappers for React (useEffect), Vue 3 (onMounted) and a framework-neutral custom element; Svelte's onMount/onDestroy and Angular's ngOnInit/ngOnDestroy follow the identical two-line pattern.

TypeScript?

No .d.ts yet (roadmap). The API surface is small and fully tabulated in §03; until typings ship, a 20-line declaration file covers it — the option and result shapes are documented field by field.

Any analytics, phones home, licenses to worry about?

Zero network calls in the engine; zero trackers on this site; MIT license for everything — engine, widget, docs, and the trainer's banks. Use it in commercial products without attribution (appreciated, not required).

How big can a bank get?

The QKC trainer runs 42 823 items across 147 grade×subject partitions on this exact format, on school-grade hardware, offline. Flattening and filtering are O(n) once per init; per-answer work is O(1). Beyond ~100K items per screen, split banks per grade/subject into separate files (lazy-load, recipe §11).

What does BETA mean here?

The API is stable and tested, but pre-1.0-style guarantees don't exist yet: minor versions may add options, and .d.ts/npm-registry publication are still in flight. Breaking changes, if ever, come with a major bump and a migration note.

Why engine.qkc.runs-on.dev? What is this domain?

runs-on.dev is an open registry of free subdomains claimed through a public GitHub repo — one name per account, records and ownership visible to everyone. This project claimed qkc.runs-on.dev and published an engine subdomain on it for this documentation; the claim is public: zordhalo/runs-on.dev#310. The record is a CNAME to GitHub Pages, so the bytes served are identical to the mirror at spaj7468-cell.github.io/qkc-engine, which works right now while the claim is pending.

15 · changelog

What changed, when.

Full details in CHANGELOG.md. Semver; QKC.VERSION reports the loaded build.

1.2.0 2026-10-05

  • onAnswer callback — per-answer events { id, value, correct, skipped, item }; normal fires on check, sprint on select/timeout, exam as a batch at finish.
  • Docs site v2 — full API reference (every option, field and return shape), in-page REPL against the real build, live event console, question-type and theme galleries, five architecture diagrams, recipes, FAQ, changelog, site themes dark/light/b/w, sticky TOC with scroll-spy and filter, mobile drawer.
  • sprintTimeout hardening; tests extended for onAnswer semantics in all three modes.

1.1.0 2026-10-05

  • QKC.init() embeddable widget: question flow, three answer types, explanations, progress, result screen with per-topic stats; returns { root, mount, restart, update, destroy, results }.
  • Own scoped CSS injected at runtime · 3 modes (normal/exam/sprint) · 3 themes (bw/light/dark) · RU/EN dictionaries with autodetect.
  • Banks accepted as flat array, {items} or nested container (QKC.toArray); a11y: radiogroup semantics, aria-live, keyboard-complete.
  • Root engine.js entry point; jsdom DOM test suite.

1.0.0 2026-10-05

  • First public release: headless engine(bank) — build / score / validate / count / topics, rng, items, norm.
  • Seeded reproducible assembly (mulberry32 + FNV-1a) · UMD + ESM builds · bank schema v1 (grade → subject → quarter, topic slicing).
16 · license & source

MIT. All of it.

Engine, widget, this documentation site, and the 42 823-question banks of the trainer — MIT licensed, © 2026 OuRi Corp. Fork it, vendor it, ship it in your product. Attribution appreciated, not required.

Contribute

Banks are data — PR a JSON array. Engine changes: keep zero dependencies, keep the tests green (npm test), keep semver honest. Issues welcome, even “the docs confused me” ones — especially those.

github.com/spaj7468-cell/qkc-engine/issues

About this domain

engine.qkc.runs-on.dev is a subdomain of the project's runs-on.dev claim — an open registry of free subdomains where ownership and DNS records are public. Claim PR: zordhalo/runs-on.dev#310; the CNAME points at GitHub Pages. Live mirror while pending: spaj7468-cell.github.io/qkc-engine.

built with JavaScript — zero dependencies, hand-written ES5 core
17 · built with this engine

What the engine becomes.

QKC — Quick Knowledge Check

A school quiz trainer built on this engine — учебный тренажёр, построенный на этом движке. 42 823 questions · grades K–11 · 20 subjects · strictly per-quarter banks · offline PWA · RU/EN. This site is about the engine; the trainer is what the engine becomes with 40K questions behind it.

Open the trainer →

QKCcode — QKC v2

An offline coding school on the same engine — офлайн-школа программирования. 7 courses · 39 lessons · full theory, runnable examples and code-checked tasks in HTML/CSS, JavaScript, Python, Lua (Roblox), GDScript (Godot) and C# (Unity). Every lesson quiz and course exam is assembled by QKC.init() right in the browser — the engine's second home.

Open QKCcode →

repo 1 · qkc-engine

The reusable core you are reading about: widget, build/score/validate, seeded RNG, bank schema. Zero dependencies, MIT.

github.com/spaj7468-cell/qkc-engine · npm: qkc-engine

repo 2 · qkc

The trainer app: banks, screens, PWA shell, solvers, achievements. Consumes the engine's rules and format — one person + one AI, transparently.

github.com/spaj7468-cell/qkc · live: spaj7468-cell.github.io/qkc

repo 3 · qkccode

The coding school: a hand-written teaching interpreter for five languages plus lesson banks the engine turns into quizzes and exams. Offline PWA, monochrome.

github.com/spaj7468-cell/qkccode · live: spaj7468-cell.github.io/qkccode

Arakul Studio · powered by Kraul

Arakul Studio is the creative workspace; Kraul Engine is its engine layer. Kraul is intended for image-processing workflows and has its own repository for architecture, API reference, guides and implementation notes. Specific API signatures and feature availability will be documented from the actual implementation.

Open Arakul Studio · Kraul Engine repository
Choose a protocol

What do you want to explore?

Переключайся между двумя разделами — протоколом тестирования QKC и архитектурой связки Arakul Studio + Kraul Engine.

18 · protocol 01

Standard test: from question bank to result.

The ordinary QKC workflow — проверка знаний, контрольная работа или короткий тренировочный тест. The engine assembles the questions, checks answers and returns a score; the host app decides what to do with the result.

1. Prepare the bankWrite questions with an answer type, options when needed, the correct answer and an explanation. Keep topics labelled so results can be grouped.
2. Validate before startingCall QKC.engine(bank).validate() to catch structural problems before learners begin.
3. Configure the testUse QKC.init() and choose normal, exam or sprint mode, a theme, language and question count. A seed can make the same test reproducible.
4. Answer and reviewIn normal mode, learners can practise and see explanations. Exam mode supports a more test-like flow; sprint mode adds a countdown.
5. Read the resultonFinish(result) provides totals, percentage and per-topic statistics. Save or display those results in your own app if needed.

Short version · RU

Подготовь вопросы → проверь структуру банка → выбери режим → пройди тест → получи результат и разбор. QKC Engine выполняет логику тестирования локально в браузере и не отправляет ответы на сервер.