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.
- package/README.md +23 -3
- package/docs/cloud-backup.md +174 -0
- package/docs/skill-evolution.md +256 -0
- package/ide/hooks/tool-event-hook.sh +101 -11
- package/package.json +1 -1
- package/pyproject.toml +3 -2
- package/src/superlocalmemory/cli/commands.py +359 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +81 -29
- package/src/superlocalmemory/cli/main.py +32 -0
- package/src/superlocalmemory/cli/setup_wizard.py +54 -11
- package/src/superlocalmemory/core/config.py +35 -0
- package/src/superlocalmemory/core/consolidation_engine.py +138 -0
- package/src/superlocalmemory/core/embedding_worker.py +1 -1
- package/src/superlocalmemory/core/engine.py +19 -0
- package/src/superlocalmemory/core/fact_consolidator.py +425 -0
- package/src/superlocalmemory/core/graph_pruner.py +290 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +44 -3
- package/src/superlocalmemory/core/recall_pipeline.py +9 -0
- package/src/superlocalmemory/core/tier_manager.py +325 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +96 -28
- package/src/superlocalmemory/evolution/__init__.py +29 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +115 -0
- package/src/superlocalmemory/evolution/evolution_store.py +302 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +181 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +555 -0
- package/src/superlocalmemory/evolution/triggers.py +367 -0
- package/src/superlocalmemory/evolution/types.py +92 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +13 -0
- package/src/superlocalmemory/infra/backup.py +63 -20
- package/src/superlocalmemory/infra/cloud_backup.py +703 -0
- package/src/superlocalmemory/learning/skill_performance_miner.py +422 -0
- package/src/superlocalmemory/mcp/server.py +4 -0
- package/src/superlocalmemory/mcp/tools_evolution.py +338 -0
- package/src/superlocalmemory/retrieval/engine.py +64 -4
- package/src/superlocalmemory/retrieval/forgetting_filter.py +22 -7
- package/src/superlocalmemory/retrieval/strategy.py +2 -2
- package/src/superlocalmemory/server/routes/backup.py +512 -8
- package/src/superlocalmemory/server/routes/behavioral.py +39 -17
- package/src/superlocalmemory/server/routes/evolution.py +213 -0
- package/src/superlocalmemory/server/routes/tiers.py +195 -0
- package/src/superlocalmemory/server/unified_daemon.py +36 -5
- package/src/superlocalmemory/storage/schema_v3410.py +159 -0
- package/src/superlocalmemory/storage/schema_v3411.py +149 -0
- package/src/superlocalmemory/ui/index.html +59 -3
- package/src/superlocalmemory/ui/js/core.js +3 -0
- package/src/superlocalmemory/ui/js/lifecycle.js +83 -0
- package/src/superlocalmemory/ui/js/ng-entities.js +27 -3
- package/src/superlocalmemory/ui/js/ng-shell.js +33 -0
- package/src/superlocalmemory/ui/js/ng-skills.js +611 -0
- package/src/superlocalmemory/ui/js/settings.js +311 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +16 -1
- 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
|
+
})();
|