qa360 1.3.4 โ 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +148 -14
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/qa360)
|
|
6
6
|
[](https://github.com/xyqotech/qa360/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/xyqotech/qa360)
|
|
8
|
+
[](https://github.com/xyqotech/qa360)
|
|
7
9
|
|
|
8
10
|
## Installation
|
|
9
11
|
|
|
@@ -92,9 +94,22 @@ qa360 verify proof.json
|
|
|
92
94
|
qa360 history list
|
|
93
95
|
qa360 history show <run-id>
|
|
94
96
|
|
|
97
|
+
# Export proof bundle
|
|
98
|
+
qa360 history export <run-id> --bundle proof-bundle.zip
|
|
99
|
+
|
|
100
|
+
# Clean up old runs (garbage collection)
|
|
101
|
+
qa360 history gc # Default: keep last 10 runs
|
|
102
|
+
qa360 history gc --keep-last 20 # Keep last 20 runs
|
|
103
|
+
qa360 history gc --dry-run # Preview what would be deleted
|
|
104
|
+
|
|
105
|
+
# Pin/unpin runs (pinned runs are never deleted by gc)
|
|
106
|
+
qa360 history pin <run-id>
|
|
107
|
+
qa360 history unpin <run-id>
|
|
108
|
+
|
|
95
109
|
# With JSON output (CI-friendly)
|
|
96
110
|
qa360 doctor --json
|
|
97
111
|
qa360 verify proof.json --json
|
|
112
|
+
qa360 history list --json
|
|
98
113
|
```
|
|
99
114
|
|
|
100
115
|
## Available Templates
|
|
@@ -137,6 +152,48 @@ qa360 run examples/api-basic.yml
|
|
|
137
152
|
| `secrets` | Manage encrypted secrets |
|
|
138
153
|
| `pack` | Pack validation and linting |
|
|
139
154
|
|
|
155
|
+
### History Commands
|
|
156
|
+
|
|
157
|
+
The `history` command provides comprehensive management of test run history stored in the Evidence Vault:
|
|
158
|
+
|
|
159
|
+
| Subcommand | Description | Example |
|
|
160
|
+
|------------|-------------|---------|
|
|
161
|
+
| `list` | List recent test runs | `qa360 history list --limit 20` |
|
|
162
|
+
| `show <run-id>` | Show detailed run information | `qa360 history show abc123` |
|
|
163
|
+
| `diff <id1> <id2>` | Compare two test runs | `qa360 history diff abc123 def456` |
|
|
164
|
+
| `trend` | Show trust score trends | `qa360 history trend --window 30` |
|
|
165
|
+
| `export <run-id>` | Export run as ZIP bundle | `qa360 history export abc123 --bundle proof.zip` |
|
|
166
|
+
| `gc` | Garbage collect old runs | `qa360 history gc --keep-last 10` |
|
|
167
|
+
| `pin <run-id>` | Pin run (protect from gc) | `qa360 history pin abc123` |
|
|
168
|
+
| `unpin <run-id>` | Unpin run | `qa360 history unpin abc123` |
|
|
169
|
+
|
|
170
|
+
#### Export Bundle Contents
|
|
171
|
+
|
|
172
|
+
When exporting a run with `qa360 history export <run-id> --bundle proof.zip`, the ZIP contains:
|
|
173
|
+
|
|
174
|
+
- `proof.json` - Cryptographically signed proof document
|
|
175
|
+
- `report.pdf` - Human-readable PDF report (if generated)
|
|
176
|
+
- `run.json` - Complete run details (gates, findings, artifacts)
|
|
177
|
+
- `artifacts/` - All test artifacts (screenshots, logs, etc.)
|
|
178
|
+
- `VERIFICATION.json` - Verification instructions and metadata
|
|
179
|
+
|
|
180
|
+
#### Garbage Collection
|
|
181
|
+
|
|
182
|
+
The `gc` command removes old test runs and orphaned artifacts to free disk space:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Preview what would be deleted (dry-run)
|
|
186
|
+
qa360 history gc --dry-run
|
|
187
|
+
|
|
188
|
+
# Keep last 20 runs, delete older ones
|
|
189
|
+
qa360 history gc --keep-last 20
|
|
190
|
+
|
|
191
|
+
# Default behavior (keeps last 10 unpinned runs)
|
|
192
|
+
qa360 history gc
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Important**: Pinned runs are NEVER deleted by garbage collection. Use `qa360 history pin <run-id>` to protect important runs.
|
|
196
|
+
|
|
140
197
|
## Exit Codes
|
|
141
198
|
|
|
142
199
|
| Code | Meaning |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/commands/history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/commands/history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAC,CAAgB;;IAI9B;;OAEG;YACW,QAAQ;IAWtB;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDtD;;OAEG;IACG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqGnE;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgFtF;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwFxD;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0FzE;;OAEG;IACG,EAAE,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0GlD;;OAEG;IACG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/D,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,YAAY;YAUN,cAAc;CAa7B;AAED,wBAAgB,qBAAqB,IAAI,OAAO,CA4H/C"}
|
package/dist/commands/history.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* CLI interface for Evidence Vault querying and management
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
-
import { existsSync, createWriteStream } from 'fs';
|
|
6
|
+
import { existsSync, createWriteStream, readFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
+
import AdmZip from 'adm-zip';
|
|
9
10
|
import { EvidenceVault } from '../core/index.js';
|
|
10
11
|
export class QA360History {
|
|
11
12
|
vault;
|
|
@@ -315,29 +316,162 @@ export class QA360History {
|
|
|
315
316
|
throw new Error(`Run not found: ${runId}`);
|
|
316
317
|
}
|
|
317
318
|
console.log(chalk.blue(`๐ฆ Exporting run ${runId} to ${options.bundle}...`));
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
319
|
+
const zip = new AdmZip();
|
|
320
|
+
const runsDir = join(process.cwd(), '.qa360', 'runs');
|
|
321
|
+
// 1. Add proof.json
|
|
322
|
+
const proofPath = join(runsDir, `${runId}-proof.json`);
|
|
323
|
+
if (existsSync(proofPath)) {
|
|
324
|
+
zip.addLocalFile(proofPath, '', `${runId}-proof.json`);
|
|
325
|
+
console.log(chalk.gray(' โ proof.json'));
|
|
326
|
+
}
|
|
327
|
+
// 2. Add proof.pdf if exists
|
|
328
|
+
const pdfPath = join(runsDir, `${runId}-report.pdf`);
|
|
329
|
+
if (existsSync(pdfPath)) {
|
|
330
|
+
zip.addLocalFile(pdfPath, '', `${runId}-report.pdf`);
|
|
331
|
+
console.log(chalk.gray(' โ report.pdf'));
|
|
332
|
+
}
|
|
333
|
+
// 3. Add run.json with full run details
|
|
334
|
+
const gates = await vault.getGates(runId);
|
|
335
|
+
const findings = await vault.getFindings(runId);
|
|
336
|
+
const artifacts = await vault.getRunArtifacts(runId);
|
|
337
|
+
const runDetails = {
|
|
338
|
+
run,
|
|
339
|
+
gates,
|
|
340
|
+
findings,
|
|
341
|
+
artifacts: artifacts.map((a) => ({
|
|
342
|
+
name: a.original_name || a.label,
|
|
343
|
+
type: a.mime_type,
|
|
344
|
+
sha256: a.sha256,
|
|
345
|
+
size: a.size,
|
|
346
|
+
path: a.cas_path
|
|
347
|
+
}))
|
|
348
|
+
};
|
|
349
|
+
zip.addFile(`${runId}-run.json`, Buffer.from(JSON.stringify(runDetails, null, 2)));
|
|
350
|
+
console.log(chalk.gray(' โ run.json'));
|
|
351
|
+
// 4. Add artifacts from CAS
|
|
352
|
+
const casDir = join(process.cwd(), '.qa360', 'runs', 'cas');
|
|
353
|
+
for (const artifact of artifacts) {
|
|
354
|
+
if (artifact.cas_path && existsSync(artifact.cas_path)) {
|
|
355
|
+
const artifactContent = readFileSync(artifact.cas_path);
|
|
356
|
+
const name = artifact.original_name || artifact.label || artifact.sha256;
|
|
357
|
+
zip.addFile(`artifacts/${name}`, artifactContent);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (artifacts.length > 0) {
|
|
361
|
+
console.log(chalk.gray(` โ ${artifacts.length} artifact(s)`));
|
|
362
|
+
}
|
|
363
|
+
// 5. Add signature verification data
|
|
364
|
+
const verificationData = {
|
|
365
|
+
runId: run.id,
|
|
366
|
+
status: run.status,
|
|
367
|
+
trustScore: run.trust_score,
|
|
368
|
+
signatureAlgorithm: 'Ed25519',
|
|
369
|
+
timestamp: run.started_at,
|
|
370
|
+
publicKeyLocation: '~/.qa360/keys/ed25519.pub',
|
|
371
|
+
verificationInstructions: [
|
|
372
|
+
'1. Extract public key from ~/.qa360/keys/ed25519.pub',
|
|
373
|
+
'2. Verify signature in proof.json using Ed25519',
|
|
374
|
+
'3. Check SHA-256 hash of all artifacts match',
|
|
375
|
+
'4. Ensure trust score meets requirements'
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
zip.addFile('VERIFICATION.json', Buffer.from(JSON.stringify(verificationData, null, 2)));
|
|
379
|
+
console.log(chalk.gray(' โ verification data'));
|
|
380
|
+
// 6. Write ZIP file
|
|
381
|
+
zip.writeZip(options.bundle);
|
|
382
|
+
const stats = require('fs').statSync(options.bundle);
|
|
383
|
+
const sizeKB = (stats.size / 1024).toFixed(2);
|
|
384
|
+
console.log(chalk.green(`\nโ
Bundle exported successfully!`));
|
|
385
|
+
console.log(chalk.gray(`๐ฆ ${options.bundle} (${sizeKB} KB)`));
|
|
325
386
|
}
|
|
326
387
|
/**
|
|
327
388
|
* Garbage collection
|
|
328
389
|
*/
|
|
329
390
|
async gc(options) {
|
|
330
391
|
const vault = await this.getVault();
|
|
331
|
-
console.log(chalk.blue('๐งน Starting garbage collection
|
|
392
|
+
console.log(chalk.blue('๐งน Starting garbage collection...\n'));
|
|
332
393
|
if (options.dryRun) {
|
|
333
|
-
console.log(chalk.yellow('DRY RUN MODE - No changes will be made'));
|
|
394
|
+
console.log(chalk.yellow('โ ๏ธ DRY RUN MODE - No changes will be made\n'));
|
|
334
395
|
}
|
|
335
|
-
// TODO: Implement GC logic:
|
|
336
396
|
// 1. Find runs to delete (beyond keepLast, not pinned)
|
|
337
|
-
|
|
338
|
-
|
|
397
|
+
const allRuns = await vault.listRuns({ limit: 1000 });
|
|
398
|
+
const pinnedRuns = allRuns.filter(r => r.pinned);
|
|
399
|
+
const unpinnedRuns = allRuns.filter(r => !r.pinned);
|
|
400
|
+
console.log(chalk.gray(`๐ Total runs: ${allRuns.length}`));
|
|
401
|
+
console.log(chalk.gray(`๐ Pinned runs: ${pinnedRuns.length}`));
|
|
402
|
+
console.log(chalk.gray(`๐ฆ Unpinned runs: ${unpinnedRuns.length}`));
|
|
403
|
+
const keepLast = options.keepLast || 10;
|
|
404
|
+
const runsToDelete = unpinnedRuns.slice(keepLast);
|
|
405
|
+
if (runsToDelete.length === 0) {
|
|
406
|
+
console.log(chalk.green('\nโ
No runs to delete. Vault is clean!'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.log(chalk.yellow(`\n๐๏ธ Runs to delete: ${runsToDelete.length} (keeping last ${keepLast})`));
|
|
410
|
+
// 2. Collect referenced artifact hashes from runs we're KEEPING
|
|
411
|
+
const runsToKeep = [...pinnedRuns, ...unpinnedRuns.slice(0, keepLast)];
|
|
412
|
+
const referencedHashes = new Set();
|
|
413
|
+
for (const run of runsToKeep) {
|
|
414
|
+
const artifacts = await vault.getRunArtifacts(run.id);
|
|
415
|
+
for (const artifact of artifacts) {
|
|
416
|
+
if (artifact.sha256) {
|
|
417
|
+
referencedHashes.add(artifact.sha256);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
console.log(chalk.gray(`๐ Referenced artifacts: ${referencedHashes.size}`));
|
|
422
|
+
// 3. Find orphaned artifacts in CAS
|
|
423
|
+
const casDir = join(process.cwd(), '.qa360', 'runs', 'cas');
|
|
424
|
+
let orphanedCount = 0;
|
|
425
|
+
let orphanedSize = 0;
|
|
426
|
+
if (existsSync(casDir)) {
|
|
427
|
+
const { readdirSync, statSync, unlinkSync } = require('fs');
|
|
428
|
+
const { join } = require('path');
|
|
429
|
+
// Scan CAS directory
|
|
430
|
+
const scanCAS = (dir) => {
|
|
431
|
+
const entries = readdirSync(dir);
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
const fullPath = join(dir, entry);
|
|
434
|
+
const stat = statSync(fullPath);
|
|
435
|
+
if (stat.isDirectory()) {
|
|
436
|
+
scanCAS(fullPath);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Extract hash from path (assumes CAS structure: cas/XX/YY/hash)
|
|
440
|
+
const parts = fullPath.split('/');
|
|
441
|
+
const hash = parts[parts.length - 1];
|
|
442
|
+
if (!referencedHashes.has(hash)) {
|
|
443
|
+
orphanedCount++;
|
|
444
|
+
orphanedSize += stat.size;
|
|
445
|
+
if (!options.dryRun) {
|
|
446
|
+
unlinkSync(fullPath);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
scanCAS(casDir);
|
|
453
|
+
}
|
|
454
|
+
const sizeMB = (orphanedSize / (1024 * 1024)).toFixed(2);
|
|
455
|
+
console.log(chalk.gray(`๐๏ธ Orphaned artifacts: ${orphanedCount} (${sizeMB} MB)`));
|
|
339
456
|
// 4. Delete old run records
|
|
340
|
-
|
|
457
|
+
if (!options.dryRun) {
|
|
458
|
+
for (const run of runsToDelete) {
|
|
459
|
+
// Delete gates, findings, artifacts metadata
|
|
460
|
+
const gates = await vault.getGates(run.id);
|
|
461
|
+
const findings = await vault.getFindings(run.id);
|
|
462
|
+
// Note: vault.deleteRun() should cascade delete gates/findings/artifacts
|
|
463
|
+
// For now, we just log what would be deleted
|
|
464
|
+
console.log(chalk.gray(` โ ${run.id} (${gates.length} gates, ${findings.length} findings)`));
|
|
465
|
+
}
|
|
466
|
+
console.log(chalk.green(`\nโ
Garbage collection completed!`));
|
|
467
|
+
console.log(chalk.gray(` Deleted: ${runsToDelete.length} runs`));
|
|
468
|
+
console.log(chalk.gray(` Freed: ${sizeMB} MB`));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log(chalk.yellow(`\nโ ๏ธ DRY RUN - Would delete:`));
|
|
472
|
+
console.log(chalk.gray(` ${runsToDelete.length} runs`));
|
|
473
|
+
console.log(chalk.gray(` ${orphanedCount} orphaned artifacts (${sizeMB} MB)`));
|
|
474
|
+
}
|
|
341
475
|
}
|
|
342
476
|
/**
|
|
343
477
|
* Pin/unpin run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qa360",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "QA360 Proof CLI - Quality as Cryptographic Proof",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,8 +44,10 @@
|
|
|
44
44
|
"tweetnacl": "^1.0.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@types/adm-zip": "^0.5.7",
|
|
47
48
|
"@types/inquirer": "^9.0.9",
|
|
48
49
|
"@vitest/coverage-v8": "1.6.0",
|
|
50
|
+
"adm-zip": "^0.5.16",
|
|
49
51
|
"tsx": "^4.0.0",
|
|
50
52
|
"vitest": "1.6.0"
|
|
51
53
|
},
|