cipher-security 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cipher.js +465 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +130 -0
- package/lib/commands.js +99 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +830 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +229 -0
- package/package.json +30 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Skill effectiveness tracking and metrics system.
|
|
7
|
+
*
|
|
8
|
+
* SQLite-backed leaderboard that records skill invocations, computes
|
|
9
|
+
* performance trends, and surfaces domain-level coverage insights.
|
|
10
|
+
*
|
|
11
|
+
* @module autonomous/leaderboard
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync } from 'node:fs';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Data classes
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export class SkillMetric {
|
|
23
|
+
constructor({
|
|
24
|
+
skillPath = '',
|
|
25
|
+
domain = '',
|
|
26
|
+
technique = '',
|
|
27
|
+
invocationCount = 0,
|
|
28
|
+
successCount = 0,
|
|
29
|
+
failureCount = 0,
|
|
30
|
+
avgScore = 0,
|
|
31
|
+
lastUsed = '',
|
|
32
|
+
lastScore = 0,
|
|
33
|
+
trend = '',
|
|
34
|
+
scoresHistory = [],
|
|
35
|
+
} = {}) {
|
|
36
|
+
this.skillPath = skillPath;
|
|
37
|
+
this.domain = domain;
|
|
38
|
+
this.technique = technique;
|
|
39
|
+
this.invocationCount = invocationCount;
|
|
40
|
+
this.successCount = successCount;
|
|
41
|
+
this.failureCount = failureCount;
|
|
42
|
+
this.avgScore = avgScore;
|
|
43
|
+
this.lastUsed = lastUsed;
|
|
44
|
+
this.lastScore = lastScore;
|
|
45
|
+
this.trend = trend;
|
|
46
|
+
this.scoresHistory = scoresHistory;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class DomainMetric {
|
|
51
|
+
constructor({
|
|
52
|
+
domain = '',
|
|
53
|
+
techniqueCount = 0,
|
|
54
|
+
totalInvocations = 0,
|
|
55
|
+
avgSuccessRate = 0,
|
|
56
|
+
avgScore = 0,
|
|
57
|
+
coverageScore = 0,
|
|
58
|
+
strongestTechnique = '',
|
|
59
|
+
weakestTechnique = '',
|
|
60
|
+
} = {}) {
|
|
61
|
+
this.domain = domain;
|
|
62
|
+
this.techniqueCount = techniqueCount;
|
|
63
|
+
this.totalInvocations = totalInvocations;
|
|
64
|
+
this.avgSuccessRate = avgSuccessRate;
|
|
65
|
+
this.avgScore = avgScore;
|
|
66
|
+
this.coverageScore = coverageScore;
|
|
67
|
+
this.strongestTechnique = strongestTechnique;
|
|
68
|
+
this.weakestTechnique = weakestTechnique;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class LeaderboardEntry {
|
|
73
|
+
constructor({
|
|
74
|
+
rank = 0,
|
|
75
|
+
skillPath = '',
|
|
76
|
+
domain = '',
|
|
77
|
+
score = 0,
|
|
78
|
+
invocations = 0,
|
|
79
|
+
successRate = 0,
|
|
80
|
+
trend = '',
|
|
81
|
+
} = {}) {
|
|
82
|
+
this.rank = rank;
|
|
83
|
+
this.skillPath = skillPath;
|
|
84
|
+
this.domain = domain;
|
|
85
|
+
this.score = score;
|
|
86
|
+
this.invocations = invocations;
|
|
87
|
+
this.successRate = successRate;
|
|
88
|
+
this.trend = trend;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// MITRE ATT&CK reference counts per domain
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
const _MITRE_TECHNIQUE_COUNTS = {
|
|
97
|
+
'initial-access': 10,
|
|
98
|
+
'execution': 14,
|
|
99
|
+
'persistence': 20,
|
|
100
|
+
'privilege-escalation': 14,
|
|
101
|
+
'defense-evasion': 43,
|
|
102
|
+
'credential-access': 17,
|
|
103
|
+
'discovery': 32,
|
|
104
|
+
'lateral-movement': 9,
|
|
105
|
+
'collection': 17,
|
|
106
|
+
'command-and-control': 16,
|
|
107
|
+
'exfiltration': 9,
|
|
108
|
+
'impact': 14,
|
|
109
|
+
'reconnaissance': 10,
|
|
110
|
+
'resource-development': 8,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// SkillLeaderboard
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export class SkillLeaderboard {
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} [dbPath='~/.cipher/leaderboard.db']
|
|
120
|
+
*/
|
|
121
|
+
constructor(dbPath = '~/.cipher/leaderboard.db') {
|
|
122
|
+
// Resolve ~ to home directory
|
|
123
|
+
const resolved = dbPath.startsWith('~')
|
|
124
|
+
? resolve(homedir(), dbPath.slice(2))
|
|
125
|
+
: resolve(dbPath);
|
|
126
|
+
|
|
127
|
+
// Ensure parent directory exists
|
|
128
|
+
try { mkdirSync(dirname(resolved), { recursive: true }); } catch { /* ok */ }
|
|
129
|
+
|
|
130
|
+
// Lazy-load better-sqlite3 to avoid import-time cost
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
132
|
+
const Database = require('better-sqlite3');
|
|
133
|
+
this._dbPath = resolved;
|
|
134
|
+
|
|
135
|
+
// ':memory:' for in-memory databases
|
|
136
|
+
if (dbPath === ':memory:') {
|
|
137
|
+
this._db = new Database(':memory:');
|
|
138
|
+
} else {
|
|
139
|
+
this._db = new Database(resolved);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._db.pragma('journal_mode = WAL');
|
|
143
|
+
this._initDb();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ------------------------------------------------------------------
|
|
147
|
+
// Schema
|
|
148
|
+
// ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
_initDb() {
|
|
151
|
+
this._db.exec(`
|
|
152
|
+
CREATE TABLE IF NOT EXISTS skill_metrics (
|
|
153
|
+
skill_path TEXT PRIMARY KEY,
|
|
154
|
+
domain TEXT NOT NULL,
|
|
155
|
+
technique TEXT NOT NULL DEFAULT '',
|
|
156
|
+
invocation_count INTEGER NOT NULL DEFAULT 0,
|
|
157
|
+
success_count INTEGER NOT NULL DEFAULT 0,
|
|
158
|
+
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
159
|
+
avg_score REAL NOT NULL DEFAULT 0.0,
|
|
160
|
+
last_used TEXT NOT NULL DEFAULT '',
|
|
161
|
+
last_score REAL NOT NULL DEFAULT 0.0,
|
|
162
|
+
trend TEXT NOT NULL DEFAULT '',
|
|
163
|
+
scores_history TEXT NOT NULL DEFAULT '[]'
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS invocation_log (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
skill_path TEXT NOT NULL,
|
|
169
|
+
timestamp TEXT NOT NULL,
|
|
170
|
+
success INTEGER NOT NULL,
|
|
171
|
+
score REAL NOT NULL,
|
|
172
|
+
context TEXT NOT NULL DEFAULT '{}'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS domain_metrics (
|
|
176
|
+
domain TEXT PRIMARY KEY,
|
|
177
|
+
technique_count INTEGER NOT NULL DEFAULT 0,
|
|
178
|
+
total_invocations INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
avg_success_rate REAL NOT NULL DEFAULT 0.0,
|
|
180
|
+
avg_score REAL NOT NULL DEFAULT 0.0,
|
|
181
|
+
coverage_score REAL NOT NULL DEFAULT 0.0,
|
|
182
|
+
strongest_technique TEXT NOT NULL DEFAULT '',
|
|
183
|
+
weakest_technique TEXT NOT NULL DEFAULT ''
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_invlog_skill ON invocation_log(skill_path);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_invlog_ts ON invocation_log(timestamp);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_skill_domain ON skill_metrics(domain);
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ------------------------------------------------------------------
|
|
193
|
+
// Recording
|
|
194
|
+
// ------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Record a single skill invocation and update aggregates.
|
|
198
|
+
* @param {string} skillPath
|
|
199
|
+
* @param {boolean} success
|
|
200
|
+
* @param {number} score
|
|
201
|
+
* @param {object} [context]
|
|
202
|
+
*/
|
|
203
|
+
recordInvocation(skillPath, success, score, context = {}) {
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
const ctxJson = JSON.stringify(context);
|
|
206
|
+
|
|
207
|
+
const parts = skillPath.replace(/^\//, '').split('/');
|
|
208
|
+
const domain = parts[0] || 'unknown';
|
|
209
|
+
const technique = parts.length > 1 ? parts[parts.length - 1] : '';
|
|
210
|
+
|
|
211
|
+
// Sanitize NaN/Infinity
|
|
212
|
+
if (!Number.isFinite(score)) score = 0;
|
|
213
|
+
|
|
214
|
+
// Insert invocation log
|
|
215
|
+
this._db.prepare(
|
|
216
|
+
'INSERT INTO invocation_log (skill_path, timestamp, success, score, context) VALUES (?, ?, ?, ?, ?)'
|
|
217
|
+
).run(skillPath, now, success ? 1 : 0, score, ctxJson);
|
|
218
|
+
|
|
219
|
+
// Upsert skill_metrics
|
|
220
|
+
const row = this._db.prepare('SELECT * FROM skill_metrics WHERE skill_path = ?').get(skillPath);
|
|
221
|
+
|
|
222
|
+
if (!row) {
|
|
223
|
+
const history = [score];
|
|
224
|
+
this._db.prepare(
|
|
225
|
+
'INSERT INTO skill_metrics (skill_path, domain, technique, invocation_count, success_count, failure_count, avg_score, last_used, last_score, trend, scores_history) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)'
|
|
226
|
+
).run(
|
|
227
|
+
skillPath, domain, technique,
|
|
228
|
+
success ? 1 : 0, success ? 0 : 1,
|
|
229
|
+
score, now, score, '', JSON.stringify(history)
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
const inv = row.invocation_count + 1;
|
|
233
|
+
const sc = row.success_count + (success ? 1 : 0);
|
|
234
|
+
const fc = row.failure_count + (success ? 0 : 1);
|
|
235
|
+
let prevHistory = JSON.parse(row.scores_history);
|
|
236
|
+
prevHistory.push(score);
|
|
237
|
+
if (prevHistory.length > 100) prevHistory = prevHistory.slice(-100);
|
|
238
|
+
const avg = prevHistory.reduce((a, b) => a + b, 0) / prevHistory.length;
|
|
239
|
+
this._db.prepare(
|
|
240
|
+
'UPDATE skill_metrics SET invocation_count = ?, success_count = ?, failure_count = ?, avg_score = ?, last_used = ?, last_score = ?, scores_history = ? WHERE skill_path = ?'
|
|
241
|
+
).run(inv, sc, fc, avg, now, score, JSON.stringify(prevHistory), skillPath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this._updateDomainAggregates(domain);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ------------------------------------------------------------------
|
|
248
|
+
// Queries — single skill
|
|
249
|
+
// ------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Return the metric snapshot for a single skill.
|
|
253
|
+
* @param {string} skillPath
|
|
254
|
+
* @returns {SkillMetric}
|
|
255
|
+
*/
|
|
256
|
+
getSkillMetric(skillPath) {
|
|
257
|
+
const row = this._db.prepare('SELECT * FROM skill_metrics WHERE skill_path = ?').get(skillPath);
|
|
258
|
+
if (!row) {
|
|
259
|
+
const parts = skillPath.replace(/^\//, '').split('/');
|
|
260
|
+
return new SkillMetric({
|
|
261
|
+
skillPath,
|
|
262
|
+
domain: parts[0] || 'unknown',
|
|
263
|
+
technique: parts.length > 1 ? parts[parts.length - 1] : '',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return new SkillMetric({
|
|
267
|
+
skillPath: row.skill_path,
|
|
268
|
+
domain: row.domain,
|
|
269
|
+
technique: row.technique,
|
|
270
|
+
invocationCount: row.invocation_count,
|
|
271
|
+
successCount: row.success_count,
|
|
272
|
+
failureCount: row.failure_count,
|
|
273
|
+
avgScore: row.avg_score,
|
|
274
|
+
lastUsed: row.last_used,
|
|
275
|
+
lastScore: row.last_score,
|
|
276
|
+
trend: row.trend,
|
|
277
|
+
scoresHistory: JSON.parse(row.scores_history),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ------------------------------------------------------------------
|
|
282
|
+
// Queries — domain
|
|
283
|
+
// ------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Return aggregate metrics for a security domain.
|
|
287
|
+
* @param {string} domain
|
|
288
|
+
* @returns {DomainMetric}
|
|
289
|
+
*/
|
|
290
|
+
getDomainMetrics(domain) {
|
|
291
|
+
const row = this._db.prepare('SELECT * FROM domain_metrics WHERE domain = ?').get(domain);
|
|
292
|
+
if (!row) {
|
|
293
|
+
return new DomainMetric({ domain });
|
|
294
|
+
}
|
|
295
|
+
return new DomainMetric({
|
|
296
|
+
domain: row.domain,
|
|
297
|
+
techniqueCount: row.technique_count,
|
|
298
|
+
totalInvocations: row.total_invocations,
|
|
299
|
+
avgSuccessRate: row.avg_success_rate,
|
|
300
|
+
avgScore: row.avg_score,
|
|
301
|
+
coverageScore: row.coverage_score,
|
|
302
|
+
strongestTechnique: row.strongest_technique,
|
|
303
|
+
weakestTechnique: row.weakest_technique,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ------------------------------------------------------------------
|
|
308
|
+
// Leaderboard queries
|
|
309
|
+
// ------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/** @private */
|
|
312
|
+
_rowsToEntries(rows, startRank = 1) {
|
|
313
|
+
return rows.map((r, i) => {
|
|
314
|
+
const inv = r.invocation_count || 1;
|
|
315
|
+
return new LeaderboardEntry({
|
|
316
|
+
rank: startRank + i,
|
|
317
|
+
skillPath: r.skill_path,
|
|
318
|
+
domain: r.domain,
|
|
319
|
+
score: r.avg_score,
|
|
320
|
+
invocations: r.invocation_count,
|
|
321
|
+
successRate: Math.round((r.success_count / inv) * 10000) / 10000,
|
|
322
|
+
trend: r.trend,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Return the top-N skills by avg_score.
|
|
329
|
+
* @param {number} [n=20]
|
|
330
|
+
* @param {string} [domain]
|
|
331
|
+
* @returns {LeaderboardEntry[]}
|
|
332
|
+
*/
|
|
333
|
+
getTopSkills(n = 20, domain) {
|
|
334
|
+
let rows;
|
|
335
|
+
if (domain) {
|
|
336
|
+
rows = this._db.prepare(
|
|
337
|
+
'SELECT * FROM skill_metrics WHERE domain = ? ORDER BY avg_score DESC LIMIT ?'
|
|
338
|
+
).all(domain, n);
|
|
339
|
+
} else {
|
|
340
|
+
rows = this._db.prepare(
|
|
341
|
+
'SELECT * FROM skill_metrics ORDER BY avg_score DESC LIMIT ?'
|
|
342
|
+
).all(n);
|
|
343
|
+
}
|
|
344
|
+
return this._rowsToEntries(rows);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Return the bottom-N skills by avg_score.
|
|
349
|
+
* @param {number} [n=20]
|
|
350
|
+
* @param {string} [domain]
|
|
351
|
+
* @returns {LeaderboardEntry[]}
|
|
352
|
+
*/
|
|
353
|
+
getBottomSkills(n = 20, domain) {
|
|
354
|
+
let rows;
|
|
355
|
+
if (domain) {
|
|
356
|
+
rows = this._db.prepare(
|
|
357
|
+
'SELECT * FROM skill_metrics WHERE domain = ? ORDER BY avg_score ASC LIMIT ?'
|
|
358
|
+
).all(domain, n);
|
|
359
|
+
} else {
|
|
360
|
+
rows = this._db.prepare(
|
|
361
|
+
'SELECT * FROM skill_metrics ORDER BY avg_score ASC LIMIT ?'
|
|
362
|
+
).all(n);
|
|
363
|
+
}
|
|
364
|
+
return this._rowsToEntries(rows);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Return skills trending in the given direction.
|
|
369
|
+
* @param {string} [direction='up']
|
|
370
|
+
* @param {number} [n=10]
|
|
371
|
+
* @returns {LeaderboardEntry[]}
|
|
372
|
+
*/
|
|
373
|
+
getTrending(direction = 'up', n = 10) {
|
|
374
|
+
const trendVal = direction === 'up' ? 'improving' : 'declining';
|
|
375
|
+
const rows = this._db.prepare(
|
|
376
|
+
'SELECT * FROM skill_metrics WHERE trend = ? ORDER BY avg_score DESC LIMIT ?'
|
|
377
|
+
).all(trendVal, n);
|
|
378
|
+
return this._rowsToEntries(rows);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Return skill paths not invoked within the last N days.
|
|
383
|
+
* @param {number} [days=30]
|
|
384
|
+
* @returns {string[]}
|
|
385
|
+
*/
|
|
386
|
+
getUnusedSkills(days = 30) {
|
|
387
|
+
const cutoff = new Date(Date.now() - days * 86400000).toISOString();
|
|
388
|
+
const rows = this._db.prepare(
|
|
389
|
+
"SELECT skill_path FROM skill_metrics WHERE last_used < ? OR last_used = ''"
|
|
390
|
+
).all(cutoff);
|
|
391
|
+
return rows.map(r => r.skill_path);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ------------------------------------------------------------------
|
|
395
|
+
// Trend computation
|
|
396
|
+
// ------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Determine trend via linear regression slope.
|
|
400
|
+
* @param {number[]} scores
|
|
401
|
+
* @returns {string}
|
|
402
|
+
*/
|
|
403
|
+
static calculateTrend(scores) {
|
|
404
|
+
if (scores.length < 3) return 'stable';
|
|
405
|
+
const n = scores.length;
|
|
406
|
+
const xMean = (n - 1) / 2;
|
|
407
|
+
const yMean = scores.reduce((a, b) => a + b, 0) / n;
|
|
408
|
+
let numerator = 0;
|
|
409
|
+
let denominator = 0;
|
|
410
|
+
for (let i = 0; i < n; i++) {
|
|
411
|
+
numerator += (i - xMean) * (scores[i] - yMean);
|
|
412
|
+
denominator += (i - xMean) ** 2;
|
|
413
|
+
}
|
|
414
|
+
if (denominator === 0) return 'stable';
|
|
415
|
+
const slope = numerator / denominator;
|
|
416
|
+
if (slope > 0.02) return 'improving';
|
|
417
|
+
if (slope < -0.02) return 'declining';
|
|
418
|
+
return 'stable';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Recompute trend field for all skills based on recent scores.
|
|
423
|
+
* @param {number} [window=10]
|
|
424
|
+
*/
|
|
425
|
+
computeTrends(window = 10) {
|
|
426
|
+
const rows = this._db.prepare('SELECT skill_path, scores_history FROM skill_metrics').all();
|
|
427
|
+
for (const r of rows) {
|
|
428
|
+
const history = JSON.parse(r.scores_history);
|
|
429
|
+
const recent = history.length > window ? history.slice(-window) : history;
|
|
430
|
+
const trend = SkillLeaderboard.calculateTrend(recent);
|
|
431
|
+
this._db.prepare('UPDATE skill_metrics SET trend = ? WHERE skill_path = ?').run(trend, r.skill_path);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ------------------------------------------------------------------
|
|
436
|
+
// Domain aggregation
|
|
437
|
+
// ------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
/** @private */
|
|
440
|
+
_updateDomainAggregates(domain) {
|
|
441
|
+
const rows = this._db.prepare('SELECT * FROM skill_metrics WHERE domain = ?').all(domain);
|
|
442
|
+
|
|
443
|
+
if (rows.length === 0) {
|
|
444
|
+
this._db.prepare('DELETE FROM domain_metrics WHERE domain = ?').run(domain);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const techniqueCount = rows.length;
|
|
449
|
+
const totalInv = rows.reduce((s, r) => s + r.invocation_count, 0);
|
|
450
|
+
const totalSuccess = rows.reduce((s, r) => s + r.success_count, 0);
|
|
451
|
+
const avgSuccessRate = totalInv > 0 ? totalSuccess / totalInv : 0;
|
|
452
|
+
const scores = rows.map(r => r.avg_score).filter(s => Number.isFinite(s));
|
|
453
|
+
const avgScore = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
|
|
454
|
+
|
|
455
|
+
const mitreTotal = _MITRE_TECHNIQUE_COUNTS[domain] || 0;
|
|
456
|
+
let coverage = mitreTotal > 0 ? techniqueCount / mitreTotal : 0;
|
|
457
|
+
coverage = Math.min(coverage, 1.0);
|
|
458
|
+
|
|
459
|
+
const best = rows.reduce((a, b) => a.avg_score >= b.avg_score ? a : b);
|
|
460
|
+
const worst = rows.reduce((a, b) => a.avg_score <= b.avg_score ? a : b);
|
|
461
|
+
|
|
462
|
+
this._db.prepare(`
|
|
463
|
+
INSERT INTO domain_metrics
|
|
464
|
+
(domain, technique_count, total_invocations, avg_success_rate, avg_score, coverage_score, strongest_technique, weakest_technique)
|
|
465
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
466
|
+
ON CONFLICT(domain) DO UPDATE SET
|
|
467
|
+
technique_count = excluded.technique_count,
|
|
468
|
+
total_invocations = excluded.total_invocations,
|
|
469
|
+
avg_success_rate = excluded.avg_success_rate,
|
|
470
|
+
avg_score = excluded.avg_score,
|
|
471
|
+
coverage_score = excluded.coverage_score,
|
|
472
|
+
strongest_technique = excluded.strongest_technique,
|
|
473
|
+
weakest_technique = excluded.weakest_technique
|
|
474
|
+
`).run(
|
|
475
|
+
domain,
|
|
476
|
+
techniqueCount,
|
|
477
|
+
totalInv,
|
|
478
|
+
Math.round(avgSuccessRate * 10000) / 10000,
|
|
479
|
+
Math.round(avgScore * 10000) / 10000,
|
|
480
|
+
Math.round(coverage * 10000) / 10000,
|
|
481
|
+
best.technique,
|
|
482
|
+
worst.technique,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ------------------------------------------------------------------
|
|
487
|
+
// Dashboard & reporting
|
|
488
|
+
// ------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
getDashboard() {
|
|
491
|
+
const skills = this._db.prepare('SELECT * FROM skill_metrics').all();
|
|
492
|
+
const domains = this._db.prepare('SELECT * FROM domain_metrics ORDER BY avg_score DESC').all();
|
|
493
|
+
|
|
494
|
+
if (skills.length === 0) {
|
|
495
|
+
return {
|
|
496
|
+
totalSkills: 0,
|
|
497
|
+
avgScore: 0,
|
|
498
|
+
totalInvocations: 0,
|
|
499
|
+
topDomains: [],
|
|
500
|
+
worstDomains: [],
|
|
501
|
+
unusedCount: 0,
|
|
502
|
+
trendSummary: { improving: 0, stable: 0, declining: 0 },
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const avgScore = skills.reduce((s, r) => s + r.avg_score, 0) / skills.length;
|
|
507
|
+
const totalInv = skills.reduce((s, r) => s + r.invocation_count, 0);
|
|
508
|
+
const unused = this.getUnusedSkills(30).length;
|
|
509
|
+
|
|
510
|
+
const trendCounts = { improving: 0, stable: 0, declining: 0 };
|
|
511
|
+
for (const r of skills) {
|
|
512
|
+
const t = r.trend in trendCounts ? r.trend : 'stable';
|
|
513
|
+
trendCounts[t] += 1;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const topDomains = domains.slice(0, 5).map(d => ({
|
|
517
|
+
domain: d.domain,
|
|
518
|
+
avgScore: d.avg_score,
|
|
519
|
+
coverage: d.coverage_score,
|
|
520
|
+
}));
|
|
521
|
+
const worstDomains = domains.slice(-5).reverse().map(d => ({
|
|
522
|
+
domain: d.domain,
|
|
523
|
+
avgScore: d.avg_score,
|
|
524
|
+
coverage: d.coverage_score,
|
|
525
|
+
}));
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
totalSkills: skills.length,
|
|
529
|
+
avgScore: Math.round(avgScore * 10000) / 10000,
|
|
530
|
+
totalInvocations: totalInv,
|
|
531
|
+
topDomains,
|
|
532
|
+
worstDomains,
|
|
533
|
+
unusedCount: unused,
|
|
534
|
+
trendSummary: trendCounts,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
exportReport() {
|
|
539
|
+
const skills = this._db.prepare('SELECT * FROM skill_metrics ORDER BY avg_score DESC').all();
|
|
540
|
+
const entries = skills.map((r, i) => {
|
|
541
|
+
const inv = r.invocation_count || 1;
|
|
542
|
+
return {
|
|
543
|
+
rank: i + 1,
|
|
544
|
+
skillPath: r.skill_path,
|
|
545
|
+
domain: r.domain,
|
|
546
|
+
technique: r.technique,
|
|
547
|
+
invocationCount: r.invocation_count,
|
|
548
|
+
successCount: r.success_count,
|
|
549
|
+
failureCount: r.failure_count,
|
|
550
|
+
avgScore: r.avg_score,
|
|
551
|
+
lastUsed: r.last_used,
|
|
552
|
+
lastScore: r.last_score,
|
|
553
|
+
trend: r.trend,
|
|
554
|
+
successRate: Math.round((r.success_count / inv) * 10000) / 10000,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
return JSON.stringify({
|
|
559
|
+
generatedAt: new Date().toISOString(),
|
|
560
|
+
totalSkills: entries.length,
|
|
561
|
+
dashboard: this.getDashboard(),
|
|
562
|
+
skills: entries,
|
|
563
|
+
}, null, 2);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ------------------------------------------------------------------
|
|
567
|
+
// EWA / ACE-style Online Adaptation
|
|
568
|
+
// ------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Compute exponentially-decayed score.
|
|
572
|
+
* @param {string} skillPath
|
|
573
|
+
* @param {number} [decayFactor=0.95]
|
|
574
|
+
* @param {number} [window=50]
|
|
575
|
+
* @returns {number}
|
|
576
|
+
*/
|
|
577
|
+
decayedScore(skillPath, decayFactor = 0.95, window = 50) {
|
|
578
|
+
const row = this._db.prepare('SELECT scores_history FROM skill_metrics WHERE skill_path = ?').get(skillPath);
|
|
579
|
+
if (!row) return 0;
|
|
580
|
+
const history = JSON.parse(row.scores_history);
|
|
581
|
+
if (history.length === 0) return 0;
|
|
582
|
+
const recent = history.slice(-window);
|
|
583
|
+
const n = recent.length;
|
|
584
|
+
const weights = Array.from({ length: n }, (_, i) => decayFactor ** (n - 1 - i));
|
|
585
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
586
|
+
if (totalWeight === 0) return 0;
|
|
587
|
+
return weights.reduce((sum, w, i) => sum + w * recent[i], 0) / totalWeight;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Close the database connection. */
|
|
591
|
+
close() {
|
|
592
|
+
this._db.close();
|
|
593
|
+
}
|
|
594
|
+
}
|