question-bank-framework 0.1.1 → 0.2.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/core/types.ts +23 -12
- package/framework/lib/question-filter.ts +3 -1
- package/package.json +1 -1
- package/template/bank.config.ts +14 -20
package/core/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Core question + bank types. Framework-owned, never ejected.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Every question has the core fields below; subjects extend the interface
|
|
3
|
+
// with their own metadata (year, marks, …) and the bank stays generic over
|
|
4
|
+
// it — no index signatures, no `unknown`, no casts in subject code.
|
|
4
5
|
|
|
5
6
|
export const questionStatuses = ['unanswered', 'revisit', 'complete'] as const;
|
|
6
7
|
|
|
@@ -12,8 +13,7 @@ export interface BaseQuestion {
|
|
|
12
13
|
title: string;
|
|
13
14
|
prompt: string;
|
|
14
15
|
tags: string[];
|
|
15
|
-
//
|
|
16
|
-
[key: string]: unknown;
|
|
16
|
+
// Subjects extend this interface with their own fields; see template/bank.config.ts.
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface QuizSession {
|
|
@@ -54,13 +54,19 @@ export interface BankFieldOption {
|
|
|
54
54
|
label: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export interface BankFieldDef {
|
|
57
|
+
export interface BankFieldDef<Q extends BaseQuestion = BaseQuestion> {
|
|
58
58
|
/** Stable field id. Reserved: id, title, tags, status. */
|
|
59
59
|
id: string;
|
|
60
60
|
label: string;
|
|
61
61
|
type: BankFieldType;
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Extract the raw value from a question. Q is inferred from the bank's
|
|
64
|
+
* questions. Method syntax (not a function property) keeps subject field
|
|
65
|
+
* defs assignable to the stored bank with zero casts — standard
|
|
66
|
+
* bivariant-method assignability, like Map/Set methods. Sound here
|
|
67
|
+
* because fields are only ever called with their own bank's questions.
|
|
68
|
+
*/
|
|
69
|
+
getValue(question: Q): string | number | null | readonly string[];
|
|
64
70
|
/**
|
|
65
71
|
* Fixed options for select/multiselect. When omitted, options are derived
|
|
66
72
|
* from distinct values across the bank's questions (sorted).
|
|
@@ -82,13 +88,13 @@ export interface BankSiteConfig {
|
|
|
82
88
|
description?: string;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
export interface DefineBankInput {
|
|
91
|
+
export interface DefineBankInput<Q extends BaseQuestion = BaseQuestion> {
|
|
86
92
|
site: BankSiteConfig;
|
|
87
93
|
/** Namespaces localStorage keys + Convex lookups. e.g. "comp20008". */
|
|
88
94
|
namespace: string;
|
|
89
95
|
/** Subject-specific schema. Built-ins (id/title/tags/status) are automatic. */
|
|
90
|
-
fields: BankFieldDef[];
|
|
91
|
-
questions:
|
|
96
|
+
fields: BankFieldDef<Q>[];
|
|
97
|
+
questions: Q[];
|
|
92
98
|
/** Suggested answers keyed by question id. Missing entries hide "Show answer". */
|
|
93
99
|
answers?: Record<string, string>;
|
|
94
100
|
/** Default sort for the grid, e.g. [{ id: "exam", desc: true }]. */
|
|
@@ -97,12 +103,17 @@ export interface DefineBankInput {
|
|
|
97
103
|
publicPath?: string;
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
export interface Bank extends DefineBankInput {
|
|
106
|
+
export interface Bank extends DefineBankInput<BaseQuestion> {
|
|
101
107
|
answers: Record<string, string>;
|
|
102
108
|
questionById: Map<string, BaseQuestion>;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Build a bank. Q is inferred from `questions`, so each field's getValue is
|
|
113
|
+
* checked against the subject's question type — accessors use plain property
|
|
114
|
+
* access, no casts or coercion helpers.
|
|
115
|
+
*/
|
|
116
|
+
export function defineBank<Q extends BaseQuestion>(input: DefineBankInput<Q>): Bank {
|
|
106
117
|
return {
|
|
107
118
|
...input,
|
|
108
119
|
answers: input.answers ?? {},
|
|
@@ -220,7 +220,9 @@ function resolveFieldValue(
|
|
|
220
220
|
if (ruleField === 'title') return question.title;
|
|
221
221
|
if (ruleField === 'id') return question.id;
|
|
222
222
|
if (ruleField === 'tags') return question.tags;
|
|
223
|
-
|
|
223
|
+
// status is handled by matchesRule before reaching here. Any other id
|
|
224
|
+
// is stale (no def, not a built-in) — match nothing.
|
|
225
|
+
return null;
|
|
224
226
|
}
|
|
225
227
|
return def.getValue(question);
|
|
226
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "question-bank-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Framework-style question bank: TanStack Start + TanStack Table grid, quizzes, and filter DSL. Bring your own bank.config.ts + content/. Eject any framework/ file with `qb reveal` (Vike-style).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/template/bank.config.ts
CHANGED
|
@@ -3,26 +3,20 @@
|
|
|
3
3
|
// config-defined — the framework never hardcodes subject fields.
|
|
4
4
|
|
|
5
5
|
import { defineBank } from 'question-bank-framework';
|
|
6
|
+
import type { BaseQuestion } from 'question-bank-framework';
|
|
6
7
|
import questionsJson from './content/questions.json' with { type: 'json' };
|
|
7
8
|
import { answers } from './content/answers.ts';
|
|
8
|
-
import type { BaseQuestion } from 'question-bank-framework';
|
|
9
|
-
|
|
10
|
-
const questions = questionsJson as BaseQuestion[];
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
/** Subject question shape. Declare every field your accessors touch. */
|
|
11
|
+
export interface Comp20008Question extends BaseQuestion {
|
|
12
|
+
year: number;
|
|
13
|
+
semester: string;
|
|
14
|
+
number: number;
|
|
15
|
+
marks: number | null;
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
|
16
18
|
|
|
17
|
-
const
|
|
18
|
-
const v = q[key];
|
|
19
|
-
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
20
|
-
if (typeof v === 'string' && v.trim() !== '') {
|
|
21
|
-
const n = Number(v);
|
|
22
|
-
return Number.isFinite(n) ? n : null;
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
25
|
-
};
|
|
19
|
+
const questions = questionsJson as Comp20008Question[];
|
|
26
20
|
|
|
27
21
|
export const bank = defineBank({
|
|
28
22
|
site: {
|
|
@@ -36,14 +30,14 @@ export const bank = defineBank({
|
|
|
36
30
|
id: 'year',
|
|
37
31
|
label: 'Year',
|
|
38
32
|
type: 'select',
|
|
39
|
-
getValue: (q) =>
|
|
33
|
+
getValue: (q) => String(q.year),
|
|
40
34
|
searchable: true,
|
|
41
35
|
},
|
|
42
36
|
{
|
|
43
37
|
id: 'semester',
|
|
44
38
|
label: 'Semester',
|
|
45
39
|
type: 'select',
|
|
46
|
-
getValue: (q) =>
|
|
40
|
+
getValue: (q) => q.semester,
|
|
47
41
|
options: [
|
|
48
42
|
{ value: 'S1', label: 'Semester 1' },
|
|
49
43
|
{ value: 'S2', label: 'Semester 2' },
|
|
@@ -53,7 +47,7 @@ export const bank = defineBank({
|
|
|
53
47
|
id: 'exam',
|
|
54
48
|
label: 'Exam',
|
|
55
49
|
type: 'text',
|
|
56
|
-
getValue: (q) => `${
|
|
50
|
+
getValue: (q) => `${q.year} ${q.semester}`,
|
|
57
51
|
searchable: true,
|
|
58
52
|
column: { header: 'Exam', width: 108, sortable: false },
|
|
59
53
|
},
|
|
@@ -61,7 +55,7 @@ export const bank = defineBank({
|
|
|
61
55
|
id: 'marks',
|
|
62
56
|
label: 'Marks',
|
|
63
57
|
type: 'number',
|
|
64
|
-
getValue: (q) =>
|
|
58
|
+
getValue: (q) => q.marks,
|
|
65
59
|
placeholder: 'Value',
|
|
66
60
|
column: { header: 'Marks', width: 78 },
|
|
67
61
|
},
|