superlocalmemory 3.4.9 → 3.4.11

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 (52) hide show
  1. package/README.md +23 -3
  2. package/docs/cloud-backup.md +174 -0
  3. package/docs/skill-evolution.md +256 -0
  4. package/ide/hooks/tool-event-hook.sh +101 -11
  5. package/package.json +1 -1
  6. package/pyproject.toml +3 -2
  7. package/src/superlocalmemory/cli/commands.py +359 -0
  8. package/src/superlocalmemory/cli/ingest_cmd.py +81 -29
  9. package/src/superlocalmemory/cli/main.py +32 -0
  10. package/src/superlocalmemory/cli/setup_wizard.py +54 -11
  11. package/src/superlocalmemory/core/config.py +35 -0
  12. package/src/superlocalmemory/core/consolidation_engine.py +138 -0
  13. package/src/superlocalmemory/core/embedding_worker.py +1 -1
  14. package/src/superlocalmemory/core/engine.py +19 -0
  15. package/src/superlocalmemory/core/fact_consolidator.py +425 -0
  16. package/src/superlocalmemory/core/graph_pruner.py +290 -0
  17. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -3
  18. package/src/superlocalmemory/core/recall_pipeline.py +9 -0
  19. package/src/superlocalmemory/core/tier_manager.py +325 -0
  20. package/src/superlocalmemory/encoding/entity_resolver.py +96 -28
  21. package/src/superlocalmemory/evolution/__init__.py +29 -0
  22. package/src/superlocalmemory/evolution/blind_verifier.py +115 -0
  23. package/src/superlocalmemory/evolution/evolution_store.py +302 -0
  24. package/src/superlocalmemory/evolution/mutation_generator.py +181 -0
  25. package/src/superlocalmemory/evolution/skill_evolver.py +555 -0
  26. package/src/superlocalmemory/evolution/triggers.py +367 -0
  27. package/src/superlocalmemory/evolution/types.py +92 -0
  28. package/src/superlocalmemory/hooks/hook_handlers.py +13 -0
  29. package/src/superlocalmemory/infra/backup.py +63 -20
  30. package/src/superlocalmemory/infra/cloud_backup.py +703 -0
  31. package/src/superlocalmemory/learning/skill_performance_miner.py +422 -0
  32. package/src/superlocalmemory/mcp/server.py +4 -0
  33. package/src/superlocalmemory/mcp/tools_evolution.py +338 -0
  34. package/src/superlocalmemory/retrieval/engine.py +64 -4
  35. package/src/superlocalmemory/retrieval/forgetting_filter.py +22 -7
  36. package/src/superlocalmemory/retrieval/strategy.py +2 -2
  37. package/src/superlocalmemory/server/routes/backup.py +512 -8
  38. package/src/superlocalmemory/server/routes/behavioral.py +39 -17
  39. package/src/superlocalmemory/server/routes/evolution.py +213 -0
  40. package/src/superlocalmemory/server/routes/tiers.py +195 -0
  41. package/src/superlocalmemory/server/unified_daemon.py +36 -5
  42. package/src/superlocalmemory/storage/schema_v3410.py +159 -0
  43. package/src/superlocalmemory/storage/schema_v3411.py +149 -0
  44. package/src/superlocalmemory/ui/index.html +59 -3
  45. package/src/superlocalmemory/ui/js/core.js +3 -0
  46. package/src/superlocalmemory/ui/js/lifecycle.js +83 -0
  47. package/src/superlocalmemory/ui/js/ng-entities.js +27 -3
  48. package/src/superlocalmemory/ui/js/ng-shell.js +33 -0
  49. package/src/superlocalmemory/ui/js/ng-skills.js +611 -0
  50. package/src/superlocalmemory/ui/js/settings.js +311 -1
  51. package/src/superlocalmemory.egg-info/PKG-INFO +16 -1
  52. package/src/superlocalmemory.egg-info/SOURCES.txt +18 -0
