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.
Files changed (95) hide show
  1. package/README.md +69 -0
  2. package/dist/ask.js +58 -0
  3. package/dist/ask.js.map +1 -0
  4. package/dist/assets/dashboard.html +407 -0
  5. package/dist/assets/tokens.css +135 -0
  6. package/dist/assets/tutor-skill.md +345 -0
  7. package/dist/cli.js +223 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/concurrency.js +41 -0
  10. package/dist/concurrency.js.map +1 -0
  11. package/dist/config.js +158 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/dashboard.js +214 -0
  14. package/dist/dashboard.js.map +1 -0
  15. package/dist/db.js +26 -0
  16. package/dist/db.js.map +1 -0
  17. package/dist/hooks/checkpoint-quiz.js +152 -0
  18. package/dist/hooks/checkpoint-quiz.js.map +1 -0
  19. package/dist/hooks/lib.js +177 -0
  20. package/dist/hooks/lib.js.map +1 -0
  21. package/dist/hooks/pre-tool-gate.js +53 -0
  22. package/dist/hooks/pre-tool-gate.js.map +1 -0
  23. package/dist/hooks/session-start.js +128 -0
  24. package/dist/hooks/session-start.js.map +1 -0
  25. package/dist/hooks/stop-quiz-check.js +147 -0
  26. package/dist/hooks/stop-quiz-check.js.map +1 -0
  27. package/dist/install.js +313 -0
  28. package/dist/install.js.map +1 -0
  29. package/dist/mcq.js +34 -0
  30. package/dist/mcq.js.map +1 -0
  31. package/dist/migrate.js +45 -0
  32. package/dist/migrate.js.map +1 -0
  33. package/dist/migrations/001_init.sql +71 -0
  34. package/dist/migrations/002_stop_markers.sql +14 -0
  35. package/dist/migrations/003_gate_repo.sql +5 -0
  36. package/dist/migrations/004_attempt_outcome.sql +15 -0
  37. package/dist/migrations/005_session_concept_origin.sql +24 -0
  38. package/dist/migrations/006_attempt_format.sql +31 -0
  39. package/dist/migrations/007_checkpoints.sql +23 -0
  40. package/dist/migrations/008_difficulty_levels.sql +57 -0
  41. package/dist/paths.js +25 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/plugin/.claude-plugin/marketplace.json +14 -0
  44. package/dist/plugin/.claude-plugin/plugin.json +17 -0
  45. package/dist/plugin/.mcp.json +11 -0
  46. package/dist/plugin/agents/tutor.md +63 -0
  47. package/dist/plugin/cli/eklavya-gate +67 -0
  48. package/dist/plugin/hooks/hooks.json +57 -0
  49. package/dist/plugin/hooks/run.mjs +169 -0
  50. package/dist/plugin/scripts/install-git-hook.sh +81 -0
  51. package/dist/plugin/skills/gate/SKILL.md +17 -0
  52. package/dist/plugin/skills/learn/SKILL.md +28 -0
  53. package/dist/plugin/skills/level/SKILL.md +53 -0
  54. package/dist/plugin/skills/mode/SKILL.md +56 -0
  55. package/dist/plugin/skills/progress/SKILL.md +72 -0
  56. package/dist/plugin/skills/quiz/SKILL.md +31 -0
  57. package/dist/plugin/skills/setup/SKILL.md +49 -0
  58. package/dist/plugin/skills/tutor/SKILL.md +345 -0
  59. package/dist/seed/git.json +49 -0
  60. package/dist/seed/node-backend.json +43 -0
  61. package/dist/seed/react.json +46 -0
  62. package/dist/seed/web-auth.json +78 -0
  63. package/dist/seed.js +112 -0
  64. package/dist/seed.js.map +1 -0
  65. package/dist/server.js +52 -0
  66. package/dist/server.js.map +1 -0
  67. package/dist/session.js +24 -0
  68. package/dist/session.js.map +1 -0
  69. package/dist/slug.js +75 -0
  70. package/dist/slug.js.map +1 -0
  71. package/dist/srs.js +206 -0
  72. package/dist/srs.js.map +1 -0
  73. package/dist/store.js +450 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/tools/config_tools.js +119 -0
  76. package/dist/tools/config_tools.js.map +1 -0
  77. package/dist/tools/get_concept_graph.js +104 -0
  78. package/dist/tools/get_concept_graph.js.map +1 -0
  79. package/dist/tools/get_gate_status.js +20 -0
  80. package/dist/tools/get_gate_status.js.map +1 -0
  81. package/dist/tools/get_learner_profile.js +142 -0
  82. package/dist/tools/get_learner_profile.js.map +1 -0
  83. package/dist/tools/get_session_quiz_plan.js +393 -0
  84. package/dist/tools/get_session_quiz_plan.js.map +1 -0
  85. package/dist/tools/index.js +45 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/log_session_concepts.js +103 -0
  88. package/dist/tools/log_session_concepts.js.map +1 -0
  89. package/dist/tools/record_attempt.js +158 -0
  90. package/dist/tools/record_attempt.js.map +1 -0
  91. package/dist/tools/types.js +4 -0
  92. package/dist/tools/types.js.map +1 -0
  93. package/dist/tools/upsert_concepts.js +89 -0
  94. package/dist/tools/upsert_concepts.js.map +1 -0
  95. 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
@@ -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"}
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"srs.js","sourceRoot":"","sources":["../src/srs.ts"],"names":[],"mappings":"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"}