@bis-code/study-dash 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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +19 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/commands/dashboard.md +8 -0
- package/commands/import.md +12 -0
- package/commands/learn.md +13 -0
- package/hooks/hooks.json +27 -0
- package/package.json +36 -0
- package/rules/tutor-mode.md +38 -0
- package/server/dist/bundle.mjs +1240 -0
- package/server/dist/dashboard/api.d.ts +16 -0
- package/server/dist/dashboard/api.js +150 -0
- package/server/dist/dashboard/api.js.map +1 -0
- package/server/dist/dashboard/server.d.ts +21 -0
- package/server/dist/dashboard/server.js +171 -0
- package/server/dist/dashboard/server.js.map +1 -0
- package/server/dist/index.d.ts +2 -0
- package/server/dist/index.js +40 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/services/curriculum.d.ts +25 -0
- package/server/dist/services/curriculum.js +110 -0
- package/server/dist/services/curriculum.js.map +1 -0
- package/server/dist/services/exercises.d.ts +35 -0
- package/server/dist/services/exercises.js +215 -0
- package/server/dist/services/exercises.js.map +1 -0
- package/server/dist/services/qa.d.ts +15 -0
- package/server/dist/services/qa.js +30 -0
- package/server/dist/services/qa.js.map +1 -0
- package/server/dist/services/viz.d.ts +8 -0
- package/server/dist/services/viz.js +21 -0
- package/server/dist/services/viz.js.map +1 -0
- package/server/dist/storage/db.d.ts +11 -0
- package/server/dist/storage/db.js +51 -0
- package/server/dist/storage/db.js.map +1 -0
- package/server/dist/storage/files.d.ts +10 -0
- package/server/dist/storage/files.js +34 -0
- package/server/dist/storage/files.js.map +1 -0
- package/server/dist/storage/schema.d.ts +3 -0
- package/server/dist/storage/schema.js +126 -0
- package/server/dist/storage/schema.js.map +1 -0
- package/server/dist/tools/curriculum.d.ts +4 -0
- package/server/dist/tools/curriculum.js +137 -0
- package/server/dist/tools/curriculum.js.map +1 -0
- package/server/dist/tools/exercises.d.ts +4 -0
- package/server/dist/tools/exercises.js +76 -0
- package/server/dist/tools/exercises.js.map +1 -0
- package/server/dist/tools/qa.d.ts +4 -0
- package/server/dist/tools/qa.js +56 -0
- package/server/dist/tools/qa.js.map +1 -0
- package/server/dist/tools/viz.d.ts +4 -0
- package/server/dist/tools/viz.js +54 -0
- package/server/dist/tools/viz.js.map +1 -0
- package/server/dist/types.d.ts +103 -0
- package/server/dist/types.js +2 -0
- package/server/dist/types.js.map +1 -0
- package/server/node_modules/better-sqlite3/LICENSE +21 -0
- package/server/node_modules/better-sqlite3/README.md +99 -0
- package/server/node_modules/better-sqlite3/binding.gyp +38 -0
- package/server/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
- package/server/node_modules/better-sqlite3/deps/common.gypi +68 -0
- package/server/node_modules/better-sqlite3/deps/copy.js +31 -0
- package/server/node_modules/better-sqlite3/deps/defines.gypi +41 -0
- package/server/node_modules/better-sqlite3/deps/download.sh +122 -0
- package/server/node_modules/better-sqlite3/deps/patches/1208.patch +15 -0
- package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +261480 -0
- package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +13715 -0
- package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3ext.h +719 -0
- package/server/node_modules/better-sqlite3/deps/sqlite3.gyp +80 -0
- package/server/node_modules/better-sqlite3/deps/test_extension.c +21 -0
- package/server/node_modules/better-sqlite3/lib/database.js +90 -0
- package/server/node_modules/better-sqlite3/lib/index.js +3 -0
- package/server/node_modules/better-sqlite3/lib/methods/aggregate.js +43 -0
- package/server/node_modules/better-sqlite3/lib/methods/backup.js +67 -0
- package/server/node_modules/better-sqlite3/lib/methods/function.js +31 -0
- package/server/node_modules/better-sqlite3/lib/methods/inspect.js +7 -0
- package/server/node_modules/better-sqlite3/lib/methods/pragma.js +12 -0
- package/server/node_modules/better-sqlite3/lib/methods/serialize.js +16 -0
- package/server/node_modules/better-sqlite3/lib/methods/table.js +189 -0
- package/server/node_modules/better-sqlite3/lib/methods/transaction.js +78 -0
- package/server/node_modules/better-sqlite3/lib/methods/wrappers.js +54 -0
- package/server/node_modules/better-sqlite3/lib/sqlite-error.js +20 -0
- package/server/node_modules/better-sqlite3/lib/util.js +12 -0
- package/server/node_modules/better-sqlite3/package.json +54 -0
- package/server/node_modules/better-sqlite3/src/better_sqlite3.cpp +2186 -0
- package/server/node_modules/better-sqlite3/src/better_sqlite3.hpp +1036 -0
- package/server/package.json +31 -0
- package/skills/import/SKILL.md +19 -0
- package/skills/learn/SKILL.md +17 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
function slugify(name) {
|
|
5
|
+
return name
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
8
|
+
.replace(/^-+|-+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
function extensionForLanguage(language) {
|
|
11
|
+
switch (language) {
|
|
12
|
+
case 'go':
|
|
13
|
+
return '.go';
|
|
14
|
+
case 'python':
|
|
15
|
+
return '.py';
|
|
16
|
+
case 'rust':
|
|
17
|
+
return '.rs';
|
|
18
|
+
case 'javascript':
|
|
19
|
+
case 'typescript':
|
|
20
|
+
return '.ts';
|
|
21
|
+
default:
|
|
22
|
+
return '.txt';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ExerciseService {
|
|
26
|
+
db;
|
|
27
|
+
fileStore;
|
|
28
|
+
constructor(db, fileStore) {
|
|
29
|
+
this.db = db;
|
|
30
|
+
this.fileStore = fileStore;
|
|
31
|
+
}
|
|
32
|
+
createExercise(topicId, data) {
|
|
33
|
+
const { title, type, description, difficulty = 'medium', est_minutes = 0, source = 'ai', starter_code = '', test_content = '', quiz_json = '{}', } = data;
|
|
34
|
+
const result = this.db.raw
|
|
35
|
+
.prepare(`INSERT INTO exercises
|
|
36
|
+
(topic_id, title, type, description, difficulty, est_minutes, source, starter_code, test_content, quiz_json)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
38
|
+
RETURNING id`)
|
|
39
|
+
.get(topicId, title, type, description, difficulty, est_minutes, source, starter_code, test_content, quiz_json);
|
|
40
|
+
const exerciseId = result.id;
|
|
41
|
+
// Write files for coding/project exercises with starter code
|
|
42
|
+
if ((type === 'coding' || type === 'project') && (starter_code || test_content)) {
|
|
43
|
+
const subject = this.getSubjectForTopic(topicId);
|
|
44
|
+
if (subject) {
|
|
45
|
+
const exerciseSlug = slugify(title);
|
|
46
|
+
const ext = extensionForLanguage(subject.language);
|
|
47
|
+
const files = {};
|
|
48
|
+
if (starter_code) {
|
|
49
|
+
files[`main${ext}`] = starter_code;
|
|
50
|
+
}
|
|
51
|
+
if (test_content) {
|
|
52
|
+
files[`main_test${ext}`] = test_content;
|
|
53
|
+
}
|
|
54
|
+
files['README.md'] = `# ${title}\n\n${description}`;
|
|
55
|
+
const filePath = this.fileStore.writeExerciseFiles(subject.slug, exerciseSlug, files);
|
|
56
|
+
this.db.raw
|
|
57
|
+
.prepare('UPDATE exercises SET file_path = ? WHERE id = ?')
|
|
58
|
+
.run(filePath, exerciseId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return this.db.raw
|
|
62
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
63
|
+
.get(exerciseId);
|
|
64
|
+
}
|
|
65
|
+
async runTests(exerciseId) {
|
|
66
|
+
const exercise = this.db.raw
|
|
67
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
68
|
+
.get(exerciseId);
|
|
69
|
+
if (!exercise)
|
|
70
|
+
throw new Error(`Exercise ${exerciseId} not found`);
|
|
71
|
+
if (!exercise.file_path)
|
|
72
|
+
throw new Error(`Exercise ${exerciseId} has no file_path`);
|
|
73
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
74
|
+
if (!subject)
|
|
75
|
+
throw new Error(`No subject found for exercise ${exerciseId}`);
|
|
76
|
+
const commandMap = {
|
|
77
|
+
go: { command: 'go', args: ['test', '-json', '-count=1', './...'] },
|
|
78
|
+
python: { command: 'python3', args: ['-m', 'pytest', '--tb=short', '-q', '.'] },
|
|
79
|
+
rust: { command: 'cargo', args: ['test'] },
|
|
80
|
+
javascript: { command: 'npx', args: ['vitest', 'run'] },
|
|
81
|
+
typescript: { command: 'npx', args: ['vitest', 'run'] },
|
|
82
|
+
};
|
|
83
|
+
const config = commandMap[subject.language];
|
|
84
|
+
if (!config)
|
|
85
|
+
throw new Error(`Unsupported language: ${subject.language}`);
|
|
86
|
+
let stdout = '';
|
|
87
|
+
let stderr = '';
|
|
88
|
+
let exitCode = 0;
|
|
89
|
+
try {
|
|
90
|
+
const result = await execFileAsync(config.command, config.args, {
|
|
91
|
+
cwd: exercise.file_path,
|
|
92
|
+
timeout: 60_000,
|
|
93
|
+
});
|
|
94
|
+
stdout = result.stdout;
|
|
95
|
+
stderr = result.stderr;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const execErr = err;
|
|
99
|
+
stdout = execErr.stdout ?? '';
|
|
100
|
+
stderr = execErr.stderr ?? '';
|
|
101
|
+
exitCode = execErr.code ?? 1;
|
|
102
|
+
}
|
|
103
|
+
// Parse results
|
|
104
|
+
const results = [];
|
|
105
|
+
if (subject.language === 'go') {
|
|
106
|
+
// Parse Go JSON test output
|
|
107
|
+
for (const line of stdout.split('\n')) {
|
|
108
|
+
if (!line.trim())
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
const event = JSON.parse(line);
|
|
112
|
+
if (event.Action === 'pass' && event.Test) {
|
|
113
|
+
results.push({ test_name: event.Test, passed: true, output: '' });
|
|
114
|
+
}
|
|
115
|
+
else if (event.Action === 'fail' && event.Test) {
|
|
116
|
+
results.push({ test_name: event.Test, passed: false, output: event.Output ?? '' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Skip non-JSON lines
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Fallback: if no per-test results parsed, use overall result
|
|
125
|
+
if (results.length === 0) {
|
|
126
|
+
results.push({
|
|
127
|
+
test_name: 'all',
|
|
128
|
+
passed: exitCode === 0,
|
|
129
|
+
output: stdout + stderr,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Clear old results
|
|
133
|
+
this.db.raw
|
|
134
|
+
.prepare('DELETE FROM exercise_results WHERE exercise_id = ?')
|
|
135
|
+
.run(exerciseId);
|
|
136
|
+
// Insert new results
|
|
137
|
+
const insertResult = this.db.raw.prepare('INSERT INTO exercise_results (exercise_id, test_name, passed, output) VALUES (?, ?, ?, ?)');
|
|
138
|
+
for (const r of results) {
|
|
139
|
+
insertResult.run(exerciseId, r.test_name, r.passed ? 1 : 0, r.output);
|
|
140
|
+
}
|
|
141
|
+
// Update exercise status
|
|
142
|
+
const allPassed = results.every((r) => r.passed);
|
|
143
|
+
this.db.raw
|
|
144
|
+
.prepare('UPDATE exercises SET status = ? WHERE id = ?')
|
|
145
|
+
.run(allPassed ? 'passed' : 'failed', exerciseId);
|
|
146
|
+
return this.db.raw
|
|
147
|
+
.prepare('SELECT * FROM exercise_results WHERE exercise_id = ?')
|
|
148
|
+
.all(exerciseId);
|
|
149
|
+
}
|
|
150
|
+
submitQuiz(exerciseId, answers) {
|
|
151
|
+
const exercise = this.db.raw
|
|
152
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
153
|
+
.get(exerciseId);
|
|
154
|
+
if (!exercise)
|
|
155
|
+
throw new Error(`Exercise ${exerciseId} not found`);
|
|
156
|
+
const payload = JSON.parse(exercise.quiz_json);
|
|
157
|
+
const questions = payload.questions;
|
|
158
|
+
let correct = 0;
|
|
159
|
+
const results = [];
|
|
160
|
+
for (let i = 0; i < questions.length; i++) {
|
|
161
|
+
const q = questions[i];
|
|
162
|
+
const answer = answers[i];
|
|
163
|
+
let isCorrect = false;
|
|
164
|
+
switch (q.type) {
|
|
165
|
+
case 'multiple_choice':
|
|
166
|
+
isCorrect = answer === q.correct;
|
|
167
|
+
break;
|
|
168
|
+
case 'true_false':
|
|
169
|
+
isCorrect = answer === q.correct;
|
|
170
|
+
break;
|
|
171
|
+
case 'fill_in':
|
|
172
|
+
isCorrect =
|
|
173
|
+
String(answer).toLowerCase().trim() === String(q.correct).toLowerCase().trim();
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (isCorrect)
|
|
177
|
+
correct++;
|
|
178
|
+
results.push({
|
|
179
|
+
test_name: `Q${i + 1}: ${q.text}`,
|
|
180
|
+
passed: isCorrect,
|
|
181
|
+
output: isCorrect ? 'Correct' : `Wrong. Expected: ${q.correct}, Got: ${answer}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const score = questions.length > 0 ? correct / questions.length : 0;
|
|
185
|
+
const passed = score >= 0.7;
|
|
186
|
+
// Clear old results
|
|
187
|
+
this.db.raw
|
|
188
|
+
.prepare('DELETE FROM exercise_results WHERE exercise_id = ?')
|
|
189
|
+
.run(exerciseId);
|
|
190
|
+
// Insert per-question results
|
|
191
|
+
const insertResult = this.db.raw.prepare('INSERT INTO exercise_results (exercise_id, test_name, passed, output) VALUES (?, ?, ?, ?)');
|
|
192
|
+
for (const r of results) {
|
|
193
|
+
insertResult.run(exerciseId, r.test_name, r.passed ? 1 : 0, r.output);
|
|
194
|
+
}
|
|
195
|
+
// Update exercise status
|
|
196
|
+
this.db.raw
|
|
197
|
+
.prepare('UPDATE exercises SET status = ? WHERE id = ?')
|
|
198
|
+
.run(passed ? 'passed' : 'failed', exerciseId);
|
|
199
|
+
return { score, total: questions.length, passed, results };
|
|
200
|
+
}
|
|
201
|
+
listForTopic(topicId) {
|
|
202
|
+
return this.db.raw
|
|
203
|
+
.prepare('SELECT * FROM exercises WHERE topic_id = ? ORDER BY created_at ASC, id ASC')
|
|
204
|
+
.all(topicId);
|
|
205
|
+
}
|
|
206
|
+
getSubjectForTopic(topicId) {
|
|
207
|
+
return this.db.raw
|
|
208
|
+
.prepare(`SELECT s.* FROM subjects s
|
|
209
|
+
JOIN phases p ON p.subject_id = s.id
|
|
210
|
+
JOIN topics t ON t.phase_id = p.id
|
|
211
|
+
WHERE t.id = ?`)
|
|
212
|
+
.get(topicId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=exercises.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exercises.js","sourceRoot":"","sources":["../../src/services/exercises.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Database } from '../storage/db.js';
|
|
2
|
+
import type { Entry } from '../types.js';
|
|
3
|
+
export declare class QAService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database);
|
|
6
|
+
logEntry(topicId: number, kind: Entry['kind'], content: string, sessionId?: string, questionId?: number): Entry;
|
|
7
|
+
listEntries(topicId: number): Entry[];
|
|
8
|
+
search(query: string): {
|
|
9
|
+
id: number;
|
|
10
|
+
topic_id: number;
|
|
11
|
+
kind: string;
|
|
12
|
+
content: string;
|
|
13
|
+
created_at: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class QAService {
|
|
2
|
+
db;
|
|
3
|
+
constructor(db) {
|
|
4
|
+
this.db = db;
|
|
5
|
+
}
|
|
6
|
+
logEntry(topicId, kind, content, sessionId, questionId) {
|
|
7
|
+
const result = this.db.raw
|
|
8
|
+
.prepare('INSERT INTO entries (topic_id, kind, content, session_id, question_id) VALUES (?, ?, ?, ?, ?) RETURNING id')
|
|
9
|
+
.get(topicId, kind, content, sessionId ?? '', questionId ?? null);
|
|
10
|
+
return this.db.raw
|
|
11
|
+
.prepare('SELECT * FROM entries WHERE id = ?')
|
|
12
|
+
.get(result.id);
|
|
13
|
+
}
|
|
14
|
+
listEntries(topicId) {
|
|
15
|
+
return this.db.raw
|
|
16
|
+
.prepare('SELECT * FROM entries WHERE topic_id = ? ORDER BY created_at ASC')
|
|
17
|
+
.all(topicId);
|
|
18
|
+
}
|
|
19
|
+
search(query) {
|
|
20
|
+
return this.db.raw
|
|
21
|
+
.prepare(`SELECT e.id, e.topic_id, e.kind, e.content, e.created_at
|
|
22
|
+
FROM entries e
|
|
23
|
+
JOIN entries_fts ON entries_fts.rowid = e.id
|
|
24
|
+
WHERE entries_fts MATCH ?
|
|
25
|
+
ORDER BY e.created_at ASC
|
|
26
|
+
LIMIT 50`)
|
|
27
|
+
.all(query);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=qa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qa.js","sourceRoot":"","sources":["../../src/services/qa.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,SAAS;IACA;IAApB,YAAoB,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAEpC,QAAQ,CACN,OAAe,EACf,IAAmB,EACnB,OAAe,EACf,SAAkB,EAClB,UAAmB;QAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG;aACvB,OAAO,CAIN,4GAA4G,CAC7G;aACA,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE,EAAE,UAAU,IAAI,IAAI,CAAC,CAAC;QAEpE,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAAkB,oCAAoC,CAAC;aAC9D,GAAG,CAAC,MAAO,CAAC,EAAE,CAAE,CAAC;IACtB,CAAC;IAED,WAAW,CAAC,OAAe;QACzB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CACN,kEAAkE,CACnE;aACA,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAIN;;;;;kBAKU,CACX;aACA,GAAG,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Database } from '../storage/db.js';
|
|
2
|
+
import type { Visualization, VizStep } from '../types.js';
|
|
3
|
+
export declare class VizService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database);
|
|
6
|
+
create(topicId: number, title: string, steps: VizStep[]): Visualization;
|
|
7
|
+
listForTopic(topicId: number): Visualization[];
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class VizService {
|
|
2
|
+
db;
|
|
3
|
+
constructor(db) {
|
|
4
|
+
this.db = db;
|
|
5
|
+
}
|
|
6
|
+
create(topicId, title, steps) {
|
|
7
|
+
const stepsJson = JSON.stringify(steps);
|
|
8
|
+
const result = this.db.raw
|
|
9
|
+
.prepare('INSERT INTO visualizations (topic_id, title, steps_json) VALUES (?, ?, ?) RETURNING id')
|
|
10
|
+
.get(topicId, title, stepsJson);
|
|
11
|
+
return this.db.raw
|
|
12
|
+
.prepare('SELECT * FROM visualizations WHERE id = ?')
|
|
13
|
+
.get(result.id);
|
|
14
|
+
}
|
|
15
|
+
listForTopic(topicId) {
|
|
16
|
+
return this.db.raw
|
|
17
|
+
.prepare('SELECT * FROM visualizations WHERE topic_id = ? ORDER BY created_at DESC, id DESC')
|
|
18
|
+
.all(topicId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=viz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viz.js","sourceRoot":"","sources":["../../src/services/viz.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,UAAU;IACD;IAApB,YAAoB,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAEpC,MAAM,CAAC,OAAe,EAAE,KAAa,EAAE,KAAgB;QACrD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG;aACvB,OAAO,CACN,wFAAwF,CACzF;aACA,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;QAElC,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAA0B,2CAA2C,CAAC;aAC7E,GAAG,CAAC,MAAO,CAAC,EAAE,CAAE,CAAC;IACtB,CAAC;IAED,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CACN,mFAAmF,CACpF;aACA,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
2
|
+
export declare class Database {
|
|
3
|
+
private db;
|
|
4
|
+
constructor(dbPath: string);
|
|
5
|
+
getSetting(key: string): string | undefined;
|
|
6
|
+
setSetting(key: string, value: string): void;
|
|
7
|
+
listTables(): string[];
|
|
8
|
+
/** Expose the raw better-sqlite3 handle for advanced operations. */
|
|
9
|
+
get raw(): BetterSqlite3.Database;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
2
|
+
import { schema, migrations } from './schema.js';
|
|
3
|
+
export class Database {
|
|
4
|
+
db;
|
|
5
|
+
constructor(dbPath) {
|
|
6
|
+
this.db = new BetterSqlite3(dbPath);
|
|
7
|
+
// Performance + integrity PRAGMAs
|
|
8
|
+
this.db.pragma('journal_mode=WAL');
|
|
9
|
+
this.db.pragma('foreign_keys=ON');
|
|
10
|
+
// Apply schema (all CREATE IF NOT EXISTS — safe to re-run)
|
|
11
|
+
this.db.exec(schema);
|
|
12
|
+
// Seed defaults on first initialisation
|
|
13
|
+
const currentVersion = this.getSetting('schema_version');
|
|
14
|
+
if (!currentVersion) {
|
|
15
|
+
this.setSetting('schema_version', '1');
|
|
16
|
+
this.setSetting('auto_viz', 'true');
|
|
17
|
+
this.setSetting('dashboard_port', '19282');
|
|
18
|
+
}
|
|
19
|
+
// Run any pending migrations beyond the baseline
|
|
20
|
+
const versionNum = parseInt(this.getSetting('schema_version') ?? '1', 10);
|
|
21
|
+
for (let i = versionNum - 1; i < migrations.length; i++) {
|
|
22
|
+
this.db.exec(migrations[i]);
|
|
23
|
+
this.setSetting('schema_version', String(i + 2));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getSetting(key) {
|
|
27
|
+
const row = this.db
|
|
28
|
+
.prepare('SELECT value FROM settings WHERE key = ?')
|
|
29
|
+
.get(key);
|
|
30
|
+
return row?.value;
|
|
31
|
+
}
|
|
32
|
+
setSetting(key, value) {
|
|
33
|
+
this.db
|
|
34
|
+
.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
|
|
35
|
+
.run(key, value);
|
|
36
|
+
}
|
|
37
|
+
listTables() {
|
|
38
|
+
const allRows = this.db
|
|
39
|
+
.prepare("SELECT name FROM sqlite_master WHERE type IN ('table')")
|
|
40
|
+
.all();
|
|
41
|
+
return allRows.map((r) => r.name);
|
|
42
|
+
}
|
|
43
|
+
/** Expose the raw better-sqlite3 handle for advanced operations. */
|
|
44
|
+
get raw() {
|
|
45
|
+
return this.db;
|
|
46
|
+
}
|
|
47
|
+
close() {
|
|
48
|
+
this.db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,OAAO,QAAQ;IACX,EAAE,CAAyB;IAEnC,YAAY,MAAc;QACxB,IAAI,CAAC,EAAE,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;QAEpC,kCAAkC;QAClC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAElC,2DAA2D;QAC3D,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAErB,wCAAwC;QACxC,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;YACvC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;YACpC,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;QAED,iDAAiD;QACjD,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1E,KAAK,IAAI,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAA8B,0CAA0C,CAAC;aAChF,GAAG,CAAC,GAAG,CAAC,CAAC;QACZ,OAAO,GAAG,EAAE,KAAK,CAAC;IACpB,CAAC;IAED,UAAU,CAAC,GAAW,EAAE,KAAa;QACnC,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,uGAAuG,CAAC;aAChH,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,UAAU;QACR,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE;aACpB,OAAO,CAAuB,wDAAwD,CAAC;aACvF,GAAG,EAAE,CAAC;QACT,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,oEAAoE;IACpE,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class FileStore {
|
|
2
|
+
private baseDir;
|
|
3
|
+
constructor(baseDir?: string);
|
|
4
|
+
get exercisesDir(): string;
|
|
5
|
+
get dataDir(): string;
|
|
6
|
+
get dbPath(): string;
|
|
7
|
+
writeExerciseFiles(subjectSlug: string, exerciseSlug: string, files: Record<string, string>): string;
|
|
8
|
+
exerciseExists(subjectSlug: string, exerciseSlug: string): boolean;
|
|
9
|
+
readFile(path: string): string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
export class FileStore {
|
|
5
|
+
baseDir;
|
|
6
|
+
constructor(baseDir) {
|
|
7
|
+
this.baseDir = baseDir ?? join(homedir(), '.claude', 'learn');
|
|
8
|
+
mkdirSync(join(this.baseDir, 'exercises'), { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
get exercisesDir() {
|
|
11
|
+
return join(this.baseDir, 'exercises');
|
|
12
|
+
}
|
|
13
|
+
get dataDir() {
|
|
14
|
+
return this.baseDir;
|
|
15
|
+
}
|
|
16
|
+
get dbPath() {
|
|
17
|
+
return join(this.baseDir, 'data.db');
|
|
18
|
+
}
|
|
19
|
+
writeExerciseFiles(subjectSlug, exerciseSlug, files) {
|
|
20
|
+
const dir = join(this.exercisesDir, subjectSlug, exerciseSlug);
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
for (const [name, content] of Object.entries(files)) {
|
|
23
|
+
writeFileSync(join(dir, name), content, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
exerciseExists(subjectSlug, exerciseSlug) {
|
|
28
|
+
return existsSync(join(this.exercisesDir, subjectSlug, exerciseSlug));
|
|
29
|
+
}
|
|
30
|
+
readFile(path) {
|
|
31
|
+
return readFileSync(path, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/storage/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,OAAO,SAAS;IACZ,OAAO,CAAS;IAExB,YAAY,OAAgB;QAC1B,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9D,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACzC,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED,kBAAkB,CAAC,WAAmB,EAAE,YAAoB,EAAE,KAA6B;QACzF,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;QAC/D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpD,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,cAAc,CAAC,WAAmB,EAAE,YAAoB;QACtD,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,QAAQ,CAAC,IAAY;QACnB,OAAO,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;CACF"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const schema = "\nPRAGMA foreign_keys=ON;\n\nCREATE TABLE IF NOT EXISTS subjects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n language TEXT NOT NULL DEFAULT '',\n source TEXT NOT NULL DEFAULT 'manual'\n CHECK (source IN ('manual','roadmap','pdf')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS phases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);\n\nCREATE TABLE IF NOT EXISTS topics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo','in_progress','done')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);\nCREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);\n\nCREATE TABLE IF NOT EXISTS entries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),\n content TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);\nCREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);\n\nCREATE TABLE IF NOT EXISTS visualizations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n steps_json TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);\n\nCREATE TABLE IF NOT EXISTS exercises (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'coding'\n CHECK (type IN ('coding','quiz','project','assignment')),\n description TEXT NOT NULL DEFAULT '',\n difficulty TEXT NOT NULL DEFAULT 'medium'\n CHECK (difficulty IN ('easy','medium','hard')),\n est_minutes INTEGER NOT NULL DEFAULT 0,\n source TEXT NOT NULL DEFAULT 'ai'\n CHECK (source IN ('ai','pdf_import')),\n starter_code TEXT NOT NULL DEFAULT '',\n test_content TEXT NOT NULL DEFAULT '',\n quiz_json TEXT NOT NULL DEFAULT '{}',\n file_path TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','in_progress','passed','failed')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);\nCREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);\n\nCREATE TABLE IF NOT EXISTS exercise_results (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,\n test_name TEXT NOT NULL DEFAULT '',\n passed INTEGER NOT NULL DEFAULT 0,\n output TEXT NOT NULL DEFAULT '',\n ran_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL DEFAULT ''\n);\n\n-- FTS5 virtual table for full-text search over entries\nCREATE VIRTUAL TABLE IF NOT EXISTS entries_fts\n USING fts5(content, content='entries', content_rowid='id');\n\n-- Sync triggers: keep entries_fts up to date with entries\nCREATE TRIGGER IF NOT EXISTS entries_ai\n AFTER INSERT ON entries BEGIN\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_ad\n AFTER DELETE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_au\n AFTER UPDATE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n";
|
|
2
|
+
/** Future migrations appended here in order (v1 is baseline — empty). */
|
|
3
|
+
export declare const migrations: string[];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const schema = `
|
|
2
|
+
PRAGMA foreign_keys=ON;
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS subjects (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
slug TEXT NOT NULL UNIQUE,
|
|
8
|
+
language TEXT NOT NULL DEFAULT '',
|
|
9
|
+
source TEXT NOT NULL DEFAULT 'manual'
|
|
10
|
+
CHECK (source IN ('manual','roadmap','pdf')),
|
|
11
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS phases (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
|
|
17
|
+
name TEXT NOT NULL,
|
|
18
|
+
description TEXT NOT NULL DEFAULT '',
|
|
19
|
+
sort_order INTEGER NOT NULL DEFAULT 0
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS topics (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
|
|
27
|
+
name TEXT NOT NULL,
|
|
28
|
+
description TEXT NOT NULL DEFAULT '',
|
|
29
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
status TEXT NOT NULL DEFAULT 'todo'
|
|
31
|
+
CHECK (status IN ('todo','in_progress','done')),
|
|
32
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
|
41
|
+
kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),
|
|
42
|
+
content TEXT NOT NULL DEFAULT '',
|
|
43
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
44
|
+
question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,
|
|
45
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS visualizations (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
|
54
|
+
title TEXT NOT NULL DEFAULT '',
|
|
55
|
+
steps_json TEXT NOT NULL DEFAULT '[]',
|
|
56
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS exercises (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
|
64
|
+
title TEXT NOT NULL DEFAULT '',
|
|
65
|
+
type TEXT NOT NULL DEFAULT 'coding'
|
|
66
|
+
CHECK (type IN ('coding','quiz','project','assignment')),
|
|
67
|
+
description TEXT NOT NULL DEFAULT '',
|
|
68
|
+
difficulty TEXT NOT NULL DEFAULT 'medium'
|
|
69
|
+
CHECK (difficulty IN ('easy','medium','hard')),
|
|
70
|
+
est_minutes INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
source TEXT NOT NULL DEFAULT 'ai'
|
|
72
|
+
CHECK (source IN ('ai','pdf_import')),
|
|
73
|
+
starter_code TEXT NOT NULL DEFAULT '',
|
|
74
|
+
test_content TEXT NOT NULL DEFAULT '',
|
|
75
|
+
quiz_json TEXT NOT NULL DEFAULT '{}',
|
|
76
|
+
file_path TEXT NOT NULL DEFAULT '',
|
|
77
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
78
|
+
CHECK (status IN ('pending','in_progress','passed','failed')),
|
|
79
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS exercise_results (
|
|
86
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
87
|
+
exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
|
|
88
|
+
test_name TEXT NOT NULL DEFAULT '',
|
|
89
|
+
passed INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
output TEXT NOT NULL DEFAULT '',
|
|
91
|
+
ran_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
97
|
+
key TEXT PRIMARY KEY,
|
|
98
|
+
value TEXT NOT NULL DEFAULT ''
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- FTS5 virtual table for full-text search over entries
|
|
102
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts
|
|
103
|
+
USING fts5(content, content='entries', content_rowid='id');
|
|
104
|
+
|
|
105
|
+
-- Sync triggers: keep entries_fts up to date with entries
|
|
106
|
+
CREATE TRIGGER IF NOT EXISTS entries_ai
|
|
107
|
+
AFTER INSERT ON entries BEGIN
|
|
108
|
+
INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);
|
|
109
|
+
END;
|
|
110
|
+
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS entries_ad
|
|
112
|
+
AFTER DELETE ON entries BEGIN
|
|
113
|
+
INSERT INTO entries_fts(entries_fts, rowid, content)
|
|
114
|
+
VALUES ('delete', old.id, old.content);
|
|
115
|
+
END;
|
|
116
|
+
|
|
117
|
+
CREATE TRIGGER IF NOT EXISTS entries_au
|
|
118
|
+
AFTER UPDATE ON entries BEGIN
|
|
119
|
+
INSERT INTO entries_fts(entries_fts, rowid, content)
|
|
120
|
+
VALUES ('delete', old.id, old.content);
|
|
121
|
+
INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);
|
|
122
|
+
END;
|
|
123
|
+
`;
|
|
124
|
+
/** Future migrations appended here in order (v1 is baseline — empty). */
|
|
125
|
+
export const migrations = [];
|
|
126
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/storage/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0HrB,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,UAAU,GAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CurriculumService } from '../services/curriculum.js';
|
|
3
|
+
import type { SessionState } from '../types.js';
|
|
4
|
+
export declare function registerCurriculumTools(server: McpServer, svc: CurriculumService, sessions: Map<string, SessionState>, notify: () => void): void;
|