learngraph 0.1.1 → 0.3.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 (150) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +165 -3
  3. package/dist/cjs/llm/adapters/anthropic.js +124 -0
  4. package/dist/cjs/llm/adapters/anthropic.js.map +1 -0
  5. package/dist/cjs/llm/adapters/base.js +100 -0
  6. package/dist/cjs/llm/adapters/base.js.map +1 -0
  7. package/dist/cjs/llm/adapters/index.js +22 -0
  8. package/dist/cjs/llm/adapters/index.js.map +1 -0
  9. package/dist/cjs/llm/adapters/ollama.js +149 -0
  10. package/dist/cjs/llm/adapters/ollama.js.map +1 -0
  11. package/dist/cjs/llm/adapters/openai.js +126 -0
  12. package/dist/cjs/llm/adapters/openai.js.map +1 -0
  13. package/dist/cjs/llm/index.js +34 -5
  14. package/dist/cjs/llm/index.js.map +1 -1
  15. package/dist/cjs/llm/orchestrator.js +219 -0
  16. package/dist/cjs/llm/orchestrator.js.map +1 -0
  17. package/dist/cjs/llm/prompts.js +367 -0
  18. package/dist/cjs/llm/prompts.js.map +1 -0
  19. package/dist/cjs/parsers/base.js +189 -0
  20. package/dist/cjs/parsers/base.js.map +1 -0
  21. package/dist/cjs/parsers/demo.js +159 -0
  22. package/dist/cjs/parsers/demo.js.map +1 -0
  23. package/dist/cjs/parsers/extractor.js +191 -0
  24. package/dist/cjs/parsers/extractor.js.map +1 -0
  25. package/dist/cjs/parsers/index.js +43 -4
  26. package/dist/cjs/parsers/index.js.map +1 -1
  27. package/dist/cjs/parsers/json.js +157 -0
  28. package/dist/cjs/parsers/json.js.map +1 -0
  29. package/dist/cjs/parsers/markdown.js +168 -0
  30. package/dist/cjs/parsers/markdown.js.map +1 -0
  31. package/dist/cjs/parsers/samples.js +139 -0
  32. package/dist/cjs/parsers/samples.js.map +1 -0
  33. package/dist/cjs/storage/base.js +231 -0
  34. package/dist/cjs/storage/base.js.map +1 -0
  35. package/dist/cjs/storage/errors.js +128 -0
  36. package/dist/cjs/storage/errors.js.map +1 -0
  37. package/dist/cjs/storage/index.js +92 -5
  38. package/dist/cjs/storage/index.js.map +1 -1
  39. package/dist/cjs/storage/levelgraph.js +855 -0
  40. package/dist/cjs/storage/levelgraph.js.map +1 -0
  41. package/dist/cjs/storage/memory.js +447 -0
  42. package/dist/cjs/storage/memory.js.map +1 -0
  43. package/dist/cjs/storage/neo4j.js +866 -0
  44. package/dist/cjs/storage/neo4j.js.map +1 -0
  45. package/dist/cjs/storage/seeds.js +565 -0
  46. package/dist/cjs/storage/seeds.js.map +1 -0
  47. package/dist/cjs/types/llm.js +8 -0
  48. package/dist/cjs/types/llm.js.map +1 -0
  49. package/dist/cjs/types/parser.js +8 -0
  50. package/dist/cjs/types/parser.js.map +1 -0
  51. package/dist/esm/llm/adapters/anthropic.js +119 -0
  52. package/dist/esm/llm/adapters/anthropic.js.map +1 -0
  53. package/dist/esm/llm/adapters/base.js +95 -0
  54. package/dist/esm/llm/adapters/base.js.map +1 -0
  55. package/dist/esm/llm/adapters/index.js +10 -0
  56. package/dist/esm/llm/adapters/index.js.map +1 -0
  57. package/dist/esm/llm/adapters/ollama.js +144 -0
  58. package/dist/esm/llm/adapters/ollama.js.map +1 -0
  59. package/dist/esm/llm/adapters/openai.js +121 -0
  60. package/dist/esm/llm/adapters/openai.js.map +1 -0
  61. package/dist/esm/llm/index.js +12 -6
  62. package/dist/esm/llm/index.js.map +1 -1
  63. package/dist/esm/llm/orchestrator.js +214 -0
  64. package/dist/esm/llm/orchestrator.js.map +1 -0
  65. package/dist/esm/llm/prompts.js +360 -0
  66. package/dist/esm/llm/prompts.js.map +1 -0
  67. package/dist/esm/parsers/base.js +179 -0
  68. package/dist/esm/parsers/base.js.map +1 -0
  69. package/dist/esm/parsers/demo.js +154 -0
  70. package/dist/esm/parsers/demo.js.map +1 -0
  71. package/dist/esm/parsers/extractor.js +187 -0
  72. package/dist/esm/parsers/extractor.js.map +1 -0
  73. package/dist/esm/parsers/index.js +24 -5
  74. package/dist/esm/parsers/index.js.map +1 -1
  75. package/dist/esm/parsers/json.js +153 -0
  76. package/dist/esm/parsers/json.js.map +1 -0
  77. package/dist/esm/parsers/markdown.js +164 -0
  78. package/dist/esm/parsers/markdown.js.map +1 -0
  79. package/dist/esm/parsers/samples.js +136 -0
  80. package/dist/esm/parsers/samples.js.map +1 -0
  81. package/dist/esm/storage/base.js +221 -0
  82. package/dist/esm/storage/base.js.map +1 -0
  83. package/dist/esm/storage/errors.js +116 -0
  84. package/dist/esm/storage/errors.js.map +1 -0
  85. package/dist/esm/storage/index.js +71 -6
  86. package/dist/esm/storage/index.js.map +1 -1
  87. package/dist/esm/storage/levelgraph.js +818 -0
  88. package/dist/esm/storage/levelgraph.js.map +1 -0
  89. package/dist/esm/storage/memory.js +443 -0
  90. package/dist/esm/storage/memory.js.map +1 -0
  91. package/dist/esm/storage/neo4j.js +829 -0
  92. package/dist/esm/storage/neo4j.js.map +1 -0
  93. package/dist/esm/storage/seeds.js +561 -0
  94. package/dist/esm/storage/seeds.js.map +1 -0
  95. package/dist/esm/types/llm.js +7 -0
  96. package/dist/esm/types/llm.js.map +1 -0
  97. package/dist/esm/types/parser.js +7 -0
  98. package/dist/esm/types/parser.js.map +1 -0
  99. package/dist/types/llm/adapters/anthropic.d.ts +21 -0
  100. package/dist/types/llm/adapters/anthropic.d.ts.map +1 -0
  101. package/dist/types/llm/adapters/base.d.ts +46 -0
  102. package/dist/types/llm/adapters/base.d.ts.map +1 -0
  103. package/dist/types/llm/adapters/index.d.ts +11 -0
  104. package/dist/types/llm/adapters/index.d.ts.map +1 -0
  105. package/dist/types/llm/adapters/ollama.d.ts +30 -0
  106. package/dist/types/llm/adapters/ollama.d.ts.map +1 -0
  107. package/dist/types/llm/adapters/openai.d.ts +22 -0
  108. package/dist/types/llm/adapters/openai.d.ts.map +1 -0
  109. package/dist/types/llm/index.d.ts +5 -0
  110. package/dist/types/llm/index.d.ts.map +1 -1
  111. package/dist/types/llm/orchestrator.d.ts +35 -0
  112. package/dist/types/llm/orchestrator.d.ts.map +1 -0
  113. package/dist/types/llm/prompts.d.ts +269 -0
  114. package/dist/types/llm/prompts.d.ts.map +1 -0
  115. package/dist/types/parsers/base.d.ts +39 -0
  116. package/dist/types/parsers/base.d.ts.map +1 -0
  117. package/dist/types/parsers/demo.d.ts +87 -0
  118. package/dist/types/parsers/demo.d.ts.map +1 -0
  119. package/dist/types/parsers/extractor.d.ts +43 -0
  120. package/dist/types/parsers/extractor.d.ts.map +1 -0
  121. package/dist/types/parsers/index.d.ts +10 -0
  122. package/dist/types/parsers/index.d.ts.map +1 -1
  123. package/dist/types/parsers/json.d.ts +71 -0
  124. package/dist/types/parsers/json.d.ts.map +1 -0
  125. package/dist/types/parsers/markdown.d.ts +43 -0
  126. package/dist/types/parsers/markdown.d.ts.map +1 -0
  127. package/dist/types/parsers/samples.d.ts +27 -0
  128. package/dist/types/parsers/samples.d.ts.map +1 -0
  129. package/dist/types/storage/base.d.ts +39 -0
  130. package/dist/types/storage/base.d.ts.map +1 -0
  131. package/dist/types/storage/errors.d.ts +74 -0
  132. package/dist/types/storage/errors.d.ts.map +1 -0
  133. package/dist/types/storage/index.d.ts +50 -2
  134. package/dist/types/storage/index.d.ts.map +1 -1
  135. package/dist/types/storage/levelgraph.d.ts +92 -0
  136. package/dist/types/storage/levelgraph.d.ts.map +1 -0
  137. package/dist/types/storage/memory.d.ts +70 -0
  138. package/dist/types/storage/memory.d.ts.map +1 -0
  139. package/dist/types/storage/neo4j.d.ts +88 -0
  140. package/dist/types/storage/neo4j.d.ts.map +1 -0
  141. package/dist/types/storage/seeds.d.ts +27 -0
  142. package/dist/types/storage/seeds.d.ts.map +1 -0
  143. package/dist/types/types/index.d.ts +2 -0
  144. package/dist/types/types/index.d.ts.map +1 -1
  145. package/dist/types/types/llm.d.ts +298 -0
  146. package/dist/types/types/llm.d.ts.map +1 -0
  147. package/dist/types/types/parser.d.ts +208 -0
  148. package/dist/types/types/parser.d.ts.map +1 -0
  149. package/package.json +4 -2
  150. package/scripts/postinstall.js +68 -0
