@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/evaluate.d.ts +47 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +149 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +26 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/load.d.ts +22 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +31 -0
- package/dist/load.js.map +1 -0
- package/dist/resolve.d.ts +9 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +22 -0
- package/dist/resolve.js.map +1 -0
- package/dist/runtime.d.ts +118 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +245 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.generated.d.ts +503 -0
- package/dist/schema.generated.d.ts.map +1 -0
- package/dist/schema.generated.js +626 -0
- package/dist/schema.generated.js.map +1 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +32 -0
- package/dist/settings.js.map +1 -0
- package/dist/stats.d.ts +33 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +79 -0
- package/dist/stats.js.map +1 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +11 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +28 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +170 -0
- package/dist/validate.js.map +1 -0
- 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"}
|
package/dist/evaluate.js
ADDED
|
@@ -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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"}
|
package/dist/events.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/load.js.map
ADDED
|
@@ -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"}
|
package/dist/resolve.js
ADDED
|
@@ -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"}
|