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.
Files changed (75) hide show
  1. package/bin/cipher.js +465 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +130 -0
  45. package/lib/commands.js +99 -0
  46. package/lib/complexity.js +377 -0
  47. package/lib/config.js +213 -0
  48. package/lib/gateway/client.js +309 -0
  49. package/lib/gateway/commands.js +830 -0
  50. package/lib/gateway/config-validate.js +109 -0
  51. package/lib/gateway/gateway.js +367 -0
  52. package/lib/gateway/index.js +62 -0
  53. package/lib/gateway/mode.js +309 -0
  54. package/lib/gateway/plugins.js +222 -0
  55. package/lib/gateway/prompt.js +214 -0
  56. package/lib/mcp/server.js +262 -0
  57. package/lib/memory/compressor.js +425 -0
  58. package/lib/memory/engine.js +763 -0
  59. package/lib/memory/evolution.js +668 -0
  60. package/lib/memory/index.js +58 -0
  61. package/lib/memory/orchestrator.js +506 -0
  62. package/lib/memory/retriever.js +515 -0
  63. package/lib/memory/synthesizer.js +333 -0
  64. package/lib/pipeline/async-scanner.js +510 -0
  65. package/lib/pipeline/binary-analysis.js +1043 -0
  66. package/lib/pipeline/dom-xss-scanner.js +435 -0
  67. package/lib/pipeline/github-actions.js +792 -0
  68. package/lib/pipeline/index.js +124 -0
  69. package/lib/pipeline/osint.js +498 -0
  70. package/lib/pipeline/sarif.js +373 -0
  71. package/lib/pipeline/scanner.js +880 -0
  72. package/lib/pipeline/template-manager.js +525 -0
  73. package/lib/pipeline/xss-scanner.js +353 -0
  74. package/lib/setup-wizard.js +229 -0
  75. 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
+ }