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/seed.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { seedDir } from './paths.js';
|
|
4
|
+
import { isValidSlug } from './slug.js';
|
|
5
|
+
/**
|
|
6
|
+
* Bump when seed content changes so existing installs pick up the new graphs on
|
|
7
|
+
* next start. Seeding is idempotent, so re-running is always safe.
|
|
8
|
+
*/
|
|
9
|
+
export const SEED_VERSION = 1;
|
|
10
|
+
const SEED_VERSION_KEY = 'seed_version';
|
|
11
|
+
export const RELATIONS = ['prerequisite_of', 'related_to', 'part_of'];
|
|
12
|
+
function validateGraph(graph, file) {
|
|
13
|
+
if (!graph.domain)
|
|
14
|
+
throw new Error(`${file}: missing "domain"`);
|
|
15
|
+
if (!Array.isArray(graph.concepts) || graph.concepts.length === 0) {
|
|
16
|
+
throw new Error(`${file}: "concepts" must be a non-empty array`);
|
|
17
|
+
}
|
|
18
|
+
const slugs = new Set();
|
|
19
|
+
for (const c of graph.concepts) {
|
|
20
|
+
if (!isValidSlug(c.slug))
|
|
21
|
+
throw new Error(`${file}: invalid slug "${c.slug}"`);
|
|
22
|
+
if (slugs.has(c.slug))
|
|
23
|
+
throw new Error(`${file}: duplicate slug "${c.slug}"`);
|
|
24
|
+
if (!c.name)
|
|
25
|
+
throw new Error(`${file}: concept "${c.slug}" missing name`);
|
|
26
|
+
if (!Number.isInteger(c.tier) || c.tier < 1 || c.tier > 5) {
|
|
27
|
+
throw new Error(`${file}: concept "${c.slug}" has tier ${c.tier}, expected 1..5`);
|
|
28
|
+
}
|
|
29
|
+
slugs.add(c.slug);
|
|
30
|
+
}
|
|
31
|
+
for (const e of graph.edges ?? []) {
|
|
32
|
+
if (!RELATIONS.includes(e.relation)) {
|
|
33
|
+
throw new Error(`${file}: unknown relation "${e.relation}"`);
|
|
34
|
+
}
|
|
35
|
+
// Edges may only point within the same seed file: cross-domain links are the
|
|
36
|
+
// LLM's job via upsert_concepts, and this keeps each file independently valid.
|
|
37
|
+
if (!slugs.has(e.from))
|
|
38
|
+
throw new Error(`${file}: edge from unknown slug "${e.from}"`);
|
|
39
|
+
if (!slugs.has(e.to))
|
|
40
|
+
throw new Error(`${file}: edge to unknown slug "${e.to}"`);
|
|
41
|
+
if (e.from === e.to)
|
|
42
|
+
throw new Error(`${file}: self-edge on "${e.from}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function loadSeedGraphs(dir = seedDir()) {
|
|
46
|
+
return fs
|
|
47
|
+
.readdirSync(dir)
|
|
48
|
+
.filter((f) => f.endsWith('.json'))
|
|
49
|
+
.sort()
|
|
50
|
+
.map((file) => {
|
|
51
|
+
const graph = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
52
|
+
validateGraph(graph, file);
|
|
53
|
+
return graph;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Upserts a graph by slug. Mastery rows are never touched — a learner's history
|
|
58
|
+
* survives any number of seed updates.
|
|
59
|
+
*/
|
|
60
|
+
export function applySeedGraph(db, graph) {
|
|
61
|
+
const upsertConcept = db.prepare(`INSERT INTO concepts (slug, name, domain, description, tier, source)
|
|
62
|
+
VALUES (@slug, @name, @domain, @description, @tier, 'seed')
|
|
63
|
+
ON CONFLICT(slug) DO UPDATE SET
|
|
64
|
+
name = excluded.name,
|
|
65
|
+
domain = excluded.domain,
|
|
66
|
+
description = excluded.description,
|
|
67
|
+
tier = excluded.tier,
|
|
68
|
+
source = 'seed'`);
|
|
69
|
+
const idOf = db.prepare('SELECT id FROM concepts WHERE slug = ?');
|
|
70
|
+
const insertEdge = db.prepare(`INSERT OR IGNORE INTO edges (from_concept, to_concept, relation) VALUES (?, ?, ?)`);
|
|
71
|
+
let edges = 0;
|
|
72
|
+
db.transaction(() => {
|
|
73
|
+
for (const c of graph.concepts) {
|
|
74
|
+
upsertConcept.run({
|
|
75
|
+
slug: c.slug,
|
|
76
|
+
name: c.name,
|
|
77
|
+
domain: c.domain ?? graph.domain,
|
|
78
|
+
description: c.description ?? null,
|
|
79
|
+
tier: c.tier,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const e of graph.edges ?? []) {
|
|
83
|
+
const from = idOf.get(e.from);
|
|
84
|
+
const to = idOf.get(e.to);
|
|
85
|
+
if (!from || !to)
|
|
86
|
+
continue;
|
|
87
|
+
insertEdge.run(from.id, to.id, e.relation);
|
|
88
|
+
edges += 1;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
return { concepts: graph.concepts.length, edges, domains: [graph.domain] };
|
|
92
|
+
}
|
|
93
|
+
export function seedAll(db, dir = seedDir()) {
|
|
94
|
+
const summary = { concepts: 0, edges: 0, domains: [] };
|
|
95
|
+
for (const graph of loadSeedGraphs(dir)) {
|
|
96
|
+
const s = applySeedGraph(db, graph);
|
|
97
|
+
summary.concepts += s.concepts;
|
|
98
|
+
summary.edges += s.edges;
|
|
99
|
+
summary.domains.push(graph.domain);
|
|
100
|
+
}
|
|
101
|
+
db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
102
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(SEED_VERSION_KEY, String(SEED_VERSION));
|
|
103
|
+
return summary;
|
|
104
|
+
}
|
|
105
|
+
/** Seeds on first run, and again whenever SEED_VERSION moves. */
|
|
106
|
+
export function seedIfNeeded(db, dir = seedDir()) {
|
|
107
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(SEED_VERSION_KEY);
|
|
108
|
+
if (row && Number(row.value) === SEED_VERSION)
|
|
109
|
+
return null;
|
|
110
|
+
return seedAll(db, dir);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=seed.js.map
|
package/dist/seed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { openDb } from './db.js';
|
|
6
|
+
import { dbPath } from './paths.js';
|
|
7
|
+
import { registerTools } from './tools/index.js';
|
|
8
|
+
// Read the version rather than hardcoding it: a literal here silently reports a
|
|
9
|
+
// stale version forever, since nothing about publishing touches this file.
|
|
10
|
+
// `../package.json` resolves the same from src/ under tsx and from dist/ built.
|
|
11
|
+
function serverVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return '0.0.0';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// stdout is the MCP transport. Anything diagnostic goes to stderr or it corrupts
|
|
21
|
+
// the protocol stream.
|
|
22
|
+
function log(message) {
|
|
23
|
+
process.stderr.write(`[eklavya-mcp] ${message}\n`);
|
|
24
|
+
}
|
|
25
|
+
async function main() {
|
|
26
|
+
const db = openDb();
|
|
27
|
+
log(`db ready at ${dbPath()}`);
|
|
28
|
+
const server = new McpServer({ name: 'eklavya', version: serverVersion() }, {
|
|
29
|
+
capabilities: { tools: {} },
|
|
30
|
+
instructions: 'Eklavya tracks what this developer has actually learned. Log the concepts your work touches, ' +
|
|
31
|
+
'and quiz from the learner profile rather than from scratch — never ask about a concept already mastered.',
|
|
32
|
+
});
|
|
33
|
+
registerTools(server, db);
|
|
34
|
+
const shutdown = () => {
|
|
35
|
+
try {
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// closing a already-closed handle on shutdown is not worth reporting
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
process.on('SIGINT', shutdown);
|
|
44
|
+
process.on('SIGTERM', shutdown);
|
|
45
|
+
await server.connect(new StdioServerTransport());
|
|
46
|
+
log('server connected over stdio');
|
|
47
|
+
}
|
|
48
|
+
main().catch((err) => {
|
|
49
|
+
log(`fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,SAAS,aAAa;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC7F,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,uBAAuB;AACvB,SAAS,GAAG,CAAC,OAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,OAAO,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IACpB,GAAG,CAAC,eAAe,MAAM,EAAE,EAAE,CAAC,CAAC;IAE/B,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAC7C;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAC3B,YAAY,EACV,+FAA+F;YAC/F,0GAA0G;KAC7G,CACF,CAAC;IAEF,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE1B,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,CAAC;YACH,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,GAAG,CAAC,6BAA6B,CAAC,CAAC;AACrC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,GAAG,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const CURRENT_SESSION_KEY = 'current_session';
|
|
2
|
+
export const FALLBACK_SESSION_ID = 'default';
|
|
3
|
+
export function getCurrentSession(db) {
|
|
4
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(CURRENT_SESSION_KEY);
|
|
5
|
+
return row?.value ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function setCurrentSession(db, sessionId) {
|
|
8
|
+
db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
9
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(CURRENT_SESSION_KEY, sessionId);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The model cannot see its own Claude Code session id, but the Phase 2 hooks
|
|
13
|
+
* receive the real one on stdin — so both sides have to agree on a resolution
|
|
14
|
+
* order or the hooks query rows that were written under a different key
|
|
15
|
+
* (phase-1 decision G1).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveSessionId(db, explicit) {
|
|
18
|
+
const candidate = (explicit && explicit.trim()) ||
|
|
19
|
+
(process.env.EKLAVYA_SESSION_ID && process.env.EKLAVYA_SESSION_ID.trim()) ||
|
|
20
|
+
getCurrentSession(db) ||
|
|
21
|
+
FALLBACK_SESSION_ID;
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAE7C,MAAM,UAAU,iBAAiB,CAAC,EAAM;IACtC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAEzE,CAAC;IACd,OAAO,GAAG,EAAE,KAAK,IAAI,IAAI,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAM,EAAE,SAAiB;IACzD,EAAE,CAAC,OAAO,CACR;2DACuD,CACxD,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAM,EAAE,QAAwB;IAC/D,MAAM,SAAS,GACb,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACzE,iBAAiB,CAAC,EAAE,CAAC;QACrB,mBAAmB,CAAC;IAEtB,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/slug.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept slugs are the join key across the whole system, and the LLM is allowed
|
|
3
|
+
* to mint new ones (PRD §8 tool 6). Normalizing hard here is what keeps slug
|
|
4
|
+
* sprawl from turning the graph into mush (PRD §15).
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeSlug(input) {
|
|
7
|
+
return input
|
|
8
|
+
.normalize('NFKD')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
.replace(/-{2,}/g, '-')
|
|
13
|
+
.slice(0, 80);
|
|
14
|
+
}
|
|
15
|
+
export function isValidSlug(slug) {
|
|
16
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug) && slug.length <= 80;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Token-set similarity between two slugs.
|
|
20
|
+
*/
|
|
21
|
+
export function tokenJaccard(a, b) {
|
|
22
|
+
const ta = new Set(a.split('-').filter(Boolean));
|
|
23
|
+
const tb = new Set(b.split('-').filter(Boolean));
|
|
24
|
+
if (ta.size === 0 || tb.size === 0)
|
|
25
|
+
return 0;
|
|
26
|
+
let intersection = 0;
|
|
27
|
+
for (const t of ta)
|
|
28
|
+
if (tb.has(t))
|
|
29
|
+
intersection += 1;
|
|
30
|
+
const union = ta.size + tb.size - intersection;
|
|
31
|
+
return union === 0 ? 0 : intersection / union;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Words that add no meaning to a concept name. Stripping these is what lets
|
|
35
|
+
* `jwt-structure-basics` find `jwt-structure` without also merging
|
|
36
|
+
* `refresh-token-rotation` into `refresh-token` — both pairs look identical to a
|
|
37
|
+
* similarity score, but only one of them is the same idea twice.
|
|
38
|
+
*/
|
|
39
|
+
const QUALIFIER_TOKENS = new Set([
|
|
40
|
+
'basic', 'basics', 'fundamental', 'fundamentals', 'intro', 'introduction',
|
|
41
|
+
'overview', 'explained', 'explainer', 'concept', 'concepts', 'general',
|
|
42
|
+
'generic', 'guide', 'tutorial', 'usage', 'primer', '101',
|
|
43
|
+
'strategy', 'strategies', 'approach', 'approaches',
|
|
44
|
+
]);
|
|
45
|
+
export function stripQualifiers(slug) {
|
|
46
|
+
const tokens = slug.split('-').filter(Boolean);
|
|
47
|
+
while (tokens.length > 1 && QUALIFIER_TOKENS.has(tokens[tokens.length - 1]))
|
|
48
|
+
tokens.pop();
|
|
49
|
+
while (tokens.length > 1 && QUALIFIER_TOKENS.has(tokens[0]))
|
|
50
|
+
tokens.shift();
|
|
51
|
+
return tokens.join('-');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Deliberately strict: 1.0 only catches a pure token reordering
|
|
55
|
+
* (`structure-jwt` ~ `jwt-structure`). Everything else has to survive the
|
|
56
|
+
* qualifier strip above. A test asserts no two shipped seed concepts match.
|
|
57
|
+
*/
|
|
58
|
+
export const FUZZY_MATCH_THRESHOLD = 0.8;
|
|
59
|
+
export function findFuzzyMatch(slug, candidates, threshold = FUZZY_MATCH_THRESHOLD) {
|
|
60
|
+
const stripped = stripQualifiers(slug);
|
|
61
|
+
const sameOnceQualifiersGo = candidates.find((c) => stripQualifiers(c.slug) === stripped);
|
|
62
|
+
if (sameOnceQualifiersGo)
|
|
63
|
+
return sameOnceQualifiersGo;
|
|
64
|
+
let best;
|
|
65
|
+
let bestScore = 0;
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
const score = tokenJaccard(stripped, stripQualifiers(candidate.slug));
|
|
68
|
+
if (score > bestScore) {
|
|
69
|
+
bestScore = score;
|
|
70
|
+
best = candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return bestScore >= threshold ? best : undefined;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=slug.js.map
|
package/dist/slug.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"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"}
|
package/dist/srs.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM-2 spaced repetition, mastery scoring and difficulty escalation (PRD §7).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: no database, no clock. `now` is always a parameter so
|
|
5
|
+
* the whole scheduling model is testable without freezing time.
|
|
6
|
+
*/
|
|
7
|
+
export const MIN_EASE = 1.3;
|
|
8
|
+
export const DEFAULT_EASE = 2.5;
|
|
9
|
+
export const MASTERY_THRESHOLD = 0.7;
|
|
10
|
+
export const MIN_REPS_FOR_KNOWN = 2;
|
|
11
|
+
/** Read-time decay (PRD §7): how fast an ignored concept loses credibility, and its floor. */
|
|
12
|
+
export const DECAY_PER_WEEK = 0.05;
|
|
13
|
+
export const DECAY_FLOOR = 0.3;
|
|
14
|
+
/** Score smoothing (phase-1 decision G2). */
|
|
15
|
+
export const SCORE_WINDOW = 5;
|
|
16
|
+
export const SCORE_ALPHA = 0.6;
|
|
17
|
+
export const MS_PER_DAY = 86_400_000;
|
|
18
|
+
export function initialMastery() {
|
|
19
|
+
return { score: 0, ease: DEFAULT_EASE, interval_d: 0, reps: 0, last_seen: null, next_review: null };
|
|
20
|
+
}
|
|
21
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
22
|
+
/**
|
|
23
|
+
* Recency-weighted mean of the last few grades, normalized to 0..1.
|
|
24
|
+
* A single attempt scores exactly `grade / 5`; older attempts fade at `SCORE_ALPHA`.
|
|
25
|
+
* Alpha is above 0.5 deliberately: at exactly 0.5 the weighting is symmetric, so a
|
|
26
|
+
* learner who is improving would score the same as one who is falling apart.
|
|
27
|
+
*/
|
|
28
|
+
export function scoreFromGrades(grades) {
|
|
29
|
+
const recent = grades.slice(-SCORE_WINDOW);
|
|
30
|
+
if (recent.length === 0)
|
|
31
|
+
return 0;
|
|
32
|
+
let score = clamp(recent[0], 0, 5) / 5;
|
|
33
|
+
for (let i = 1; i < recent.length; i += 1) {
|
|
34
|
+
score = SCORE_ALPHA * (clamp(recent[i], 0, 5) / 5) + (1 - SCORE_ALPHA) * score;
|
|
35
|
+
}
|
|
36
|
+
return Number(score.toFixed(6));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Standard SM-2 ease adjustment. Applied on every grade, not only on passes:
|
|
40
|
+
* getting a concept wrong should make it come back harder, which is the whole
|
|
41
|
+
* point of tracking ease separately from the interval.
|
|
42
|
+
*/
|
|
43
|
+
export function nextEase(ease, grade) {
|
|
44
|
+
const q = clamp(grade, 0, 5);
|
|
45
|
+
const adjusted = ease + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02));
|
|
46
|
+
return Number(Math.max(MIN_EASE, adjusted).toFixed(6));
|
|
47
|
+
}
|
|
48
|
+
/** Applies one graded attempt, returning the new mastery state. */
|
|
49
|
+
export function applyGrade({ state, grade, grades, now }) {
|
|
50
|
+
const q = clamp(Math.round(grade), 0, 5);
|
|
51
|
+
const ease = nextEase(state.ease, q);
|
|
52
|
+
let reps;
|
|
53
|
+
let interval;
|
|
54
|
+
if (q >= 3) {
|
|
55
|
+
reps = state.reps + 1;
|
|
56
|
+
if (reps === 1)
|
|
57
|
+
interval = 1;
|
|
58
|
+
else if (reps === 2)
|
|
59
|
+
interval = 6;
|
|
60
|
+
else
|
|
61
|
+
interval = Math.round(state.interval_d * ease);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// A failed recall restarts the ladder — the learner sees it again tomorrow.
|
|
65
|
+
reps = 0;
|
|
66
|
+
interval = 1;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
score: scoreFromGrades(grades),
|
|
70
|
+
ease,
|
|
71
|
+
interval_d: interval,
|
|
72
|
+
reps,
|
|
73
|
+
last_seen: now.toISOString(),
|
|
74
|
+
next_review: new Date(now.getTime() + interval * MS_PER_DAY).toISOString(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Soft decay for concepts whose review date is long past. Computed at read time
|
|
79
|
+
* and never persisted (PRD §7), so an ignored concept resurfaces without its
|
|
80
|
+
* history being rewritten.
|
|
81
|
+
*/
|
|
82
|
+
export function decayedScore(score, nextReview, now) {
|
|
83
|
+
if (!nextReview)
|
|
84
|
+
return score;
|
|
85
|
+
const overdueMs = now.getTime() - Date.parse(nextReview);
|
|
86
|
+
if (!Number.isFinite(overdueMs) || overdueMs <= 0)
|
|
87
|
+
return score;
|
|
88
|
+
const weeks = Math.floor(overdueMs / (7 * MS_PER_DAY));
|
|
89
|
+
if (weeks <= 0)
|
|
90
|
+
return score;
|
|
91
|
+
const decayed = score - DECAY_PER_WEEK * weeks;
|
|
92
|
+
// The floor must never *raise* a score that was already below it.
|
|
93
|
+
return Number((decayed < DECAY_FLOOR ? Math.min(score, DECAY_FLOOR) : decayed).toFixed(6));
|
|
94
|
+
}
|
|
95
|
+
export function isKnown(state) {
|
|
96
|
+
return state.score >= MASTERY_THRESHOLD && state.reps >= MIN_REPS_FOR_KNOWN;
|
|
97
|
+
}
|
|
98
|
+
export function isDue(nextReview, now) {
|
|
99
|
+
if (!nextReview)
|
|
100
|
+
return false;
|
|
101
|
+
const t = Date.parse(nextReview);
|
|
102
|
+
return Number.isFinite(t) && t <= now.getTime();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Difficulty escalation (phase-1 decision G3). Tier belongs to the *question*,
|
|
106
|
+
* not the concept: a fundamentals concept gets asked at tier 4 of someone who
|
|
107
|
+
* keeps nailing it, which is how "never ask the same question twice" is honored
|
|
108
|
+
* without running out of concepts.
|
|
109
|
+
*/
|
|
110
|
+
export function nextTierToAsk(input) {
|
|
111
|
+
const { conceptTier, lastDifficulty, lastGrade, score = 0 } = input;
|
|
112
|
+
if (lastDifficulty == null || lastGrade == null) {
|
|
113
|
+
return clamp(Math.round(conceptTier), 1, 5);
|
|
114
|
+
}
|
|
115
|
+
if (lastGrade >= 4 && score >= MASTERY_THRESHOLD)
|
|
116
|
+
return clamp(lastDifficulty + 1, 1, 5);
|
|
117
|
+
if (lastGrade <= 2)
|
|
118
|
+
return clamp(lastDifficulty - 1, 1, 5);
|
|
119
|
+
return clamp(lastDifficulty, 1, 5);
|
|
120
|
+
}
|
|
121
|
+
/** Median tier of what the learner already knows (phase-1 decision G4). */
|
|
122
|
+
export function suggestedTier(knownTiers) {
|
|
123
|
+
if (knownTiers.length === 0)
|
|
124
|
+
return 1;
|
|
125
|
+
const sorted = [...knownTiers].sort((a, b) => a - b);
|
|
126
|
+
const mid = Math.floor(sorted.length / 2);
|
|
127
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
128
|
+
return clamp(Math.round(median), 1, 5);
|
|
129
|
+
}
|
|
130
|
+
/** In order. Promotion walks this array; nothing else may assume an ordering. */
|
|
131
|
+
export const LEVELS = ['easy', 'medium', 'hard'];
|
|
132
|
+
export const START_LEVEL = 'easy';
|
|
133
|
+
/**
|
|
134
|
+
* The tier band each level may ask within.
|
|
135
|
+
*
|
|
136
|
+
* The bands overlap on purpose: promotion is not a cliff, so a learner who has
|
|
137
|
+
* just reached `medium` still meets tier-2 questions -- what changed is that 3
|
|
138
|
+
* and 4 are now reachable. And `hard` drops the tier-1 floor, because a
|
|
139
|
+
* definition question is a wasted question for someone a hundred answers into
|
|
140
|
+
* the concept.
|
|
141
|
+
*
|
|
142
|
+
* `easy` capping at 2 is the load-bearing decision of the whole phase. Tiers 1
|
|
143
|
+
* and 2 are answerable by someone who was *watching* the agent work, which is
|
|
144
|
+
* the state Eklavya finds people in; tier 3 asks them to defend a choice they
|
|
145
|
+
* did not make. An unanswerable first fortnight is how this gets switched off.
|
|
146
|
+
*/
|
|
147
|
+
export const LEVEL_BANDS = {
|
|
148
|
+
easy: { min: 1, max: 2 },
|
|
149
|
+
medium: { min: 2, max: 4 },
|
|
150
|
+
hard: { min: 3, max: 5 },
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Ceiling on the distinct concepts a promotion needs among its passing answers.
|
|
154
|
+
*
|
|
155
|
+
* Not a config key: it is not a preference, it is what stops one concept
|
|
156
|
+
* re-asked forty ways from carrying a promotion, and a learner able to lower it
|
|
157
|
+
* would be lowering the only anti-grind guard there is.
|
|
158
|
+
*/
|
|
159
|
+
export const LEVEL_UP_MIN_CONCEPTS = 15;
|
|
160
|
+
/**
|
|
161
|
+
* Distinct concepts required at a given answer threshold.
|
|
162
|
+
*
|
|
163
|
+
* Scaled rather than fixed, because a fixed 15 turns into a trap the moment
|
|
164
|
+
* anyone shortens the runway: `level_up_after: 5` would need fifteen distinct
|
|
165
|
+
* concepts among five passing answers, which is unsatisfiable, and the promotion
|
|
166
|
+
* would never fire with nothing on screen explaining why. A third of the
|
|
167
|
+
* threshold keeps the guard proportional to whatever runway is configured.
|
|
168
|
+
*/
|
|
169
|
+
export function requiredConcepts(after) {
|
|
170
|
+
return Math.max(1, Math.min(LEVEL_UP_MIN_CONCEPTS, Math.ceil(after / 3)));
|
|
171
|
+
}
|
|
172
|
+
export function clampToLevel(tier, level) {
|
|
173
|
+
const band = LEVEL_BANDS[level];
|
|
174
|
+
return clamp(Math.round(tier), band.min, band.max);
|
|
175
|
+
}
|
|
176
|
+
export function nextLevel(level) {
|
|
177
|
+
const i = LEVELS.indexOf(level);
|
|
178
|
+
return i >= 0 && i < LEVELS.length - 1 ? LEVELS[i + 1] : null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Every condition a promotion requires, evaluated together.
|
|
182
|
+
*
|
|
183
|
+
* Accuracy exists because the answer count on its own is a measure of endurance:
|
|
184
|
+
* a hundred answers of which sixty were wrong is evidence the level is too hard,
|
|
185
|
+
* and promoting on it would be the exact failure levels were added to prevent.
|
|
186
|
+
*
|
|
187
|
+
* Declines are excluded from `answered` upstream, so they cost nothing in either
|
|
188
|
+
* half of the fraction. Skipping a question honestly must never damage a level,
|
|
189
|
+
* or the level becomes a reason to guess rather than to skip.
|
|
190
|
+
*/
|
|
191
|
+
export function checkPromotion(input) {
|
|
192
|
+
const { level, counts, after, minAccuracy } = input;
|
|
193
|
+
const accuracy = counts.answered > 0 ? Number((counts.passed / counts.answered).toFixed(6)) : 0;
|
|
194
|
+
const to = nextLevel(level);
|
|
195
|
+
const unmet = [];
|
|
196
|
+
if (!to)
|
|
197
|
+
unmet.push('max_level');
|
|
198
|
+
if (counts.passed < after)
|
|
199
|
+
unmet.push('answers');
|
|
200
|
+
if (accuracy < minAccuracy)
|
|
201
|
+
unmet.push('accuracy');
|
|
202
|
+
if (counts.concepts < requiredConcepts(after))
|
|
203
|
+
unmet.push('concepts');
|
|
204
|
+
return { promote: unmet.length === 0, to: unmet.length === 0 ? to : null, accuracy, unmet };
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=srs.js.map
|
package/dist/srs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"srs.js","sourceRoot":"","sources":["../src/srs.ts"],"names":[],"mappings":"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"}
|