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 CHANGED
@@ -1,6 +1,7 @@
1
1
  // Core question + bank types. Framework-owned, never ejected.
2
- // A bank's question shape is config-defined: every question has the four
3
- // core fields, plus arbitrary subject-specific metadata (year, marks, …).
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
- // Subject-specific fields (e.g. year, semester, marks, source).
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
- /** Extract the raw value from a question. */
63
- getValue: (question: BaseQuestion) => string | number | null | readonly string[];
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: BaseQuestion[];
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
- export function defineBank(input: DefineBankInput): Bank {
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
- return (question[ruleField] as string | number | null | readonly string[]) ?? null;
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.1.1",
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",
@@ -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
- const str = (q: BaseQuestion, key: string): string => {
13
- const v = q[key];
14
- return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
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 num = (q: BaseQuestion, key: string): number | null => {
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) => (typeof q['year'] === 'number' ? String(q['year']) : ''),
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) => str(q, 'semester'),
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) => `${str(q, 'year')} ${str(q, 'semester')}`.trim(),
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) => num(q, 'marks'),
58
+ getValue: (q) => q.marks,
65
59
  placeholder: 'Value',
66
60
  column: { header: 'Marks', width: 78 },
67
61
  },