Skip to content

Commit 8f4b67e

Browse files
committed
feat: config-driven LPI exam practice engine with original sample pool
Offline-first practice engine for LPI certification exams. - config/exam.js: single source of truth for format, topics and official objective weights (010-160 v1.6) - data/questions.js: 40 original questions written from the published objectives, one per weight point - assets/js/sampler.js: weighted apportionment, verified at 7/9/9/8/7 - CI: question bank integrity, provenance guard, offline guarantee - Accessibility: contrast, colour vision, dyslexia, reduced motion, hideable timer, screen reader support - Content boundary documented in docs/QUESTION-SOURCING.md Assisted-by: Claude Opus 5 (Anthropic)
1 parent 3c47c6f commit 8f4b67e

31 files changed

Lines changed: 4196 additions & 2 deletions

.gitattributes

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
# Auto detect text files and perform LF normalization
2-
* text=auto
1+
* text=auto eol=lf

.githooks/prepare-commit-msg

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
# Appends an Assisted-by trailer to commit messages.
3+
# Not Co-Authored-By: that asserts co-authorship by a tool, and the address
4+
# conventionally paired with it is not controlled by Anthropic.
5+
# Edit MODEL when the assisting model changes. Leave empty for unassisted commits.
6+
7+
MODEL="Claude Opus 5 (Anthropic)"
8+
9+
[ -z "$MODEL" ] && exit 0
10+
grep -q "^Assisted-by:" "$1" && exit 0
11+
printf "\nAssisted-by: %s\n" "$MODEL" >> "$1"

.github/workflows/validate.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Validate question bank
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
validate:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-node@v4
13+
with:
14+
node-version: '24'
15+
- name: Validate the sample bank against the exam config
16+
run: node tools/validate-questions.js data/questions.js config/exam.js
17+
18+
- name: Verify the app has no remote dependencies
19+
run: node tools/check-offline.js

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Personal question pool — generated from your own copy of the LPI learning
2+
# material. Never commit this: the material is CC BY-NC-ND 4.0 and derived
3+
# content cannot be redistributed. See docs/QUESTION-SOURCING.md.
4+
data/questions.personal.js
5+
private/
6+
*.pdf
7+
8+
# OS
9+
.DS_Store
10+
Thumbs.db
11+
desktop.ini
12+
13+
# Editors
14+
.vscode/
15+
.idea/
16+
*.swp
17+
*~
18+
19+
# Node
20+
node_modules/
21+
npm-debug.log*
22+
package-lock.json
23+
24+
# Scratch
25+
*.local
26+
*.bak
27+
tmp/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 KalevIT
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