@@ -0,0 +1,611 @@
1
+ // Neural Glass — Skill Evolution Tab
2
+ // Browse skill performance, evolution history, and health status (v3.4.10)
3
+ // API: /api/behavioral/assertions (category=skill_performance, skill_correlation)
4
+ // /api/behavioral/tool-events (tool_name=Skill)
5
+ // /api/entity/list (type filter for skill entities)
6
+
7
+ (function() {
8
+ 'use strict';
9
+
10
+ window.loadSkillEvolution = function() {
11
+ fetchEvolutionEngine();
12
+ fetchSkillOverview();
13
+ fetchSkillPerformance();
14
+ fetchSkillLineage();
15
+ };
16
+
17
+ function fetchEvolutionEngine() {
18
+ var container = document.getElementById('skills-overview-cards');
19
+ if (!container) return;
20
+
21
+ fetch('/api/evolution/status')
22
+ .then(function(r) { return r.json(); })
23
+ .then(function(data) {
24
+ var enabled = data.enabled || false;
25
+ var backend = data.backend || 'none';
26
+ var stats = data.stats || {};
27
+
28
+ var statusColor = enabled ? '#10b981' : '#888';
29
+ var statusText = enabled ? 'Enabled (' + backend + ')' : 'Disabled';
30
+ var statusIcon = enabled ? 'bi-lightning-charge-fill' : 'bi-lightning-charge';
31
+
32
+ var html = '<div class="card" style="padding:16px;margin-bottom:16px;border-left:3px solid ' + statusColor + '">' +
33
+ '<div style="display:flex;justify-content:space-between;align-items:center">' +
34
+ '<div>' +
35
+ '<div style="font-weight:600;font-size:1rem;margin-bottom:4px">' +
36
+ '<i class="bi ' + statusIcon + '" style="color:' + statusColor + ';margin-right:6px"></i>' +
37
+ 'Evolution Engine: ' + statusText +
38
+ '</div>' +
39
+ '<div style="font-size:0.8125rem;color:var(--bs-body-color)">' +
40
+ (enabled
41
+ ? 'Backend: <strong>' + escapeHtml(backend) + '</strong> | ' +
42
+ 'Evolved: ' + (stats.promoted || 0) + ' | ' +
43
+ 'Rejected: ' + (stats.rejected || 0) + ' | ' +
44
+ 'Budget: ' + (stats.cycle_budget_remaining || 3) + ' remaining this cycle'
45
+ : 'Enable via CLI: <code>slm config set evolution.enabled true</code> or via <code>slm setup</code>') +
46
+ '</div>' +
47
+ '</div>' +
48
+ '<div style="display:flex;gap:8px">' +
49
+ (enabled
50
+ ? '<button class="btn btn-sm btn-outline-success" onclick="triggerEvolution()"><i class="bi bi-play-fill"></i> Run Now</button>'
51
+ : '<button class="btn btn-sm btn-outline-primary" onclick="enableEvolution()"><i class="bi bi-power"></i> Enable</button>') +
52
+ '</div>' +
53
+ '</div>';
54
+
55
+ // Evolution history (if any)
56
+ if (data.recent && data.recent.length > 0) {
57
+ html += '<div style="margin-top:12px;border-top:1px solid var(--bs-border-color);padding-top:12px">' +
58
+ '<div style="font-size:0.8125rem;font-weight:600;margin-bottom:8px">Recent Evolution</div>';
59
+ data.recent.forEach(function(r) {
60
+ var sColor = r.status === 'promoted' ? '#10b981' : r.status === 'rejected' ? '#ef4444' : '#f59e0b';
61
+ html += '<div style="display:flex;justify-content:space-between;font-size:0.75rem;padding:4px 0;border-bottom:1px solid var(--bs-border-color)">' +
62
+ '<span><i class="bi bi-lightning-charge" style="color:#8b5cf6"></i> ' + escapeHtml(r.skill_name) + '</span>' +
63
+ '<span style="color:' + sColor + '">' + escapeHtml(r.status) + ' (' + escapeHtml(r.evolution_type) + ')</span>' +
64
+ '</div>';
65
+ });
66
+ html += '</div>';
67
+ }
68
+
69
+ html += '</div>';
70
+
71
+ // Insert before the overview cards
72
+ var overviewInner = document.getElementById('skills-overview-inner');
73
+ if (overviewInner) {
74
+ overviewInner.insertAdjacentHTML('beforebegin', html);
75
+ } else {
76
+ container.insertAdjacentHTML('afterbegin', html);
77
+ }
78
+ })
79
+ .catch(function() {
80
+ // API not available yet — skip silently
81
+ });
82
+ }
83
+
84
+ window.enableEvolution = function() {
85
+ fetch('/api/evolution/enable', { method: 'POST' })
86
+ .then(function(r) { return r.json(); })
87
+ .then(function(data) {
88
+ if (data.ok) {
89
+ loadSkillEvolution();
90
+ } else {
91
+ alert('Could not enable: ' + (data.error || 'unknown'));
92
+ }
93
+ })
94
+ .catch(function(err) { alert('Error: ' + err.message); });
95
+ };
96
+
97
+ window.triggerEvolution = function() {
98
+ fetch('/api/evolution/run', { method: 'POST' })
99
+ .then(function(r) { return r.json(); })
100
+ .then(function(data) {
101
+ alert('Evolution cycle: ' + (data.evolved || 0) + ' evolved, ' +
102
+ (data.rejected || 0) + ' rejected, ' + (data.candidates || 0) + ' candidates');
103
+ loadSkillEvolution();
104
+ })
105
+ .catch(function(err) { alert('Error: ' + err.message); });
106
+ };
107
+
108
+ function fetchSkillOverview() {
109
+ var el = document.getElementById('skills-overview-cards');
110
+ if (!el) return;
111
+
112
+ // Compatibility notice + ECC credit + docs links
113
+ var noticeHtml =
114
+ '<div class="card" style="padding:12px 16px;margin-bottom:16px;border-left:3px solid #8b5cf6">' +
115
+ '<div style="font-size:0.8125rem;color:var(--bs-body-color)">' +
116
+ '<i class="bi bi-info-circle" style="color:#8b5cf6;margin-right:6px"></i>' +
117
+ '<strong>Skill Evolution</strong> currently tracks <strong>Claude Code</strong> skills. ' +
118
+ 'The <code>/api/v3/tool-event</code> endpoint accepts events from any IDE client. ' +
119
+ 'Enhanced observation support available with ' +
120
+ '<a href="https://github.com/affaan-m/everything-claude-code" target="_blank" style="color:#8b5cf6">Everything Claude Code (ECC)</a> ' +
121
+ 'via <code>slm ingest --source ecc</code>.' +
122
+ '</div>' +
123
+ '<div style="font-size:0.75rem;color:var(--bs-secondary-color);margin-top:8px">' +
124
+ '<a href="https://superlocalmemory.com/skill-evolution" target="_blank" style="color:#8b5cf6;margin-right:12px"><i class="bi bi-globe"></i> Learn more</a>' +
125
+ '<a href="https://github.com/qualixar/superlocalmemory/blob/main/docs/skill-evolution.md" target="_blank" style="color:#8b5cf6"><i class="bi bi-book"></i> Documentation</a>' +
126
+ '</div>' +
127
+ '</div>';
128
+ el.innerHTML = noticeHtml + '<div id="skills-overview-inner"></div>';
129
+ el = document.getElementById('skills-overview-inner');
130
+
131
+ // Fetch tool events for Skill calls + assertions for skill_performance
132
+ Promise.all([
133
+ fetch('/api/behavioral/tool-events?tool_name=Skill&limit=500').then(function(r) { return r.json(); }),
134
+ fetch('/api/behavioral/assertions?category=skill_performance&limit=50').then(function(r) { return r.json(); }),
135
+ fetch('/api/behavioral/assertions?category=skill_correlation&limit=20').then(function(r) { return r.json(); }),
136
+ ]).then(function(results) {
137
+ var events = results[0].events || [];
138
+ var perfAssertions = results[1].assertions || [];
139
+ var corrAssertions = results[2].assertions || [];
140
+
141
+ // Count unique skills from events
142
+ var skillNames = {};
143
+ events.forEach(function(e) {
144
+ var name = extractSkillName(e);
145
+ if (name) skillNames[name] = (skillNames[name] || 0) + 1;
146
+ });
147
+
148
+ var html = '<div class="row g-3 mb-4">' +
149
+ overviewCard('Total Skill Events', events.length, 'bi-lightning-charge', 'var(--ng-accent)') +
150
+ overviewCard('Unique Skills', Object.keys(skillNames).length, 'bi-grid-3x3', '#8b5cf6') +
151
+ overviewCard('Performance Assertions', perfAssertions.length, 'bi-graph-up', '#10b981') +
152
+ overviewCard('Skill Correlations', corrAssertions.length, 'bi-link-45deg', '#f59e0b') +
153
+ '</div>';
154
+
155
+ el.innerHTML = html;
156
+ }).catch(function() {
157
+ el.innerHTML = '<div class="alert alert-warning">Could not load skill overview</div>';
158
+ });
159
+ }
160
+
161
+ function fetchSkillPerformance() {
162
+ var el = document.getElementById('skills-list');
163
+ if (!el) return;
164
+
165
+ Promise.all([
166
+ fetch('/api/behavioral/assertions?category=skill_performance&limit=50').then(function(r) { return r.json(); }),
167
+ fetch('/api/behavioral/assertions?category=skill_correlation&limit=20').then(function(r) { return r.json(); }),
168
+ fetch('/api/behavioral/tool-events?tool_name=Skill&limit=500').then(function(r) { return r.json(); }),
169
+ ]).then(function(results) {
170
+ var perfAssertions = results[0].assertions || [];
171
+ var corrAssertions = results[1].assertions || [];
172
+ var events = results[2].events || [];
173
+
174
+ var html = '';
175
+
176
+ // Section 1: Skill Performance
177
+ html += '<h5 style="margin-bottom:16px"><i class="bi bi-lightning-charge" style="color:#8b5cf6"></i> Skill Performance</h5>';
178
+
179
+ if (perfAssertions.length === 0 && events.length === 0) {
180
+ html += '<div class="card" style="padding:24px;text-align:center;color:var(--bs-secondary-color)">' +
181
+ '<i class="bi bi-lightning-charge" style="font-size:2.5rem;display:block;margin-bottom:12px;opacity:0.3"></i>' +
182
+ '<div style="font-size:1rem;margin-bottom:4px;color:var(--bs-body-color)">No skill performance data yet</div>' +
183
+ '<div style="font-size:0.8125rem">' +
184
+ 'Skill tracking starts automatically after the enriched hook captures data.<br>' +
185
+ 'Use skills in your sessions — performance assertions will appear after consolidation.' +
186
+ '</div>' +
187
+ '</div>';
188
+ } else if (perfAssertions.length > 0) {
189
+ html += '<div class="row g-3">';
190
+ perfAssertions.forEach(function(a) {
191
+ html += renderSkillCard(a);
192
+ });
193
+ html += '</div>';
194
+ } else {
195
+ // We have events but no assertions yet (need consolidation)
196
+ html += '<div class="card" style="padding:16px;margin-bottom:16px">' +
197
+ '<div style="font-size:0.875rem;color:var(--bs-body-color)">' +
198
+ '<i class="bi bi-info-circle" style="color:#8b5cf6;margin-right:6px"></i>' +
199
+ events.length + ' skill events collected. Run consolidation to generate performance assertions.' +
200
+ '</div>' +
201
+ '</div>';
202
+
203
+ // Show raw event summary
204
+ var skillCounts = {};
205
+ events.forEach(function(e) {
206
+ var name = extractSkillName(e);
207
+ if (name) skillCounts[name] = (skillCounts[name] || 0) + 1;
208
+ });
209
+
210
+ html += '<div class="row g-3">';
211
+ Object.keys(skillCounts).sort(function(a, b) {
212
+ return skillCounts[b] - skillCounts[a];
213
+ }).forEach(function(name) {
214
+ html += '<div class="col-md-6 col-lg-4"><div class="card" style="padding:16px;border-left:3px solid #8b5cf6">' +
215
+ '<div style="display:flex;justify-content:space-between;align-items:center">' +
216
+ '<div style="font-weight:600;font-size:0.9375rem">' +
217
+ '<i class="bi bi-lightning-charge" style="color:#8b5cf6;margin-right:4px"></i>' +
218
+ escapeHtml(name) +
219
+ '</div>' +
220
+ '<span class="badge" style="background:#8b5cf620;color:#8b5cf6;font-size:0.75rem">' + skillCounts[name] + ' events</span>' +
221
+ '</div>' +
222
+ '</div></div>';
223
+ });
224
+ html += '</div>';
225
+ }
226
+
227
+ // Section 2: Skill Correlations
228
+ if (corrAssertions.length > 0) {
229
+ html += '<h5 style="margin-top:32px;margin-bottom:16px"><i class="bi bi-link-45deg" style="color:#f59e0b"></i> Skill Correlations</h5>';
230
+ html += '<div class="row g-3">';
231
+ corrAssertions.forEach(function(a) {
232
+ html += '<div class="col-md-6"><div class="card" style="padding:12px">' +
233
+ '<div style="font-size:0.875rem">' +
234
+ '<strong>' + escapeHtml(a.trigger_condition || '') + '</strong>' +
235
+ '</div>' +
236
+ '<div style="font-size:0.8125rem;color:var(--bs-body-color);margin-top:4px">' +
237
+ escapeHtml(a.action || '') +
238
+ '</div>' +
239
+ '<div style="font-size:0.75rem;color:var(--bs-secondary-color);margin-top:4px">' +
240
+ 'Confidence: ' + ((a.confidence || 0) * 100).toFixed(0) + '%' +
241
+ '</div>' +
242
+ '</div></div>';
243
+ });
244
+ html += '</div>';
245
+ }
246
+
247
+ el.innerHTML = html;
248
+ }).catch(function(err) {
249
+ el.innerHTML = '<div class="text-center" style="padding:24px;color:var(--ng-text-tertiary)">' +
250
+ 'Error loading skill data: ' + err.message + '</div>';
251
+ });
252
+ }
253
+
254
+ function renderSkillCard(assertion) {
255
+ var conf = assertion.confidence || 0;
256
+ var confPct = (conf * 100).toFixed(0);
257
+ var confColor = conf >= 0.7 ? '#10b981' : conf >= 0.5 ? '#f59e0b' : '#ef4444';
258
+
259
+ // Extract skill name from trigger_condition
260
+ var skillName = (assertion.trigger_condition || '').replace('when considering skill ', '');
261
+
262
+ return '<div class="col-md-6 col-lg-4">' +
263
+ '<div class="card" style="padding:16px;border-left:3px solid #8b5cf6;cursor:pointer">' +
264
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px">' +
265
+ '<div style="font-weight:600;font-size:0.9375rem">' +
266
+ '<i class="bi bi-lightning-charge" style="color:#8b5cf6;margin-right:4px"></i>' +
267
+ escapeHtml(skillName) +
268
+ '</div>' +
269
+ '<span class="badge" style="background:' + confColor + ';color:#fff;font-size:0.75rem">' +
270
+ confPct + '%' +
271
+ '</span>' +
272
+ '</div>' +
273
+ '<div style="font-size:0.8125rem;color:var(--bs-body-color);margin-bottom:8px">' +
274
+ escapeHtml(assertion.action || 'No performance data yet') +
275
+ '</div>' +
276
+ '<div style="display:flex;justify-content:space-between;align-items:center;font-size:0.75rem;color:var(--bs-secondary-color)">' +
277
+ '<span>Evidence: ' + (assertion.evidence_count || 0) + ' invocations</span>' +
278
+ '<span>Reinforced: ' + (assertion.reinforcement_count || 0) + 'x</span>' +
279
+ '</div>' +
280
+ '</div>' +
281
+ '</div>';
282
+ }
283
+
284
+ function extractSkillName(event) {
285
+ var input = event.input_summary || '';
286
+ var output = event.output_summary || '';
287
+
288
+ // Try input_summary (enriched hook format)
289
+ if (input) {
290
+ try {
291
+ var inp = JSON.parse(input);
292
+ if (inp.skill) return inp.skill;
293
+ } catch(e) {}
294
+ }
295
+
296
+ // Try output_summary (ECC ingestion format)
297
+ if (output) {
298
+ try {
299
+ var out = JSON.parse(output);
300
+ if (out.commandName) return out.commandName;
301
+ } catch(e) {}
302
+ }
303
+
304
+ return null;
305
+ }
306
+
307
+ function overviewCard(label, value, icon, color) {
308
+ return '<div class="col-md-3 col-6"><div class="card" style="padding:12px;text-align:center">' +
309
+ '<i class="bi ' + icon + '" style="color:' + color + ';font-size:1.125rem;display:block;margin-bottom:4px"></i>' +
310
+ '<div style="font-size:1.25rem;font-weight:600">' + value + '</div>' +
311
+ '<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--bs-secondary-color)">' + label + '</div>' +
312
+ '</div></div>';
313
+ }
314
+
315
+ function escapeHtml(s) {
316
+ var d = document.createElement('div');
317
+ d.textContent = s || '';
318
+ return d.innerHTML;
319
+ }
320
+
321
+ // ── Skill Lineage (Version DAG) ────────────────────────────
322
+
323
+ var LINEAGE_COLORS = {
324
+ promoted: '#22c55e',
325
+ rejected: '#ef4444',
326
+ pending: '#eab308',
327
+ original: '#6b7280'
328
+ };
329
+
330
+ function fetchSkillLineage() {
331
+ var container = document.getElementById('skill-lineage-container');
332
+ if (!container) return;
333
+
334
+ fetch('/api/evolution/lineage')
335
+ .then(function(r) { return r.json(); })
336
+ .then(function(data) {
337
+ var lineage = data.lineage || [];
338
+ if (lineage.length === 0) {
339
+ container.innerHTML =
340
+ '<div class="card" style="padding:24px;text-align:center;color:var(--bs-secondary-color)">' +
341
+ '<i class="bi bi-diagram-3" style="font-size:2.5rem;display:block;margin-bottom:12px;opacity:0.3"></i>' +
342
+ '<div style="font-size:1rem;margin-bottom:4px;color:var(--bs-body-color)">No skill lineage data yet</div>' +
343
+ '<div style="font-size:0.8125rem">' +
344
+ 'Lineage appears after skills evolve. Run an evolution cycle to generate skill versions.' +
345
+ '</div>' +
346
+ '</div>';
347
+ return;
348
+ }
349
+ var html = '<h5 style="margin-bottom:16px"><i class="bi bi-diagram-3" style="color:#8b5cf6"></i> Skill Lineage</h5>';
350
+ html += '<div class="card" style="padding:16px;margin-bottom:16px">';
351
+ html += '<div id="lineage-dag-wrapper" style="max-height:400px;overflow:auto;position:relative"></div>';
352
+ html += '</div>';
353
+ html += '<div id="lineage-table-wrapper"></div>';
354
+ container.innerHTML = html;
355
+ renderLineageDAG(lineage);
356
+ renderLineageTable(lineage);
357
+ })
358
+ .catch(function() {
359
+ container.innerHTML = '';
360
+ });
361
+ }
362
+
363
+ function renderLineageDAG(lineage) {
364
+ var wrapper = document.getElementById('lineage-dag-wrapper');
365
+ if (!wrapper) return;
366
+
367
+ // Build adjacency: id -> node, parent_skill_id -> children
368
+ var nodeMap = {};
369
+ var childrenMap = {};
370
+ var roots = [];
371
+
372
+ lineage.forEach(function(item) {
373
+ nodeMap[item.id] = item;
374
+ if (!childrenMap[item.id]) childrenMap[item.id] = [];
375
+ });
376
+
377
+ lineage.forEach(function(item) {
378
+ var pid = item.parent_skill_id;
379
+ if (pid && nodeMap[pid]) {
380
+ if (!childrenMap[pid]) childrenMap[pid] = [];
381
+ childrenMap[pid].push(item.id);
382
+ } else {
383
+ roots.push(item.id);
384
+ }
385
+ });
386
+
387
+ // If no explicit roots found, treat all nodes as roots
388
+ if (roots.length === 0) {
389
+ lineage.forEach(function(item) { roots.push(item.id); });
390
+ }
391
+
392
+ // Assign layers via BFS (Sugiyama layer assignment)
393
+ var layers = {};
394
+ var queue = [];
395
+ var visited = {};
396
+ roots.forEach(function(rid) {
397
+ layers[rid] = 0;
398
+ queue.push(rid);
399
+ visited[rid] = true;
400
+ });
401
+ var maxLayer = 0;
402
+ while (queue.length > 0) {
403
+ var nid = queue.shift();
404
+ var children = childrenMap[nid] || [];
405
+ children.forEach(function(cid) {
406
+ if (!visited[cid]) {
407
+ visited[cid] = true;
408
+ layers[cid] = (layers[nid] || 0) + 1;
409
+ if (layers[cid] > maxLayer) maxLayer = layers[cid];
410
+ queue.push(cid);
411
+ }
412
+ });
413
+ }
414
+
415
+ // Handle nodes not reached by BFS (disconnected)
416
+ lineage.forEach(function(item) {
417
+ if (layers[item.id] === undefined) {
418
+ layers[item.id] = 0;
419
+ }
420
+ });
421
+
422
+ // Group nodes by layer
423
+ var layerGroups = {};
424
+ Object.keys(layers).forEach(function(nid) {
425
+ var l = layers[nid];
426
+ if (!layerGroups[l]) layerGroups[l] = [];
427
+ layerGroups[l].push(nid);
428
+ });
429
+
430
+ // Layout constants
431
+ var nodeW = 140;
432
+ var nodeH = 44;
433
+ var layerGap = 80;
434
+ var nodeGap = 24;
435
+ var padX = 20;
436
+ var padY = 20;
437
+
438
+ // Compute max width needed
439
+ var maxNodesInLayer = 0;
440
+ for (var l = 0; l <= maxLayer; l++) {
441
+ var count = (layerGroups[l] || []).length;
442
+ if (count > maxNodesInLayer) maxNodesInLayer = count;
443
+ }
444
+
445
+ var svgW = Math.max(300, padX * 2 + maxNodesInLayer * (nodeW + nodeGap) - nodeGap);
446
+ var svgH = padY * 2 + (maxLayer + 1) * (nodeH + layerGap) - layerGap;
447
+
448
+ // Compute positions
449
+ var positions = {};
450
+ for (var ly = 0; ly <= maxLayer; ly++) {
451
+ var group = layerGroups[ly] || [];
452
+ var totalW = group.length * nodeW + (group.length - 1) * nodeGap;
453
+ var startX = (svgW - totalW) / 2;
454
+ var yPos = padY + ly * (nodeH + layerGap);
455
+ group.forEach(function(nid, idx) {
456
+ positions[nid] = {
457
+ x: startX + idx * (nodeW + nodeGap),
458
+ y: yPos
459
+ };
460
+ });
461
+ }
462
+
463
+ // Build SVG
464
+ var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + svgW + '" height="' + svgH + '" ' +
465
+ 'style="display:block;margin:0 auto;font-family:system-ui,-apple-system,sans-serif">';
466
+
467
+ // Arrowhead marker
468
+ svg += '<defs><marker id="lineage-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" ' +
469
+ 'markerWidth="8" markerHeight="6" orient="auto-start-reverse">' +
470
+ '<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#888"/></marker></defs>';
471
+
472
+ // Draw edges
473
+ lineage.forEach(function(item) {
474
+ var pid = item.parent_skill_id;
475
+ if (pid && positions[pid] && positions[item.id]) {
476
+ var from = positions[pid];
477
+ var to = positions[item.id];
478
+ var x1 = from.x + nodeW / 2;
479
+ var y1 = from.y + nodeH;
480
+ var x2 = to.x + nodeW / 2;
481
+ var y2 = to.y;
482
+ // Curved path
483
+ var midY = (y1 + y2) / 2;
484
+ svg += '<path d="M ' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2 + '" ' +
485
+ 'fill="none" stroke="#888" stroke-width="1.5" marker-end="url(#lineage-arrow)"/>';
486
+ // Edge label
487
+ var etype = item.evolution_type || '';
488
+ if (etype) {
489
+ var lx = (x1 + x2) / 2;
490
+ var labelY = midY - 4;
491
+ svg += '<text x="' + lx + '" y="' + labelY + '" text-anchor="middle" ' +
492
+ 'fill="#888" font-size="10" font-weight="500">' + escapeHtml(etype) + '</text>';
493
+ }
494
+ }
495
+ });
496
+
497
+ // Draw nodes
498
+ lineage.forEach(function(item) {
499
+ var pos = positions[item.id];
500
+ if (!pos) return;
501
+ var status = (item.status || 'original').toLowerCase();
502
+ var fillColor = LINEAGE_COLORS[status] || LINEAGE_COLORS.original;
503
+ var label = (item.skill_name || 'unknown');
504
+ if (label.length > 16) label = label.substring(0, 14) + '..';
505
+
506
+ svg += '<g class="lineage-node" data-id="' + item.id + '" style="cursor:pointer">';
507
+ svg += '<rect x="' + pos.x + '" y="' + pos.y + '" width="' + nodeW + '" height="' + nodeH + '" ' +
508
+ 'rx="8" ry="8" fill="' + fillColor + '" fill-opacity="0.15" stroke="' + fillColor + '" stroke-width="2"/>';
509
+ // Node label (skill name)
510
+ svg += '<text x="' + (pos.x + nodeW / 2) + '" y="' + (pos.y + 18) + '" text-anchor="middle" ' +
511
+ 'fill="' + fillColor + '" font-size="12" font-weight="600">' + escapeHtml(label) + '</text>';
512
+ // Status sub-label
513
+ svg += '<text x="' + (pos.x + nodeW / 2) + '" y="' + (pos.y + 34) + '" text-anchor="middle" ' +
514
+ 'fill="' + fillColor + '" font-size="10" opacity="0.7">' + escapeHtml(status) + '</text>';
515
+ svg += '</g>';
516
+ });
517
+
518
+ svg += '</svg>';
519
+ wrapper.innerHTML = svg;
520
+
521
+ // Click handler: highlight row in table
522
+ wrapper.querySelectorAll('.lineage-node').forEach(function(g) {
523
+ g.addEventListener('click', function() {
524
+ var id = g.getAttribute('data-id');
525
+ highlightLineageRow(id);
526
+ });
527
+ });
528
+ }
529
+
530
+ function renderLineageTable(lineage) {
531
+ var wrapper = document.getElementById('lineage-table-wrapper');
532
+ if (!wrapper) return;
533
+
534
+ var html = '<div class="card" style="padding:16px">' +
535
+ '<div style="font-weight:600;font-size:0.9375rem;margin-bottom:12px">' +
536
+ '<i class="bi bi-table" style="color:#8b5cf6;margin-right:6px"></i>Lineage Details' +
537
+ '</div>' +
538
+ '<div class="table-responsive"><table class="table table-sm table-hover" style="font-size:0.8125rem;margin-bottom:0">' +
539
+ '<thead><tr>' +
540
+ '<th>Skill Name</th>' +
541
+ '<th>Type</th>' +
542
+ '<th>Parent</th>' +
543
+ '<th>Status</th>' +
544
+ '<th>Verified</th>' +
545
+ '<th>Created</th>' +
546
+ '</tr></thead><tbody>';
547
+
548
+ // Build a quick lookup for parent names
549
+ var nameMap = {};
550
+ lineage.forEach(function(item) {
551
+ nameMap[item.id] = item.skill_name || item.id;
552
+ });
553
+
554
+ lineage.forEach(function(item) {
555
+ var status = (item.status || 'original').toLowerCase();
556
+ var color = LINEAGE_COLORS[status] || LINEAGE_COLORS.original;
557
+ var parentName = item.parent_skill_id ? (nameMap[item.parent_skill_id] || item.parent_skill_id) : '-';
558
+ var verified = item.blind_verified ? '<i class="bi bi-check-circle-fill" style="color:#22c55e"></i>' : '<i class="bi bi-dash-circle" style="color:var(--bs-secondary-color)"></i>';
559
+ var created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '-';
560
+
561
+ html += '<tr class="lineage-table-row" data-id="' + item.id + '" style="cursor:pointer">' +
562
+ '<td style="font-weight:500">' + escapeHtml(item.skill_name || '') + '</td>' +
563
+ '<td><span class="badge" style="background:' + color + '20;color:' + color + ';font-size:0.6875rem">' +
564
+ escapeHtml(item.evolution_type || 'ORIGINAL') + '</span></td>' +
565
+ '<td>' + escapeHtml(parentName) + '</td>' +
566
+ '<td><span style="color:' + color + ';font-weight:500">' + escapeHtml(status) + '</span></td>' +
567
+ '<td>' + verified + '</td>' +
568
+ '<td style="color:var(--bs-secondary-color)">' + created + '</td>' +
569
+ '</tr>';
570
+ });
571
+
572
+ html += '</tbody></table></div></div>';
573
+ wrapper.innerHTML = html;
574
+
575
+ // Click rows to highlight in DAG
576
+ wrapper.querySelectorAll('.lineage-table-row').forEach(function(row) {
577
+ row.addEventListener('click', function() {
578
+ var id = row.getAttribute('data-id');
579
+ highlightLineageNode(id);
580
+ highlightLineageRow(id);
581
+ });
582
+ });
583
+ }
584
+
585
+ function highlightLineageRow(id) {
586
+ // Clear previous highlights
587
+ document.querySelectorAll('.lineage-table-row').forEach(function(row) {
588
+ row.style.background = '';
589
+ });
590
+ var target = document.querySelector('.lineage-table-row[data-id="' + id + '"]');
591
+ if (target) {
592
+ target.style.background = 'rgba(139, 92, 246, 0.12)';
593
+ target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
594
+ }
595
+ }
596
+
597
+ function highlightLineageNode(id) {
598
+ // Reset all nodes
599
+ document.querySelectorAll('.lineage-node rect').forEach(function(rect) {
600
+ rect.setAttribute('stroke-width', '2');
601
+ });
602
+ // Highlight selected
603
+ var node = document.querySelector('.lineage-node[data-id="' + id + '"] rect');
604
+ if (node) {
605
+ node.setAttribute('stroke-width', '4');
606
+ node.closest('.lineage-node').parentElement.closest('svg')
607
+ .closest('#lineage-dag-wrapper')
608
+ .scrollTop = 0; // Scroll DAG to top if needed
609
+ }
610
+ }
611
+ })();