Run the live demo
Answer a sample quiz and inspect the embedded widget.
Open demo →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.
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.
Answer a sample quiz and inspect the embedded widget.
Open demo →Follow the setup steps and see the minimum integration code.
Quick start →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.
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.
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:
QKC (or import it as ESM/CJS). No CSS file, no fonts, no network calls.QKC.init({ bank, el })The bank is flattened, validated, and a seeded PRNG picks count questions and shuffles their options.normal/exam/sprint).onFinish(result) hands you { pct, correct, wrong, skip, perTopic, seed, mode }. Store it, chart it, ignore it — the engine never phones home.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.
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) → instanceMounts 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.
| option | type | default | description |
|---|---|---|---|
bank | array | object | required | Questions: a flat array of items, { items: [...] }, or the nested container { grade: { subject: { quarter: [...] } } } — anything QKC.toArray() accepts. |
el | string | Element | appended to body | Mount point: CSS selector or DOM node. Its content is cleared. If omitted (or not found), a new <div> is appended to body. |
count | number | 10 | How many questions to pick (min 1, capped at bank length). |
seed | string | number | time-based | PRNG 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' | autodetect | UI language. Autodetect: <html lang> → navigator.language → 'en'. |
timePerQuestion | number (sec) | 30 | Sprint countdown per question. On timeout the question is auto-advanced and counted as skipped. |
title | string | 'QKC' | Header title inside the widget. |
shuffleOptions | boolean | true | Shuffle choice options (deterministically, from the seed). Set false to keep bank order. |
onFinish | (result) => void | null | Fires when the quiz finishes, with the full result object (below). Exceptions inside are caught — a broken callback can't break the widget. |
onAnswer | (ev) => void | null | New 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 } }. |
document | Document | global | Document to render into — for jsdom/tests or multi-window setups. |
init() returns| member | type | description |
|---|---|---|
root | HTMLElement | The .qkc-root node — style it, measure it, query inside it. |
mount | HTMLElement | The resolved mount point (el or the created div). |
restart(o?) | () → instance | Apply options (optional) and start over: question 1, empty answers, fresh seeded build. |
update(o?) | () → instance | Apply options live — bank, mode, theme, lang, title, count, seed, timePerQuestion, onFinish, onAnswer — and re-render. This is how the demo switches above work. |
destroy() | () → void | Stop timers; clear the mount (or remove it if the engine created it). |
results() | () → result | null | The result of the current/last finished run — or a live score of the current attempt; null before any question is built. |
onFinish(r) / instance.results()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 coreNo DOM, no widget — build tests and score answers yourself (custom UI, server-side grading, Node scripts, generators).
| method | returns | description |
|---|---|---|
eng.build(opts?) | test | Assemble 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) | result | Grade 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?) | number | How many items match the filter. |
eng.topics(filter?) | [{ topic, count }] | Topic histogram, sorted by topic name. |
eng.bank | bank | The bank it was created with. |
| function | signature | description |
|---|---|---|
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) → uint32 | FNV-1a string hash — turns 'spring-2026' into a numeric seed. |
QKC.norm | (v) → string | The 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.VERSION | string | Semver of the build, e.g. '1.2.0'. |
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.
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.
Real item #0 of OURI_BANKS[7].algebra[1] — the bank loaded on this page.
| field | type | meaning |
|---|---|---|
t | string | Question text. Shown as-is (no HTML). |
ty | 'choice' | 'bool' | 'input' | Answer type — see §05. |
o | string[] | Options. Required for choice (≥ 2, must contain c). Ignored otherwise; bool auto-renders True/False. |
c | string | Correct answer, compared via norm() (trim / case / whitespace / ,→.). For bool: 'true' or 'false'. |
e | string | Explanation shown after checking (normal mode) — optional but your users will thank you. |
tp | string | Topic. Drives topics(), the topics filter and per-topic stats. |
q | number? | Quarter/term index when items live in a container. |
The simplest: [item, item, …]. Exactly what the playground and most embeds use.
An object wrapper — handy when a bank file also carries meta (title, version, …); the engine reads .items.
{ grade: { subject: { quarter: [items] } } } — the QKC trainer shape. build() filters slice it by grade/subject/quarter/topic.
QKC.validate() checksbank 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'.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.
| type | required fields | rendered as | graded as |
|---|---|---|---|
choice | t, o (≥2), c ∈ o | role="radiogroup" of buttons with letter keys; order shuffled from seed unless shuffleOptions:false | selected option string, norm()-compared to c |
bool | t, c: 'true'|'false' | two options; labels localized (True/False · Верно/Неверно) | 'true'/'false' string |
input | t, c | text input, monospace, placeholder from lang; Enter triggers the primary action | typed text, norm()-compared — case/whitespace-insensitive, , = . |
The mode changes interaction, grading timing and what onAnswer does — not the API.
Switch it live with instance.update({ mode }) or the demo above.
| normal | exam | sprint | |
|---|---|---|---|
| instant feedback | ✓ after check | — none until finish | — never (anti-cheat pace) |
| navigation | forward only | next + prev, any order | auto-advance |
| timer | — | — | per question (timePerQuestion) |
| explanations | after each check | not shown | not shown |
onAnswer timing | on each check | batch at finish | on select / timeout |
| typical use | learning, practice | mock tests, control works | fluency drills, tables, facts |
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:
| variable | paints | bw | light | dark |
|---|---|---|---|---|
--qkc-bg | widget background | #fff | #fafafa | #131317 |
--qkc-fg | text, primary buttons, progress fill | #000 | #15151a | #f2f2f5 |
--qkc-line | borders | #000 | #d7d7de | #2d2d36 |
--qkc-muted | secondary text, chips | #3d3d3d | #6b6b76 | #90909b |
--qkc-soft | progress track | #f2f2f2 | #efeff2 | #1b1b21 |
--qkc-sel / --qkc-selfg | selected option bg / fg | #000 / #fff | #15151a / #fff | #f2f2f5 / #131317 |
--qkc-r | corner radius (bw = 0) | 0px | 14px | 14px |
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.
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.
The engine is UMD + ESM with committed dist/ — a <script> tag,
an import, a require and a CDN link all load byte-identical code.
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.
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.<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”.seed (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.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.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.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' })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": {}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.
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.
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.
role="radiogroup"/"radio" + aria-checked options, aria-live result announcements, visible focus, keyboard-complete flow, prefers-reduced-motion respected by the host docs.
| part | share | notes |
|---|---|---|
| headless core | ~35% | rng, traversal, validate, build/score |
| widget JS | ~40% | renderers, events, lifecycle, i18n dicts |
| widget CSS | ~25% | injected once as a string |
| dependencies | 0 KB | there 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.
Short, complete, honest — each snippet runs against the documented API, no pseudo-code.
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.
c inside o, flat-array and container banks.update()/destroy()/restart(), RU/EN strings, theme attributes, onFinish and onAnswer semantics per mode.Public surface = the tables in §03. Additive changes → minor; breaking → major + migration note in CHANGELOG. QKC.VERSION always tells the truth.
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.
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 answerdist/ 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 point | format | status | url |
|---|---|---|---|
| GitHub Pages mirror | UMD (<script>) | works now | https://spaj7468-cell.github.io/qkc-engine/engine.js |
| GitHub Pages mirror | ESM (import) | works now | https://spaj7468-cell.github.io/qkc-engine/dist/qkc-engine.mjs |
| jsDelivr CDN | UMD | works now | https://cdn.jsdelivr.net/gh/spaj7468-cell/qkc-engine@main/dist/qkc-engine.js |
| git install | UMD + ESM | works now | npm i github:spaj7468-cell/qkc-engine |
| engine.qkc.runs-on.dev | UMD | after the runs-on.dev claim merges (PR #310) | https://engine.qkc.runs-on.dev/engine.js |
Package contents (files): dist/, src/, engine.js, README, LICENSE, CHANGELOG. No postinstall scripts, no telemetry, no surprises.
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.
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.
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.
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.
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.
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).
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.
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.
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).
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).
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.
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.
Full details in CHANGELOG.md. Semver; QKC.VERSION reports the loaded build.
onAnswer callback — per-answer events { id, value, correct, skipped, item }; normal fires on check, sprint on select/timeout, exam as a batch at finish.sprintTimeout hardening; tests extended for onAnswer semantics in all three modes.QKC.init() embeddable widget: question flow, three answer types, explanations, progress, result screen with per-topic stats; returns { root, mount, restart, update, destroy, results }.{items} or nested container (QKC.toArray); a11y: radiogroup semantics, aria-live, keyboard-complete.engine.js entry point; jsdom DOM test suite.engine(bank) — build / score / validate / count / topics, rng, items, norm.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.
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.
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 coreA 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.
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.
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-engineThe 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/qkcThe 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/qkccodeArakul 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Переключайся между двумя разделами — протоколом тестирования QKC и архитектурой связки Arakul Studio + Kraul Engine.
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.
QKC.engine(bank).validate() to catch structural problems before learners begin.QKC.init() and choose normal, exam or sprint mode, a theme, language and question count. A seed can make the same test reproducible.onFinish(result) provides totals, percentage and per-topic statistics. Save or display those results in your own app if needed.Подготовь вопросы → проверь структуру банка → выбери режим → пройди тест → получи результат и разбор. QKC Engine выполняет логику тестирования локально в браузере и не отправляет ответы на сервер.
Arakul Studio is the workspace; Kraul Engine is the engine behind it. Kraul has its own repository and is intended to support image-processing workflows, with a dedicated API and substantial technical documentation.
The user-facing creative workspace. It brings tools and workflows together in one interface; it is the product layer, not the engine itself.
The underlying engine project. Its scope includes image-processing workflows such as background removal and foreground isolation, subject to the features actually implemented in the codebase.
Kraul has a dedicated place for architecture notes, API reference, guides, examples and limitations. Exact method names, parameters and response shapes must be taken from the implementation rather than guessed.
QKC Engine handles quizzes and test results. Kraul Engine is the engine associated with Arakul Studio and is documented separately. The two engine projects have different roles within the OuRi ecosystem.