@kensai/quiz-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/evaluate.d.ts +47 -0
  4. package/dist/evaluate.d.ts.map +1 -0
  5. package/dist/evaluate.js +149 -0
  6. package/dist/evaluate.js.map +1 -0
  7. package/dist/events.d.ts +10 -0
  8. package/dist/events.d.ts.map +1 -0
  9. package/dist/events.js +26 -0
  10. package/dist/events.js.map +1 -0
  11. package/dist/index.d.ts +26 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +19 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/load.d.ts +22 -0
  16. package/dist/load.d.ts.map +1 -0
  17. package/dist/load.js +31 -0
  18. package/dist/load.js.map +1 -0
  19. package/dist/resolve.d.ts +9 -0
  20. package/dist/resolve.d.ts.map +1 -0
  21. package/dist/resolve.js +22 -0
  22. package/dist/resolve.js.map +1 -0
  23. package/dist/runtime.d.ts +118 -0
  24. package/dist/runtime.d.ts.map +1 -0
  25. package/dist/runtime.js +245 -0
  26. package/dist/runtime.js.map +1 -0
  27. package/dist/schema.generated.d.ts +503 -0
  28. package/dist/schema.generated.d.ts.map +1 -0
  29. package/dist/schema.generated.js +626 -0
  30. package/dist/schema.generated.js.map +1 -0
  31. package/dist/settings.d.ts +19 -0
  32. package/dist/settings.d.ts.map +1 -0
  33. package/dist/settings.js +32 -0
  34. package/dist/settings.js.map +1 -0
  35. package/dist/stats.d.ts +33 -0
  36. package/dist/stats.d.ts.map +1 -0
  37. package/dist/stats.js +79 -0
  38. package/dist/stats.js.map +1 -0
  39. package/dist/text.d.ts +5 -0
  40. package/dist/text.d.ts.map +1 -0
  41. package/dist/text.js +11 -0
  42. package/dist/text.js.map +1 -0
  43. package/dist/types.d.ts +136 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +9 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/validate.d.ts +28 -0
  48. package/dist/validate.d.ts.map +1 -0
  49. package/dist/validate.js +170 -0
  50. package/dist/validate.js.map +1 -0
  51. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Efraín García
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.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @kensai/quiz-core
2
+
3
+ Headless, UI-agnostic engine for the [Kensai Quiz format](../../spec). It loads and
4
+ validates a quiz file, evaluates answers, runs an **attempt** (a framework-agnostic
5
+ state machine), and computes **per-category statistics**. No DOM, no timers, no UI
6
+ dependency — any web project brings its own UI and consumes this package.
7
+
8
+ ```text
9
+ quiz file (YAML/JSON) ──loadQuiz──▶ Quiz ──Attempt──▶ events + QuizResult + QuizStats
10
+
11
+ └─ validated against spec/quiz.schema.json (authoritative)
12
+ ```
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @kensai/quiz-core
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```ts
23
+ import { loadQuiz, Attempt } from "@kensai/quiz-core";
24
+
25
+ const quiz = loadQuiz(yamlString); // parse + validate (throws on error)
26
+
27
+ const attempt = new Attempt(quiz, {
28
+ settings: { feedback: "immediate" }, // per-attempt overrides (optional)
29
+ });
30
+
31
+ attempt.on("answered", ({ result }) => { // `result` only under immediate feedback
32
+ if (result) console.log(result.correct ? "✓" : "✗");
33
+ });
34
+ attempt.on("finished", ({ result }) => {
35
+ console.log(`${result.score}/${result.maxScore}`, result.passed);
36
+ for (const c of result.stats.byCategory) {
37
+ console.log(c.label ?? c.categoryId, `${c.correct}/${c.total}`);
38
+ }
39
+ });
40
+
41
+ attempt.start();
42
+ attempt.answer(["verb"]); // answer current question (shape depends on type — see below)
43
+ attempt.next();
44
+ // ...
45
+ attempt.finish();
46
+ ```
47
+
48
+ Prefer stateless calls? Skip the attempt and evaluate directly:
49
+
50
+ ```ts
51
+ import { evaluateQuiz } from "@kensai/quiz-core";
52
+ const result = evaluateQuiz(quiz, { q1: ["verb"], q7: "mice" }); // keyed by question id
53
+ ```
54
+
55
+ ## Answer shapes (per question type)
56
+
57
+ | Type | Answer value |
58
+ |------|--------------|
59
+ | `choice` | option id (`"a"`) or list of ids (`["a","b"]`) |
60
+ | `true_false` | `boolean` |
61
+ | `fill_blank` | `{ [blankId]: text-or-optionId }` |
62
+ | `classify` | `{ [itemId]: groupId }` |
63
+ | `matching` | `{ [leftId]: rightId }` |
64
+ | `ordering` | ordered id list (`["b","a","c"]`) |
65
+ | `short_answer` | `string` |
66
+
67
+ Answers are keyed by the question's `id`, falling back to its index (as a string)
68
+ when it has none.
69
+
70
+ ## API surface
71
+
72
+ - **Load / validate** — `loadQuiz`, `parseQuizSource`, `validateQuiz`,
73
+ `validateSchema`, `validateReferences`, `QuizValidationError`. Validation combines
74
+ the JSON Schema (structure) with referential checks (answer ids exist, `*_from`
75
+ names resolve, placeholders match blanks, ordering answers are permutations, …).
76
+ - **Evaluate** — `evaluateQuiz`, `evaluateQuestion`, `gradeQuestion`. Partial credit
77
+ is awarded where meaningful (`fill_blank`, `classify`, `matching`, `ordering`);
78
+ `choice` / `true_false` / `short_answer` are all-or-nothing.
79
+ - **Statistics** — `computeStats` → overall + per-category counts, scores, accuracy.
80
+ - **Runtime** — `Attempt` (navigation, answers, feedback timing, optional time limit,
81
+ events `started` / `answered` / `navigated` / `time_up` / `finished`, plus
82
+ `toJSON()` / `Attempt.resume()` for persistence). The clock and RNG are injectable
83
+ (`now`, `rng`) for testing and SSR; the attempt never starts real timers.
84
+ - **Settings** — `resolveSettings`, `DEFAULT_SETTINGS`. Defaults: `navigation: all`,
85
+ `order: fixed`, `feedback: on_finish`, unlimited time, `shuffle_options: false`.
86
+
87
+ ## Development
88
+
89
+ ```bash
90
+ pnpm sync:schema # regenerate src/schema.generated.ts from spec/quiz.schema.json
91
+ pnpm build # tsc → dist (ESM + .d.ts)
92
+ pnpm test # vitest (unit + validates every spec/examples/*.yaml)
93
+ pnpm typecheck # tsc --noEmit
94
+ ```
95
+
96
+ The JSON Schema in `spec/` is the single source of truth for validation; it is inlined
97
+ into `src/schema.generated.ts` (a generated file) so the package needs no runtime file
98
+ read and bundles cleanly for the browser. Regenerate with `pnpm sync:schema` after any
99
+ schema change.
@@ -0,0 +1,47 @@
1
+ import type { AnswerInput, Question, Quiz } from "./types.js";
2
+ import { type ResolvedSettings } from "./settings.js";
3
+ import { type QuizStats } from "./stats.js";
4
+ export interface QuestionResult {
5
+ /** Index into `quiz.questions`. */
6
+ index: number;
7
+ id?: string;
8
+ type: Question["type"];
9
+ category?: string;
10
+ answered: boolean;
11
+ /** Whole-question correctness (`fraction === 1`). */
12
+ correct: boolean;
13
+ /** Correctness in 0..1 — partial where meaningful (fill_blank, classify, matching, ordering). */
14
+ fraction: number;
15
+ /** Earned points: `fraction * maxScore`. */
16
+ score: number;
17
+ /** `points` (default 1). */
18
+ maxScore: number;
19
+ }
20
+ export interface QuizResult {
21
+ results: QuestionResult[];
22
+ score: number;
23
+ maxScore: number;
24
+ /** `score / maxScore` (0 when `maxScore` is 0). */
25
+ ratio: number;
26
+ /** Pass/fail vs the resolved `passing_score`; `null` when it is unset. */
27
+ passed: boolean | null;
28
+ stats: QuizStats;
29
+ }
30
+ export interface EvaluateOptions {
31
+ /** Per-attempt settings overrides (e.g. a player-provided `passing_score`). */
32
+ settingsOverride?: Partial<ResolvedSettings>;
33
+ }
34
+ /**
35
+ * Grade a single question, returning correctness in 0..1. Types with independent
36
+ * sub-parts (fill_blank, classify, matching, ordering) return partial credit;
37
+ * choice/true_false/short_answer are all-or-nothing.
38
+ */
39
+ export declare function gradeQuestion(quiz: Quiz, question: Question, answer: AnswerInput | undefined): number;
40
+ /** Evaluate one question against a submitted answer (or `undefined` if unanswered). */
41
+ export declare function evaluateQuestion(quiz: Quiz, question: Question, index: number, answer: AnswerInput | undefined): QuestionResult;
42
+ /**
43
+ * Evaluate a whole quiz. `answers` is keyed by question id, falling back to the
44
+ * stringified question index for questions without an id.
45
+ */
46
+ export declare function evaluateQuiz(quiz: Quiz, answers: Record<string, AnswerInput>, options?: EvaluateOptions): QuizResult;
47
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../src/evaluate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAG9D,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9C;AAYD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,MAAM,CA8ER;AAED,uFAAuF;AACvF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,cAAc,CAchB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,OAAO,GAAE,eAAoB,GAC5B,UAAU,CAcZ"}
@@ -0,0 +1,149 @@
1
+ import { resolveChoiceOptions } from "./resolve.js";
2
+ import { normalizeText } from "./text.js";
3
+ import { resolveSettings } from "./settings.js";
4
+ import { computeStats } from "./stats.js";
5
+ const pointsOf = (question) => question.points ?? 1;
6
+ function isBlankAnswer(answer) {
7
+ if (answer === undefined || answer === null)
8
+ return true;
9
+ if (typeof answer === "string")
10
+ return answer.trim() === "";
11
+ if (Array.isArray(answer))
12
+ return answer.length === 0;
13
+ if (typeof answer === "object")
14
+ return Object.keys(answer).length === 0;
15
+ return false; // boolean (true_false) is always a real answer
16
+ }
17
+ /**
18
+ * Grade a single question, returning correctness in 0..1. Types with independent
19
+ * sub-parts (fill_blank, classify, matching, ordering) return partial credit;
20
+ * choice/true_false/short_answer are all-or-nothing.
21
+ */
22
+ export function gradeQuestion(quiz, question, answer) {
23
+ if (answer === undefined || answer === null)
24
+ return 0;
25
+ switch (question.type) {
26
+ case "choice": {
27
+ const optionIds = new Set(resolveChoiceOptions(quiz, question).map((o) => o.id));
28
+ const expectedList = (Array.isArray(question.answer) ? question.answer : [question.answer])
29
+ .filter((id) => optionIds.has(id));
30
+ const expected = new Set(expectedList);
31
+ if (expected.size === 0)
32
+ return 0;
33
+ const givenList = (Array.isArray(answer) ? answer : [answer]).filter((a) => typeof a === "string");
34
+ const given = new Set(givenList);
35
+ if (given.size !== expected.size)
36
+ return 0;
37
+ for (const id of given)
38
+ if (!expected.has(id))
39
+ return 0;
40
+ return 1;
41
+ }
42
+ case "true_false":
43
+ return answer === question.answer ? 1 : 0;
44
+ case "short_answer": {
45
+ if (typeof answer !== "string")
46
+ return 0;
47
+ const caseSensitive = question.case_sensitive ?? false;
48
+ const given = normalizeText(answer, caseSensitive);
49
+ return question.accept.some((a) => normalizeText(a, caseSensitive) === given) ? 1 : 0;
50
+ }
51
+ case "fill_blank": {
52
+ const given = asRecord(answer);
53
+ const keys = Object.keys(question.blanks);
54
+ if (keys.length === 0)
55
+ return 0;
56
+ let correct = 0;
57
+ for (const key of keys) {
58
+ const blank = question.blanks[key];
59
+ const value = given[key];
60
+ if (value === undefined)
61
+ continue;
62
+ if (blank.options && blank.answer !== undefined) {
63
+ if (value === blank.answer)
64
+ correct++;
65
+ }
66
+ else if (blank.accept) {
67
+ const caseSensitive = blank.case_sensitive ?? false;
68
+ const normalized = normalizeText(value, caseSensitive);
69
+ if (blank.accept.some((a) => normalizeText(a, caseSensitive) === normalized))
70
+ correct++;
71
+ }
72
+ }
73
+ return correct / keys.length;
74
+ }
75
+ case "classify": {
76
+ const given = asRecord(answer);
77
+ if (question.items.length === 0)
78
+ return 0;
79
+ let correct = 0;
80
+ for (const item of question.items)
81
+ if (given[item.id] === item.answer)
82
+ correct++;
83
+ return correct / question.items.length;
84
+ }
85
+ case "matching": {
86
+ const given = asRecord(answer);
87
+ const keys = Object.keys(question.answer);
88
+ if (keys.length === 0)
89
+ return 0;
90
+ let correct = 0;
91
+ for (const key of keys)
92
+ if (given[key] === question.answer[key])
93
+ correct++;
94
+ return correct / keys.length;
95
+ }
96
+ case "ordering": {
97
+ const given = Array.isArray(answer) ? answer : [];
98
+ const expected = question.answer;
99
+ if (expected.length === 0)
100
+ return 0;
101
+ let correct = 0;
102
+ for (let i = 0; i < expected.length; i++)
103
+ if (given[i] === expected[i])
104
+ correct++;
105
+ return correct / expected.length;
106
+ }
107
+ default:
108
+ return 0;
109
+ }
110
+ }
111
+ /** Evaluate one question against a submitted answer (or `undefined` if unanswered). */
112
+ export function evaluateQuestion(quiz, question, index, answer) {
113
+ const fraction = gradeQuestion(quiz, question, answer);
114
+ const maxScore = pointsOf(question);
115
+ return {
116
+ index,
117
+ id: question.id,
118
+ type: question.type,
119
+ category: question.category,
120
+ answered: !isBlankAnswer(answer),
121
+ correct: fraction >= 1,
122
+ fraction,
123
+ score: fraction * maxScore,
124
+ maxScore,
125
+ };
126
+ }
127
+ /**
128
+ * Evaluate a whole quiz. `answers` is keyed by question id, falling back to the
129
+ * stringified question index for questions without an id.
130
+ */
131
+ export function evaluateQuiz(quiz, answers, options = {}) {
132
+ const settings = resolveSettings(quiz.settings, options.settingsOverride);
133
+ const results = quiz.questions.map((question, index) => {
134
+ const key = question.id ?? String(index);
135
+ return evaluateQuestion(quiz, question, index, answers[key]);
136
+ });
137
+ const score = results.reduce((sum, r) => sum + r.score, 0);
138
+ const maxScore = results.reduce((sum, r) => sum + r.maxScore, 0);
139
+ const ratio = maxScore > 0 ? score / maxScore : 0;
140
+ const passed = settings.passing_score == null ? null : ratio >= settings.passing_score;
141
+ const stats = computeStats(quiz, results);
142
+ return { results, score, maxScore, ratio, passed, stats };
143
+ }
144
+ function asRecord(value) {
145
+ return value && typeof value === "object" && !Array.isArray(value)
146
+ ? value
147
+ : {};
148
+ }
149
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../src/evaluate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export type Listener<T> = (payload: T) => void;
2
+ /** Minimal, dependency-free typed event emitter. */
3
+ export declare class Emitter<Events extends Record<string, unknown>> {
4
+ private readonly listeners;
5
+ /** Subscribe. Returns an unsubscribe function. */
6
+ on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void;
7
+ off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void;
8
+ emit<K extends keyof Events>(event: K, payload: Events[K]): void;
9
+ }
10
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;AAE/C,oDAAoD;AACpD,qBAAa,OAAO,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmD;IAE7E,kDAAkD;IAClD,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAU/E,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAI1E,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;CAMjE"}
package/dist/events.js ADDED
@@ -0,0 +1,26 @@
1
+ /** Minimal, dependency-free typed event emitter. */
2
+ export class Emitter {
3
+ listeners = new Map();
4
+ /** Subscribe. Returns an unsubscribe function. */
5
+ on(event, listener) {
6
+ let set = this.listeners.get(event);
7
+ if (!set) {
8
+ set = new Set();
9
+ this.listeners.set(event, set);
10
+ }
11
+ set.add(listener);
12
+ return () => this.off(event, listener);
13
+ }
14
+ off(event, listener) {
15
+ this.listeners.get(event)?.delete(listener);
16
+ }
17
+ emit(event, payload) {
18
+ const set = this.listeners.get(event);
19
+ if (!set)
20
+ return;
21
+ // Copy so listeners may unsubscribe during dispatch.
22
+ for (const listener of [...set])
23
+ listener(payload);
24
+ }
25
+ }
26
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,OAAO,OAAO;IACD,SAAS,GAAG,IAAI,GAAG,EAAwC,CAAC;IAE7E,kDAAkD;IAClD,EAAE,CAAyB,KAAQ,EAAE,QAA6B;QAChE,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,QAA6B,CAAC,CAAC;QACvC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,GAAG,CAAyB,KAAQ,EAAE,QAA6B;QACjE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAA6B,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,CAAyB,KAAQ,EAAE,OAAkB;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,qDAAqD;QACrD,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,GAAG,CAAC;YAAG,QAAgC,CAAC,OAAO,CAAC,CAAC;IAC9E,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @kensai/quiz-core — headless engine for the Kensai Quiz format.
3
+ *
4
+ * Load and validate a quiz, evaluate answers, run an attempt (a UI-agnostic
5
+ * state machine), and compute per-category statistics. No UI or DOM dependency;
6
+ * any player consumes this package.
7
+ */
8
+ export * from "./types.js";
9
+ export { DEFAULT_SETTINGS, resolveSettings } from "./settings.js";
10
+ export type { ResolvedSettings } from "./settings.js";
11
+ export { quizSchema } from "./schema.generated.js";
12
+ export { loadQuiz, parseQuizSource, QuizValidationError } from "./load.js";
13
+ export type { LoadOptions, SourceFormat } from "./load.js";
14
+ export { validateQuiz, validateSchema, validateReferences } from "./validate.js";
15
+ export type { ValidationResult, ValidationIssue, IssueSeverity, IssueKind, } from "./validate.js";
16
+ export { evaluateQuiz, evaluateQuestion, gradeQuestion } from "./evaluate.js";
17
+ export type { QuizResult, QuestionResult, EvaluateOptions } from "./evaluate.js";
18
+ export { computeStats } from "./stats.js";
19
+ export type { QuizStats, CategoryStat, OverallStat } from "./stats.js";
20
+ export { resolveChoiceOptions, resolveGroups } from "./resolve.js";
21
+ export { normalizeText, textMatches } from "./text.js";
22
+ export { Attempt } from "./runtime.js";
23
+ export type { AttemptOptions, AttemptEvents, AttemptStatus, AttemptSnapshot, CurrentQuestion, } from "./runtime.js";
24
+ export { Emitter } from "./events.js";
25
+ export type { Listener } from "./events.js";
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC3E,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACjF,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,SAAS,GACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9E,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEvE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @kensai/quiz-core — headless engine for the Kensai Quiz format.
3
+ *
4
+ * Load and validate a quiz, evaluate answers, run an attempt (a UI-agnostic
5
+ * state machine), and compute per-category statistics. No UI or DOM dependency;
6
+ * any player consumes this package.
7
+ */
8
+ export * from "./types.js";
9
+ export { DEFAULT_SETTINGS, resolveSettings } from "./settings.js";
10
+ export { quizSchema } from "./schema.generated.js";
11
+ export { loadQuiz, parseQuizSource, QuizValidationError } from "./load.js";
12
+ export { validateQuiz, validateSchema, validateReferences } from "./validate.js";
13
+ export { evaluateQuiz, evaluateQuestion, gradeQuestion } from "./evaluate.js";
14
+ export { computeStats } from "./stats.js";
15
+ export { resolveChoiceOptions, resolveGroups } from "./resolve.js";
16
+ export { normalizeText, textMatches } from "./text.js";
17
+ export { Attempt } from "./runtime.js";
18
+ export { Emitter } from "./events.js";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGlE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAG3E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAQjF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9E,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AASvC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
package/dist/load.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Quiz } from "./types.js";
2
+ import { type ValidationIssue } from "./validate.js";
3
+ /** Thrown by `loadQuiz` when validation fails. Carries the error-severity issues. */
4
+ export declare class QuizValidationError extends Error {
5
+ readonly issues: ValidationIssue[];
6
+ constructor(issues: ValidationIssue[]);
7
+ }
8
+ export type SourceFormat = "yaml" | "json";
9
+ /** Parse a quiz source string into a plain object. Does not validate. */
10
+ export declare function parseQuizSource(source: string, format?: SourceFormat): unknown;
11
+ export interface LoadOptions {
12
+ /** Source format when `source` is a string. Default: `"yaml"`. */
13
+ format?: SourceFormat;
14
+ /** Validate against the schema + references before returning. Default: `true`. */
15
+ validate?: boolean;
16
+ }
17
+ /**
18
+ * Load a quiz from a YAML/JSON string or an already-parsed object. Validates by
19
+ * default and throws `QuizValidationError` on any error-severity issue.
20
+ */
21
+ export declare function loadQuiz(source: string | object, options?: LoadOptions): Quiz;
22
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE,qFAAqF;AACrF,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC;gBAEvB,MAAM,EAAE,eAAe,EAAE;CAMtC;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3C,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,YAAqB,GAAG,OAAO,CAEtF;AAED,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAQjF"}
package/dist/load.js ADDED
@@ -0,0 +1,31 @@
1
+ import yaml from "js-yaml";
2
+ import { validateQuiz } from "./validate.js";
3
+ /** Thrown by `loadQuiz` when validation fails. Carries the error-severity issues. */
4
+ export class QuizValidationError extends Error {
5
+ issues;
6
+ constructor(issues) {
7
+ const detail = issues.map((i) => ` [${i.path}] ${i.message}`).join("\n");
8
+ super(`Quiz validation failed with ${issues.length} error(s):\n${detail}`);
9
+ this.name = "QuizValidationError";
10
+ this.issues = issues;
11
+ }
12
+ }
13
+ /** Parse a quiz source string into a plain object. Does not validate. */
14
+ export function parseQuizSource(source, format = "yaml") {
15
+ return format === "json" ? JSON.parse(source) : yaml.load(source);
16
+ }
17
+ /**
18
+ * Load a quiz from a YAML/JSON string or an already-parsed object. Validates by
19
+ * default and throws `QuizValidationError` on any error-severity issue.
20
+ */
21
+ export function loadQuiz(source, options = {}) {
22
+ const { format = "yaml", validate = true } = options;
23
+ const data = typeof source === "string" ? parseQuizSource(source, format) : source;
24
+ if (validate) {
25
+ const result = validateQuiz(data);
26
+ if (!result.valid)
27
+ throw new QuizValidationError(result.errors);
28
+ }
29
+ return data;
30
+ }
31
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AAE3B,OAAO,EAAE,YAAY,EAAwB,MAAM,eAAe,CAAC;AAEnE,qFAAqF;AACrF,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,MAAM,CAAoB;IAEnC,YAAY,MAAyB;QACnC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1E,KAAK,CAAC,+BAA+B,MAAM,CAAC,MAAM,eAAe,MAAM,EAAE,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAID,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,SAAuB,MAAM;IAC3E,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACpE,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAuB,EAAE,UAAuB,EAAE;IACzE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACrD,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACnF,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,IAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ChoiceQuestion, ClassifyQuestion, Group, Option, Quiz } from "./types.js";
2
+ /** Resolve a choice question's options, whether inline or from an option_group. */
3
+ export declare function resolveChoiceOptions(quiz: Quiz, question: ChoiceQuestion): Option[];
4
+ /**
5
+ * Resolve a classify question's buckets. Inline `groups` are used as-is; a
6
+ * `groups_from` option group is adapted (its `text` becomes the bucket `label`).
7
+ */
8
+ export declare function resolveGroups(quiz: Quiz, question: ClassifyQuestion): Group[];
9
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAExF,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,EAAE,CAInF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,gBAAgB,GAAG,KAAK,EAAE,CAO7E"}
@@ -0,0 +1,22 @@
1
+ /** Resolve a choice question's options, whether inline or from an option_group. */
2
+ export function resolveChoiceOptions(quiz, question) {
3
+ if (question.options)
4
+ return question.options;
5
+ if (question.options_from)
6
+ return quiz.option_groups?.[question.options_from] ?? [];
7
+ return [];
8
+ }
9
+ /**
10
+ * Resolve a classify question's buckets. Inline `groups` are used as-is; a
11
+ * `groups_from` option group is adapted (its `text` becomes the bucket `label`).
12
+ */
13
+ export function resolveGroups(quiz, question) {
14
+ if (question.groups)
15
+ return question.groups;
16
+ if (question.groups_from) {
17
+ const group = quiz.option_groups?.[question.groups_from] ?? [];
18
+ return group.map((option) => ({ id: option.id, label: option.text }));
19
+ }
20
+ return [];
21
+ }
22
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,MAAM,UAAU,oBAAoB,CAAC,IAAU,EAAE,QAAwB;IACvE,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC9C,IAAI,QAAQ,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;IACpF,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAU,EAAE,QAA0B;IAClE,IAAI,QAAQ,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC,MAAM,CAAC;IAC5C,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC/D,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}