@@ -0,0 +1,818 @@
1
+ /**
2
+ * LevelGraph storage adapter
3
+ *
4
+ * Browser-compatible graph storage using LevelGraph (triple store)
5
+ * built on LevelDB/IndexedDB.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import { GRAPH_VERSION, createSkillId, createEdgeId } from '../types/index.js';
10
+ import { NotFoundError, DuplicateError, ReferenceError as StorageReferenceError, ConnectionError, } from './errors.js';
11
+ import { nowISO, inputToSkillNode, inputToEdge, validateSkillInput, validateEdgeInput, requireConnection, calculateStats, } from './base.js';
12
+ /**
13
+ * Skill property predicates for triple mapping
14
+ */
15
+ const SKILL_PREDICATES = {
16
+ TYPE: 'rdf:type',
17
+ NAME: 'lg:name',
18
+ DESCRIPTION: 'lg:description',
19
+ BLOOM_LEVEL: 'lg:bloomLevel',
20
+ DIFFICULTY: 'lg:difficulty',
21
+ IS_THRESHOLD: 'lg:isThresholdConcept',
22
+ MASTERY_THRESHOLD: 'lg:masteryThreshold',
23
+ ESTIMATED_MINUTES: 'lg:estimatedMinutes',
24
+ TAGS: 'lg:tags',
25
+ STANDARD_ALIGNMENT: 'lg:standardAlignment',
26
+ DOMAIN: 'lg:domain',
27
+ GRADE_LEVEL: 'lg:gradeLevel',
28
+ METADATA: 'lg:metadata',
29
+ CREATED_AT: 'lg:createdAt',
30
+ UPDATED_AT: 'lg:updatedAt',
31
+ };
32
+ /**
33
+ * Edge property predicates for triple mapping
34
+ */
35
+ const EDGE_PREDICATES = {
36
+ TYPE: 'rdf:type',
37
+ PREREQUISITE_OF: 'lg:prerequisiteOf',
38
+ EDGE_STRENGTH: 'lg:strength',
39
+ EDGE_TYPE: 'lg:edgeType',
40
+ REASONING: 'lg:reasoning',
41
+ METADATA: 'lg:edgeMetadata',
42
+ CREATED_AT: 'lg:edgeCreatedAt',
43
+ SOURCE: 'lg:source',
44
+ TARGET: 'lg:target',
45
+ };
46
+ /**
47
+ * LevelGraph storage adapter.
48
+ *
49
+ * Stores skill graphs using RDF-style triples, compatible with
50
+ * LevelDB (Node.js) and IndexedDB (browser).
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { LevelGraphStorage } from 'learngraph/storage';
55
+ * import levelgraph from 'levelgraph';
56
+ * import level from 'level';
57
+ *
58
+ * // Node.js usage
59
+ * const storage = new LevelGraphStorage();
60
+ * await storage.connect({
61
+ * backend: 'levelgraph',
62
+ * path: './my-graph-db',
63
+ * });
64
+ *
65
+ * // Browser usage (with level-js)
66
+ * await storage.connect({
67
+ * backend: 'levelgraph',
68
+ * dbName: 'my-graph-db',
69
+ * });
70
+ * ```
71
+ */
72
+ export class LevelGraphStorage {
73
+ db = null;
74
+ _connected = false;
75
+ config = null;
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+ // Triple Conversion Utilities
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ /**
80
+ * Convert a SkillNode to triples
81
+ */
82
+ skillToTriples(skill) {
83
+ const id = skill.id;
84
+ const triples = [
85
+ { subject: id, predicate: SKILL_PREDICATES.TYPE, object: 'Skill' },
86
+ { subject: id, predicate: SKILL_PREDICATES.NAME, object: skill.name },
87
+ { subject: id, predicate: SKILL_PREDICATES.DESCRIPTION, object: skill.description },
88
+ { subject: id, predicate: SKILL_PREDICATES.BLOOM_LEVEL, object: skill.bloomLevel },
89
+ { subject: id, predicate: SKILL_PREDICATES.DIFFICULTY, object: String(skill.difficulty) },
90
+ { subject: id, predicate: SKILL_PREDICATES.IS_THRESHOLD, object: String(skill.isThresholdConcept) },
91
+ { subject: id, predicate: SKILL_PREDICATES.MASTERY_THRESHOLD, object: String(skill.masteryThreshold) },
92
+ { subject: id, predicate: SKILL_PREDICATES.ESTIMATED_MINUTES, object: String(skill.estimatedMinutes) },
93
+ { subject: id, predicate: SKILL_PREDICATES.TAGS, object: JSON.stringify(skill.tags) },
94
+ { subject: id, predicate: SKILL_PREDICATES.METADATA, object: JSON.stringify(skill.metadata) },
95
+ { subject: id, predicate: SKILL_PREDICATES.CREATED_AT, object: skill.createdAt },
96
+ { subject: id, predicate: SKILL_PREDICATES.UPDATED_AT, object: skill.updatedAt },
97
+ ];
98
+ if (skill.standardAlignment) {
99
+ triples.push({
100
+ subject: id,
101
+ predicate: SKILL_PREDICATES.STANDARD_ALIGNMENT,
102
+ object: JSON.stringify(skill.standardAlignment),
103
+ });
104
+ }
105
+ if (skill.domain) {
106
+ triples.push({
107
+ subject: id,
108
+ predicate: SKILL_PREDICATES.DOMAIN,
109
+ object: skill.domain,
110
+ });
111
+ }
112
+ if (skill.gradeLevel) {
113
+ triples.push({
114
+ subject: id,
115
+ predicate: SKILL_PREDICATES.GRADE_LEVEL,
116
+ object: skill.gradeLevel,
117
+ });
118
+ }
119
+ return triples;
120
+ }
121
+ /**
122
+ * Convert triples back to a SkillNode
123
+ */
124
+ triplesToSkill(triples) {
125
+ if (triples.length === 0)
126
+ return null;
127
+ const props = {};
128
+ const id = triples[0]?.subject;
129
+ if (!id)
130
+ return null;
131
+ for (const triple of triples) {
132
+ props[triple.predicate] = triple.object;
133
+ }
134
+ const tagsStr = props[SKILL_PREDICATES.TAGS];
135
+ const metadataStr = props[SKILL_PREDICATES.METADATA];
136
+ const node = {
137
+ id: createSkillId(id),
138
+ name: props[SKILL_PREDICATES.NAME] ?? '',
139
+ description: props[SKILL_PREDICATES.DESCRIPTION] ?? '',
140
+ bloomLevel: (props[SKILL_PREDICATES.BLOOM_LEVEL] ?? 'remember'),
141
+ difficulty: parseFloat(props[SKILL_PREDICATES.DIFFICULTY] ?? '0.5'),
142
+ isThresholdConcept: props[SKILL_PREDICATES.IS_THRESHOLD] === 'true',
143
+ masteryThreshold: parseFloat(props[SKILL_PREDICATES.MASTERY_THRESHOLD] ?? '0.8'),
144
+ estimatedMinutes: parseInt(props[SKILL_PREDICATES.ESTIMATED_MINUTES] ?? '30', 10),
145
+ tags: tagsStr ? JSON.parse(tagsStr) : [],
146
+ metadata: metadataStr ? JSON.parse(metadataStr) : {},
147
+ createdAt: props[SKILL_PREDICATES.CREATED_AT] ?? nowISO(),
148
+ updatedAt: props[SKILL_PREDICATES.UPDATED_AT] ?? nowISO(),
149
+ };
150
+ // Only add optional fields if they have values
151
+ const standardAlignmentStr = props[SKILL_PREDICATES.STANDARD_ALIGNMENT];
152
+ const domainStr = props[SKILL_PREDICATES.DOMAIN];
153
+ const gradeLevelStr = props[SKILL_PREDICATES.GRADE_LEVEL];
154
+ if (standardAlignmentStr) {
155
+ node.standardAlignment = JSON.parse(standardAlignmentStr);
156
+ }
157
+ if (domainStr) {
158
+ node.domain = domainStr;
159
+ }
160
+ if (gradeLevelStr) {
161
+ node.gradeLevel = gradeLevelStr;
162
+ }
163
+ return node;
164
+ }
165
+ /**
166
+ * Convert a PrerequisiteEdge to triples
167
+ */
168
+ edgeToTriples(edge) {
169
+ const id = edge.id;
170
+ const triples = [
171
+ { subject: id, predicate: EDGE_PREDICATES.TYPE, object: 'Edge' },
172
+ { subject: id, predicate: EDGE_PREDICATES.SOURCE, object: edge.sourceId },
173
+ { subject: id, predicate: EDGE_PREDICATES.TARGET, object: edge.targetId },
174
+ { subject: id, predicate: EDGE_PREDICATES.EDGE_STRENGTH, object: String(edge.strength) },
175
+ { subject: id, predicate: EDGE_PREDICATES.EDGE_TYPE, object: edge.type },
176
+ { subject: id, predicate: EDGE_PREDICATES.METADATA, object: JSON.stringify(edge.metadata) },
177
+ { subject: id, predicate: EDGE_PREDICATES.CREATED_AT, object: edge.createdAt },
178
+ // Store the relationship triple
179
+ {
180
+ subject: edge.sourceId,
181
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
182
+ object: edge.targetId,
183
+ },
184
+ ];
185
+ if (edge.reasoning) {
186
+ triples.push({
187
+ subject: id,
188
+ predicate: EDGE_PREDICATES.REASONING,
189
+ object: edge.reasoning,
190
+ });
191
+ }
192
+ return triples;
193
+ }
194
+ /**
195
+ * Convert triples back to a PrerequisiteEdge
196
+ */
197
+ triplesToEdge(triples) {
198
+ if (triples.length === 0)
199
+ return null;
200
+ const props = {};
201
+ const id = triples[0]?.subject;
202
+ if (!id)
203
+ return null;
204
+ for (const triple of triples) {
205
+ props[triple.predicate] = triple.object;
206
+ }
207
+ const metadataStr = props[EDGE_PREDICATES.METADATA];
208
+ const reasoningStr = props[EDGE_PREDICATES.REASONING];
209
+ const edge = {
210
+ id: createEdgeId(id),
211
+ sourceId: createSkillId(props[EDGE_PREDICATES.SOURCE] ?? ''),
212
+ targetId: createSkillId(props[EDGE_PREDICATES.TARGET] ?? ''),
213
+ strength: parseFloat(props[EDGE_PREDICATES.EDGE_STRENGTH] ?? '0.5'),
214
+ type: (props[EDGE_PREDICATES.EDGE_TYPE] ?? 'soft'),
215
+ metadata: metadataStr ? JSON.parse(metadataStr) : {},
216
+ createdAt: props[EDGE_PREDICATES.CREATED_AT] ?? nowISO(),
217
+ };
218
+ // Only add optional fields if they have values
219
+ if (reasoningStr) {
220
+ edge.reasoning = reasoningStr;
221
+ }
222
+ return edge;
223
+ }
224
+ // ─────────────────────────────────────────────────────────────────────────────
225
+ // Promisified LevelGraph Operations
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ put(triples) {
228
+ return new Promise((resolve, reject) => {
229
+ if (!this.db)
230
+ return reject(new Error('Not connected'));
231
+ this.db.put(triples, (err) => {
232
+ if (err)
233
+ reject(err);
234
+ else
235
+ resolve();
236
+ });
237
+ });
238
+ }
239
+ del(triples) {
240
+ return new Promise((resolve, reject) => {
241
+ if (!this.db)
242
+ return reject(new Error('Not connected'));
243
+ this.db.del(triples, (err) => {
244
+ if (err)
245
+ reject(err);
246
+ else
247
+ resolve();
248
+ });
249
+ });
250
+ }
251
+ get(pattern) {
252
+ return new Promise((resolve, reject) => {
253
+ if (!this.db)
254
+ return reject(new Error('Not connected'));
255
+ this.db.get(pattern, (err, triples) => {
256
+ if (err)
257
+ reject(err);
258
+ else
259
+ resolve(triples);
260
+ });
261
+ });
262
+ }
263
+ // ─────────────────────────────────────────────────────────────────────────────
264
+ // Connection Management
265
+ // ─────────────────────────────────────────────────────────────────────────────
266
+ async connect(config) {
267
+ if (config.backend !== 'levelgraph') {
268
+ throw new Error('LevelGraphStorage only supports "levelgraph" backend');
269
+ }
270
+ this.config = config;
271
+ try {
272
+ // Dynamic import of levelgraph and level
273
+ const levelgraph = await import('levelgraph');
274
+ const level = await import('level');
275
+ const dbPath = this.config.path || this.config.dbName || './learngraph-db';
276
+ const leveldb = new level.Level(dbPath);
277
+ // Cast the result to our LevelGraphDB interface
278
+ this.db = levelgraph.default(leveldb);
279
+ this._connected = true;
280
+ }
281
+ catch (error) {
282
+ throw new ConnectionError(`Failed to connect to LevelGraph: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
283
+ }
284
+ }
285
+ async disconnect() {
286
+ this.db = null;
287
+ this._connected = false;
288
+ }
289
+ isConnected() {
290
+ return this._connected;
291
+ }
292
+ async getStatus() {
293
+ const startTime = Date.now();
294
+ if (!this._connected || !this.db) {
295
+ return {
296
+ connected: false,
297
+ backend: 'levelgraph',
298
+ lastChecked: new Date(),
299
+ error: 'Not connected',
300
+ };
301
+ }
302
+ try {
303
+ // Simple health check - try to get any triple
304
+ await this.get({ predicate: SKILL_PREDICATES.TYPE });
305
+ return {
306
+ connected: true,
307
+ backend: 'levelgraph',
308
+ latencyMs: Date.now() - startTime,
309
+ lastChecked: new Date(),
310
+ };
311
+ }
312
+ catch (error) {
313
+ return {
314
+ connected: false,
315
+ backend: 'levelgraph',
316
+ lastChecked: new Date(),
317
+ error: error instanceof Error ? error.message : String(error),
318
+ };
319
+ }
320
+ }
321
+ // ─────────────────────────────────────────────────────────────────────────────
322
+ // Skill Node CRUD
323
+ // ─────────────────────────────────────────────────────────────────────────────
324
+ async createSkill(input) {
325
+ requireConnection(this._connected);
326
+ validateSkillInput(input);
327
+ const skill = inputToSkillNode(input);
328
+ // Check for duplicate
329
+ const existing = await this.get({
330
+ subject: skill.id,
331
+ predicate: SKILL_PREDICATES.TYPE,
332
+ object: 'Skill',
333
+ });
334
+ if (existing.length > 0) {
335
+ throw new DuplicateError('skill', skill.id);
336
+ }
337
+ const triples = this.skillToTriples(skill);
338
+ await this.put(triples);
339
+ return skill;
340
+ }
341
+ async getSkill(id) {
342
+ requireConnection(this._connected);
343
+ const triples = await this.get({ subject: id });
344
+ if (triples.length === 0)
345
+ return null;
346
+ // Check if it's actually a Skill
347
+ const typeTriple = triples.find((t) => t.predicate === SKILL_PREDICATES.TYPE && t.object === 'Skill');
348
+ if (!typeTriple)
349
+ return null;
350
+ return this.triplesToSkill(triples);
351
+ }
352
+ async getSkills(ids) {
353
+ requireConnection(this._connected);
354
+ const skills = [];
355
+ for (const id of ids) {
356
+ const skill = await this.getSkill(id);
357
+ if (skill)
358
+ skills.push(skill);
359
+ }
360
+ return skills;
361
+ }
362
+ async updateSkill(id, updates) {
363
+ requireConnection(this._connected);
364
+ const existing = await this.getSkill(id);
365
+ if (!existing) {
366
+ throw new NotFoundError('skill', id);
367
+ }
368
+ // Delete old triples
369
+ const oldTriples = await this.get({ subject: id });
370
+ await this.del(oldTriples.filter((t) => t.predicate.startsWith('lg:')));
371
+ // Create updated skill
372
+ const updated = {
373
+ ...existing,
374
+ ...updates,
375
+ id: existing.id,
376
+ createdAt: existing.createdAt,
377
+ updatedAt: nowISO(),
378
+ };
379
+ // Insert new triples
380
+ const newTriples = this.skillToTriples(updated);
381
+ await this.put(newTriples);
382
+ return updated;
383
+ }
384
+ async deleteSkill(id) {
385
+ requireConnection(this._connected);
386
+ // Delete skill triples
387
+ const skillTriples = await this.get({ subject: id });
388
+ if (skillTriples.length > 0) {
389
+ await this.del(skillTriples);
390
+ }
391
+ // Delete edges referencing this skill
392
+ const sourceEdges = await this.get({
393
+ predicate: EDGE_PREDICATES.SOURCE,
394
+ object: id,
395
+ });
396
+ const targetEdges = await this.get({
397
+ predicate: EDGE_PREDICATES.TARGET,
398
+ object: id,
399
+ });
400
+ for (const edge of [...sourceEdges, ...targetEdges]) {
401
+ const edgeTriples = await this.get({ subject: edge.subject });
402
+ await this.del(edgeTriples);
403
+ }
404
+ // Delete prerequisiteOf relationships
405
+ const prereqTriples = await this.get({
406
+ subject: id,
407
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
408
+ });
409
+ if (prereqTriples.length > 0) {
410
+ await this.del(prereqTriples);
411
+ }
412
+ const depTriples = await this.get({
413
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
414
+ object: id,
415
+ });
416
+ if (depTriples.length > 0) {
417
+ await this.del(depTriples);
418
+ }
419
+ }
420
+ async findSkills(query) {
421
+ requireConnection(this._connected);
422
+ // Get all skills
423
+ const typeTriples = await this.get({
424
+ predicate: SKILL_PREDICATES.TYPE,
425
+ object: 'Skill',
426
+ });
427
+ const skills = [];
428
+ for (const typeTriple of typeTriples) {
429
+ const skill = await this.getSkill(createSkillId(typeTriple.subject));
430
+ if (skill)
431
+ skills.push(skill);
432
+ }
433
+ // Apply filters in memory (LevelGraph doesn't support complex queries)
434
+ let results = skills;
435
+ if (query.filters) {
436
+ for (const filter of query.filters) {
437
+ results = results.filter((skill) => {
438
+ const value = skill[filter.field];
439
+ switch (filter.operator) {
440
+ case 'eq':
441
+ return value === filter.value;
442
+ case 'neq':
443
+ return value !== filter.value;
444
+ case 'gt':
445
+ return typeof value === 'number' && value > filter.value;
446
+ case 'gte':
447
+ return typeof value === 'number' && value >= filter.value;
448
+ case 'lt':
449
+ return typeof value === 'number' && value < filter.value;
450
+ case 'lte':
451
+ return typeof value === 'number' && value <= filter.value;
452
+ case 'in':
453
+ return Array.isArray(filter.value) && filter.value.includes(value);
454
+ case 'contains':
455
+ if (Array.isArray(value)) {
456
+ return value.includes(filter.value);
457
+ }
458
+ if (typeof value === 'string') {
459
+ return value.includes(filter.value);
460
+ }
461
+ return false;
462
+ default:
463
+ return true;
464
+ }
465
+ });
466
+ }
467
+ }
468
+ // Apply sorting
469
+ if (query.sorting && query.sorting.length > 0) {
470
+ results.sort((a, b) => {
471
+ for (const sortItem of query.sorting) {
472
+ const aVal = a[sortItem.field];
473
+ const bVal = b[sortItem.field];
474
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
475
+ if (cmp !== 0) {
476
+ return sortItem.direction === 'asc' ? cmp : -cmp;
477
+ }
478
+ }
479
+ return 0;
480
+ });
481
+ }
482
+ // Apply pagination
483
+ if (query.pagination) {
484
+ const { offset = 0, limit = 100 } = query.pagination;
485
+ results = results.slice(offset, offset + limit);
486
+ }
487
+ return results;
488
+ }
489
+ async countSkills(query) {
490
+ requireConnection(this._connected);
491
+ if (!query) {
492
+ const typeTriples = await this.get({
493
+ predicate: SKILL_PREDICATES.TYPE,
494
+ object: 'Skill',
495
+ });
496
+ return typeTriples.length;
497
+ }
498
+ const { pagination, ...queryWithoutPagination } = query;
499
+ const results = await this.findSkills(queryWithoutPagination);
500
+ return results.length;
501
+ }
502
+ // ─────────────────────────────────────────────────────────────────────────────
503
+ // Prerequisite Edge CRUD
504
+ // ─────────────────────────────────────────────────────────────────────────────
505
+ async createPrerequisite(input) {
506
+ requireConnection(this._connected);
507
+ validateEdgeInput(input);
508
+ // Verify source and target exist
509
+ const source = await this.getSkill(input.sourceId);
510
+ if (!source) {
511
+ throw new StorageReferenceError(input.id || 'new', input.sourceId, 'source');
512
+ }
513
+ const target = await this.getSkill(input.targetId);
514
+ if (!target) {
515
+ throw new StorageReferenceError(input.id || 'new', input.targetId, 'target');
516
+ }
517
+ const edge = inputToEdge(input);
518
+ // Check for duplicate
519
+ const existing = await this.get({
520
+ subject: edge.id,
521
+ predicate: EDGE_PREDICATES.TYPE,
522
+ object: 'Edge',
523
+ });
524
+ if (existing.length > 0) {
525
+ throw new DuplicateError('edge', edge.id);
526
+ }
527
+ const triples = this.edgeToTriples(edge);
528
+ await this.put(triples);
529
+ return edge;
530
+ }
531
+ async getPrerequisite(id) {
532
+ requireConnection(this._connected);
533
+ const triples = await this.get({ subject: id });
534
+ if (triples.length === 0)
535
+ return null;
536
+ const typeTriple = triples.find((t) => t.predicate === EDGE_PREDICATES.TYPE && t.object === 'Edge');
537
+ if (!typeTriple)
538
+ return null;
539
+ return this.triplesToEdge(triples);
540
+ }
541
+ async deletePrerequisite(id) {
542
+ requireConnection(this._connected);
543
+ const edge = await this.getPrerequisite(id);
544
+ if (!edge)
545
+ return;
546
+ // Delete edge triples
547
+ const edgeTriples = await this.get({ subject: id });
548
+ if (edgeTriples.length > 0) {
549
+ await this.del(edgeTriples);
550
+ }
551
+ // Delete the relationship triple
552
+ await this.del([
553
+ {
554
+ subject: edge.sourceId,
555
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
556
+ object: edge.targetId,
557
+ },
558
+ ]);
559
+ }
560
+ async findPrerequisites(criteria) {
561
+ requireConnection(this._connected);
562
+ // Get all edges
563
+ const typeTriples = await this.get({
564
+ predicate: EDGE_PREDICATES.TYPE,
565
+ object: 'Edge',
566
+ });
567
+ const edges = [];
568
+ for (const typeTriple of typeTriples) {
569
+ const edge = await this.getPrerequisite(createEdgeId(typeTriple.subject));
570
+ if (edge)
571
+ edges.push(edge);
572
+ }
573
+ // Apply filters
574
+ let results = edges;
575
+ if (criteria.sourceId) {
576
+ results = results.filter((e) => e.sourceId === criteria.sourceId);
577
+ }
578
+ if (criteria.targetId) {
579
+ results = results.filter((e) => e.targetId === criteria.targetId);
580
+ }
581
+ if (criteria.type) {
582
+ results = results.filter((e) => e.type === criteria.type);
583
+ }
584
+ if (criteria.minStrength !== undefined) {
585
+ results = results.filter((e) => e.strength >= criteria.minStrength);
586
+ }
587
+ return results;
588
+ }
589
+ // ─────────────────────────────────────────────────────────────────────────────
590
+ // Graph Traversal
591
+ // ─────────────────────────────────────────────────────────────────────────────
592
+ async getPrerequisitesOf(skillId) {
593
+ requireConnection(this._connected);
594
+ const prereqTriples = await this.get({
595
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
596
+ object: skillId,
597
+ });
598
+ const skills = [];
599
+ for (const triple of prereqTriples) {
600
+ const skill = await this.getSkill(createSkillId(triple.subject));
601
+ if (skill)
602
+ skills.push(skill);
603
+ }
604
+ return skills;
605
+ }
606
+ async getDependentsOf(skillId) {
607
+ requireConnection(this._connected);
608
+ const depTriples = await this.get({
609
+ subject: skillId,
610
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
611
+ });
612
+ const skills = [];
613
+ for (const triple of depTriples) {
614
+ const skill = await this.getSkill(createSkillId(triple.object));
615
+ if (skill)
616
+ skills.push(skill);
617
+ }
618
+ return skills;
619
+ }
620
+ async getSubgraph(rootId, depth) {
621
+ requireConnection(this._connected);
622
+ const visitedNodes = new Set();
623
+ const subgraphNodes = [];
624
+ const subgraphEdges = [];
625
+ const queue = [
626
+ { id: rootId, level: 0 },
627
+ ];
628
+ while (queue.length > 0) {
629
+ const { id, level } = queue.shift();
630
+ if (visitedNodes.has(id) || level > depth) {
631
+ continue;
632
+ }
633
+ visitedNodes.add(id);
634
+ const node = await this.getSkill(id);
635
+ if (node) {
636
+ subgraphNodes.push(node);
637
+ // Get prerequisites
638
+ const prereqTriples = await this.get({
639
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
640
+ object: id,
641
+ });
642
+ for (const triple of prereqTriples) {
643
+ queue.push({ id: createSkillId(triple.subject), level: level + 1 });
644
+ }
645
+ }
646
+ }
647
+ // Get all edges between visited nodes
648
+ const allEdges = await this.findPrerequisites({});
649
+ for (const edge of allEdges) {
650
+ if (visitedNodes.has(edge.sourceId) &&
651
+ visitedNodes.has(edge.targetId)) {
652
+ subgraphEdges.push(edge);
653
+ }
654
+ }
655
+ return {
656
+ rootId,
657
+ depth,
658
+ nodes: subgraphNodes,
659
+ edges: subgraphEdges,
660
+ };
661
+ }
662
+ async getRootSkills() {
663
+ requireConnection(this._connected);
664
+ const allSkills = await this.findSkills({});
665
+ const hasPrereq = new Set();
666
+ const prereqTriples = await this.get({
667
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
668
+ });
669
+ for (const triple of prereqTriples) {
670
+ hasPrereq.add(triple.object);
671
+ }
672
+ return allSkills.filter((skill) => !hasPrereq.has(skill.id));
673
+ }
674
+ async getLeafSkills() {
675
+ requireConnection(this._connected);
676
+ const allSkills = await this.findSkills({});
677
+ const hasDependents = new Set();
678
+ const prereqTriples = await this.get({
679
+ predicate: EDGE_PREDICATES.PREREQUISITE_OF,
680
+ });
681
+ for (const triple of prereqTriples) {
682
+ hasDependents.add(triple.subject);
683
+ }
684
+ return allSkills.filter((skill) => !hasDependents.has(skill.id));
685
+ }
686
+ async getPath(fromId, toId) {
687
+ requireConnection(this._connected);
688
+ const visited = new Set();
689
+ const parent = new Map();
690
+ const queue = [fromId];
691
+ visited.add(fromId);
692
+ while (queue.length > 0) {
693
+ const current = queue.shift();
694
+ if (current === toId) {
695
+ const path = [];
696
+ let node = toId;
697
+ while (node) {
698
+ const skillNode = await this.getSkill(createSkillId(node));
699
+ if (skillNode)
700
+ path.unshift(skillNode);
701
+ node = parent.get(node);
702
+ }
703
+ return path;
704
+ }
705
+ const dependents = await this.getDependentsOf(current);
706
+ for (const dep of dependents) {
707
+ const depId = dep.id;
708
+ if (!visited.has(depId)) {
709
+ visited.add(depId);
710
+ parent.set(depId, current);
711
+ queue.push(dep.id);
712
+ }
713
+ }
714
+ }
715
+ return null;
716
+ }
717
+ // ─────────────────────────────────────────────────────────────────────────────
718
+ // Bulk Operations
719
+ // ─────────────────────────────────────────────────────────────────────────────
720
+ async importGraph(nodes, edges, options) {
721
+ requireConnection(this._connected);
722
+ const startTime = Date.now();
723
+ const result = {
724
+ nodesCreated: 0,
725
+ edgesCreated: 0,
726
+ nodesSkipped: 0,
727
+ edgesSkipped: 0,
728
+ errors: [],
729
+ durationMs: 0,
730
+ };
731
+ if (options?.clearExisting) {
732
+ await this.clearAll();
733
+ }
734
+ // Import nodes
735
+ for (const nodeInput of nodes) {
736
+ try {
737
+ const skill = inputToSkillNode(nodeInput);
738
+ const existing = await this.getSkill(skill.id);
739
+ if (existing) {
740
+ result.nodesSkipped++;
741
+ }
742
+ else {
743
+ const triples = this.skillToTriples(skill);
744
+ await this.put(triples);
745
+ result.nodesCreated++;
746
+ }
747
+ }
748
+ catch (error) {
749
+ result.errors.push({
750
+ type: 'node',
751
+ id: nodeInput.id || 'unknown',
752
+ error: error instanceof Error ? error.message : String(error),
753
+ });
754
+ }
755
+ }
756
+ // Import edges
757
+ for (const edgeInput of edges) {
758
+ try {
759
+ const edge = inputToEdge(edgeInput);
760
+ const source = await this.getSkill(edge.sourceId);
761
+ if (!source) {
762
+ throw new Error(`Source skill not found: ${edge.sourceId}`);
763
+ }
764
+ const target = await this.getSkill(edge.targetId);
765
+ if (!target) {
766
+ throw new Error(`Target skill not found: ${edge.targetId}`);
767
+ }
768
+ const existing = await this.getPrerequisite(edge.id);
769
+ if (existing) {
770
+ result.edgesSkipped++;
771
+ }
772
+ else {
773
+ const triples = this.edgeToTriples(edge);
774
+ await this.put(triples);
775
+ result.edgesCreated++;
776
+ }
777
+ }
778
+ catch (error) {
779
+ result.errors.push({
780
+ type: 'edge',
781
+ id: edgeInput.id || 'unknown',
782
+ error: error instanceof Error ? error.message : String(error),
783
+ });
784
+ }
785
+ }
786
+ result.durationMs = Date.now() - startTime;
787
+ return result;
788
+ }
789
+ async exportGraph() {
790
+ requireConnection(this._connected);
791
+ const nodes = await this.findSkills({});
792
+ const edges = await this.findPrerequisites({});
793
+ return {
794
+ version: GRAPH_VERSION,
795
+ exportedAt: nowISO(),
796
+ nodes,
797
+ edges,
798
+ };
799
+ }
800
+ async clearAll() {
801
+ requireConnection(this._connected);
802
+ // Get all triples and delete them
803
+ const allTriples = await this.get({});
804
+ if (allTriples.length > 0) {
805
+ await this.del(allTriples);
806
+ }
807
+ }
808
+ // ─────────────────────────────────────────────────────────────────────────────
809
+ // Analytics
810
+ // ─────────────────────────────────────────────────────────────────────────────
811
+ async getStats() {
812
+ requireConnection(this._connected);
813
+ const nodes = await this.findSkills({});
814
+ const edges = await this.findPrerequisites({});
815
+ return calculateStats(nodes, edges);
816
+ }
817
+ }
818
+ //# sourceMappingURL=levelgraph.js.map