@open-skills-hub/core 1.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.
@@ -0,0 +1,1838 @@
1
+ /**
2
+ * Open Skills Hub - SQLite Storage Implementation (using sql.js)
3
+ */
4
+
5
+ import initSqlJs, { Database as SqlJsDatabase, SqlValue } from 'sql.js';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import {
9
+ IStorage,
10
+ PaginatedResult,
11
+ SkillQueryOptions,
12
+ VersionQueryOptions,
13
+ AuditQueryOptions,
14
+ FeedbackQueryOptions,
15
+ QueueQueryOptions,
16
+ PaginationOptions,
17
+ } from './interface.js';
18
+ import type {
19
+ Skill,
20
+ Version,
21
+ ScanRecord,
22
+ ScanIssue,
23
+ Feedback,
24
+ FeedbackQueueItem,
25
+ AuditLog,
26
+ CacheMetadata,
27
+ UseRecord,
28
+ } from '../models/types.js';
29
+ import { now } from '../utils/helpers.js';
30
+ import { logger } from '../utils/logger.js';
31
+
32
+ // Type alias for SQL parameters
33
+ type SqlParams = SqlValue[];
34
+
35
+ // ============================================================================
36
+ // SQLite Schema
37
+ // ============================================================================
38
+
39
+ const SCHEMA = `
40
+ -- Skills table
41
+ CREATE TABLE IF NOT EXISTS skills (
42
+ id TEXT PRIMARY KEY,
43
+ name TEXT NOT NULL,
44
+ scope TEXT,
45
+ full_name TEXT UNIQUE NOT NULL,
46
+
47
+ owner_id TEXT,
48
+ owner_type TEXT DEFAULT 'user',
49
+
50
+ display_name TEXT,
51
+ description TEXT NOT NULL,
52
+ category TEXT,
53
+ keywords TEXT DEFAULT '[]',
54
+ license TEXT,
55
+
56
+ repository TEXT,
57
+ homepage TEXT,
58
+
59
+ derived_from TEXT,
60
+
61
+ latest_version TEXT,
62
+ latest_version_id TEXT,
63
+
64
+ visibility TEXT DEFAULT 'public',
65
+ status TEXT DEFAULT 'active',
66
+ deprecated_message TEXT,
67
+
68
+ security_score INTEGER,
69
+ security_level TEXT,
70
+ last_scan_at TEXT,
71
+
72
+ stats TEXT DEFAULT '{"totalUses":0,"weeklyUses":0,"monthlyUses":0,"versionCount":0,"derivationCount":0}',
73
+ rating TEXT DEFAULT '{"average":0,"count":0}',
74
+
75
+ searchable_text TEXT,
76
+
77
+ created_at TEXT DEFAULT (datetime('now')),
78
+ updated_at TEXT DEFAULT (datetime('now')),
79
+ published_at TEXT
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
83
+ CREATE INDEX IF NOT EXISTS idx_skills_full_name ON skills(full_name);
84
+ CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
85
+ CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility);
86
+ CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner_id);
87
+
88
+ -- Versions table
89
+ CREATE TABLE IF NOT EXISTS versions (
90
+ id TEXT PRIMARY KEY,
91
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
92
+
93
+ version TEXT NOT NULL,
94
+ tag TEXT,
95
+
96
+ content TEXT NOT NULL,
97
+
98
+ package_url TEXT NOT NULL,
99
+ package_size INTEGER NOT NULL,
100
+ package_hash TEXT NOT NULL,
101
+
102
+ changelog TEXT,
103
+
104
+ scan_record_id TEXT,
105
+ security_score INTEGER,
106
+ security_level TEXT,
107
+
108
+ status TEXT DEFAULT 'published',
109
+ deprecated_message TEXT,
110
+
111
+ published_by TEXT,
112
+ uses INTEGER DEFAULT 0,
113
+
114
+ created_at TEXT DEFAULT (datetime('now')),
115
+ published_at TEXT DEFAULT (datetime('now')),
116
+ deprecated_at TEXT,
117
+
118
+ UNIQUE(skill_id, version)
119
+ );
120
+
121
+ CREATE INDEX IF NOT EXISTS idx_versions_skill ON versions(skill_id);
122
+ CREATE INDEX IF NOT EXISTS idx_versions_tag ON versions(skill_id, tag);
123
+
124
+ -- Scan records table
125
+ CREATE TABLE IF NOT EXISTS scan_records (
126
+ id TEXT PRIMARY KEY,
127
+
128
+ skill_id TEXT REFERENCES skills(id) ON DELETE SET NULL,
129
+ version_id TEXT REFERENCES versions(id) ON DELETE SET NULL,
130
+ user_id TEXT,
131
+
132
+ input_type TEXT NOT NULL,
133
+ content_hash TEXT NOT NULL,
134
+ content_size INTEGER NOT NULL,
135
+
136
+ score INTEGER,
137
+ level TEXT,
138
+ issue_count TEXT DEFAULT '{"high":0,"medium":0,"low":0}',
139
+
140
+ scanner_version TEXT,
141
+ rules_version TEXT,
142
+ rules_applied INTEGER,
143
+ duration INTEGER,
144
+
145
+ status TEXT DEFAULT 'pending',
146
+ error_message TEXT,
147
+
148
+ created_at TEXT DEFAULT (datetime('now')),
149
+ completed_at TEXT
150
+ );
151
+
152
+ CREATE INDEX IF NOT EXISTS idx_scan_records_skill ON scan_records(skill_id);
153
+
154
+ -- Scan issues table
155
+ CREATE TABLE IF NOT EXISTS scan_issues (
156
+ id TEXT PRIMARY KEY,
157
+ scan_record_id TEXT NOT NULL REFERENCES scan_records(id) ON DELETE CASCADE,
158
+
159
+ rule_id TEXT NOT NULL,
160
+ rule_name TEXT NOT NULL,
161
+ rule_category TEXT NOT NULL,
162
+
163
+ severity TEXT NOT NULL,
164
+
165
+ file TEXT,
166
+ line INTEGER,
167
+ column_num INTEGER,
168
+ end_line INTEGER,
169
+ end_column INTEGER,
170
+
171
+ content TEXT NOT NULL,
172
+ context TEXT,
173
+ message TEXT NOT NULL,
174
+ suggestion TEXT,
175
+
176
+ acknowledged INTEGER DEFAULT 0,
177
+ acknowledged_at TEXT,
178
+ acknowledged_by TEXT,
179
+ acknowledged_reason TEXT,
180
+
181
+ created_at TEXT DEFAULT (datetime('now'))
182
+ );
183
+
184
+ CREATE INDEX IF NOT EXISTS idx_scan_issues_record ON scan_issues(scan_record_id);
185
+
186
+ -- Feedbacks table
187
+ CREATE TABLE IF NOT EXISTS feedbacks (
188
+ id TEXT PRIMARY KEY,
189
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
190
+ skill_version TEXT NOT NULL,
191
+
192
+ feedback_type TEXT NOT NULL,
193
+ rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
194
+ comment TEXT,
195
+
196
+ context TEXT DEFAULT '{}',
197
+
198
+ status TEXT DEFAULT 'pending',
199
+ reviewer_notes TEXT,
200
+ reviewed_at TEXT,
201
+ reviewed_by TEXT,
202
+
203
+ source_ip_hash TEXT,
204
+ created_at TEXT DEFAULT (datetime('now'))
205
+ );
206
+
207
+ CREATE INDEX IF NOT EXISTS idx_feedbacks_skill ON feedbacks(skill_id);
208
+ CREATE INDEX IF NOT EXISTS idx_feedbacks_type ON feedbacks(feedback_type);
209
+ CREATE INDEX IF NOT EXISTS idx_feedbacks_status ON feedbacks(status);
210
+
211
+ -- Feedback queue table
212
+ CREATE TABLE IF NOT EXISTS feedback_queue (
213
+ id TEXT PRIMARY KEY,
214
+ skill_name TEXT NOT NULL,
215
+ skill_version TEXT NOT NULL,
216
+ feedback TEXT NOT NULL,
217
+
218
+ status TEXT DEFAULT 'pending',
219
+
220
+ attempts INTEGER DEFAULT 0,
221
+ max_attempts INTEGER DEFAULT 5,
222
+ last_attempt_at TEXT,
223
+ next_retry_at TEXT,
224
+ last_error TEXT,
225
+
226
+ base_delay INTEGER DEFAULT 1000,
227
+ current_delay INTEGER DEFAULT 1000,
228
+ max_delay INTEGER DEFAULT 3600000,
229
+
230
+ created_at TEXT DEFAULT (datetime('now')),
231
+ processed_at TEXT
232
+ );
233
+
234
+ CREATE INDEX IF NOT EXISTS idx_feedback_queue_status ON feedback_queue(status);
235
+ CREATE INDEX IF NOT EXISTS idx_feedback_queue_next_retry ON feedback_queue(next_retry_at);
236
+
237
+ -- Audit logs table
238
+ CREATE TABLE IF NOT EXISTS audit_logs (
239
+ id TEXT PRIMARY KEY,
240
+ timestamp TEXT DEFAULT (datetime('now')),
241
+
242
+ event_type TEXT NOT NULL,
243
+
244
+ actor TEXT NOT NULL,
245
+ resource TEXT NOT NULL,
246
+
247
+ action TEXT NOT NULL,
248
+ result TEXT NOT NULL,
249
+
250
+ details TEXT,
251
+ changes TEXT,
252
+
253
+ error_code TEXT,
254
+ error_message TEXT
255
+ );
256
+
257
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
258
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type ON audit_logs(event_type);
259
+
260
+ -- Cache metadata table
261
+ CREATE TABLE IF NOT EXISTS cache_metadata (
262
+ id TEXT PRIMARY KEY,
263
+ skill_name TEXT NOT NULL,
264
+ version TEXT NOT NULL,
265
+
266
+ cached_at TEXT DEFAULT (datetime('now')),
267
+ expires_at TEXT,
268
+
269
+ size INTEGER,
270
+ hit_count INTEGER DEFAULT 0,
271
+ last_hit_at TEXT,
272
+
273
+ source TEXT,
274
+ integrity_hash TEXT,
275
+
276
+ UNIQUE(skill_name, version)
277
+ );
278
+
279
+ CREATE INDEX IF NOT EXISTS idx_cache_metadata_expires ON cache_metadata(expires_at);
280
+
281
+ -- Use records table
282
+ CREATE TABLE IF NOT EXISTS use_records (
283
+ id TEXT PRIMARY KEY,
284
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
285
+ version_id TEXT REFERENCES versions(id),
286
+ user_id TEXT,
287
+
288
+ source TEXT NOT NULL,
289
+ cache_hit INTEGER DEFAULT 0,
290
+ client_version TEXT,
291
+
292
+ platform TEXT,
293
+ arch TEXT,
294
+
295
+ ip TEXT NOT NULL,
296
+ user_agent TEXT,
297
+ country TEXT,
298
+ region TEXT,
299
+
300
+ created_at TEXT DEFAULT (datetime('now'))
301
+ );
302
+
303
+ CREATE INDEX IF NOT EXISTS idx_use_records_skill ON use_records(skill_id, created_at);
304
+ `;
305
+
306
+ // ============================================================================
307
+ // SQLite Storage Implementation
308
+ // ============================================================================
309
+
310
+ export class SQLiteStorage implements IStorage {
311
+ private db: SqlJsDatabase | null = null;
312
+ private dbPath: string;
313
+ private saveInterval: ReturnType<typeof setInterval> | null = null;
314
+ private isDirty = false;
315
+
316
+ constructor(dbPath: string) {
317
+ this.dbPath = dbPath;
318
+ }
319
+
320
+ // -------------------------------------------------------------------------
321
+ // Lifecycle
322
+ // -------------------------------------------------------------------------
323
+
324
+ async initialize(): Promise<void> {
325
+ try {
326
+ const SQL = await initSqlJs();
327
+
328
+ // Load existing database or create new one
329
+ if (fs.existsSync(this.dbPath)) {
330
+ const buffer = fs.readFileSync(this.dbPath);
331
+ this.db = new SQL.Database(buffer);
332
+ } else {
333
+ // Ensure directory exists
334
+ const dir = path.dirname(this.dbPath);
335
+ if (!fs.existsSync(dir)) {
336
+ fs.mkdirSync(dir, { recursive: true });
337
+ }
338
+ this.db = new SQL.Database();
339
+ }
340
+
341
+ // Enable foreign keys and run schema
342
+ this.db.run('PRAGMA foreign_keys = ON;');
343
+ this.db.run(SCHEMA);
344
+
345
+ // Save periodically (every 5 seconds if dirty)
346
+ this.saveInterval = setInterval(() => {
347
+ if (this.isDirty) {
348
+ this.persist();
349
+ }
350
+ }, 5000);
351
+
352
+ logger.info('SQLite storage initialized', { path: this.dbPath });
353
+ } catch (error) {
354
+ logger.error('Failed to initialize SQLite storage', { error });
355
+ throw error;
356
+ }
357
+ }
358
+
359
+ private persist(): void {
360
+ if (this.db && this.isDirty) {
361
+ const data = this.db.export();
362
+ const buffer = Buffer.from(data);
363
+ fs.writeFileSync(this.dbPath, buffer);
364
+ this.isDirty = false;
365
+ }
366
+ }
367
+
368
+ async close(): Promise<void> {
369
+ if (this.saveInterval) {
370
+ clearInterval(this.saveInterval);
371
+ this.saveInterval = null;
372
+ }
373
+ if (this.db) {
374
+ this.persist();
375
+ this.db.close();
376
+ this.db = null;
377
+ logger.info('SQLite storage closed');
378
+ }
379
+ }
380
+
381
+ async healthCheck(): Promise<boolean> {
382
+ try {
383
+ this.getDb().exec('SELECT 1');
384
+ return true;
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+
390
+ async sync(): Promise<void> {
391
+ this.persist();
392
+ logger.debug('SQLite storage synced to disk');
393
+ }
394
+
395
+ /**
396
+ * Reload the database from disk file
397
+ * This is useful when external processes (like CLI) have updated the database
398
+ */
399
+ async reload(): Promise<void> {
400
+ try {
401
+ // First persist any pending changes
402
+ this.persist();
403
+
404
+ // Check if database file exists
405
+ if (!fs.existsSync(this.dbPath)) {
406
+ logger.warn('Database file not found during reload', { path: this.dbPath });
407
+ return;
408
+ }
409
+
410
+ // Read the updated file
411
+ const buffer = fs.readFileSync(this.dbPath);
412
+
413
+ // Get SQL.js constructor
414
+ const SQL = await initSqlJs();
415
+
416
+ // Create new database from file
417
+ const newDb = new SQL.Database(buffer);
418
+ newDb.run('PRAGMA foreign_keys = ON;');
419
+
420
+ // Close old database
421
+ if (this.db) {
422
+ this.db.close();
423
+ }
424
+
425
+ // Replace with new database
426
+ this.db = newDb;
427
+ this.isDirty = false;
428
+
429
+ logger.info('Database reloaded from disk', { path: this.dbPath });
430
+ } catch (error) {
431
+ logger.error('Failed to reload database', { error });
432
+ throw error;
433
+ }
434
+ }
435
+
436
+ private getDb(): SqlJsDatabase {
437
+ if (!this.db) {
438
+ throw new Error('Database not initialized');
439
+ }
440
+ return this.db;
441
+ }
442
+
443
+ private run(sql: string, params?: Record<string, unknown>): void {
444
+ const db = this.getDb();
445
+ if (params) {
446
+ db.run(sql, params as Record<string, string | number | null | Uint8Array>);
447
+ } else {
448
+ db.run(sql);
449
+ }
450
+ this.isDirty = true;
451
+ }
452
+
453
+ private get<T>(sql: string, params?: SqlParams): T | undefined {
454
+ const db = this.getDb();
455
+ const stmt = db.prepare(sql);
456
+ if (params) {
457
+ stmt.bind(params);
458
+ }
459
+ if (stmt.step()) {
460
+ const columns = stmt.getColumnNames();
461
+ const values = stmt.get();
462
+ stmt.free();
463
+ const row: Record<string, unknown> = {};
464
+ columns.forEach((col, i) => {
465
+ row[col] = values[i];
466
+ });
467
+ return row as T;
468
+ }
469
+ stmt.free();
470
+ return undefined;
471
+ }
472
+
473
+ private all<T>(sql: string, params?: SqlParams): T[] {
474
+ const db = this.getDb();
475
+ const stmt = db.prepare(sql);
476
+ if (params) {
477
+ stmt.bind(params);
478
+ }
479
+ const results: T[] = [];
480
+ const columns = stmt.getColumnNames();
481
+ while (stmt.step()) {
482
+ const values = stmt.get();
483
+ const row: Record<string, unknown> = {};
484
+ columns.forEach((col, i) => {
485
+ row[col] = values[i];
486
+ });
487
+ results.push(row as T);
488
+ }
489
+ stmt.free();
490
+ return results;
491
+ }
492
+
493
+ // -------------------------------------------------------------------------
494
+ // Skills
495
+ // -------------------------------------------------------------------------
496
+
497
+ async createSkill(skill: Skill): Promise<Skill> {
498
+ const searchableText = [
499
+ skill.name,
500
+ skill.displayName,
501
+ skill.description,
502
+ skill.keywords.join(' '),
503
+ ].filter(Boolean).join(' ');
504
+
505
+ this.run(`
506
+ INSERT INTO skills (
507
+ id, name, scope, full_name, owner_id, owner_type,
508
+ display_name, description, category, keywords, license,
509
+ repository, homepage, derived_from, latest_version, latest_version_id,
510
+ visibility, status, deprecated_message, security_score, security_level,
511
+ last_scan_at, stats, rating, searchable_text, created_at, updated_at, published_at
512
+ ) VALUES (
513
+ $id, $name, $scope, $fullName, $ownerId, $ownerType,
514
+ $displayName, $description, $category, $keywords, $license,
515
+ $repository, $homepage, $derivedFrom, $latestVersion, $latestVersionId,
516
+ $visibility, $status, $deprecatedMessage, $securityScore, $securityLevel,
517
+ $lastScanAt, $stats, $rating, $searchableText, $createdAt, $updatedAt, $publishedAt
518
+ )
519
+ `, {
520
+ $id: skill.id,
521
+ $name: skill.name,
522
+ $scope: skill.scope ?? null,
523
+ $fullName: skill.fullName,
524
+ $ownerId: skill.ownerId ?? null,
525
+ $ownerType: skill.ownerType,
526
+ $displayName: skill.displayName ?? null,
527
+ $description: skill.description,
528
+ $category: skill.category ?? null,
529
+ $keywords: JSON.stringify(skill.keywords),
530
+ $license: skill.license ?? null,
531
+ $repository: skill.repository ?? null,
532
+ $homepage: skill.homepage ?? null,
533
+ $derivedFrom: skill.derivedFrom ? JSON.stringify(skill.derivedFrom) : null,
534
+ $latestVersion: skill.latestVersion,
535
+ $latestVersionId: skill.latestVersionId ?? null,
536
+ $visibility: skill.visibility,
537
+ $status: skill.status,
538
+ $deprecatedMessage: skill.deprecatedMessage ?? null,
539
+ $securityScore: skill.securityScore ?? null,
540
+ $securityLevel: skill.securityLevel ?? null,
541
+ $lastScanAt: skill.lastScanAt ?? null,
542
+ $stats: JSON.stringify(skill.stats),
543
+ $rating: JSON.stringify(skill.rating),
544
+ $searchableText: searchableText,
545
+ $createdAt: skill.createdAt,
546
+ $updatedAt: skill.updatedAt,
547
+ $publishedAt: skill.publishedAt ?? null,
548
+ });
549
+
550
+ return skill;
551
+ }
552
+
553
+ async getSkillById(id: string): Promise<Skill | null> {
554
+ const row = this.get<Record<string, unknown>>('SELECT * FROM skills WHERE id = ?', [id]);
555
+ return row ? this.rowToSkill(row) : null;
556
+ }
557
+
558
+ async getSkillByName(fullName: string): Promise<Skill | null> {
559
+ const row = this.get<Record<string, unknown>>('SELECT * FROM skills WHERE full_name = ?', [fullName]);
560
+ return row ? this.rowToSkill(row) : null;
561
+ }
562
+
563
+ async updateSkill(id: string, updates: Partial<Skill>): Promise<Skill | null> {
564
+ const existing = await this.getSkillById(id);
565
+ if (!existing) return null;
566
+
567
+ const updated = { ...existing, ...updates, updatedAt: now() };
568
+
569
+ const searchableText = [
570
+ updated.name,
571
+ updated.displayName,
572
+ updated.description,
573
+ updated.keywords.join(' '),
574
+ ].filter(Boolean).join(' ');
575
+
576
+ this.run(`
577
+ UPDATE skills SET
578
+ name = $name,
579
+ scope = $scope,
580
+ full_name = $fullName,
581
+ owner_id = $ownerId,
582
+ owner_type = $ownerType,
583
+ display_name = $displayName,
584
+ description = $description,
585
+ category = $category,
586
+ keywords = $keywords,
587
+ license = $license,
588
+ repository = $repository,
589
+ homepage = $homepage,
590
+ derived_from = $derivedFrom,
591
+ latest_version = $latestVersion,
592
+ latest_version_id = $latestVersionId,
593
+ visibility = $visibility,
594
+ status = $status,
595
+ deprecated_message = $deprecatedMessage,
596
+ security_score = $securityScore,
597
+ security_level = $securityLevel,
598
+ last_scan_at = $lastScanAt,
599
+ stats = $stats,
600
+ rating = $rating,
601
+ searchable_text = $searchableText,
602
+ updated_at = $updatedAt,
603
+ published_at = $publishedAt
604
+ WHERE id = $id
605
+ `, {
606
+ $id: updated.id,
607
+ $name: updated.name,
608
+ $scope: updated.scope ?? null,
609
+ $fullName: updated.fullName,
610
+ $ownerId: updated.ownerId ?? null,
611
+ $ownerType: updated.ownerType,
612
+ $displayName: updated.displayName ?? null,
613
+ $description: updated.description,
614
+ $category: updated.category ?? null,
615
+ $keywords: JSON.stringify(updated.keywords),
616
+ $license: updated.license ?? null,
617
+ $repository: updated.repository ?? null,
618
+ $homepage: updated.homepage ?? null,
619
+ $derivedFrom: updated.derivedFrom ? JSON.stringify(updated.derivedFrom) : null,
620
+ $latestVersion: updated.latestVersion,
621
+ $latestVersionId: updated.latestVersionId ?? null,
622
+ $visibility: updated.visibility,
623
+ $status: updated.status,
624
+ $deprecatedMessage: updated.deprecatedMessage ?? null,
625
+ $securityScore: updated.securityScore ?? null,
626
+ $securityLevel: updated.securityLevel ?? null,
627
+ $lastScanAt: updated.lastScanAt ?? null,
628
+ $stats: JSON.stringify(updated.stats),
629
+ $rating: JSON.stringify(updated.rating),
630
+ $searchableText: searchableText,
631
+ $updatedAt: updated.updatedAt,
632
+ $publishedAt: updated.publishedAt ?? null,
633
+ });
634
+
635
+ return updated;
636
+ }
637
+
638
+ async deleteSkill(id: string): Promise<boolean> {
639
+ const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE id = ?', [id]);
640
+ this.run('DELETE FROM skills WHERE id = ?', { $id: id });
641
+ const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE id = ?', [id]);
642
+ return (before?.count ?? 0) > (after?.count ?? 0);
643
+ }
644
+
645
+ async searchSkills(options: SkillQueryOptions): Promise<PaginatedResult<Skill>> {
646
+ const limit = Math.min(options.limit ?? 20, 100);
647
+ const conditions: string[] = ["visibility = 'public'", "status = 'active'"];
648
+ const params: SqlParams = [];
649
+
650
+ // Basic search using LIKE (sql.js doesn't support FTS5)
651
+ if (options.query) {
652
+ conditions.push('(name LIKE ? OR display_name LIKE ? OR description LIKE ? OR searchable_text LIKE ?)');
653
+ const searchTerm = `%${options.query}%`;
654
+ params.push(searchTerm, searchTerm, searchTerm, searchTerm);
655
+ }
656
+
657
+ if (options.category) {
658
+ conditions.push('category = ?');
659
+ params.push(options.category);
660
+ }
661
+ if (options.author) {
662
+ conditions.push('owner_id = ?');
663
+ params.push(options.author);
664
+ }
665
+ if (options.visibility) {
666
+ // Override the default public visibility
667
+ conditions[0] = 'visibility = ?';
668
+ params.unshift(options.visibility);
669
+ }
670
+
671
+ // Sorting
672
+ const sortField = options.sort ?? 'updated';
673
+ const sortOrder = options.order ?? 'desc';
674
+ const sortMap: Record<string, string> = {
675
+ relevance: 'updated_at',
676
+ uses: "json_extract(stats, '$.totalUses')",
677
+ updated: 'updated_at',
678
+ created: 'created_at',
679
+ name: 'name',
680
+ };
681
+
682
+ let query = `SELECT * FROM skills WHERE ${conditions.join(' AND ')}`;
683
+ let countQuery = `SELECT COUNT(*) as count FROM skills WHERE ${conditions.join(' AND ')}`;
684
+
685
+ if (options.cursor) {
686
+ query += ' AND id > ?';
687
+ params.push(options.cursor);
688
+ }
689
+
690
+ query += ` ORDER BY ${sortMap[sortField]} ${sortOrder.toUpperCase()} LIMIT ?`;
691
+ params.push(limit + 1);
692
+
693
+ const rows = this.all<Record<string, unknown>>(query, params);
694
+ const countParams = options.cursor ? params.slice(0, -2) : params.slice(0, -1);
695
+ const countResult = this.get<{ count: number }>(countQuery, countParams);
696
+
697
+ const hasMore = rows.length > limit;
698
+ const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
699
+
700
+ return {
701
+ items,
702
+ pagination: {
703
+ total: countResult?.count ?? 0,
704
+ limit,
705
+ hasMore,
706
+ cursor: options.cursor,
707
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
708
+ },
709
+ };
710
+ }
711
+
712
+ async getSkillsByOwner(ownerId: string, options?: PaginationOptions): Promise<PaginatedResult<Skill>> {
713
+ const limit = Math.min(options?.limit ?? 20, 100);
714
+ const params: SqlParams = [ownerId];
715
+
716
+ let query = 'SELECT * FROM skills WHERE owner_id = ?';
717
+
718
+ if (options?.cursor) {
719
+ query += ' AND id > ?';
720
+ params.push(options.cursor);
721
+ }
722
+
723
+ query += ' ORDER BY created_at DESC LIMIT ?';
724
+ params.push(limit + 1);
725
+
726
+ const rows = this.all<Record<string, unknown>>(query, params);
727
+ const countResult = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE owner_id = ?', [ownerId]);
728
+
729
+ const hasMore = rows.length > limit;
730
+ const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
731
+
732
+ return {
733
+ items,
734
+ pagination: {
735
+ total: countResult?.count ?? 0,
736
+ limit,
737
+ hasMore,
738
+ cursor: options?.cursor,
739
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
740
+ },
741
+ };
742
+ }
743
+
744
+ async getDerivedSkills(skillId: string, options?: PaginationOptions): Promise<PaginatedResult<Skill>> {
745
+ const limit = Math.min(options?.limit ?? 20, 100);
746
+ const params: SqlParams = [skillId];
747
+
748
+ let query = "SELECT * FROM skills WHERE json_extract(derived_from, '$.skillId') = ?";
749
+
750
+ if (options?.cursor) {
751
+ query += ' AND id > ?';
752
+ params.push(options.cursor);
753
+ }
754
+
755
+ query += ' ORDER BY created_at DESC LIMIT ?';
756
+ params.push(limit + 1);
757
+
758
+ const rows = this.all<Record<string, unknown>>(query, params);
759
+ const countResult = this.get<{ count: number }>(
760
+ "SELECT COUNT(*) as count FROM skills WHERE json_extract(derived_from, '$.skillId') = ?",
761
+ [skillId]
762
+ );
763
+
764
+ const hasMore = rows.length > limit;
765
+ const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
766
+
767
+ return {
768
+ items,
769
+ pagination: {
770
+ total: countResult?.count ?? 0,
771
+ limit,
772
+ hasMore,
773
+ cursor: options?.cursor,
774
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
775
+ },
776
+ };
777
+ }
778
+
779
+ async incrementSkillUses(id: string): Promise<void> {
780
+ this.run(`
781
+ UPDATE skills
782
+ SET stats = json_set(stats, '$.totalUses', json_extract(stats, '$.totalUses') + 1)
783
+ WHERE id = ?
784
+ `, { $id: id });
785
+ }
786
+
787
+ // -------------------------------------------------------------------------
788
+ // Versions
789
+ // -------------------------------------------------------------------------
790
+
791
+ async createVersion(version: Version): Promise<Version> {
792
+ this.run(`
793
+ INSERT INTO versions (
794
+ id, skill_id, version, tag, content, package_url, package_size, package_hash,
795
+ changelog, scan_record_id, security_score, security_level, status,
796
+ deprecated_message, published_by, uses, created_at, published_at, deprecated_at
797
+ ) VALUES (
798
+ $id, $skillId, $version, $tag, $content, $packageUrl, $packageSize, $packageHash,
799
+ $changelog, $scanRecordId, $securityScore, $securityLevel, $status,
800
+ $deprecatedMessage, $publishedBy, $uses, $createdAt, $publishedAt, $deprecatedAt
801
+ )
802
+ `, {
803
+ $id: version.id,
804
+ $skillId: version.skillId,
805
+ $version: version.version,
806
+ $tag: version.tag ?? null,
807
+ $content: JSON.stringify(version.content),
808
+ $packageUrl: version.packageUrl,
809
+ $packageSize: version.packageSize,
810
+ $packageHash: version.packageHash,
811
+ $changelog: version.changelog ?? null,
812
+ $scanRecordId: version.scanRecordId ?? null,
813
+ $securityScore: version.securityScore ?? null,
814
+ $securityLevel: version.securityLevel ?? null,
815
+ $status: version.status,
816
+ $deprecatedMessage: version.deprecatedMessage ?? null,
817
+ $publishedBy: version.publishedBy ?? null,
818
+ $uses: version.uses,
819
+ $createdAt: version.createdAt,
820
+ $publishedAt: version.publishedAt,
821
+ $deprecatedAt: version.deprecatedAt ?? null,
822
+ });
823
+
824
+ return version;
825
+ }
826
+
827
+ async getVersionById(id: string): Promise<Version | null> {
828
+ const row = this.get<Record<string, unknown>>('SELECT * FROM versions WHERE id = ?', [id]);
829
+ return row ? this.rowToVersion(row) : null;
830
+ }
831
+
832
+ async getVersion(skillId: string, version: string): Promise<Version | null> {
833
+ const row = this.get<Record<string, unknown>>(
834
+ 'SELECT * FROM versions WHERE skill_id = ? AND version = ?',
835
+ [skillId, version]
836
+ );
837
+ return row ? this.rowToVersion(row) : null;
838
+ }
839
+
840
+ async getLatestVersion(skillId: string): Promise<Version | null> {
841
+ let row = this.get<Record<string, unknown>>(
842
+ "SELECT * FROM versions WHERE skill_id = ? AND tag = 'latest' LIMIT 1",
843
+ [skillId]
844
+ );
845
+
846
+ if (!row) {
847
+ row = this.get<Record<string, unknown>>(
848
+ 'SELECT * FROM versions WHERE skill_id = ? ORDER BY published_at DESC LIMIT 1',
849
+ [skillId]
850
+ );
851
+ }
852
+
853
+ return row ? this.rowToVersion(row) : null;
854
+ }
855
+
856
+ async getVersions(skillId: string, options?: VersionQueryOptions): Promise<PaginatedResult<Version>> {
857
+ const limit = Math.min(options?.limit ?? 20, 100);
858
+ const params: SqlParams = [skillId];
859
+
860
+ let query = 'SELECT * FROM versions WHERE skill_id = ?';
861
+
862
+ if (!options?.includeDeprecated) {
863
+ query += " AND status != 'deprecated'";
864
+ }
865
+
866
+ if (options?.cursor) {
867
+ query += ' AND id > ?';
868
+ params.push(options.cursor);
869
+ }
870
+
871
+ query += ' ORDER BY published_at DESC LIMIT ?';
872
+ params.push(limit + 1);
873
+
874
+ const rows = this.all<Record<string, unknown>>(query, params);
875
+ const countResult = this.get<{ count: number }>(
876
+ 'SELECT COUNT(*) as count FROM versions WHERE skill_id = ?',
877
+ [skillId]
878
+ );
879
+
880
+ const hasMore = rows.length > limit;
881
+ const items = rows.slice(0, limit).map(row => this.rowToVersion(row));
882
+
883
+ return {
884
+ items,
885
+ pagination: {
886
+ total: countResult?.count ?? 0,
887
+ limit,
888
+ hasMore,
889
+ cursor: options?.cursor,
890
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
891
+ },
892
+ };
893
+ }
894
+
895
+ async updateVersion(id: string, updates: Partial<Version>): Promise<Version | null> {
896
+ const existing = await this.getVersionById(id);
897
+ if (!existing) return null;
898
+
899
+ const updated = { ...existing, ...updates };
900
+
901
+ this.run(`
902
+ UPDATE versions SET
903
+ tag = $tag,
904
+ changelog = $changelog,
905
+ scan_record_id = $scanRecordId,
906
+ security_score = $securityScore,
907
+ security_level = $securityLevel,
908
+ status = $status,
909
+ deprecated_message = $deprecatedMessage,
910
+ uses = $uses,
911
+ deprecated_at = $deprecatedAt
912
+ WHERE id = $id
913
+ `, {
914
+ $id: updated.id,
915
+ $tag: updated.tag ?? null,
916
+ $changelog: updated.changelog ?? null,
917
+ $scanRecordId: updated.scanRecordId ?? null,
918
+ $securityScore: updated.securityScore ?? null,
919
+ $securityLevel: updated.securityLevel ?? null,
920
+ $status: updated.status,
921
+ $deprecatedMessage: updated.deprecatedMessage ?? null,
922
+ $uses: updated.uses,
923
+ $deprecatedAt: updated.deprecatedAt ?? null,
924
+ });
925
+
926
+ return updated;
927
+ }
928
+
929
+ async deleteVersion(id: string): Promise<boolean> {
930
+ const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM versions WHERE id = ?', [id]);
931
+ this.run('DELETE FROM versions WHERE id = ?', { $id: id });
932
+ const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM versions WHERE id = ?', [id]);
933
+ return (before?.count ?? 0) > (after?.count ?? 0);
934
+ }
935
+
936
+ async deprecateVersion(id: string, message: string): Promise<Version | null> {
937
+ return this.updateVersion(id, {
938
+ status: 'deprecated',
939
+ deprecatedMessage: message,
940
+ deprecatedAt: now(),
941
+ });
942
+ }
943
+
944
+ async incrementVersionUses(id: string): Promise<void> {
945
+ this.run('UPDATE versions SET uses = uses + 1 WHERE id = ?', { $id: id });
946
+ }
947
+
948
+ // -------------------------------------------------------------------------
949
+ // Scan Records
950
+ // -------------------------------------------------------------------------
951
+
952
+ async createScanRecord(record: ScanRecord): Promise<ScanRecord> {
953
+ this.run(`
954
+ INSERT INTO scan_records (
955
+ id, skill_id, version_id, user_id, input_type, content_hash, content_size,
956
+ score, level, issue_count, scanner_version, rules_version, rules_applied,
957
+ duration, status, error_message, created_at, completed_at
958
+ ) VALUES (
959
+ $id, $skillId, $versionId, $userId, $inputType, $contentHash, $contentSize,
960
+ $score, $level, $issueCount, $scannerVersion, $rulesVersion, $rulesApplied,
961
+ $duration, $status, $errorMessage, $createdAt, $completedAt
962
+ )
963
+ `, {
964
+ $id: record.id,
965
+ $skillId: record.skillId ?? null,
966
+ $versionId: record.versionId ?? null,
967
+ $userId: record.userId ?? null,
968
+ $inputType: record.inputType,
969
+ $contentHash: record.contentHash,
970
+ $contentSize: record.contentSize,
971
+ $score: record.score ?? null,
972
+ $level: record.level ?? null,
973
+ $issueCount: JSON.stringify(record.issueCount),
974
+ $scannerVersion: record.scannerVersion ?? null,
975
+ $rulesVersion: record.rulesVersion ?? null,
976
+ $rulesApplied: record.rulesApplied ?? null,
977
+ $duration: record.duration ?? null,
978
+ $status: record.status,
979
+ $errorMessage: record.errorMessage ?? null,
980
+ $createdAt: record.createdAt,
981
+ $completedAt: record.completedAt ?? null,
982
+ });
983
+
984
+ return record;
985
+ }
986
+
987
+ async getScanRecordById(id: string): Promise<ScanRecord | null> {
988
+ const row = this.get<Record<string, unknown>>('SELECT * FROM scan_records WHERE id = ?', [id]);
989
+ return row ? this.rowToScanRecord(row) : null;
990
+ }
991
+
992
+ async getScanRecords(skillId: string, options?: PaginationOptions): Promise<PaginatedResult<ScanRecord>> {
993
+ const limit = Math.min(options?.limit ?? 20, 100);
994
+ const params: SqlParams = [skillId];
995
+
996
+ let query = 'SELECT * FROM scan_records WHERE skill_id = ?';
997
+
998
+ if (options?.cursor) {
999
+ query += ' AND id > ?';
1000
+ params.push(options.cursor);
1001
+ }
1002
+
1003
+ query += ' ORDER BY created_at DESC LIMIT ?';
1004
+ params.push(limit + 1);
1005
+
1006
+ const rows = this.all<Record<string, unknown>>(query, params);
1007
+ const countResult = this.get<{ count: number }>(
1008
+ 'SELECT COUNT(*) as count FROM scan_records WHERE skill_id = ?',
1009
+ [skillId]
1010
+ );
1011
+
1012
+ const hasMore = rows.length > limit;
1013
+ const items = rows.slice(0, limit).map(row => this.rowToScanRecord(row));
1014
+
1015
+ return {
1016
+ items,
1017
+ pagination: {
1018
+ total: countResult?.count ?? 0,
1019
+ limit,
1020
+ hasMore,
1021
+ cursor: options?.cursor,
1022
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
1023
+ },
1024
+ };
1025
+ }
1026
+
1027
+ async updateScanRecord(id: string, updates: Partial<ScanRecord>): Promise<ScanRecord | null> {
1028
+ const existing = await this.getScanRecordById(id);
1029
+ if (!existing) return null;
1030
+
1031
+ const updated = { ...existing, ...updates };
1032
+
1033
+ this.run(`
1034
+ UPDATE scan_records SET
1035
+ score = $score,
1036
+ level = $level,
1037
+ issue_count = $issueCount,
1038
+ scanner_version = $scannerVersion,
1039
+ rules_version = $rulesVersion,
1040
+ rules_applied = $rulesApplied,
1041
+ duration = $duration,
1042
+ status = $status,
1043
+ error_message = $errorMessage,
1044
+ completed_at = $completedAt
1045
+ WHERE id = $id
1046
+ `, {
1047
+ $id: updated.id,
1048
+ $score: updated.score ?? null,
1049
+ $level: updated.level ?? null,
1050
+ $issueCount: JSON.stringify(updated.issueCount),
1051
+ $scannerVersion: updated.scannerVersion ?? null,
1052
+ $rulesVersion: updated.rulesVersion ?? null,
1053
+ $rulesApplied: updated.rulesApplied ?? null,
1054
+ $duration: updated.duration ?? null,
1055
+ $status: updated.status,
1056
+ $errorMessage: updated.errorMessage ?? null,
1057
+ $completedAt: updated.completedAt ?? null,
1058
+ });
1059
+
1060
+ return updated;
1061
+ }
1062
+
1063
+ // -------------------------------------------------------------------------
1064
+ // Scan Issues
1065
+ // -------------------------------------------------------------------------
1066
+
1067
+ async createScanIssues(issues: ScanIssue[]): Promise<ScanIssue[]> {
1068
+ for (const issue of issues) {
1069
+ this.run(`
1070
+ INSERT INTO scan_issues (
1071
+ id, scan_record_id, rule_id, rule_name, rule_category, severity,
1072
+ file, line, column_num, end_line, end_column, content, context,
1073
+ message, suggestion, acknowledged, acknowledged_at, acknowledged_by,
1074
+ acknowledged_reason, created_at
1075
+ ) VALUES (
1076
+ $id, $scanRecordId, $ruleId, $ruleName, $ruleCategory, $severity,
1077
+ $file, $line, $column, $endLine, $endColumn, $content, $context,
1078
+ $message, $suggestion, $acknowledged, $acknowledgedAt, $acknowledgedBy,
1079
+ $acknowledgedReason, $createdAt
1080
+ )
1081
+ `, {
1082
+ $id: issue.id,
1083
+ $scanRecordId: issue.scanRecordId,
1084
+ $ruleId: issue.ruleId,
1085
+ $ruleName: issue.ruleName,
1086
+ $ruleCategory: issue.ruleCategory,
1087
+ $severity: issue.severity,
1088
+ $file: issue.file ?? null,
1089
+ $line: issue.line ?? null,
1090
+ $column: issue.column ?? null,
1091
+ $endLine: issue.endLine ?? null,
1092
+ $endColumn: issue.endColumn ?? null,
1093
+ $content: issue.content,
1094
+ $context: issue.context ?? null,
1095
+ $message: issue.message,
1096
+ $suggestion: issue.suggestion ?? null,
1097
+ $acknowledged: issue.acknowledged ? 1 : 0,
1098
+ $acknowledgedAt: issue.acknowledgedAt ?? null,
1099
+ $acknowledgedBy: issue.acknowledgedBy ?? null,
1100
+ $acknowledgedReason: issue.acknowledgedReason ?? null,
1101
+ $createdAt: issue.createdAt,
1102
+ });
1103
+ }
1104
+
1105
+ return issues;
1106
+ }
1107
+
1108
+ async getScanIssues(scanRecordId: string): Promise<ScanIssue[]> {
1109
+ const rows = this.all<Record<string, unknown>>(
1110
+ 'SELECT * FROM scan_issues WHERE scan_record_id = ? ORDER BY severity DESC, line ASC',
1111
+ [scanRecordId]
1112
+ );
1113
+ return rows.map(row => this.rowToScanIssue(row));
1114
+ }
1115
+
1116
+ async acknowledgeIssue(id: string, userId: string, reason?: string): Promise<ScanIssue | null> {
1117
+ this.run(`
1118
+ UPDATE scan_issues SET
1119
+ acknowledged = 1,
1120
+ acknowledged_at = $acknowledgedAt,
1121
+ acknowledged_by = $acknowledgedBy,
1122
+ acknowledged_reason = $acknowledgedReason
1123
+ WHERE id = $id
1124
+ `, {
1125
+ $id: id,
1126
+ $acknowledgedAt: now(),
1127
+ $acknowledgedBy: userId,
1128
+ $acknowledgedReason: reason ?? null,
1129
+ });
1130
+
1131
+ const row = this.get<Record<string, unknown>>('SELECT * FROM scan_issues WHERE id = ?', [id]);
1132
+ return row ? this.rowToScanIssue(row) : null;
1133
+ }
1134
+
1135
+ // -------------------------------------------------------------------------
1136
+ // Feedback
1137
+ // -------------------------------------------------------------------------
1138
+
1139
+ async createFeedback(feedback: Feedback): Promise<Feedback> {
1140
+ this.run(`
1141
+ INSERT INTO feedbacks (
1142
+ id, skill_id, skill_version, feedback_type, rating, comment,
1143
+ context, status, reviewer_notes, reviewed_at, reviewed_by,
1144
+ source_ip_hash, created_at
1145
+ ) VALUES (
1146
+ $id, $skillId, $skillVersion, $feedbackType, $rating, $comment,
1147
+ $context, $status, $reviewerNotes, $reviewedAt, $reviewedBy,
1148
+ $sourceIpHash, $createdAt
1149
+ )
1150
+ `, {
1151
+ $id: feedback.id,
1152
+ $skillId: feedback.skillId,
1153
+ $skillVersion: feedback.skillVersion,
1154
+ $feedbackType: feedback.feedbackType,
1155
+ $rating: feedback.rating,
1156
+ $comment: feedback.comment ?? null,
1157
+ $context: JSON.stringify(feedback.context),
1158
+ $status: feedback.status,
1159
+ $reviewerNotes: feedback.reviewerNotes ?? null,
1160
+ $reviewedAt: feedback.reviewedAt ?? null,
1161
+ $reviewedBy: feedback.reviewedBy ?? null,
1162
+ $sourceIpHash: feedback.sourceIpHash ?? null,
1163
+ $createdAt: feedback.createdAt,
1164
+ });
1165
+
1166
+ return feedback;
1167
+ }
1168
+
1169
+ async getFeedbackById(id: string): Promise<Feedback | null> {
1170
+ const row = this.get<Record<string, unknown>>('SELECT * FROM feedbacks WHERE id = ?', [id]);
1171
+ return row ? this.rowToFeedback(row) : null;
1172
+ }
1173
+
1174
+ async getFeedbacks(skillId: string, options?: FeedbackQueryOptions): Promise<PaginatedResult<Feedback>> {
1175
+ const limit = Math.min(options?.limit ?? 20, 100);
1176
+ const params: SqlParams = [skillId];
1177
+ const conditions: string[] = ['skill_id = ?'];
1178
+
1179
+ if (options?.feedbackType) {
1180
+ conditions.push('feedback_type = ?');
1181
+ params.push(options.feedbackType);
1182
+ }
1183
+
1184
+ if (options?.status) {
1185
+ conditions.push('status = ?');
1186
+ params.push(options.status);
1187
+ }
1188
+
1189
+ if (options?.cursor) {
1190
+ conditions.push('id > ?');
1191
+ params.push(options.cursor);
1192
+ }
1193
+
1194
+ const whereClause = conditions.join(' AND ');
1195
+ const query = `SELECT * FROM feedbacks WHERE ${whereClause} ORDER BY created_at DESC LIMIT ?`;
1196
+ params.push(limit + 1);
1197
+
1198
+ const rows = this.all<Record<string, unknown>>(query, params);
1199
+ const countResult = this.get<{ count: number }>(
1200
+ 'SELECT COUNT(*) as count FROM feedbacks WHERE skill_id = ?',
1201
+ [skillId]
1202
+ );
1203
+
1204
+ const hasMore = rows.length > limit;
1205
+ const items = rows.slice(0, limit).map(row => this.rowToFeedback(row));
1206
+
1207
+ return {
1208
+ items,
1209
+ pagination: {
1210
+ total: countResult?.count ?? 0,
1211
+ limit,
1212
+ hasMore,
1213
+ cursor: options?.cursor,
1214
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
1215
+ },
1216
+ };
1217
+ }
1218
+
1219
+ async updateFeedback(id: string, updates: Partial<Feedback>): Promise<Feedback | null> {
1220
+ const existing = await this.getFeedbackById(id);
1221
+ if (!existing) return null;
1222
+
1223
+ const updated = { ...existing, ...updates };
1224
+
1225
+ this.run(`
1226
+ UPDATE feedbacks SET
1227
+ status = $status,
1228
+ reviewer_notes = $reviewerNotes,
1229
+ reviewed_at = $reviewedAt,
1230
+ reviewed_by = $reviewedBy
1231
+ WHERE id = $id
1232
+ `, {
1233
+ $id: updated.id,
1234
+ $status: updated.status,
1235
+ $reviewerNotes: updated.reviewerNotes ?? null,
1236
+ $reviewedAt: updated.reviewedAt ?? null,
1237
+ $reviewedBy: updated.reviewedBy ?? null,
1238
+ });
1239
+
1240
+ return updated;
1241
+ }
1242
+
1243
+ // -------------------------------------------------------------------------
1244
+ // Feedback Queue
1245
+ // -------------------------------------------------------------------------
1246
+
1247
+ async enqueueFeedback(item: FeedbackQueueItem): Promise<FeedbackQueueItem> {
1248
+ this.run(`
1249
+ INSERT INTO feedback_queue (
1250
+ id, skill_name, skill_version, feedback, status, attempts, max_attempts,
1251
+ last_attempt_at, next_retry_at, last_error, base_delay, current_delay,
1252
+ max_delay, created_at, processed_at
1253
+ ) VALUES (
1254
+ $id, $skillName, $skillVersion, $feedback, $status, $attempts, $maxAttempts,
1255
+ $lastAttemptAt, $nextRetryAt, $lastError, $baseDelay, $currentDelay,
1256
+ $maxDelay, $createdAt, $processedAt
1257
+ )
1258
+ `, {
1259
+ $id: item.id,
1260
+ $skillName: item.skillName,
1261
+ $skillVersion: item.skillVersion,
1262
+ $feedback: JSON.stringify(item.feedback),
1263
+ $status: item.status,
1264
+ $attempts: item.attempts,
1265
+ $maxAttempts: item.maxAttempts,
1266
+ $lastAttemptAt: item.lastAttemptAt ?? null,
1267
+ $nextRetryAt: item.nextRetryAt ?? null,
1268
+ $lastError: item.lastError ?? null,
1269
+ $baseDelay: item.baseDelay,
1270
+ $currentDelay: item.currentDelay,
1271
+ $maxDelay: item.maxDelay,
1272
+ $createdAt: item.createdAt,
1273
+ $processedAt: item.processedAt ?? null,
1274
+ });
1275
+
1276
+ return item;
1277
+ }
1278
+
1279
+ async getPendingQueueItems(options?: QueueQueryOptions): Promise<FeedbackQueueItem[]> {
1280
+ const limit = options?.limit ?? 10;
1281
+
1282
+ let query: string;
1283
+ let params: SqlParams;
1284
+
1285
+ if (options?.status) {
1286
+ query = `
1287
+ SELECT * FROM feedback_queue
1288
+ WHERE status = ?
1289
+ AND (next_retry_at IS NULL OR next_retry_at <= datetime('now'))
1290
+ ORDER BY created_at ASC LIMIT ?
1291
+ `;
1292
+ params = [options.status, limit];
1293
+ } else {
1294
+ query = `
1295
+ SELECT * FROM feedback_queue
1296
+ WHERE status IN ('pending', 'retrying')
1297
+ AND (next_retry_at IS NULL OR next_retry_at <= datetime('now'))
1298
+ ORDER BY created_at ASC LIMIT ?
1299
+ `;
1300
+ params = [limit];
1301
+ }
1302
+
1303
+ const rows = this.all<Record<string, unknown>>(query, params);
1304
+ return rows.map(row => this.rowToQueueItem(row));
1305
+ }
1306
+
1307
+ async updateQueueItem(id: string, updates: Partial<FeedbackQueueItem>): Promise<FeedbackQueueItem | null> {
1308
+ const row = this.get<Record<string, unknown>>('SELECT * FROM feedback_queue WHERE id = ?', [id]);
1309
+ if (!row) return null;
1310
+
1311
+ const existing = this.rowToQueueItem(row);
1312
+ const updated = { ...existing, ...updates };
1313
+
1314
+ this.run(`
1315
+ UPDATE feedback_queue SET
1316
+ status = $status,
1317
+ attempts = $attempts,
1318
+ last_attempt_at = $lastAttemptAt,
1319
+ next_retry_at = $nextRetryAt,
1320
+ last_error = $lastError,
1321
+ current_delay = $currentDelay,
1322
+ processed_at = $processedAt
1323
+ WHERE id = $id
1324
+ `, {
1325
+ $id: updated.id,
1326
+ $status: updated.status,
1327
+ $attempts: updated.attempts,
1328
+ $lastAttemptAt: updated.lastAttemptAt ?? null,
1329
+ $nextRetryAt: updated.nextRetryAt ?? null,
1330
+ $lastError: updated.lastError ?? null,
1331
+ $currentDelay: updated.currentDelay,
1332
+ $processedAt: updated.processedAt ?? null,
1333
+ });
1334
+
1335
+ return updated;
1336
+ }
1337
+
1338
+ async removeFromQueue(id: string): Promise<boolean> {
1339
+ const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM feedback_queue WHERE id = ?', [id]);
1340
+ this.run('DELETE FROM feedback_queue WHERE id = ?', { $id: id });
1341
+ const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM feedback_queue WHERE id = ?', [id]);
1342
+ return (before?.count ?? 0) > (after?.count ?? 0);
1343
+ }
1344
+
1345
+ async getQueueSize(): Promise<number> {
1346
+ const result = this.get<{ count: number }>(
1347
+ "SELECT COUNT(*) as count FROM feedback_queue WHERE status IN ('pending', 'retrying')"
1348
+ );
1349
+ return result?.count ?? 0;
1350
+ }
1351
+
1352
+ async getDeadLetterItems(options?: PaginationOptions): Promise<PaginatedResult<FeedbackQueueItem>> {
1353
+ const limit = Math.min(options?.limit ?? 20, 100);
1354
+ const params: SqlParams = [];
1355
+
1356
+ let query = "SELECT * FROM feedback_queue WHERE status = 'dead_letter'";
1357
+
1358
+ if (options?.cursor) {
1359
+ query += ' AND id > ?';
1360
+ params.push(options.cursor);
1361
+ }
1362
+
1363
+ query += ' ORDER BY created_at DESC LIMIT ?';
1364
+ params.push(limit + 1);
1365
+
1366
+ const rows = this.all<Record<string, unknown>>(query, params);
1367
+ const countResult = this.get<{ count: number }>(
1368
+ "SELECT COUNT(*) as count FROM feedback_queue WHERE status = 'dead_letter'"
1369
+ );
1370
+
1371
+ const hasMore = rows.length > limit;
1372
+ const items = rows.slice(0, limit).map(row => this.rowToQueueItem(row));
1373
+
1374
+ return {
1375
+ items,
1376
+ pagination: {
1377
+ total: countResult?.count ?? 0,
1378
+ limit,
1379
+ hasMore,
1380
+ cursor: options?.cursor,
1381
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
1382
+ },
1383
+ };
1384
+ }
1385
+
1386
+ // -------------------------------------------------------------------------
1387
+ // Audit Logs
1388
+ // -------------------------------------------------------------------------
1389
+
1390
+ async createAuditLog(log: AuditLog): Promise<AuditLog> {
1391
+ this.run(`
1392
+ INSERT INTO audit_logs (
1393
+ id, timestamp, event_type, actor, resource, action, result,
1394
+ details, changes, error_code, error_message
1395
+ ) VALUES (
1396
+ $id, $timestamp, $eventType, $actor, $resource, $action, $result,
1397
+ $details, $changes, $errorCode, $errorMessage
1398
+ )
1399
+ `, {
1400
+ $id: log.id,
1401
+ $timestamp: log.timestamp,
1402
+ $eventType: log.eventType,
1403
+ $actor: JSON.stringify(log.actor),
1404
+ $resource: JSON.stringify(log.resource),
1405
+ $action: log.action,
1406
+ $result: log.result,
1407
+ $details: log.details ? JSON.stringify(log.details) : null,
1408
+ $changes: log.changes ? JSON.stringify(log.changes) : null,
1409
+ $errorCode: log.errorCode ?? null,
1410
+ $errorMessage: log.errorMessage ?? null,
1411
+ });
1412
+
1413
+ return log;
1414
+ }
1415
+
1416
+ async getAuditLogs(options?: AuditQueryOptions): Promise<PaginatedResult<AuditLog>> {
1417
+ const limit = Math.min(options?.limit ?? 50, 100);
1418
+ const conditions: string[] = ['1=1'];
1419
+ const params: SqlParams = [];
1420
+
1421
+ if (options?.eventType) {
1422
+ conditions.push('event_type = ?');
1423
+ params.push(options.eventType);
1424
+ }
1425
+
1426
+ if (options?.actor) {
1427
+ conditions.push("json_extract(actor, '$.id') = ?");
1428
+ params.push(options.actor);
1429
+ }
1430
+
1431
+ if (options?.resourceType) {
1432
+ conditions.push("json_extract(resource, '$.type') = ?");
1433
+ params.push(options.resourceType);
1434
+ }
1435
+
1436
+ if (options?.resourceName) {
1437
+ conditions.push("json_extract(resource, '$.name') = ?");
1438
+ params.push(options.resourceName);
1439
+ }
1440
+
1441
+ if (options?.from) {
1442
+ conditions.push('timestamp >= ?');
1443
+ params.push(options.from);
1444
+ }
1445
+
1446
+ if (options?.to) {
1447
+ conditions.push('timestamp <= ?');
1448
+ params.push(options.to);
1449
+ }
1450
+
1451
+ if (options?.cursor) {
1452
+ conditions.push('id > ?');
1453
+ params.push(options.cursor);
1454
+ }
1455
+
1456
+ const whereClause = conditions.join(' AND ');
1457
+ const countQuery = `SELECT COUNT(*) as count FROM audit_logs WHERE ${whereClause}`;
1458
+ const query = `SELECT * FROM audit_logs WHERE ${whereClause} ORDER BY timestamp DESC LIMIT ?`;
1459
+
1460
+ const countParams = [...params];
1461
+ params.push(limit + 1);
1462
+
1463
+ const rows = this.all<Record<string, unknown>>(query, params);
1464
+ const countResult = this.get<{ count: number }>(countQuery, countParams);
1465
+
1466
+ const hasMore = rows.length > limit;
1467
+ const items = rows.slice(0, limit).map(row => this.rowToAuditLog(row));
1468
+
1469
+ return {
1470
+ items,
1471
+ pagination: {
1472
+ total: countResult?.count ?? 0,
1473
+ limit,
1474
+ hasMore,
1475
+ cursor: options?.cursor,
1476
+ nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
1477
+ },
1478
+ };
1479
+ }
1480
+
1481
+ async getResourceAuditLogs(
1482
+ resourceType: string,
1483
+ resourceId: string,
1484
+ options?: PaginationOptions
1485
+ ): Promise<PaginatedResult<AuditLog>> {
1486
+ return this.getAuditLogs({
1487
+ ...options,
1488
+ resourceType,
1489
+ resourceName: resourceId,
1490
+ });
1491
+ }
1492
+
1493
+ // -------------------------------------------------------------------------
1494
+ // Cache Metadata
1495
+ // -------------------------------------------------------------------------
1496
+
1497
+ async setCacheMetadata(metadata: CacheMetadata): Promise<CacheMetadata> {
1498
+ this.run(`
1499
+ INSERT OR REPLACE INTO cache_metadata (
1500
+ id, skill_name, version, cached_at, expires_at, size,
1501
+ hit_count, last_hit_at, source, integrity_hash
1502
+ ) VALUES (
1503
+ $id, $skillName, $version, $cachedAt, $expiresAt, $size,
1504
+ $hitCount, $lastHitAt, $source, $integrityHash
1505
+ )
1506
+ `, {
1507
+ $id: metadata.id,
1508
+ $skillName: metadata.skillName,
1509
+ $version: metadata.version,
1510
+ $cachedAt: metadata.cachedAt,
1511
+ $expiresAt: metadata.expiresAt ?? null,
1512
+ $size: metadata.size ?? null,
1513
+ $hitCount: metadata.hitCount,
1514
+ $lastHitAt: metadata.lastHitAt ?? null,
1515
+ $source: metadata.source ?? null,
1516
+ $integrityHash: metadata.integrityHash ?? null,
1517
+ });
1518
+
1519
+ return metadata;
1520
+ }
1521
+
1522
+ async getCacheMetadata(skillName: string, version: string): Promise<CacheMetadata | null> {
1523
+ const row = this.get<Record<string, unknown>>(
1524
+ 'SELECT * FROM cache_metadata WHERE skill_name = ? AND version = ?',
1525
+ [skillName, version]
1526
+ );
1527
+ return row ? this.rowToCacheMetadata(row) : null;
1528
+ }
1529
+
1530
+ async getAllCacheMetadata(): Promise<CacheMetadata[]> {
1531
+ const rows = this.all<Record<string, unknown>>('SELECT * FROM cache_metadata ORDER BY cached_at DESC');
1532
+ return rows.map(row => this.rowToCacheMetadata(row));
1533
+ }
1534
+
1535
+ async deleteCacheMetadata(skillName: string, version?: string): Promise<boolean> {
1536
+ const before = version
1537
+ ? this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ? AND version = ?', [skillName, version])
1538
+ : this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ?', [skillName]);
1539
+
1540
+ if (version) {
1541
+ this.run('DELETE FROM cache_metadata WHERE skill_name = ? AND version = ?', { $skillName: skillName, $version: version });
1542
+ } else {
1543
+ this.run('DELETE FROM cache_metadata WHERE skill_name = ?', { $skillName: skillName });
1544
+ }
1545
+
1546
+ const after = version
1547
+ ? this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ? AND version = ?', [skillName, version])
1548
+ : this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ?', [skillName]);
1549
+
1550
+ return (before?.count ?? 0) > (after?.count ?? 0);
1551
+ }
1552
+
1553
+ async incrementCacheHit(skillName: string, version: string): Promise<void> {
1554
+ this.run(`
1555
+ UPDATE cache_metadata SET
1556
+ hit_count = hit_count + 1,
1557
+ last_hit_at = datetime('now')
1558
+ WHERE skill_name = ? AND version = ?
1559
+ `, { $skillName: skillName, $version: version });
1560
+ }
1561
+
1562
+ async getExpiredCacheEntries(): Promise<CacheMetadata[]> {
1563
+ const rows = this.all<Record<string, unknown>>(
1564
+ "SELECT * FROM cache_metadata WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')"
1565
+ );
1566
+ return rows.map(row => this.rowToCacheMetadata(row));
1567
+ }
1568
+
1569
+ // -------------------------------------------------------------------------
1570
+ // Use Records
1571
+ // -------------------------------------------------------------------------
1572
+
1573
+ async createUseRecord(record: UseRecord): Promise<UseRecord> {
1574
+ this.run(`
1575
+ INSERT INTO use_records (
1576
+ id, skill_id, version_id, user_id, source, cache_hit, client_version,
1577
+ platform, arch, ip, user_agent, country, region, created_at
1578
+ ) VALUES (
1579
+ $id, $skillId, $versionId, $userId, $source, $cacheHit, $clientVersion,
1580
+ $platform, $arch, $ip, $userAgent, $country, $region, $createdAt
1581
+ )
1582
+ `, {
1583
+ $id: record.id,
1584
+ $skillId: record.skillId,
1585
+ $versionId: record.versionId ?? null,
1586
+ $userId: record.userId ?? null,
1587
+ $source: record.source,
1588
+ $cacheHit: record.cacheHit ? 1 : 0,
1589
+ $clientVersion: record.clientVersion ?? null,
1590
+ $platform: record.platform ?? null,
1591
+ $arch: record.arch ?? null,
1592
+ $ip: record.ip,
1593
+ $userAgent: record.userAgent ?? null,
1594
+ $country: record.country ?? null,
1595
+ $region: record.region ?? null,
1596
+ $createdAt: record.createdAt,
1597
+ });
1598
+
1599
+ return record;
1600
+ }
1601
+
1602
+ async getSkillUseStats(skillId: string, days: number): Promise<{
1603
+ total: number;
1604
+ bySource: Record<string, number>;
1605
+ cacheHitRate: number;
1606
+ daily: { date: string; count: number }[];
1607
+ }> {
1608
+ const fromDate = new Date();
1609
+ fromDate.setDate(fromDate.getDate() - days);
1610
+ const fromDateStr = fromDate.toISOString();
1611
+
1612
+ // Total uses
1613
+ const totalResult = this.get<{ count: number }>(`
1614
+ SELECT COUNT(*) as count FROM use_records
1615
+ WHERE skill_id = ? AND created_at >= ?
1616
+ `, [skillId, fromDateStr]);
1617
+
1618
+ // By source
1619
+ const sourceRows = this.all<{ source: string; count: number }>(`
1620
+ SELECT source, COUNT(*) as count FROM use_records
1621
+ WHERE skill_id = ? AND created_at >= ?
1622
+ GROUP BY source
1623
+ `, [skillId, fromDateStr]);
1624
+
1625
+ const bySource: Record<string, number> = {};
1626
+ sourceRows.forEach(row => {
1627
+ bySource[row.source] = row.count;
1628
+ });
1629
+
1630
+ // Cache hit rate
1631
+ const cacheHitResult = this.get<{ hits: number; total: number }>(`
1632
+ SELECT
1633
+ SUM(cache_hit) as hits,
1634
+ COUNT(*) as total
1635
+ FROM use_records
1636
+ WHERE skill_id = ? AND created_at >= ?
1637
+ `, [skillId, fromDateStr]);
1638
+
1639
+ const cacheHitRate = (cacheHitResult?.total ?? 0) > 0
1640
+ ? (cacheHitResult?.hits ?? 0) / (cacheHitResult?.total ?? 1)
1641
+ : 0;
1642
+
1643
+ // Daily breakdown
1644
+ const dailyRows = this.all<{ date: string; count: number }>(`
1645
+ SELECT
1646
+ date(created_at) as date,
1647
+ COUNT(*) as count
1648
+ FROM use_records
1649
+ WHERE skill_id = ? AND created_at >= ?
1650
+ GROUP BY date(created_at)
1651
+ ORDER BY date ASC
1652
+ `, [skillId, fromDateStr]);
1653
+
1654
+ return {
1655
+ total: totalResult?.count ?? 0,
1656
+ bySource,
1657
+ cacheHitRate,
1658
+ daily: dailyRows,
1659
+ };
1660
+ }
1661
+
1662
+ // -------------------------------------------------------------------------
1663
+ // Row Converters
1664
+ // -------------------------------------------------------------------------
1665
+
1666
+ private rowToSkill(row: Record<string, unknown>): Skill {
1667
+ return {
1668
+ id: row['id'] as string,
1669
+ name: row['name'] as string,
1670
+ scope: row['scope'] as string | undefined,
1671
+ fullName: row['full_name'] as string,
1672
+ ownerId: row['owner_id'] as string | undefined,
1673
+ ownerType: (row['owner_type'] as 'user' | 'organization') ?? 'user',
1674
+ displayName: row['display_name'] as string | undefined,
1675
+ description: row['description'] as string,
1676
+ category: row['category'] as string | undefined,
1677
+ keywords: JSON.parse(row['keywords'] as string || '[]'),
1678
+ license: row['license'] as string | undefined,
1679
+ repository: row['repository'] as string | undefined,
1680
+ homepage: row['homepage'] as string | undefined,
1681
+ derivedFrom: row['derived_from'] ? JSON.parse(row['derived_from'] as string) : undefined,
1682
+ latestVersion: row['latest_version'] as string,
1683
+ latestVersionId: row['latest_version_id'] as string | undefined,
1684
+ visibility: (row['visibility'] as 'public' | 'private' | 'unlisted') ?? 'public',
1685
+ status: (row['status'] as 'active' | 'deprecated' | 'deleted') ?? 'active',
1686
+ deprecatedMessage: row['deprecated_message'] as string | undefined,
1687
+ securityScore: row['security_score'] as number | undefined,
1688
+ securityLevel: row['security_level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
1689
+ lastScanAt: row['last_scan_at'] as string | undefined,
1690
+ stats: JSON.parse(row['stats'] as string || '{}'),
1691
+ rating: JSON.parse(row['rating'] as string || '{"average":0,"count":0}'),
1692
+ createdAt: row['created_at'] as string,
1693
+ updatedAt: row['updated_at'] as string,
1694
+ publishedAt: row['published_at'] as string | undefined,
1695
+ };
1696
+ }
1697
+
1698
+ private rowToVersion(row: Record<string, unknown>): Version {
1699
+ return {
1700
+ id: row['id'] as string,
1701
+ skillId: row['skill_id'] as string,
1702
+ version: row['version'] as string,
1703
+ tag: row['tag'] as string | undefined,
1704
+ content: JSON.parse(row['content'] as string),
1705
+ packageUrl: row['package_url'] as string,
1706
+ packageSize: row['package_size'] as number,
1707
+ packageHash: row['package_hash'] as string,
1708
+ changelog: row['changelog'] as string | undefined,
1709
+ scanRecordId: row['scan_record_id'] as string | undefined,
1710
+ securityScore: row['security_score'] as number | undefined,
1711
+ securityLevel: row['security_level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
1712
+ status: (row['status'] as 'published' | 'deprecated' | 'yanked') ?? 'published',
1713
+ deprecatedMessage: row['deprecated_message'] as string | undefined,
1714
+ publishedBy: row['published_by'] as string | undefined,
1715
+ uses: row['uses'] as number,
1716
+ createdAt: row['created_at'] as string,
1717
+ publishedAt: row['published_at'] as string,
1718
+ deprecatedAt: row['deprecated_at'] as string | undefined,
1719
+ };
1720
+ }
1721
+
1722
+ private rowToScanRecord(row: Record<string, unknown>): ScanRecord {
1723
+ return {
1724
+ id: row['id'] as string,
1725
+ skillId: row['skill_id'] as string | undefined,
1726
+ versionId: row['version_id'] as string | undefined,
1727
+ userId: row['user_id'] as string | undefined,
1728
+ inputType: row['input_type'] as 'publish' | 'manual' | 'scheduled' | 'api' | 'cache',
1729
+ contentHash: row['content_hash'] as string,
1730
+ contentSize: row['content_size'] as number,
1731
+ score: row['score'] as number | undefined,
1732
+ level: row['level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
1733
+ issueCount: JSON.parse(row['issue_count'] as string || '{"high":0,"medium":0,"low":0}'),
1734
+ scannerVersion: row['scanner_version'] as string | undefined,
1735
+ rulesVersion: row['rules_version'] as string | undefined,
1736
+ rulesApplied: row['rules_applied'] as number | undefined,
1737
+ duration: row['duration'] as number | undefined,
1738
+ status: (row['status'] as 'pending' | 'scanning' | 'completed' | 'failed') ?? 'pending',
1739
+ errorMessage: row['error_message'] as string | undefined,
1740
+ createdAt: row['created_at'] as string,
1741
+ completedAt: row['completed_at'] as string | undefined,
1742
+ };
1743
+ }
1744
+
1745
+ private rowToScanIssue(row: Record<string, unknown>): ScanIssue {
1746
+ return {
1747
+ id: row['id'] as string,
1748
+ scanRecordId: row['scan_record_id'] as string,
1749
+ ruleId: row['rule_id'] as string,
1750
+ ruleName: row['rule_name'] as string,
1751
+ ruleCategory: row['rule_category'] as 'command' | 'secret' | 'injection' | 'resource' | 'privacy' | 'network',
1752
+ severity: row['severity'] as 'high' | 'medium' | 'low',
1753
+ file: row['file'] as string | undefined,
1754
+ line: row['line'] as number | undefined,
1755
+ column: row['column_num'] as number | undefined,
1756
+ endLine: row['end_line'] as number | undefined,
1757
+ endColumn: row['end_column'] as number | undefined,
1758
+ content: row['content'] as string,
1759
+ context: row['context'] as string | undefined,
1760
+ message: row['message'] as string,
1761
+ suggestion: row['suggestion'] as string | undefined,
1762
+ acknowledged: !!row['acknowledged'],
1763
+ acknowledgedAt: row['acknowledged_at'] as string | undefined,
1764
+ acknowledgedBy: row['acknowledged_by'] as string | undefined,
1765
+ acknowledgedReason: row['acknowledged_reason'] as string | undefined,
1766
+ createdAt: row['created_at'] as string,
1767
+ };
1768
+ }
1769
+
1770
+ private rowToFeedback(row: Record<string, unknown>): Feedback {
1771
+ return {
1772
+ id: row['id'] as string,
1773
+ skillId: row['skill_id'] as string,
1774
+ skillVersion: row['skill_version'] as string,
1775
+ feedbackType: row['feedback_type'] as 'success' | 'failure' | 'suggestion' | 'bug',
1776
+ rating: row['rating'] as number,
1777
+ comment: row['comment'] as string | undefined,
1778
+ context: JSON.parse(row['context'] as string || '{}'),
1779
+ status: (row['status'] as 'pending' | 'reviewed' | 'accepted' | 'rejected') ?? 'pending',
1780
+ reviewerNotes: row['reviewer_notes'] as string | undefined,
1781
+ reviewedAt: row['reviewed_at'] as string | undefined,
1782
+ reviewedBy: row['reviewed_by'] as string | undefined,
1783
+ sourceIpHash: row['source_ip_hash'] as string | undefined,
1784
+ createdAt: row['created_at'] as string,
1785
+ };
1786
+ }
1787
+
1788
+ private rowToQueueItem(row: Record<string, unknown>): FeedbackQueueItem {
1789
+ return {
1790
+ id: row['id'] as string,
1791
+ skillName: row['skill_name'] as string,
1792
+ skillVersion: row['skill_version'] as string,
1793
+ feedback: JSON.parse(row['feedback'] as string),
1794
+ status: (row['status'] as 'pending' | 'retrying' | 'failed' | 'dead_letter') ?? 'pending',
1795
+ attempts: row['attempts'] as number,
1796
+ maxAttempts: row['max_attempts'] as number,
1797
+ lastAttemptAt: row['last_attempt_at'] as string | undefined,
1798
+ nextRetryAt: row['next_retry_at'] as string | undefined,
1799
+ lastError: row['last_error'] as string | undefined,
1800
+ baseDelay: row['base_delay'] as number,
1801
+ currentDelay: row['current_delay'] as number,
1802
+ maxDelay: row['max_delay'] as number,
1803
+ createdAt: row['created_at'] as string,
1804
+ processedAt: row['processed_at'] as string | undefined,
1805
+ };
1806
+ }
1807
+
1808
+ private rowToAuditLog(row: Record<string, unknown>): AuditLog {
1809
+ return {
1810
+ id: row['id'] as string,
1811
+ timestamp: row['timestamp'] as string,
1812
+ eventType: row['event_type'] as AuditLog['eventType'],
1813
+ actor: JSON.parse(row['actor'] as string),
1814
+ resource: JSON.parse(row['resource'] as string),
1815
+ action: row['action'] as string,
1816
+ result: row['result'] as 'success' | 'failure',
1817
+ details: row['details'] ? JSON.parse(row['details'] as string) : undefined,
1818
+ changes: row['changes'] ? JSON.parse(row['changes'] as string) : undefined,
1819
+ errorCode: row['error_code'] as string | undefined,
1820
+ errorMessage: row['error_message'] as string | undefined,
1821
+ };
1822
+ }
1823
+
1824
+ private rowToCacheMetadata(row: Record<string, unknown>): CacheMetadata {
1825
+ return {
1826
+ id: row['id'] as string,
1827
+ skillName: row['skill_name'] as string,
1828
+ version: row['version'] as string,
1829
+ cachedAt: row['cached_at'] as string,
1830
+ expiresAt: row['expires_at'] as string | undefined,
1831
+ size: row['size'] as number | undefined,
1832
+ hitCount: row['hit_count'] as number,
1833
+ lastHitAt: row['last_hit_at'] as string | undefined,
1834
+ source: row['source'] as 'remote' | 'local' | undefined,
1835
+ integrityHash: row['integrity_hash'] as string | undefined,
1836
+ };
1837
+ }
1838
+ }