qa360 1.0.4 → 1.1.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 (108) hide show
  1. package/dist/commands/history.js +1 -1
  2. package/dist/commands/pack.js +1 -1
  3. package/dist/commands/run.d.ts +1 -1
  4. package/dist/commands/run.d.ts.map +1 -1
  5. package/dist/commands/run.js +1 -1
  6. package/dist/commands/secrets.js +1 -1
  7. package/dist/commands/serve.js +1 -1
  8. package/dist/commands/verify.js +1 -1
  9. package/dist/core/adapters/gitleaks-secrets.d.ts +115 -0
  10. package/dist/core/adapters/gitleaks-secrets.d.ts.map +1 -0
  11. package/dist/core/adapters/gitleaks-secrets.js +410 -0
  12. package/dist/core/adapters/k6-perf.d.ts +86 -0
  13. package/dist/core/adapters/k6-perf.d.ts.map +1 -0
  14. package/dist/core/adapters/k6-perf.js +398 -0
  15. package/dist/core/adapters/osv-deps.d.ts +124 -0
  16. package/dist/core/adapters/osv-deps.d.ts.map +1 -0
  17. package/dist/core/adapters/osv-deps.js +372 -0
  18. package/dist/core/adapters/playwright-api.d.ts +82 -0
  19. package/dist/core/adapters/playwright-api.d.ts.map +1 -0
  20. package/dist/core/adapters/playwright-api.js +252 -0
  21. package/dist/core/adapters/playwright-ui.d.ts +115 -0
  22. package/dist/core/adapters/playwright-ui.d.ts.map +1 -0
  23. package/dist/core/adapters/playwright-ui.js +346 -0
  24. package/dist/core/adapters/semgrep-sast.d.ts +100 -0
  25. package/dist/core/adapters/semgrep-sast.d.ts.map +1 -0
  26. package/dist/core/adapters/semgrep-sast.js +322 -0
  27. package/dist/core/adapters/zap-dast.d.ts +134 -0
  28. package/dist/core/adapters/zap-dast.d.ts.map +1 -0
  29. package/dist/core/adapters/zap-dast.js +424 -0
  30. package/dist/core/hooks/compose.d.ts +62 -0
  31. package/dist/core/hooks/compose.d.ts.map +1 -0
  32. package/dist/core/hooks/compose.js +225 -0
  33. package/dist/core/hooks/runner.d.ts +69 -0
  34. package/dist/core/hooks/runner.d.ts.map +1 -0
  35. package/dist/core/hooks/runner.js +303 -0
  36. package/dist/core/index.d.ts +74 -0
  37. package/dist/core/index.d.ts.map +1 -0
  38. package/dist/core/index.js +39 -0
  39. package/dist/core/pack/migrator.d.ts +52 -0
  40. package/dist/core/pack/migrator.d.ts.map +1 -0
  41. package/dist/core/pack/migrator.js +304 -0
  42. package/dist/core/pack/validator.d.ts +43 -0
  43. package/dist/core/pack/validator.d.ts.map +1 -0
  44. package/dist/core/pack/validator.js +292 -0
  45. package/dist/core/proof/bundle.d.ts +138 -0
  46. package/dist/core/proof/bundle.d.ts.map +1 -0
  47. package/dist/core/proof/bundle.js +160 -0
  48. package/dist/core/proof/canonicalize.d.ts +48 -0
  49. package/dist/core/proof/canonicalize.d.ts.map +1 -0
  50. package/dist/core/proof/canonicalize.js +105 -0
  51. package/dist/core/proof/index.d.ts +14 -0
  52. package/dist/core/proof/index.d.ts.map +1 -0
  53. package/dist/core/proof/index.js +18 -0
  54. package/dist/core/proof/schema.d.ts +218 -0
  55. package/dist/core/proof/schema.d.ts.map +1 -0
  56. package/dist/core/proof/schema.js +263 -0
  57. package/dist/core/proof/signer.d.ts +112 -0
  58. package/dist/core/proof/signer.d.ts.map +1 -0
  59. package/dist/core/proof/signer.js +226 -0
  60. package/dist/core/proof/verifier.d.ts +98 -0
  61. package/dist/core/proof/verifier.d.ts.map +1 -0
  62. package/dist/core/proof/verifier.js +302 -0
  63. package/dist/core/runner/phase3-runner.d.ts +102 -0
  64. package/dist/core/runner/phase3-runner.d.ts.map +1 -0
  65. package/dist/core/runner/phase3-runner.js +471 -0
  66. package/dist/core/secrets/crypto.d.ts +76 -0
  67. package/dist/core/secrets/crypto.d.ts.map +1 -0
  68. package/dist/core/secrets/crypto.js +225 -0
  69. package/dist/core/secrets/manager.d.ts +77 -0
  70. package/dist/core/secrets/manager.d.ts.map +1 -0
  71. package/dist/core/secrets/manager.js +219 -0
  72. package/dist/core/security/redaction-patterns-extended.d.ts +28 -0
  73. package/dist/core/security/redaction-patterns-extended.d.ts.map +1 -0
  74. package/dist/core/security/redaction-patterns-extended.js +247 -0
  75. package/dist/core/security/redactor.d.ts +72 -0
  76. package/dist/core/security/redactor.d.ts.map +1 -0
  77. package/dist/core/security/redactor.js +279 -0
  78. package/dist/core/serve/diagnostics-collector.d.ts +33 -0
  79. package/dist/core/serve/diagnostics-collector.d.ts.map +1 -0
  80. package/dist/core/serve/diagnostics-collector.js +149 -0
  81. package/dist/core/serve/health-checker.d.ts +45 -0
  82. package/dist/core/serve/health-checker.d.ts.map +1 -0
  83. package/dist/core/serve/health-checker.js +219 -0
  84. package/dist/core/serve/index.d.ts +9 -0
  85. package/dist/core/serve/index.d.ts.map +1 -0
  86. package/dist/core/serve/index.js +8 -0
  87. package/dist/core/serve/metrics-collector.d.ts +25 -0
  88. package/dist/core/serve/metrics-collector.d.ts.map +1 -0
  89. package/dist/core/serve/metrics-collector.js +322 -0
  90. package/dist/core/serve/process-manager.d.ts +37 -0
  91. package/dist/core/serve/process-manager.d.ts.map +1 -0
  92. package/dist/core/serve/process-manager.js +213 -0
  93. package/dist/core/serve/server.d.ts +37 -0
  94. package/dist/core/serve/server.d.ts.map +1 -0
  95. package/dist/core/serve/server.js +191 -0
  96. package/dist/core/types/pack-v1.d.ts +162 -0
  97. package/dist/core/types/pack-v1.d.ts.map +1 -0
  98. package/dist/core/types/pack-v1.js +5 -0
  99. package/dist/core/types/trust-score.d.ts +70 -0
  100. package/dist/core/types/trust-score.d.ts.map +1 -0
  101. package/dist/core/types/trust-score.js +191 -0
  102. package/dist/core/vault/cas.d.ts +87 -0
  103. package/dist/core/vault/cas.d.ts.map +1 -0
  104. package/dist/core/vault/cas.js +255 -0
  105. package/dist/core/vault/index.d.ts +205 -0
  106. package/dist/core/vault/index.d.ts.map +1 -0
  107. package/dist/core/vault/index.js +631 -0
  108. package/package.json +12 -6
