eklavya 1.7.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/README.md +69 -0
- package/dist/ask.js +58 -0
- package/dist/ask.js.map +1 -0
- package/dist/assets/dashboard.html +407 -0
- package/dist/assets/tokens.css +135 -0
- package/dist/assets/tutor-skill.md +345 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/concurrency.js +41 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.js +214 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +152 -0
- package/dist/hooks/checkpoint-quiz.js.map +1 -0
- package/dist/hooks/lib.js +177 -0
- package/dist/hooks/lib.js.map +1 -0
- package/dist/hooks/pre-tool-gate.js +53 -0
- package/dist/hooks/pre-tool-gate.js.map +1 -0
- package/dist/hooks/session-start.js +128 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/stop-quiz-check.js +147 -0
- package/dist/hooks/stop-quiz-check.js.map +1 -0
- package/dist/install.js +313 -0
- package/dist/install.js.map +1 -0
- package/dist/mcq.js +34 -0
- package/dist/mcq.js.map +1 -0
- package/dist/migrate.js +45 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/001_init.sql +71 -0
- package/dist/migrations/002_stop_markers.sql +14 -0
- package/dist/migrations/003_gate_repo.sql +5 -0
- package/dist/migrations/004_attempt_outcome.sql +15 -0
- package/dist/migrations/005_session_concept_origin.sql +24 -0
- package/dist/migrations/006_attempt_format.sql +31 -0
- package/dist/migrations/007_checkpoints.sql +23 -0
- package/dist/migrations/008_difficulty_levels.sql +57 -0
- package/dist/paths.js +25 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/.claude-plugin/marketplace.json +14 -0
- package/dist/plugin/.claude-plugin/plugin.json +17 -0
- package/dist/plugin/.mcp.json +11 -0
- package/dist/plugin/agents/tutor.md +63 -0
- package/dist/plugin/cli/eklavya-gate +67 -0
- package/dist/plugin/hooks/hooks.json +57 -0
- package/dist/plugin/hooks/run.mjs +169 -0
- package/dist/plugin/scripts/install-git-hook.sh +81 -0
- package/dist/plugin/skills/gate/SKILL.md +17 -0
- package/dist/plugin/skills/learn/SKILL.md +28 -0
- package/dist/plugin/skills/level/SKILL.md +53 -0
- package/dist/plugin/skills/mode/SKILL.md +56 -0
- package/dist/plugin/skills/progress/SKILL.md +72 -0
- package/dist/plugin/skills/quiz/SKILL.md +31 -0
- package/dist/plugin/skills/setup/SKILL.md +49 -0
- package/dist/plugin/skills/tutor/SKILL.md +345 -0
- package/dist/seed/git.json +49 -0
- package/dist/seed/node-backend.json +43 -0
- package/dist/seed/react.json +46 -0
- package/dist/seed/web-auth.json +78 -0
- package/dist/seed.js +112 -0
- package/dist/seed.js.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +24 -0
- package/dist/session.js.map +1 -0
- package/dist/slug.js +75 -0
- package/dist/slug.js.map +1 -0
- package/dist/srs.js +206 -0
- package/dist/srs.js.map +1 -0
- package/dist/store.js +450 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/config_tools.js +119 -0
- package/dist/tools/config_tools.js.map +1 -0
- package/dist/tools/get_concept_graph.js +104 -0
- package/dist/tools/get_concept_graph.js.map +1 -0
- package/dist/tools/get_gate_status.js +20 -0
- package/dist/tools/get_gate_status.js.map +1 -0
- package/dist/tools/get_learner_profile.js +142 -0
- package/dist/tools/get_learner_profile.js.map +1 -0
- package/dist/tools/get_session_quiz_plan.js +393 -0
- package/dist/tools/get_session_quiz_plan.js.map +1 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log_session_concepts.js +103 -0
- package/dist/tools/log_session_concepts.js.map +1 -0
- package/dist/tools/record_attempt.js +158 -0
- package/dist/tools/record_attempt.js.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/upsert_concepts.js +89 -0
- package/dist/tools/upsert_concepts.js.map +1 -0
- package/package.json +66 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { applyGrade, checkPromotion, decayedScore, initialMastery, isKnown, nextLevel, requiredConcepts, SCORE_WINDOW, START_LEVEL, } from './srs.js';
|
|
2
|
+
import { stripAskFooter } from './ask.js';
|
|
3
|
+
export function conceptBySlug(db, slug) {
|
|
4
|
+
return db.prepare('SELECT * FROM concepts WHERE slug = ?').get(slug);
|
|
5
|
+
}
|
|
6
|
+
export function allConceptSlugs(db) {
|
|
7
|
+
return db.prepare('SELECT id, slug, domain FROM concepts').all();
|
|
8
|
+
}
|
|
9
|
+
export function insertConcept(db, c) {
|
|
10
|
+
db.prepare(`INSERT INTO concepts (slug, name, domain, description, tier, source)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(c.slug, c.name, c.domain, c.description ?? null, c.tier, c.source ?? 'llm');
|
|
12
|
+
return conceptBySlug(db, c.slug);
|
|
13
|
+
}
|
|
14
|
+
export function masteryFor(db, conceptId) {
|
|
15
|
+
const row = db.prepare('SELECT * FROM mastery WHERE concept_id = ?').get(conceptId);
|
|
16
|
+
if (!row)
|
|
17
|
+
return initialMastery();
|
|
18
|
+
return {
|
|
19
|
+
score: row.score,
|
|
20
|
+
ease: row.ease,
|
|
21
|
+
interval_d: row.interval_d,
|
|
22
|
+
reps: row.reps,
|
|
23
|
+
last_seen: row.last_seen,
|
|
24
|
+
next_review: row.next_review,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function writeMastery(db, conceptId, state) {
|
|
28
|
+
db.prepare(`INSERT INTO mastery (concept_id, score, ease, interval_d, reps, last_seen, next_review)
|
|
29
|
+
VALUES (@id, @score, @ease, @interval_d, @reps, @last_seen, @next_review)
|
|
30
|
+
ON CONFLICT(concept_id) DO UPDATE SET
|
|
31
|
+
score = excluded.score, ease = excluded.ease, interval_d = excluded.interval_d,
|
|
32
|
+
reps = excluded.reps, last_seen = excluded.last_seen, next_review = excluded.next_review`).run({ id: conceptId, ...state });
|
|
33
|
+
}
|
|
34
|
+
/** Chronological grades, oldest first, windowed to what the score actually uses. */
|
|
35
|
+
export function gradesFor(db, conceptId, limit = SCORE_WINDOW) {
|
|
36
|
+
const rows = db
|
|
37
|
+
.prepare('SELECT grade FROM attempts WHERE concept_id = ? ORDER BY id DESC LIMIT ?')
|
|
38
|
+
.all(conceptId, limit);
|
|
39
|
+
return rows.map((r) => r.grade).reverse();
|
|
40
|
+
}
|
|
41
|
+
export function lastAttempt(db, conceptId) {
|
|
42
|
+
return db
|
|
43
|
+
.prepare('SELECT grade, difficulty, ts FROM attempts WHERE concept_id = ? ORDER BY id DESC LIMIT 1')
|
|
44
|
+
.get(conceptId);
|
|
45
|
+
}
|
|
46
|
+
export function recordAttemptRow(db, a) {
|
|
47
|
+
db.prepare(`INSERT INTO attempts
|
|
48
|
+
(concept_id, session_id, question, answer, grade, difficulty, feedback, outcome, format,
|
|
49
|
+
options, repo, level)
|
|
50
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(a.conceptId, a.sessionId, a.question, a.answer, a.grade, a.difficulty, a.feedback, a.outcome, a.format,
|
|
51
|
+
// Stored beside the stem, never inside it: `question` is what gets
|
|
52
|
+
// fingerprinted, and options that move would defeat the repeat check.
|
|
53
|
+
a.options ? JSON.stringify(a.options) : null,
|
|
54
|
+
// The project this was earned in, and the band it was asked at. Progress
|
|
55
|
+
// toward the next level is counted from these two (migration 008).
|
|
56
|
+
a.repo, a.level);
|
|
57
|
+
}
|
|
58
|
+
/** Applies one grade end to end: attempt row, then recomputed SM-2 state. */
|
|
59
|
+
export function gradeConcept(db, input) {
|
|
60
|
+
const before = masteryFor(db, input.conceptId);
|
|
61
|
+
recordAttemptRow(db, input);
|
|
62
|
+
const grades = gradesFor(db, input.conceptId);
|
|
63
|
+
const after = applyGrade({ state: before, grade: input.grade, grades, now: input.now });
|
|
64
|
+
writeMastery(db, input.conceptId, after);
|
|
65
|
+
return after;
|
|
66
|
+
}
|
|
67
|
+
export function logSessionConcept(db, sessionId, conceptId, context, origin = 'work') {
|
|
68
|
+
db.prepare(`INSERT INTO session_concepts (session_id, concept_id, context, origin)
|
|
69
|
+
VALUES (?, ?, ?, ?)
|
|
70
|
+
ON CONFLICT(session_id, concept_id) DO UPDATE SET
|
|
71
|
+
context = COALESCE(excluded.context, session_concepts.context),
|
|
72
|
+
-- Work wins and never degrades: a concept quizzed as review debt and
|
|
73
|
+
-- then genuinely touched by the task is part of the task.
|
|
74
|
+
origin = CASE
|
|
75
|
+
WHEN excluded.origin = 'work' THEN 'work'
|
|
76
|
+
ELSE COALESCE(session_concepts.origin, excluded.origin)
|
|
77
|
+
END`).run(sessionId, conceptId, context, origin);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `origin: 'work'` restricts this to what the task actually touched, excluding
|
|
81
|
+
* concepts a quiz pulled in as review debt. The gate's bar must be computed from
|
|
82
|
+
* that subset: `passedCount` only counts 'work', so letting review rows raise
|
|
83
|
+
* `required` would raise a bar that nothing the learner does can clear.
|
|
84
|
+
*/
|
|
85
|
+
export function sessionConcepts(db, sessionId, origin) {
|
|
86
|
+
return db
|
|
87
|
+
.prepare(`SELECT c.*, sc.context, sc.ts AS logged_at
|
|
88
|
+
FROM session_concepts sc JOIN concepts c ON c.id = sc.concept_id
|
|
89
|
+
WHERE sc.session_id = ?
|
|
90
|
+
${origin ? `AND COALESCE(sc.origin, 'work') = ?` : ''}
|
|
91
|
+
ORDER BY sc.ts ASC, c.tier ASC, c.slug ASC`)
|
|
92
|
+
.all(...(origin ? [sessionId, origin] : [sessionId]));
|
|
93
|
+
}
|
|
94
|
+
export function newConceptsThisSession(db, sessionId) {
|
|
95
|
+
return db
|
|
96
|
+
.prepare(`SELECT count(*) n FROM session_concepts sc
|
|
97
|
+
JOIN concepts c ON c.id = sc.concept_id
|
|
98
|
+
WHERE sc.session_id = ? AND c.source = 'llm'`)
|
|
99
|
+
.get(sessionId).n;
|
|
100
|
+
}
|
|
101
|
+
export function lastAttemptAt(db, sessionId) {
|
|
102
|
+
const row = db
|
|
103
|
+
.prepare('SELECT ts FROM attempts WHERE session_id = ? ORDER BY id DESC LIMIT 1')
|
|
104
|
+
.get(sessionId);
|
|
105
|
+
return row?.ts ?? null;
|
|
106
|
+
}
|
|
107
|
+
export const PASSING_GRADE = 3;
|
|
108
|
+
export function gateRow(db, sessionId) {
|
|
109
|
+
return db.prepare('SELECT * FROM gates WHERE session_id = ?').get(sessionId);
|
|
110
|
+
}
|
|
111
|
+
function countAnswered(db, sessionId) {
|
|
112
|
+
const row = db
|
|
113
|
+
.prepare(
|
|
114
|
+
// `answered` counts everything -- quizzing on review debt is not free.
|
|
115
|
+
// `passedCount` counts only 'work', because `required` is derived from the
|
|
116
|
+
// concepts the work touched and the two ends have to measure the same
|
|
117
|
+
// thing. Without this, `concept` focus widening to domain siblings, or
|
|
118
|
+
// `learn` focus selecting from an unrelated topic, would let a gate whose
|
|
119
|
+
// bar was set by today's diff be cleared without a single question about
|
|
120
|
+
// today's diff. NULL is pre-migration and reads as 'work' (migration 005).
|
|
121
|
+
`SELECT
|
|
122
|
+
count(DISTINCT a.concept_id) AS answered,
|
|
123
|
+
count(DISTINCT CASE
|
|
124
|
+
WHEN a.grade >= ? AND COALESCE(sc.origin, 'work') = 'work'
|
|
125
|
+
THEN a.concept_id
|
|
126
|
+
END) AS passedCount
|
|
127
|
+
FROM attempts a
|
|
128
|
+
JOIN session_concepts sc
|
|
129
|
+
ON sc.concept_id = a.concept_id AND sc.session_id = a.session_id
|
|
130
|
+
WHERE a.session_id = ?`)
|
|
131
|
+
.get(PASSING_GRADE, sessionId);
|
|
132
|
+
return row;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Recomputes and persists the gate for a session. `requiredHint` raises the bar
|
|
136
|
+
* (never lowers it) when new unmastered concepts show up mid-session.
|
|
137
|
+
*/
|
|
138
|
+
export function syncGate(db, sessionId, config, opts = {}) {
|
|
139
|
+
const existing = gateRow(db, sessionId);
|
|
140
|
+
const { answered, passedCount } = countAnswered(db, sessionId);
|
|
141
|
+
const required = Math.max(existing?.required ?? 0, opts.requiredHint ?? 0);
|
|
142
|
+
// Keep a repo once known: a later call from a different cwd must not blank it,
|
|
143
|
+
// or the git pre-commit hook loses the row it enforces against.
|
|
144
|
+
const repo = opts.repo ?? existing?.repo ?? null;
|
|
145
|
+
const needed = Math.ceil(required * config.pass_threshold);
|
|
146
|
+
const passed = required === 0 ? true : passedCount >= needed;
|
|
147
|
+
db.prepare(`INSERT INTO gates (session_id, mode, required, answered, passed, updated_at, repo)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'), ?)
|
|
149
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
150
|
+
mode = excluded.mode, required = excluded.required,
|
|
151
|
+
answered = excluded.answered, passed = excluded.passed,
|
|
152
|
+
updated_at = excluded.updated_at, repo = excluded.repo`).run(sessionId, config.mode, required, answered, passed ? 1 : 0, repo);
|
|
153
|
+
return { mode: config.mode, required, answered, passed, pass_threshold: config.pass_threshold, repo };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The best grade a multiple-choice answer can earn.
|
|
157
|
+
*
|
|
158
|
+
* Grade 5 on the SM-2 scale means "correct, and explained why". Picking the
|
|
159
|
+
* right option out of four cannot demonstrate that -- and one in four is a coin,
|
|
160
|
+
* so even a clean 4 is generous. Capping here rather than trusting the tutor to
|
|
161
|
+
* remember keeps recognition from inflating mastery, which is the one thing that
|
|
162
|
+
* would make the whole record untrustworthy.
|
|
163
|
+
*
|
|
164
|
+
* 4 still reaches `known` (0.8 >= MASTERY_THRESHOLD), so this limits how fast
|
|
165
|
+
* mastery is claimed, not whether it can be.
|
|
166
|
+
*/
|
|
167
|
+
export const MAX_MCQ_GRADE = 4;
|
|
168
|
+
/** The last few questions actually asked about a concept, newest first. */
|
|
169
|
+
export function recentQuestions(db, conceptId, limit = 3) {
|
|
170
|
+
const rows = db
|
|
171
|
+
.prepare(`SELECT question, difficulty AS tier, grade, outcome, format FROM attempts
|
|
172
|
+
WHERE concept_id = ? AND question <> ''
|
|
173
|
+
ORDER BY id DESC LIMIT ?`)
|
|
174
|
+
.all(conceptId, limit);
|
|
175
|
+
return rows.map((r) => ({ ...r, taught: r.outcome === 'dont_know' }));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Loose match: whitespace and punctuation differences are still the same question.
|
|
179
|
+
*
|
|
180
|
+
* The ask footer is stripped before hashing. It is presentation -- the dials that
|
|
181
|
+
* asked -- and it changes when the level or the focus changes, so leaving it in
|
|
182
|
+
* would make one question fingerprint as several and quietly undo "never the same
|
|
183
|
+
* question twice".
|
|
184
|
+
*/
|
|
185
|
+
export function questionFingerprint(question) {
|
|
186
|
+
return stripAskFooter(question).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
187
|
+
}
|
|
188
|
+
export function hasAskedQuestion(db, conceptId, question) {
|
|
189
|
+
const target = questionFingerprint(question);
|
|
190
|
+
if (!target)
|
|
191
|
+
return false;
|
|
192
|
+
const rows = db
|
|
193
|
+
.prepare('SELECT question FROM attempts WHERE concept_id = ? ORDER BY id DESC LIMIT 20')
|
|
194
|
+
.all(conceptId);
|
|
195
|
+
return rows.some((r) => questionFingerprint(r.question) === target);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Has this concept ever been blanked on and taught?
|
|
199
|
+
*
|
|
200
|
+
* Deliberately not derived from `recentQuestions`, which is windowed: once a few
|
|
201
|
+
* newer attempts pile up, the `dont_know` row falls out of that window and the
|
|
202
|
+
* flag flips back to false. The concepts that accumulate attempts fastest are
|
|
203
|
+
* the unmastered ones that keep resurfacing -- exactly the ones most likely to
|
|
204
|
+
* have been blanked on -- so deriving it there loses the flag precisely where it
|
|
205
|
+
* matters most, and the next question opens cold on a topic already explained.
|
|
206
|
+
*/
|
|
207
|
+
export function wasEverTaught(db, conceptId) {
|
|
208
|
+
const row = db
|
|
209
|
+
.prepare(`SELECT 1 FROM attempts WHERE concept_id = ? AND outcome = 'dont_know' LIMIT 1`)
|
|
210
|
+
.get(conceptId);
|
|
211
|
+
return row !== undefined;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Concepts this session attempted but did not pass, minus the ones the learner
|
|
215
|
+
* declined.
|
|
216
|
+
*
|
|
217
|
+
* This is the way out of an otherwise unreachable gate. A blank grades 0, and a
|
|
218
|
+
* concept with any attempt is filtered from the session plan, so a session
|
|
219
|
+
* answered entirely with "I don't know" leaves `required` permanently unmet: the
|
|
220
|
+
* planner returns nothing, and the commit gate denies forever. Offering those
|
|
221
|
+
* concepts again -- once everything else is exhausted, a tier lower, and with
|
|
222
|
+
* `asked_before` forcing a different question -- turns the deadlock into a
|
|
223
|
+
* second lap over material that has now been taught.
|
|
224
|
+
*
|
|
225
|
+
* Declines are excluded on purpose, judged on the *latest* attempt so someone
|
|
226
|
+
* who declined and later engaged is not held to the earlier answer. "Leave me
|
|
227
|
+
* alone" is a choice, and enforced mode holding the gate against it is the
|
|
228
|
+
* enforcement working, not a deadlock.
|
|
229
|
+
*/
|
|
230
|
+
export function gateRetryConcepts(db, sessionId) {
|
|
231
|
+
return db
|
|
232
|
+
.prepare(
|
|
233
|
+
// Carries `context` so a retry question stays grounded in the same code
|
|
234
|
+
// the first one was about -- the concept was taught, not the file.
|
|
235
|
+
`SELECT c.*, sc.context, sc.ts AS logged_at FROM concepts c
|
|
236
|
+
JOIN session_concepts sc ON sc.concept_id = c.id AND sc.session_id = ?
|
|
237
|
+
JOIN (
|
|
238
|
+
SELECT a.concept_id,
|
|
239
|
+
max(a.grade) AS best_grade,
|
|
240
|
+
(SELECT x.outcome FROM attempts x
|
|
241
|
+
WHERE x.session_id = a.session_id AND x.concept_id = a.concept_id
|
|
242
|
+
ORDER BY x.id DESC LIMIT 1) AS last_outcome
|
|
243
|
+
FROM attempts a
|
|
244
|
+
WHERE a.session_id = ?
|
|
245
|
+
GROUP BY a.concept_id
|
|
246
|
+
) t ON t.concept_id = c.id
|
|
247
|
+
WHERE t.best_grade < ?
|
|
248
|
+
AND (t.last_outcome IS NULL OR t.last_outcome <> 'declined')
|
|
249
|
+
ORDER BY sc.ts ASC`)
|
|
250
|
+
.all(sessionId, sessionId, PASSING_GRADE);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Concepts in the same domains as the session's work, for `concept` focus.
|
|
254
|
+
*
|
|
255
|
+
* `project` focus asks about the diff; `concept` focus asks about the ideas the
|
|
256
|
+
* diff is an instance of, which means reaching past what the task happened to
|
|
257
|
+
* touch. Prerequisites of touched concepts come too: they are the part of "the
|
|
258
|
+
* general version" a learner is most likely to be missing.
|
|
259
|
+
*/
|
|
260
|
+
export function domainSiblings(db, domains, exclude) {
|
|
261
|
+
if (domains.length === 0)
|
|
262
|
+
return [];
|
|
263
|
+
const rows = db
|
|
264
|
+
.prepare(`SELECT c.* FROM concepts c
|
|
265
|
+
WHERE c.domain IN (${domains.map(() => '?').join(',')})
|
|
266
|
+
ORDER BY c.tier ASC, c.slug ASC`)
|
|
267
|
+
.all(...domains);
|
|
268
|
+
return rows.filter((c) => !exclude.has(c.id));
|
|
269
|
+
}
|
|
270
|
+
/** Prerequisites of the given concepts, nearest first. Used to widen `concept` focus. */
|
|
271
|
+
export function prereqsOf(db, conceptIds) {
|
|
272
|
+
if (conceptIds.length === 0)
|
|
273
|
+
return [];
|
|
274
|
+
return db
|
|
275
|
+
.prepare(`SELECT DISTINCT c.* FROM concepts c
|
|
276
|
+
JOIN edges e ON e.from_concept = c.id
|
|
277
|
+
WHERE e.relation = 'prerequisite_of'
|
|
278
|
+
AND e.to_concept IN (${conceptIds.map(() => '?').join(',')})
|
|
279
|
+
ORDER BY c.tier ASC, c.slug ASC`)
|
|
280
|
+
.all(...conceptIds);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Resolve a free-text topic ("caching", "web auth") onto the graph, for `learn`
|
|
284
|
+
* focus.
|
|
285
|
+
*
|
|
286
|
+
* Tried in order of confidence: an exact domain, then a domain whose name the
|
|
287
|
+
* topic contains or is contained by, then concepts matching on slug or name.
|
|
288
|
+
* Returning both a domain and slugs lets the caller prefer the domain when the
|
|
289
|
+
* topic names one and fall back to loose concept matches when it does not.
|
|
290
|
+
*
|
|
291
|
+
* An empty result is meaningful and must not be papered over -- it means the
|
|
292
|
+
* graph has nothing on this topic yet, and the honest answer is to say so and
|
|
293
|
+
* offer to teach from first principles rather than to invent questions.
|
|
294
|
+
*/
|
|
295
|
+
export function resolveTopic(db, topic) {
|
|
296
|
+
const needle = topic.trim().toLowerCase();
|
|
297
|
+
if (!needle)
|
|
298
|
+
return { domain: null, slugs: [] };
|
|
299
|
+
const slugged = needle.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
300
|
+
const domains = db.prepare('SELECT DISTINCT domain FROM concepts').all().map((r) => r.domain);
|
|
301
|
+
const exact = domains.find((d) => d.toLowerCase() === needle || d.toLowerCase() === slugged);
|
|
302
|
+
const loose = exact ??
|
|
303
|
+
domains.find((d) => {
|
|
304
|
+
const l = d.toLowerCase();
|
|
305
|
+
return l.includes(slugged) || slugged.includes(l);
|
|
306
|
+
});
|
|
307
|
+
const rows = db
|
|
308
|
+
.prepare(`SELECT slug FROM concepts
|
|
309
|
+
WHERE slug LIKE ? OR lower(name) LIKE ?
|
|
310
|
+
ORDER BY tier ASC, slug ASC
|
|
311
|
+
LIMIT 40`)
|
|
312
|
+
.all(`%${slugged}%`, `%${needle}%`);
|
|
313
|
+
return { domain: loose ?? null, slugs: rows.map((r) => r.slug) };
|
|
314
|
+
}
|
|
315
|
+
/** Concepts already asked about in this session — they have had their turn. */
|
|
316
|
+
export function attemptedConceptIds(db, sessionId) {
|
|
317
|
+
const rows = db
|
|
318
|
+
.prepare('SELECT DISTINCT concept_id FROM attempts WHERE session_id = ?')
|
|
319
|
+
.all(sessionId);
|
|
320
|
+
return new Set(rows.map((r) => r.concept_id));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Prerequisites of a concept the learner has not mastered yet. A tier-3
|
|
324
|
+
* judgement question about a concept whose foundations are missing is not a hard
|
|
325
|
+
* question, it is an unfair one — the tutor needs to see this before asking.
|
|
326
|
+
*/
|
|
327
|
+
export function unmetPrereqs(db, conceptId, now) {
|
|
328
|
+
const rows = db
|
|
329
|
+
.prepare(`SELECT c.slug, m.score, m.reps, m.next_review
|
|
330
|
+
FROM edges e
|
|
331
|
+
JOIN concepts c ON c.id = e.from_concept
|
|
332
|
+
LEFT JOIN mastery m ON m.concept_id = c.id
|
|
333
|
+
WHERE e.to_concept = ? AND e.relation = 'prerequisite_of'
|
|
334
|
+
ORDER BY c.tier ASC, c.slug ASC`)
|
|
335
|
+
.all(conceptId);
|
|
336
|
+
return rows
|
|
337
|
+
.filter((r) => !isKnown({ score: decayedScore(r.score ?? 0, r.next_review, now), reps: r.reps ?? 0 }))
|
|
338
|
+
.map((r) => r.slug);
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Difficulty levels (phase-9)
|
|
342
|
+
//
|
|
343
|
+
// The level is per project and earned. `srs.ts` holds the rules; this holds the
|
|
344
|
+
// two facts they are computed from -- which band the project is on, and what has
|
|
345
|
+
// been answered since it got there.
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
/**
|
|
348
|
+
* The bucket for work outside any git repository.
|
|
349
|
+
*
|
|
350
|
+
* A scratch directory still deserves a level, but keying on `cwd` would mint a
|
|
351
|
+
* row per temp folder and mean nobody ever accumulates a hundred answers
|
|
352
|
+
* anywhere.
|
|
353
|
+
*/
|
|
354
|
+
export const GLOBAL_PROJECT = '*';
|
|
355
|
+
export function projectKey(repoRoot) {
|
|
356
|
+
return repoRoot && repoRoot.trim() ? repoRoot : GLOBAL_PROJECT;
|
|
357
|
+
}
|
|
358
|
+
export function projectLevelRow(db, repo) {
|
|
359
|
+
return db.prepare('SELECT * FROM project_levels WHERE repo = ?').get(repo);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Counts the evidence at one level in one project.
|
|
363
|
+
*
|
|
364
|
+
* Derived, never stored. A counter column would drift from the attempt rows that
|
|
365
|
+
* justify it, and it would freeze `level_up_after` at whatever it was when each
|
|
366
|
+
* attempt landed -- so lowering the threshold would not release the learners
|
|
367
|
+
* already past it, which is the one change most likely to be made after watching
|
|
368
|
+
* a real learner.
|
|
369
|
+
*
|
|
370
|
+
* `since` is the level's `promoted_at`: answers from the previous band are spent.
|
|
371
|
+
* Declines are excluded from both halves of the accuracy fraction -- skipping a
|
|
372
|
+
* question honestly must never cost a level, or the level becomes a reason to
|
|
373
|
+
* guess.
|
|
374
|
+
*/
|
|
375
|
+
export function levelCounts(db, repo, level, since) {
|
|
376
|
+
const row = db
|
|
377
|
+
.prepare(`SELECT
|
|
378
|
+
COALESCE(sum(CASE WHEN grade >= ? THEN 1 ELSE 0 END), 0) AS passed,
|
|
379
|
+
count(*) AS answered,
|
|
380
|
+
count(DISTINCT CASE WHEN grade >= ? THEN concept_id END) AS concepts
|
|
381
|
+
FROM attempts
|
|
382
|
+
WHERE repo = ? AND level = ?
|
|
383
|
+
AND COALESCE(outcome, 'answered') <> 'declined'
|
|
384
|
+
AND (? IS NULL OR ts >= ?)`)
|
|
385
|
+
.get(PASSING_GRADE, PASSING_GRADE, repo, level, since, since);
|
|
386
|
+
return { passed: row.passed, answered: row.answered, concepts: row.concepts };
|
|
387
|
+
}
|
|
388
|
+
/** Everything a caller needs to ask at the right band and report the runway. */
|
|
389
|
+
export function levelStanding(db, config, repoRoot) {
|
|
390
|
+
const repo = projectKey(repoRoot);
|
|
391
|
+
const pinned = config.difficulty !== 'auto';
|
|
392
|
+
const row = projectLevelRow(db, repo);
|
|
393
|
+
// A pin wins outright, and does not disturb the earned row underneath it: drop
|
|
394
|
+
// the pin later and the project is back where its evidence left it.
|
|
395
|
+
const level = pinned ? config.difficulty : (row?.level ?? START_LEVEL);
|
|
396
|
+
const entered_at = pinned ? null : (row?.promoted_at ?? null);
|
|
397
|
+
const counts = levelCounts(db, repo, level, entered_at);
|
|
398
|
+
const verdict = checkPromotion({
|
|
399
|
+
level,
|
|
400
|
+
counts,
|
|
401
|
+
after: config.level_up_after,
|
|
402
|
+
minAccuracy: config.level_up_accuracy,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
repo,
|
|
406
|
+
level,
|
|
407
|
+
pinned,
|
|
408
|
+
entered_at,
|
|
409
|
+
next: nextLevel(level),
|
|
410
|
+
counts,
|
|
411
|
+
accuracy: verdict.accuracy,
|
|
412
|
+
needed: {
|
|
413
|
+
answers: config.level_up_after,
|
|
414
|
+
accuracy: config.level_up_accuracy,
|
|
415
|
+
concepts: requiredConcepts(config.level_up_after),
|
|
416
|
+
},
|
|
417
|
+
unmet: verdict.unmet,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Promotes the project if this attempt has just earned it. Call inside the same
|
|
422
|
+
* transaction as the grade, or a crash between the two loses the evidence.
|
|
423
|
+
*
|
|
424
|
+
* `datetime('now')` rather than a JS timestamp, deliberately: `attempts.ts`
|
|
425
|
+
* defaults to SQLite's own format, and `promoted_at` is compared against it as a
|
|
426
|
+
* string. An ISO timestamp with a `T` in it would sort wrong against
|
|
427
|
+
* `2026-08-27 09:31:00` and silently count the previous band's answers.
|
|
428
|
+
*/
|
|
429
|
+
export function promoteIfEarned(db, config, repoRoot) {
|
|
430
|
+
// A pinned level is a decision, not a stage: no progression, and no row written.
|
|
431
|
+
if (config.difficulty !== 'auto')
|
|
432
|
+
return null;
|
|
433
|
+
const standing = levelStanding(db, config, repoRoot);
|
|
434
|
+
const verdict = checkPromotion({
|
|
435
|
+
level: standing.level,
|
|
436
|
+
counts: standing.counts,
|
|
437
|
+
after: config.level_up_after,
|
|
438
|
+
minAccuracy: config.level_up_accuracy,
|
|
439
|
+
});
|
|
440
|
+
if (!verdict.promote || !verdict.to)
|
|
441
|
+
return null;
|
|
442
|
+
db.prepare(`INSERT INTO project_levels (repo, level, promoted_at, updated_at)
|
|
443
|
+
VALUES (?, ?, datetime('now'), datetime('now'))
|
|
444
|
+
ON CONFLICT(repo) DO UPDATE SET
|
|
445
|
+
level = excluded.level,
|
|
446
|
+
promoted_at = excluded.promoted_at,
|
|
447
|
+
updated_at = excluded.updated_at`).run(standing.repo, verdict.to);
|
|
448
|
+
return { from: standing.level, to: verdict.to, counts: standing.counts };
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig, writeConfigFile, REPO_CONFIG_FILE } from '../config.js';
|
|
4
|
+
import { CWD_HINT } from './types.js';
|
|
5
|
+
export const getConfig = {
|
|
6
|
+
name: 'get_config',
|
|
7
|
+
title: 'Get config',
|
|
8
|
+
description: 'The effective Eklavya config: global ~/.eklavya/config.json merged with the repo .eklavya.json, repo winning.',
|
|
9
|
+
inputSchema: { cwd: z.string().optional().describe(CWD_HINT) },
|
|
10
|
+
handler: (args) => {
|
|
11
|
+
const resolved = loadConfig(args.cwd);
|
|
12
|
+
return {
|
|
13
|
+
config: resolved.config,
|
|
14
|
+
global_path: resolved.globalPath,
|
|
15
|
+
repo_path: resolved.repoPath,
|
|
16
|
+
repo_root: resolved.repoRoot,
|
|
17
|
+
// Which of the learner's own settings this repo is overriding. Say it
|
|
18
|
+
// rather than let a personal focus silently stop applying.
|
|
19
|
+
overridden_by_repo: resolved.overrides,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export const setConfig = {
|
|
24
|
+
name: 'set_config',
|
|
25
|
+
title: 'Set config',
|
|
26
|
+
description: 'Update Eklavya config. Scope "global" writes ~/.eklavya/config.json; scope "repo" writes .eklavya.json at the repo root, which is how a team lead pins enforced mode — or a difficulty level — for one project.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
scope: z.enum(['global', 'repo']).optional().describe('Defaults to global.'),
|
|
29
|
+
cwd: z.string().optional().describe(CWD_HINT),
|
|
30
|
+
mode: z
|
|
31
|
+
.enum(['ambient', 'enforced', 'off'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('How hard Eklavya pushes: ambient offers, enforced gates commits, off is dormant.'),
|
|
34
|
+
focus: z
|
|
35
|
+
.enum(['project', 'concept', 'learn'])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('What Eklavya teaches, independent of mode. "project" quizzes the code just written; "concept" asks the transferable version of the same ideas; "learn" follows focus_topic. Defaults to project.'),
|
|
38
|
+
cadence: z
|
|
39
|
+
.enum(['interleaved', 'end'])
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('When the questions land. "interleaved" (default) asks one question mid-task, at the seam where a concept was logged, and the Stop hook then only sweeps up what is left of max_questions_per_task. "end" is the old behaviour: nothing until the task is finished.'),
|
|
42
|
+
min_minutes_between_checkpoints: z.number().int().min(0).max(120).optional(),
|
|
43
|
+
difficulty: z
|
|
44
|
+
.enum(['auto', 'easy', 'medium', 'hard'])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('How hard questions on a project may get. "auto" (default) earns the level per project: everyone starts at easy (tiers 1-2), then medium (2-4), then hard (3-5). A literal level pins it and stops progression — "easy" on a repo keeps an onboarding codebase gentle for everyone, "hard" globally skips the runway.'),
|
|
47
|
+
level_up_after: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(1000)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Passing answers needed at a level, in one project, before it promotes. Defaults to 100.'),
|
|
54
|
+
level_up_accuracy: z
|
|
55
|
+
.number()
|
|
56
|
+
.min(0)
|
|
57
|
+
.max(1)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Minimum accuracy over those answers, declines excluded. Defaults to 0.7.'),
|
|
60
|
+
focus_topic: z
|
|
61
|
+
.string()
|
|
62
|
+
.nullable()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('The topic "learn" focus teaches, e.g. "caching". Pass null to clear it.'),
|
|
65
|
+
pass_threshold: z.number().min(0).max(1).optional(),
|
|
66
|
+
max_questions_per_task: z.number().int().min(1).max(10).optional(),
|
|
67
|
+
min_minutes_between_quizzes: z.number().int().min(0).optional(),
|
|
68
|
+
max_new_concepts_per_session: z.number().int().min(0).max(50).optional(),
|
|
69
|
+
max_stop_blocks_per_session: z.number().int().min(0).max(20).optional(),
|
|
70
|
+
quiet: z.boolean().optional(),
|
|
71
|
+
domains_enabled: z.array(z.string()).optional(),
|
|
72
|
+
},
|
|
73
|
+
handler: (args) => {
|
|
74
|
+
const scope = args.scope ?? 'global';
|
|
75
|
+
const cwd = args.cwd;
|
|
76
|
+
const resolved = loadConfig(cwd);
|
|
77
|
+
const patch = {};
|
|
78
|
+
for (const key of [
|
|
79
|
+
'mode',
|
|
80
|
+
'focus',
|
|
81
|
+
'focus_topic',
|
|
82
|
+
'cadence',
|
|
83
|
+
'difficulty',
|
|
84
|
+
'level_up_after',
|
|
85
|
+
'level_up_accuracy',
|
|
86
|
+
'min_minutes_between_checkpoints',
|
|
87
|
+
'pass_threshold',
|
|
88
|
+
'max_questions_per_task',
|
|
89
|
+
'min_minutes_between_quizzes',
|
|
90
|
+
'max_new_concepts_per_session',
|
|
91
|
+
'max_stop_blocks_per_session',
|
|
92
|
+
'quiet',
|
|
93
|
+
'domains_enabled',
|
|
94
|
+
]) {
|
|
95
|
+
if (args[key] !== undefined)
|
|
96
|
+
patch[key] = args[key];
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(patch).length === 0) {
|
|
99
|
+
return { error: 'nothing_to_set', detail: 'Pass at least one setting to change.' };
|
|
100
|
+
}
|
|
101
|
+
let target;
|
|
102
|
+
if (scope === 'repo') {
|
|
103
|
+
const root = resolved.repoRoot;
|
|
104
|
+
if (!root) {
|
|
105
|
+
return {
|
|
106
|
+
error: 'no_repo_root',
|
|
107
|
+
detail: 'No git repository found from this directory, so there is nowhere to write .eklavya.json.',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
target = resolved.repoPath ?? path.join(root, REPO_CONFIG_FILE);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
target = resolved.globalPath;
|
|
114
|
+
}
|
|
115
|
+
writeConfigFile(target, patch);
|
|
116
|
+
return { written_to: target, scope, config: loadConfig(cwd).config };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=config_tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config_tools.js","sourceRoot":"","sources":["../../src/tools/config_tools.ts"],"names":[],"mappings":"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"}
|