LICENSE-CONTENT.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Content Licensing
2+
3+
`LICENSE` (MIT) covers the software. This file covers everything that is not code, because
4+
questions and explanations are not software and the distinction matters here.
5+
6+
## Questions shipped in this repository
7+
8+
The 40 questions in `data/questions.js` are original work by the repository author. They
9+
were written from the **published exam objectives** and general technical knowledge of
10+
Linux. Nothing in them is copied, paraphrased or adapted from any LPI publication.
11+
12+
They are released under the **MIT License**, the same terms as the code.
13+
14+
## Material this repository does not contain
15+
16+
The **LPI Learning Materials** — the free textbook and PDF for Linux Essentials 010-160 —
17+
are © Linux Professional Institute and licensed
18+
**[CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/)**.
19+
20+
That licence permits sharing the work unchanged, for non-commercial purposes, with
21+
attribution. It does **not** permit distributing adapted or derived material.
22+
23+
Accordingly, this repository:
24+
25+
- does not contain the material or any part of it
26+
- does not contain questions derived from it
27+
- does not redistribute it in any form
28+
29+
`.gitignore` excludes `data/questions.personal.js` and `*.pdf`, and the CI validator fails
30+
the build if a question tagged as material-derived (`src: 'mat'`) appears in a committed
31+
bank. See [`docs/QUESTION-SOURCING.md`](docs/QUESTION-SOURCING.md).
32+
33+
## Your own question pool
34+
35+
LPI distributes the learning material free of charge to anyone. Generating a personal
36+
question pool from your own copy, for your own study, is private use — the NoDerivatives
37+
clause restricts *distribution* of adapted material, not private study.
38+
39+
Publishing that pool is a different act, and this project does not do it. Keep your pool in
40+
`data/questions.personal.js`, which is git-ignored.
41+
42+
## Exam objectives
43+
44+
The objective names and weights in `config/exam.js` are transcribed from the
45+
[published exam objectives](https://www.lpi.org/our-certifications/exam-010-objectives/).
46+
They are the specification of what the exam covers — a short factual list published so that
47+
candidates and educators can prepare. Third-party certification books have been written
48+
against published objectives for decades.
49+
50+
## Not legal advice
51+
52+
This document records the reasoning this project operates on. It is not a legal opinion and
53+
its author is not a lawyer. If you need certainty for your own use — particularly anything
54+
commercial — the material's own imprint invites questions at `learning@lpi.org`.
55+
56+
## Trademarks
57+
58+
"LPI", "Linux Professional Institute", "Linux Essentials" and related marks belong to their
59+
owners. This project is not affiliated with, endorsed by or sponsored by the Linux
60+
Professional Institute.

README.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# 🐧 LPI Exam Practice Engine — Linux Essentials 010-160
2+
3+
![License](https://img.shields.io/badge/Code-MIT-yellow?style=flat)
4+
![Content](https://img.shields.io/badge/Questions-original%2C%20objective--derived-blue?style=flat)
5+
![Sample](https://img.shields.io/badge/Sample%20pool-40%20questions-green?style=flat)
6+
![Offline](https://img.shields.io/badge/Offline-First-lightgrey?style=flat)
7+
![Accessibility](https://img.shields.io/badge/Accessibility-WCAG--oriented-purple?style=flat)
8+
![Validated](https://img.shields.io/badge/Question%20bank-CI%20validated-brightgreen?style=flat)
9+
10+
> **Fully offline** · No server · No build step · No dependencies · Runs from a USB stick
11+
> **Accessible by design** · Screen readers · Dyslexia · Colour vision · Reduced motion
12+
13+
A browser-based practice engine for LPI certification exams, shipping with a configuration
14+
and a sample question pool for **Linux Essentials (010-160)**.
15+
16+
Open `index.html` and it works immediately.
17+
18+
---
19+
20+
## What this is, and what it deliberately is not
21+
22+
**It is an engine.** The exam is described by one file, `config/exam.js`: format, timing,
23+
topics, objectives and their official weights. Point it at a different exam and the same
24+
code serves that exam. Nothing exam-specific exists outside `config/` and `data/`.
25+
26+
**It ships a sample, not a study bank.** `data/questions.js` contains **40 questions**
27+
exactly one per official weight point, so it forms precisely one full weighted exam and
28+
demonstrates the sampler working. Forty questions will not prepare anyone for a
29+
certification, and it is not trying to.
30+
31+
**You bring your own pool.** LPI publishes the complete Linux Essentials learning material
32+
free of charge. You generate your own question bank from your own copy, keep it on your
33+
machine, and the engine picks it up automatically. The procedure is in
34+
[`docs/QUESTION-SOURCING.md`](docs/QUESTION-SOURCING.md).
35+
36+
### Why it works this way
37+
38+
The LPI Learning Materials are licensed **CC BY-NC-ND 4.0** — no commercial use, no
39+
distribution of derived works. Generating a personal question pool from them for your own
40+
study is fine. Publishing that pool under MIT is not, because MIT would grant everyone
41+
else rights that cannot be granted.
42+
43+
The shipped questions are therefore written from the **published exam objectives** and
44+
general technical knowledge, which is what every third-party certification book has always
45+
done. Full reasoning, and the boundary this project draws, in
46+
[`docs/QUESTION-SOURCING.md`](docs/QUESTION-SOURCING.md) and
47+
[ADR-0004](docs/decisions/0004-content-licensing-boundary.md).
48+
49+
---
50+
51+
## Modes
52+
53+
| Mode | What it does |
54+
|------|--------------|
55+
| 📖 **Study** | Browse questions with instant feedback and explanations. Filter by topic, type or source. No timer. |
56+
|**Exam** | 40 questions · 60-minute timer · no feedback until submission. |
57+
| 🎯 **Drill** | One objective at a time, until it sticks. |
58+
| 🔍 **Hard** | The trickier questions: multi-select, fill-in, common traps. |
59+
60+
Question types: single choice · multi-select · fill-in (type the command).
61+
62+
A sixth mode, concept maps, is held back pending a provenance decision — see
63+
[`docs/MAPS-PENDING.md`](docs/MAPS-PENDING.md).
64+
65+
---
66+
67+
## Exam fidelity
68+
69+
The real exam distributes 40 questions across 19 objectives by published weight. Objective
70+
3.3 alone is worth 4 of those 40 points; 1.3, 4.1 and 5.4 are worth 1 each.
71+
72+
`assets/js/sampler.js` reproduces that weighting using largest-remainder apportionment —
73+
not rounding, which does not sum back to 40 and silently yields 39- or 41-question exams.
74+
Measured over 2,000 simulated exams: **7 / 9 / 9 / 8 / 7** questions across Topics 1–5,
75+
40.00 questions per exam.
76+
77+
The 75% pass threshold is a practical proxy. Official scoring is 500 on a 200–800 scale
78+
and is not publicly reducible to a percentage. The UI says so.
79+
80+
---
81+
82+
## ♿ Accessibility
83+
84+
Accessibility was a first-class requirement, not a retrofit. Every option is a toggle in
85+
the panel injected by `a11y.js`, persists in `localStorage`, and works offline — there are
86+
no downloaded fonts or remote assets anywhere in the app.
87+
88+
| Need | What is provided |
89+
|---|---|
90+
| **Low vision** | High-contrast mode, dark theme, adjustable text size |
91+
| **Colour vision deficiency** | Three palettes: achromatopsia, deuteranopia, tritanopia |
92+
| **Dyslexia** | Wide-letterform font, loosened line spacing, text never justified |
93+
| **Dyscalculia / test anxiety** | The exam timer can be hidden entirely, or paused |
94+
| **Motor impairment** | Enlarged tap and click targets |
95+
| **Vestibular disorders** | Reduced-motion mode |
96+
| **Screen readers** | ARIA labels and roles throughout, `aria-live` regions, skip links |
97+
| **Keyboard-only use** | Full keyboard navigation, managed focus order |
98+
99+
**On the dyslexia font:** it uses widely available system faces (Comic Sans MS, Verdana)
100+
rather than OpenDyslexic. OpenDyslexic would have to be downloaded, and this application
101+
has to work from a USB stick with no network. A font that is present everywhere beats a
102+
better font that sometimes fails to load.
103+
104+
`localStorage` is used for these preferences only. No analytics, no cookies, nothing leaves
105+
the machine.
106+
107+
---
108+
109+
## Quick start
110+
111+
```bash
112+
git clone <your-fork-url>
113+
cd lpi-exam-engine
114+
```
115+
116+
Open `index.html` in a browser. No server, no install.
117+
118+
To build a real study bank, follow [`docs/QUESTION-SOURCING.md`](docs/QUESTION-SOURCING.md):
119+
download the free material from LPI, generate `data/questions.personal.js`, and the engine
120+
uses it instead of the sample. That file is git-ignored and never leaves your machine.
121+
122+
---
123+
124+
## Structure
125+
126+
```
127+
├── index.html · study.html · exam.html · drill.html · weak.html
128+
├── style.css
129+
├── a11y.js accessibility controller
130+
131+
├── config/
132+
│ └── exam.js THE exam definition — format, topics, official weights
133+
├── data/
134+
│ ├── questions.js sample pool, 40 original questions
135+
│ └── questions.personal.js your pool — git-ignored, never committed
136+
├── assets/js/
137+
│ ├── core.js helpers and metadata derived from the config
138+
│ └── sampler.js weighted exam sampling
139+
├── tools/
140+
│ ├── validate-questions.js CI gate: bank integrity and provenance
141+
│ └── check-offline.js CI gate: no remote dependencies
142+
143+
├── docs/
144+
│ ├── QUESTION-SOURCING.md where questions may come from, and the boundary
145+
│ ├── MAPS-PENDING.md why the concept maps are not shipped yet
146+
│ ├── ARCHITECTURE.md engine design and fork procedure
147+
│ ├── AUTHORS.md who did what
148+
│ └── decisions/ architecture decision records
149+
150+
├── .github/workflows/validate.yml
151+
├── .githooks/prepare-commit-msg adds the Assisted-by trailer (opt in locally)
152+
├── .gitattributes · .gitignore
153+
├── LICENSE MIT — the code
154+
└── LICENSE-CONTENT.md the questions and what you may do with them
155+
```
156+
157+
`config/exam.js` is JavaScript rather than JSON on purpose: the app must run from `file://`
158+
off a USB stick, and browsers block `fetch()` on `file://` origins. A script tag is not
159+
blocked, and the Node tooling reads the same file. See
160+
[ADR-0003](docs/decisions/0003-config-as-javascript.md).
161+
162+
---
163+
164+
## Forking for another LPI exam
165+
166+
Verified formats: Security Essentials (020-100) and Web Development Essentials (030-100)
167+
are 40 questions in 60 minutes like Linux Essentials; LPIC-1 (101-500 / 102-500) is 60
168+
questions in 90 minutes. The engine reads all of that from the config, which is why those
169+
numbers are not hardcoded anywhere.
170+
171+
1. Use this repository as a template
172+
2. Replace `config/exam.js` — identity, format, topics, objectives and weights,
173+
transcribed from that exam's objectives page
174+
3. Replace `data/questions.js` with your own sample; every `s` value must exist in the config
175+
4. `node tools/validate-questions.js` — green means the fork is coherent
176+
177+
All four steps work today. The one gap is concept maps, which are not shipped —
178+
see [`docs/MAPS-PENDING.md`](docs/MAPS-PENDING.md).
179+
180+
---
181+
182+
## Quality controls
183+
184+
GitHub Actions runs the validator on every push and pull request. It fails on schema
185+
violations, answer indices out of range, duplicate question text, duplicate IDs, objective
186+
keys absent from the config, topic/objective mismatches, and any question tagged as derived
187+
from the LPI material appearing in a committed bank. It warns when objective coverage
188+
drifts more than 2 points from the official weight share.
189+
190+
```bash
191+
node tools/validate-questions.js data/questions.js config/exam.js
192+
node tools/check-offline.js
193+
```
194+
195+
`check-offline.js` fails the build if a remote script, stylesheet, image, webfont, CSS
196+
import, `fetch()` or `XMLHttpRequest` is introduced anywhere in the app. The
197+
offline-from-USB guarantee is a promise, and a promise nothing enforces decays. Plain
198+
`<a href>` links are not flagged — they are navigation, not a loaded resource.
199+
200+
**What it cannot check:** whether an answer key is actually correct. Structural validity is
201+
mechanical; factual correctness is not. Verify your own questions.
202+
203+
---
204+
205+
## Attribution
206+
207+
Three roles, kept separate because merging them would misstate all of them.
208+
209+
| Role | Who |
210+
|---|---|
211+
| **Concept, direction and every decision** | **[KalevIT](https://github.com/KalevIT)** — originated the project, set the scope and requirements, made accessibility a first-class constraint, commissioned the audit, raised the licensing question that shaped the content boundary, and ruled on every architectural trade-off |
212+
| **Application layer** | Claude Sonnet 4.6 (Anthropic) — the mode pages, stylesheet and accessibility controller |
213+
| **Engine, audit and documentation** | Claude Opus 5 (Anthropic) — config-driven engine, weighted sampler, CI gates, sample question pool, docs |
214+
215+
The models are tools. Every decision recorded in
216+
[`docs/decisions/`](docs/decisions/) is the author's, including the ones taken against the
217+
advice given — and responsibility for this repository, defects included, is his alone.
218+
219+
[`docs/AUTHORS.md`](docs/AUTHORS.md) has the full breakdown and, more usefully, the list of
220+
what was independently verified and what was not.
221+
222+
---
223+
224+
## Licensing
225+
226+
- **Code**`core.js`, `sampler.js`, the validator, the pages, the CI: **MIT**. See `LICENSE`.
227+
- **Questions in `data/questions.js`** — original work, MIT alongside the code. See
228+
[`LICENSE-CONTENT.md`](LICENSE-CONTENT.md).
229+
- **The LPI Learning Materials** — © Linux Professional Institute, CC BY-NC-ND 4.0. Not
230+
included in this repository, not redistributed by it. Download your own free copy from
231+
[learning.lpi.org](https://learning.lpi.org/en/learning-materials/010-160/).
232+
233+
Not affiliated with, endorsed by or sponsored by the Linux Professional Institute.
234+
Not official exam questions and no reproduction of real exam content.
235+
236+
---
237+
238+
## Source material
239+
240+
- [LPI Learning Materials: Linux Essentials v1.6](https://learning.lpi.org/en/learning-materials/010-160/) — free, CC BY-NC-ND 4.0
241+
- [Official exam 010 objectives](https://www.lpi.org/our-certifications/exam-010-objectives/) — the weights in `config/exam.js` come from here
242+
243+
## License
244+
245+
MIT for the code. See `LICENSE` and [`LICENSE-CONTENT.md`](LICENSE-CONTENT.md).

0 commit comments

Comments
 (0)