@@ -0,0 +1,631 @@
1
+ /**
2
+ * QA360 Evidence Vault Engine
3
+ * SQLite WAL-based storage with idempotence and CAS integration
4
+ */
5
+ import sqlite3pkg from 'sqlite3';
6
+ const { Database } = sqlite3pkg;
7
+ import { existsSync, mkdirSync } from 'fs';
8
+ import { join, resolve } from 'path';
9
+ import { randomUUID } from 'crypto';
10
+ import { createHash } from 'crypto';
11
+ import { ContentAddressableStorage } from './cas.js';
12
+ import { SecurityRedactor } from '../security/redactor.js';
13
+ export class EvidenceVault {
14
+ db;
15
+ cas;
16
+ redactor;
17
+ dbPath;
18
+ baseDir;
19
+ constructor(config) {
20
+ this.baseDir = resolve(config.baseDir);
21
+ this.dbPath = join(this.baseDir, 'vault.db');
22
+ this.cas = new ContentAddressableStorage(join(this.baseDir, 'runs'));
23
+ this.redactor = SecurityRedactor.forReports();
24
+ this.ensureDirectories();
25
+ this.initializeDatabase(config);
26
+ }
27
+ /**
28
+ * Open vault connection and initialize schema
29
+ */
30
+ static async open(baseDir, config) {
31
+ const fullConfig = {
32
+ baseDir,
33
+ enableWAL: true,
34
+ maxConnections: 10,
35
+ retentionDays: 90,
36
+ ...config
37
+ };
38
+ const vault = new EvidenceVault(fullConfig);
39
+ await vault.initializeSchema();
40
+ return vault;
41
+ }
42
+ /**
43
+ * Begin a new run with idempotence support
44
+ */
45
+ async beginRun(options) {
46
+ // Check for existing run with same run_key
47
+ if (options.run_key) {
48
+ const existing = await this.getRunByKey(options.run_key);
49
+ if (existing) {
50
+ console.log(`[VAULT] RUN_IDEMPOTENT_REUSED: ${existing.id} (key: ${options.run_key})`);
51
+ return { runId: existing.id, isReused: true };
52
+ }
53
+ }
54
+ const runId = randomUUID();
55
+ const now = Date.now();
56
+ const run = {
57
+ id: runId,
58
+ run_key: options.run_key,
59
+ started_at: now,
60
+ pack_hash: options.pack_hash,
61
+ pack_path: options.pack_path,
62
+ status: 'running',
63
+ weights_json: options.weights ? JSON.stringify(options.weights) : undefined,
64
+ pinned: 0,
65
+ created_at: now,
66
+ updated_at: now
67
+ };
68
+ await this.insertRun(run);
69
+ console.log(`[VAULT] RUN_STARTED: ${runId} (key: ${options.run_key || 'none'})`);
70
+ return { runId, isReused: false };
71
+ }
72
+ /**
73
+ * Finish a run with final status and signature
74
+ */
75
+ async finishRun(runId, options) {
76
+ const now = Date.now();
77
+ const updates = {
78
+ ended_at: now,
79
+ status: options.status,
80
+ trust_score: options.trust_score,
81
+ signature_hex: options.signature,
82
+ updated_at: now
83
+ };
84
+ if (options.weights) {
85
+ updates.weights_json = JSON.stringify(options.weights);
86
+ }
87
+ if (options.proof_pdf_sha) {
88
+ const artifact = await this.getArtifact(options.proof_pdf_sha);
89
+ if (artifact) {
90
+ updates.proof_pdf_path = artifact.cas_path;
91
+ await this.attachArtifact(runId, { sha256: options.proof_pdf_sha, label: 'proof_pdf' });
92
+ }
93
+ }
94
+ await this.updateRun(runId, updates);
95
+ console.log(`[VAULT] RUN_FINISHED: ${runId} (status: ${options.status}, trust: ${options.trust_score})`);
96
+ }
97
+ /**
98
+ * Record gate execution result
99
+ */
100
+ async recordGate(runId, gate) {
101
+ const gateRecord = {
102
+ run_id: runId,
103
+ created_at: Date.now(),
104
+ ...gate
105
+ };
106
+ const gateId = await this.insertGate(gateRecord);
107
+ console.log(`[VAULT] GATE_RECORDED: ${runId}/${gate.name} (status: ${gate.status}, duration: ${gate.duration_ms}ms)`);
108
+ return gateId;
109
+ }
110
+ /**
111
+ * Record security/quality finding
112
+ */
113
+ async recordFinding(runId, finding) {
114
+ // Redact sensitive information
115
+ const redactedFinding = {
116
+ run_id: runId,
117
+ created_at: Date.now(),
118
+ ...finding,
119
+ message: this.redactor.redact(finding.message),
120
+ raw_output: finding.raw_output ? this.redactor.redact(finding.raw_output) : undefined,
121
+ fingerprint: finding.fingerprint || this.generateFindingFingerprint(finding)
122
+ };
123
+ const findingId = await this.insertFinding(redactedFinding);
124
+ console.log(`[VAULT] FINDING_RECORDED: ${runId}/${finding.gate} (severity: ${finding.severity}, rule: ${finding.rule})`);
125
+ return findingId;
126
+ }
127
+ /**
128
+ * Store artifact in CAS and link to run
129
+ */
130
+ async storeArtifact(runId, content, mimeType, label, originalName) {
131
+ const casArtifact = await this.cas.saveArtifact(content, mimeType, originalName);
132
+ // Store artifact metadata
133
+ const artifact = {
134
+ sha256: casArtifact.sha256,
135
+ mime_type: mimeType,
136
+ size_bytes: casArtifact.size,
137
+ cas_path: casArtifact.path,
138
+ original_name: originalName,
139
+ created_at: Date.now(),
140
+ last_accessed: Date.now()
141
+ };
142
+ await this.insertArtifact(artifact);
143
+ await this.attachArtifact(runId, { sha256: casArtifact.sha256, label });
144
+ console.log(`[VAULT] ARTIFACT_STORED: ${runId}/${label} (sha256: ${casArtifact.sha256.substring(0, 8)}..., size: ${casArtifact.size})`);
145
+ return casArtifact;
146
+ }
147
+ /**
148
+ * Attach existing artifact to run
149
+ */
150
+ async attachArtifact(runId, link) {
151
+ const linkRecord = {
152
+ run_id: runId,
153
+ sha256: link.sha256,
154
+ label: link.label,
155
+ created_at: Date.now()
156
+ };
157
+ await this.insertRunArtifact(linkRecord);
158
+ }
159
+ /**
160
+ * Get run by ID
161
+ */
162
+ async getRun(runId) {
163
+ return new Promise((resolve, reject) => {
164
+ this.db.get('SELECT * FROM runs WHERE id = ?', [runId], (err, row) => {
165
+ if (err)
166
+ reject(err);
167
+ else
168
+ resolve(row || null);
169
+ });
170
+ });
171
+ }
172
+ /**
173
+ * Get run by run_key
174
+ */
175
+ async getRunByKey(runKey) {
176
+ return new Promise((resolve, reject) => {
177
+ this.db.get('SELECT * FROM runs WHERE run_key = ?', [runKey], (err, row) => {
178
+ if (err)
179
+ reject(err);
180
+ else
181
+ resolve(row || null);
182
+ });
183
+ });
184
+ }
185
+ /**
186
+ * List runs with filters
187
+ */
188
+ async listRuns(filters) {
189
+ let query = 'SELECT * FROM runs WHERE 1=1';
190
+ const params = [];
191
+ if (filters?.status?.length) {
192
+ query += ` AND status IN (${filters.status.map(() => '?').join(',')})`;
193
+ params.push(...filters.status);
194
+ }
195
+ if (filters?.since) {
196
+ query += ' AND started_at >= ?';
197
+ params.push(filters.since.getTime());
198
+ }
199
+ if (filters?.until) {
200
+ query += ' AND started_at <= ?';
201
+ params.push(filters.until.getTime());
202
+ }
203
+ if (filters?.pack_hash) {
204
+ query += ' AND pack_hash = ?';
205
+ params.push(filters.pack_hash);
206
+ }
207
+ query += ' ORDER BY started_at DESC';
208
+ if (filters?.limit) {
209
+ query += ' LIMIT ?';
210
+ params.push(filters.limit);
211
+ if (filters?.offset) {
212
+ query += ' OFFSET ?';
213
+ params.push(filters.offset);
214
+ }
215
+ }
216
+ return new Promise((resolve, reject) => {
217
+ this.db.all(query, params, (err, rows) => {
218
+ if (err)
219
+ reject(err);
220
+ else
221
+ resolve(rows);
222
+ });
223
+ });
224
+ }
225
+ /**
226
+ * Get gates for a run
227
+ */
228
+ async getGates(runId) {
229
+ return new Promise((resolve, reject) => {
230
+ this.db.all('SELECT * FROM gates WHERE run_id = ? ORDER BY created_at', [runId], (err, rows) => {
231
+ if (err)
232
+ reject(err);
233
+ else
234
+ resolve(rows);
235
+ });
236
+ });
237
+ }
238
+ /**
239
+ * Get findings for a run
240
+ */
241
+ async getFindings(runId, gate) {
242
+ let query = 'SELECT * FROM findings WHERE run_id = ?';
243
+ const params = [runId];
244
+ if (gate) {
245
+ query += ' AND gate = ?';
246
+ params.push(gate);
247
+ }
248
+ query += ' ORDER BY severity DESC, created_at';
249
+ return new Promise((resolve, reject) => {
250
+ this.db.all(query, params, (err, rows) => {
251
+ if (err)
252
+ reject(err);
253
+ else
254
+ resolve(rows);
255
+ });
256
+ });
257
+ }
258
+ /**
259
+ * Get artifacts for a run
260
+ */
261
+ async getRunArtifacts(runId) {
262
+ return new Promise((resolve, reject) => {
263
+ this.db.all(`
264
+ SELECT a.*, ra.label
265
+ FROM artifacts a
266
+ JOIN run_artifacts ra ON a.sha256 = ra.sha256
267
+ WHERE ra.run_id = ?
268
+ ORDER BY ra.created_at
269
+ `, [runId], (err, rows) => {
270
+ if (err)
271
+ reject(err);
272
+ else
273
+ resolve(rows);
274
+ });
275
+ });
276
+ }
277
+ /**
278
+ * Get artifact by SHA256
279
+ */
280
+ async getArtifact(sha256) {
281
+ return new Promise((resolve, reject) => {
282
+ this.db.get('SELECT * FROM artifacts WHERE sha256 = ?', [sha256], (err, row) => {
283
+ if (err)
284
+ reject(err);
285
+ else
286
+ resolve(row || null);
287
+ });
288
+ });
289
+ }
290
+ /**
291
+ * Pin/unpin run to protect from GC
292
+ */
293
+ async pinRun(runId, pinned = true) {
294
+ await this.updateRun(runId, { pinned: pinned ? 1 : 0 });
295
+ console.log(`[VAULT] RUN_${pinned ? 'PINNED' : 'UNPINNED'}: ${runId}`);
296
+ }
297
+ /**
298
+ * Calculate pack hash for idempotence
299
+ */
300
+ static calculatePackHash(packContent) {
301
+ // Normalize pack content (remove comments, sort keys, etc.)
302
+ const normalized = packContent
303
+ .split('\n')
304
+ .filter(line => !line.trim().startsWith('#'))
305
+ .join('\n')
306
+ .trim();
307
+ return createHash('sha256').update(normalized).digest('hex');
308
+ }
309
+ /**
310
+ * Get vault statistics
311
+ */
312
+ async getStats() {
313
+ const queries = [
314
+ 'SELECT COUNT(*) as count FROM runs',
315
+ 'SELECT COUNT(*) as count FROM gates',
316
+ 'SELECT COUNT(*) as count FROM findings',
317
+ 'SELECT COUNT(*) as count FROM artifacts',
318
+ 'SELECT MIN(started_at) as oldest, MAX(started_at) as newest FROM runs'
319
+ ];
320
+ const results = await Promise.all(queries.map(query => new Promise((resolve, reject) => {
321
+ this.db.get(query, (err, row) => {
322
+ if (err)
323
+ reject(err);
324
+ else
325
+ resolve(row);
326
+ });
327
+ })));
328
+ const casStats = await this.cas.getStats();
329
+ return {
330
+ totalRuns: results[0].count,
331
+ totalGates: results[1].count,
332
+ totalFindings: results[2].count,
333
+ totalArtifacts: results[3].count,
334
+ vaultSizeBytes: casStats.totalBytes,
335
+ oldestRun: results[4].oldest ? new Date(results[4].oldest) : undefined,
336
+ newestRun: results[4].newest ? new Date(results[4].newest) : undefined
337
+ };
338
+ }
339
+ // Private helper methods
340
+ async insertRun(run) {
341
+ const fields = Object.keys(run).join(', ');
342
+ const placeholders = Object.keys(run).map(() => '?').join(', ');
343
+ const values = Object.values(run);
344
+ return new Promise((resolve, reject) => {
345
+ this.db.run(`INSERT INTO runs (${fields}) VALUES (${placeholders})`, values, (err) => {
346
+ if (err)
347
+ reject(err);
348
+ else
349
+ resolve();
350
+ });
351
+ });
352
+ }
353
+ async updateRun(runId, updates) {
354
+ const fields = Object.keys(updates).map(key => `${key} = ?`).join(', ');
355
+ const values = [...Object.values(updates), runId];
356
+ return new Promise((resolve, reject) => {
357
+ this.db.run(`UPDATE runs SET ${fields} WHERE id = ?`, values, (err) => {
358
+ if (err)
359
+ reject(err);
360
+ else
361
+ resolve();
362
+ });
363
+ });
364
+ }
365
+ async insertGate(gate) {
366
+ const fields = Object.keys(gate).join(', ');
367
+ const placeholders = Object.keys(gate).map(() => '?').join(', ');
368
+ const values = Object.values(gate);
369
+ return new Promise((resolve, reject) => {
370
+ this.db.run(`INSERT INTO gates (${fields}) VALUES (${placeholders})`, values, function (err) {
371
+ if (err)
372
+ reject(err);
373
+ else
374
+ resolve(this.lastID);
375
+ });
376
+ });
377
+ }
378
+ async insertFinding(finding) {
379
+ const fields = Object.keys(finding).join(', ');
380
+ const placeholders = Object.keys(finding).map(() => '?').join(', ');
381
+ const values = Object.values(finding);
382
+ return new Promise((resolve, reject) => {
383
+ this.db.run(`INSERT INTO findings (${fields}) VALUES (${placeholders})`, values, function (err) {
384
+ if (err)
385
+ reject(err);
386
+ else
387
+ resolve(this.lastID);
388
+ });
389
+ });
390
+ }
391
+ async insertArtifact(artifact) {
392
+ return new Promise((resolve, reject) => {
393
+ this.db.run(`INSERT OR IGNORE INTO artifacts (sha256, mime_type, size_bytes, cas_path, original_name, created_at, last_accessed)
394
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
395
+ artifact.sha256,
396
+ artifact.mime_type,
397
+ artifact.size_bytes,
398
+ artifact.cas_path,
399
+ artifact.original_name,
400
+ artifact.created_at,
401
+ artifact.last_accessed
402
+ ], (err) => {
403
+ if (err)
404
+ reject(err);
405
+ else
406
+ resolve();
407
+ });
408
+ });
409
+ }
410
+ async insertRunArtifact(link) {
411
+ return new Promise((resolve, reject) => {
412
+ this.db.run('INSERT OR IGNORE INTO run_artifacts (run_id, sha256, label, created_at) VALUES (?, ?, ?, ?)', [link.run_id, link.sha256, link.label, link.created_at], (err) => {
413
+ if (err)
414
+ reject(err);
415
+ else
416
+ resolve();
417
+ });
418
+ });
419
+ }
420
+ generateFindingFingerprint(finding) {
421
+ const content = `${finding.gate}:${finding.rule}:${finding.location || ''}:${finding.message}`;
422
+ return createHash('sha256').update(content).digest('hex').substring(0, 16);
423
+ }
424
+ /**
425
+ * Initialize database connection
426
+ */
427
+ initializeDatabase(config) {
428
+ this.db = new Database(this.dbPath);
429
+ if (config.enableWAL) {
430
+ this.db.exec('PRAGMA journal_mode = WAL');
431
+ this.db.exec('PRAGMA synchronous = NORMAL');
432
+ }
433
+ this.db.exec('PRAGMA foreign_keys = ON');
434
+ this.db.exec('PRAGMA cache_size = 10000');
435
+ this.db.exec('PRAGMA temp_store = MEMORY');
436
+ }
437
+ /**
438
+ * Initialize database schema
439
+ */
440
+ async initializeSchema() {
441
+ const schema = `
442
+ -- QA360 Evidence Vault Schema
443
+ -- SQLite WAL mode for concurrent access and performance
444
+
445
+ -- Runs table: core execution records
446
+ CREATE TABLE IF NOT EXISTS runs (
447
+ id TEXT PRIMARY KEY,
448
+ run_key TEXT UNIQUE,
449
+ started_at INTEGER NOT NULL,
450
+ ended_at INTEGER,
451
+ pack_hash TEXT NOT NULL,
452
+ pack_path TEXT NOT NULL,
453
+ status TEXT NOT NULL DEFAULT 'running',
454
+ trust_score INTEGER,
455
+ weights_json TEXT,
456
+ sign_alg TEXT DEFAULT 'ed25519',
457
+ signature_hex TEXT,
458
+ proof_pdf_path TEXT,
459
+ pinned INTEGER DEFAULT 0,
460
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
461
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
462
+ );
463
+
464
+ -- Gates table: individual test gate results
465
+ CREATE TABLE IF NOT EXISTS gates (
466
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
467
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
468
+ name TEXT NOT NULL,
469
+ status TEXT NOT NULL,
470
+ duration_ms INTEGER,
471
+ metrics_json TEXT,
472
+ budgets_json TEXT,
473
+ started_at INTEGER,
474
+ ended_at INTEGER,
475
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
476
+ );
477
+
478
+ -- Findings table: security and quality findings
479
+ CREATE TABLE IF NOT EXISTS findings (
480
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
481
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
482
+ gate TEXT NOT NULL,
483
+ severity TEXT NOT NULL,
484
+ rule TEXT NOT NULL,
485
+ location TEXT,
486
+ message TEXT,
487
+ raw_output TEXT,
488
+ fingerprint TEXT,
489
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
490
+ );
491
+
492
+ -- Artifacts table: Content-Addressable Storage metadata
493
+ CREATE TABLE IF NOT EXISTS artifacts (
494
+ sha256 TEXT PRIMARY KEY,
495
+ mime_type TEXT NOT NULL,
496
+ size_bytes INTEGER NOT NULL,
497
+ cas_path TEXT NOT NULL,
498
+ original_name TEXT,
499
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
500
+ last_accessed INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
501
+ );
502
+
503
+ -- Run artifacts table: N-N relationship between runs and artifacts
504
+ CREATE TABLE IF NOT EXISTS run_artifacts (
505
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
506
+ sha256 TEXT NOT NULL REFERENCES artifacts(sha256) ON DELETE CASCADE,
507
+ label TEXT NOT NULL,
508
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
509
+ PRIMARY KEY (run_id, sha256, label)
510
+ );
511
+
512
+ -- Vault metadata table: schema version and configuration
513
+ CREATE TABLE IF NOT EXISTS vault_metadata (
514
+ key TEXT PRIMARY KEY,
515
+ value TEXT NOT NULL,
516
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
517
+ );
518
+
519
+ -- Indexes for performance
520
+ CREATE INDEX IF NOT EXISTS idx_runs_started_at ON runs(started_at DESC);
521
+ CREATE INDEX IF NOT EXISTS idx_runs_run_key ON runs(run_key) WHERE run_key IS NOT NULL;
522
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
523
+ CREATE INDEX IF NOT EXISTS idx_runs_pack_hash ON runs(pack_hash);
524
+ CREATE INDEX IF NOT EXISTS idx_runs_pinned ON runs(pinned) WHERE pinned = 1;
525
+
526
+ CREATE INDEX IF NOT EXISTS idx_gates_run_id ON gates(run_id);
527
+ CREATE INDEX IF NOT EXISTS idx_gates_run_name ON gates(run_id, name);
528
+ CREATE INDEX IF NOT EXISTS idx_gates_status ON gates(status);
529
+ CREATE INDEX IF NOT EXISTS idx_gates_name ON gates(name);
530
+
531
+ CREATE INDEX IF NOT EXISTS idx_findings_run_id ON findings(run_id);
532
+ CREATE INDEX IF NOT EXISTS idx_findings_run_gate ON findings(run_id, gate);
533
+ CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
534
+ CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule);
535
+ CREATE INDEX IF NOT EXISTS idx_findings_fingerprint ON findings(fingerprint);
536
+
537
+ CREATE INDEX IF NOT EXISTS idx_artifacts_mime_type ON artifacts(mime_type);
538
+ CREATE INDEX IF NOT EXISTS idx_artifacts_size ON artifacts(size_bytes);
539
+ CREATE INDEX IF NOT EXISTS idx_artifacts_created_at ON artifacts(created_at);
540
+ CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed);
541
+
542
+ CREATE INDEX IF NOT EXISTS idx_run_artifacts_run_id ON run_artifacts(run_id);
543
+ CREATE INDEX IF NOT EXISTS idx_run_artifacts_sha256 ON run_artifacts(sha256);
544
+ CREATE INDEX IF NOT EXISTS idx_run_artifacts_label ON run_artifacts(label);
545
+
546
+ -- Insert initial metadata
547
+ INSERT OR IGNORE INTO vault_metadata (key, value) VALUES
548
+ ('schema_version', '1.0.0'),
549
+ ('created_at', strftime('%s', 'now') * 1000),
550
+ ('vault_format', 'qa360-evidence-vault-v1');
551
+
552
+ -- Triggers for updated_at timestamps
553
+ CREATE TRIGGER IF NOT EXISTS trigger_runs_updated_at
554
+ AFTER UPDATE ON runs
555
+ BEGIN
556
+ UPDATE runs SET updated_at = strftime('%s', 'now') * 1000 WHERE id = NEW.id;
557
+ END;
558
+
559
+ CREATE TRIGGER IF NOT EXISTS trigger_artifacts_last_accessed
560
+ AFTER UPDATE ON artifacts
561
+ BEGIN
562
+ UPDATE artifacts SET last_accessed = strftime('%s', 'now') * 1000 WHERE sha256 = NEW.sha256;
563
+ END;
564
+
565
+ -- Views for common queries
566
+ CREATE VIEW IF NOT EXISTS v_recent_runs AS
567
+ SELECT
568
+ r.id,
569
+ r.run_key,
570
+ r.started_at,
571
+ r.ended_at,
572
+ r.status,
573
+ r.trust_score,
574
+ r.pack_path,
575
+ COUNT(g.id) as gate_count,
576
+ COUNT(CASE WHEN g.status = 'passed' THEN 1 END) as gates_passed,
577
+ COUNT(CASE WHEN g.status = 'failed' THEN 1 END) as gates_failed,
578
+ COUNT(f.id) as finding_count,
579
+ COUNT(CASE WHEN f.severity IN ('high', 'critical') THEN 1 END) as critical_findings
580
+ FROM runs r
581
+ LEFT JOIN gates g ON r.id = g.run_id
582
+ LEFT JOIN findings f ON r.id = f.run_id
583
+ GROUP BY r.id
584
+ ORDER BY r.started_at DESC;
585
+
586
+ CREATE VIEW IF NOT EXISTS v_gate_trends AS
587
+ SELECT
588
+ g.name,
589
+ g.status,
590
+ r.started_at,
591
+ r.trust_score,
592
+ g.duration_ms,
593
+ g.metrics_json
594
+ FROM gates g
595
+ JOIN runs r ON g.run_id = r.id
596
+ WHERE r.status IN ('passed', 'failed')
597
+ ORDER BY r.started_at DESC;
598
+ `;
599
+ return new Promise((resolve, reject) => {
600
+ this.db.exec(schema, (err) => {
601
+ if (err) {
602
+ reject(new Error(`Failed to initialize schema: ${err.message}`));
603
+ }
604
+ else {
605
+ resolve();
606
+ }
607
+ });
608
+ });
609
+ }
610
+ /**
611
+ * Ensure required directories exist
612
+ */
613
+ ensureDirectories() {
614
+ if (!existsSync(this.baseDir)) {
615
+ mkdirSync(this.baseDir, { recursive: true });
616
+ }
617
+ }
618
+ /**
619
+ * Close database connection
620
+ */
621
+ async close() {
622
+ return new Promise((resolve, reject) => {
623
+ this.db.close((err) => {
624
+ if (err)
625
+ reject(err);
626
+ else
627
+ resolve();
628
+ });
629
+ });
630
+ }
631
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,21 +18,27 @@
18
18
  "scripts": {
19
19
  "build": "tsc --project tsconfig.json",
20
20
  "build:pkg": "tsc",
21
+ "build:bundle": "bash scripts/bundle-for-npm.sh",
21
22
  "test": "vitest run",
22
23
  "test:coverage": "vitest run --coverage",
23
24
  "test:watch": "vitest",
24
25
  "dev": "tsx watch src/index.ts",
25
- "clean": "rimraf dist",
26
- "prepublishOnly": "npm run build",
27
- "publish:dry": "npm publish --dry-run",
28
- "publish:real": "npm publish --access public --provenance"
26
+ "clean": "rimraf dist src/core",
27
+ "prepublishOnly": "npm run build:bundle",
28
+ "publish:dry": "npm run build:bundle && npm publish --dry-run",
29
+ "publish:real": "npm run build:bundle && npm publish --access public --provenance"
29
30
  },
30
31
  "dependencies": {
32
+ "ajv": "^8.17.1",
33
+ "ajv-draft-04": "^1.0.0",
34
+ "ajv-formats": "^2.1.1",
31
35
  "chalk": "^4.1.2",
32
36
  "commander": "^11.0.0",
33
37
  "inquirer": "^8.2.7",
38
+ "js-yaml": "^4.1.0",
34
39
  "ora": "^5.4.1",
35
- "qa360-core": "^1.0.0"
40
+ "sqlite3": "^5.1.6",
41
+ "tweetnacl": "^1.0.3"
36
42
  },
37
43
  "devDependencies": {
38
44
  "@types/inquirer": "^9.0.9",