agentic-qe 3.8.8 → 3.8.10
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/.claude/skills/skills-manifest.json +1 -1
- package/CHANGELOG.md +27 -0
- package/dist/cli/bundle.js +731 -730
- package/dist/cli/commands/ruvector-commands.js +41 -1
- package/dist/coordination/protocols/defect-investigation.js +3 -3
- package/dist/domains/code-intelligence/services/knowledge-graph.js +3 -0
- package/dist/domains/coverage-analysis/services/coverage-analyzer.d.ts +6 -0
- package/dist/domains/coverage-analysis/services/coverage-analyzer.js +35 -1
- package/dist/domains/coverage-analysis/services/coverage-parser.d.ts +72 -4
- package/dist/domains/coverage-analysis/services/coverage-parser.js +559 -6
- package/dist/domains/defect-intelligence/services/defect-predictor.js +16 -6
- package/dist/domains/quality-assessment/coordinator.js +8 -1
- package/dist/domains/quality-assessment/plugin.js +8 -5
- package/dist/domains/quality-assessment/services/quality-analyzer.d.ts +0 -1
- package/dist/domains/quality-assessment/services/quality-analyzer.js +30 -17
- package/dist/domains/test-execution/interfaces.d.ts +11 -0
- package/dist/domains/test-execution/services/test-executor.d.ts +25 -0
- package/dist/domains/test-execution/services/test-executor.js +236 -13
- package/dist/governance/proof-envelope-integration.js +10 -4
- package/dist/integrations/coherence/engines/witness-adapter.d.ts +5 -5
- package/dist/integrations/coherence/engines/witness-adapter.js +10 -22
- package/dist/integrations/ruvector/coherence-gate.d.ts +14 -5
- package/dist/integrations/ruvector/coherence-gate.js +34 -6
- package/dist/learning/agent-routing.d.ts +7 -2
- package/dist/learning/agent-routing.js +17 -1
- package/dist/mcp/bundle.js +378 -377
- package/dist/mcp/tools/coverage-analysis/index.d.ts +12 -0
- package/dist/mcp/tools/coverage-analysis/index.js +27 -4
- package/dist/workers/workers/coverage-tracker.js +25 -30
- package/package.json +1 -1
|
@@ -106,7 +106,6 @@ export declare class QualityAnalyzerService implements IQualityAnalyzerService {
|
|
|
106
106
|
getQualityTrend(metric: string, days: number): Promise<Result<QualityTrend, Error>>;
|
|
107
107
|
private collectFileMetrics;
|
|
108
108
|
private getStoredCoverage;
|
|
109
|
-
private hashFilePath;
|
|
110
109
|
private aggregateMetrics;
|
|
111
110
|
private calculateQualityScore;
|
|
112
111
|
private generateTrends;
|
|
@@ -381,15 +381,22 @@ Focus on:
|
|
|
381
381
|
}
|
|
382
382
|
if (includeAll || includeMetrics.includes('coverage')) {
|
|
383
383
|
// Coverage requires external data (from test runners)
|
|
384
|
-
//
|
|
384
|
+
// Only use real stored coverage — never fabricate a number
|
|
385
385
|
const storedCoverage = await this.getStoredCoverage(file);
|
|
386
|
-
|
|
386
|
+
if (storedCoverage !== null) {
|
|
387
|
+
metrics.coverage = storedCoverage;
|
|
388
|
+
}
|
|
389
|
+
// If no coverage data exists, omit the metric rather than guessing
|
|
387
390
|
}
|
|
388
391
|
}
|
|
389
392
|
else {
|
|
390
393
|
// Fallback for files that couldn't be analyzed
|
|
391
394
|
if (includeAll || includeMetrics.includes('coverage')) {
|
|
392
|
-
|
|
395
|
+
// Only use real stored coverage — never fabricate a number
|
|
396
|
+
const storedCoverage = await this.getStoredCoverage(file);
|
|
397
|
+
if (storedCoverage !== null) {
|
|
398
|
+
metrics.coverage = storedCoverage;
|
|
399
|
+
}
|
|
393
400
|
}
|
|
394
401
|
if (includeAll || includeMetrics.includes('complexity')) {
|
|
395
402
|
metrics.complexity = 10;
|
|
@@ -409,19 +416,23 @@ Focus on:
|
|
|
409
416
|
return fileMetrics;
|
|
410
417
|
}
|
|
411
418
|
async getStoredCoverage(file) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
419
|
+
try {
|
|
420
|
+
// 1. Try per-file coverage stored by test executor
|
|
421
|
+
const fileCoverage = await this.memory.get(`coverage:file:${file}`);
|
|
422
|
+
if (fileCoverage && typeof fileCoverage.line === 'number') {
|
|
423
|
+
return fileCoverage.line;
|
|
424
|
+
}
|
|
425
|
+
// 2. Fall back to project-wide summary from coverage-analyzer
|
|
426
|
+
const summary = await this.memory.get('coverage:latest');
|
|
427
|
+
if (summary && typeof summary.line === 'number') {
|
|
428
|
+
return summary.line;
|
|
429
|
+
}
|
|
430
|
+
// No coverage data available — return null (never fabricate)
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
425
436
|
}
|
|
426
437
|
aggregateMetrics(fileMetrics) {
|
|
427
438
|
const aggregated = new Map();
|
|
@@ -469,9 +480,11 @@ Focus on:
|
|
|
469
480
|
totalWeight += weight;
|
|
470
481
|
}
|
|
471
482
|
const overall = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
|
|
483
|
+
// Use -1 to signal "no data available" rather than 0 which implies "0% coverage"
|
|
484
|
+
const coverageMetric = metrics.find((m) => m.name === 'coverage');
|
|
472
485
|
return {
|
|
473
486
|
overall,
|
|
474
|
-
coverage:
|
|
487
|
+
coverage: coverageMetric ? coverageMetric.value : -1,
|
|
475
488
|
complexity: metrics.find((m) => m.name === 'complexity')?.value || 0,
|
|
476
489
|
maintainability: metrics.find((m) => m.name === 'maintainability')?.value || 0,
|
|
477
490
|
security: 85, // Placeholder - would come from security analysis
|
|
@@ -68,6 +68,15 @@ export interface ITestRunResult {
|
|
|
68
68
|
duration: number;
|
|
69
69
|
failedTests: IFailedTest[];
|
|
70
70
|
coverage?: ICoverageData;
|
|
71
|
+
/** Per-file coverage data for granular quality assessment */
|
|
72
|
+
fileCoverages?: IFileCoverageData[];
|
|
73
|
+
}
|
|
74
|
+
export interface IFileCoverageData {
|
|
75
|
+
path: string;
|
|
76
|
+
line: number;
|
|
77
|
+
branch: number;
|
|
78
|
+
function: number;
|
|
79
|
+
statement: number;
|
|
71
80
|
}
|
|
72
81
|
export interface IFailedTest {
|
|
73
82
|
testId: string;
|
|
@@ -304,6 +313,8 @@ export type TestRunResult = ITestRunResult;
|
|
|
304
313
|
export type FailedTest = IFailedTest;
|
|
305
314
|
/** @deprecated Use ICoverageData */
|
|
306
315
|
export type CoverageData = ICoverageData;
|
|
316
|
+
/** @deprecated Use IFileCoverageData */
|
|
317
|
+
export type FileCoverageData = IFileCoverageData;
|
|
307
318
|
/** @deprecated Use IFlakyDetectionRequest */
|
|
308
319
|
export type FlakyDetectionRequest = IFlakyDetectionRequest;
|
|
309
320
|
/** @deprecated Use IFlakyTestReport */
|
|
@@ -144,7 +144,32 @@ export declare class TestExecutorService implements ITestExecutionService {
|
|
|
144
144
|
private shardTests;
|
|
145
145
|
private executeWorker;
|
|
146
146
|
private aggregateResults;
|
|
147
|
+
/**
|
|
148
|
+
* Aggregate coverage from multiple test file results.
|
|
149
|
+
*
|
|
150
|
+
* Note: uses unweighted average across entries. When coverage comes from
|
|
151
|
+
* readCoverageFromDisk() the runner's own weighted total is used directly
|
|
152
|
+
* (bypassing this method), so the unweighted average only applies when
|
|
153
|
+
* individual file JSON outputs each report their own summary.
|
|
154
|
+
*/
|
|
147
155
|
private aggregateCoverage;
|
|
156
|
+
/**
|
|
157
|
+
* Extract coverage percentages from vitest/jest JSON output.
|
|
158
|
+
*
|
|
159
|
+
* Both runners include a `coverageMap` (or `coverageSummary`) object when
|
|
160
|
+
* --coverage is passed. The shape varies, so we handle the common cases:
|
|
161
|
+
* - Jest/vitest v8: json.coverageMap with per-file entries
|
|
162
|
+
* - Jest coverageSummary: json.coverageSummary.total with pct fields
|
|
163
|
+
*
|
|
164
|
+
* Returns both aggregate and per-file coverage data.
|
|
165
|
+
*/
|
|
166
|
+
private extractCoverageFromJson;
|
|
167
|
+
/**
|
|
168
|
+
* Read coverage from disk files written by vitest/jest.
|
|
169
|
+
* Both runners write coverage-summary.json to a coverage/ directory when
|
|
170
|
+
* --coverage is passed, rather than embedding it in stdout JSON.
|
|
171
|
+
*/
|
|
172
|
+
private readCoverageFromDisk;
|
|
148
173
|
private storeResults;
|
|
149
174
|
}
|
|
150
175
|
/**
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { LoggerFactory } from '../../../logging/index.js';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
8
9
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
10
|
import { ok, err } from '../../../shared/types';
|
|
10
11
|
import { TEST_EXECUTION_CONSTANTS, LLM_ANALYSIS_CONSTANTS } from '../../constants.js';
|
|
@@ -66,6 +67,7 @@ export class TestExecutorService {
|
|
|
66
67
|
duration,
|
|
67
68
|
failedTests: results.failedTests,
|
|
68
69
|
coverage: results.coverage,
|
|
70
|
+
fileCoverages: results.fileCoverages,
|
|
69
71
|
};
|
|
70
72
|
// Store results
|
|
71
73
|
this.runResults.set(runId, runResult);
|
|
@@ -118,6 +120,7 @@ export class TestExecutorService {
|
|
|
118
120
|
duration,
|
|
119
121
|
failedTests: aggregated.failedTests,
|
|
120
122
|
coverage: aggregated.coverage,
|
|
123
|
+
fileCoverages: aggregated.fileCoverages,
|
|
121
124
|
};
|
|
122
125
|
// Store results
|
|
123
126
|
this.runResults.set(runId, runResult);
|
|
@@ -295,6 +298,8 @@ Provide:
|
|
|
295
298
|
async runTests(request) {
|
|
296
299
|
const { testFiles, framework, timeout = 30000 } = request;
|
|
297
300
|
const failedTests = [];
|
|
301
|
+
const coverages = [];
|
|
302
|
+
const allFileCoverages = [];
|
|
298
303
|
let passed = 0;
|
|
299
304
|
let failed = 0;
|
|
300
305
|
let skipped = 0;
|
|
@@ -304,6 +309,12 @@ Provide:
|
|
|
304
309
|
failed += result.failed;
|
|
305
310
|
skipped += result.skipped;
|
|
306
311
|
failedTests.push(...result.failedTests);
|
|
312
|
+
if (result.coverage) {
|
|
313
|
+
coverages.push(result.coverage);
|
|
314
|
+
}
|
|
315
|
+
if (result.fileCoverages) {
|
|
316
|
+
allFileCoverages.push(...result.fileCoverages);
|
|
317
|
+
}
|
|
307
318
|
}
|
|
308
319
|
return {
|
|
309
320
|
total: passed + failed + skipped,
|
|
@@ -311,7 +322,8 @@ Provide:
|
|
|
311
322
|
failed,
|
|
312
323
|
skipped,
|
|
313
324
|
failedTests,
|
|
314
|
-
coverage: this.aggregateCoverage(
|
|
325
|
+
coverage: this.aggregateCoverage(coverages),
|
|
326
|
+
fileCoverages: allFileCoverages.length > 0 ? allFileCoverages : undefined,
|
|
315
327
|
};
|
|
316
328
|
}
|
|
317
329
|
async executeTestFile(file, framework, timeout) {
|
|
@@ -384,6 +396,17 @@ Provide:
|
|
|
384
396
|
}
|
|
385
397
|
// Parse results based on framework
|
|
386
398
|
const parseResult = this.parseTestOutput(stdout, stderr, file, framework, code);
|
|
399
|
+
// If no coverage in stdout JSON, try reading from disk
|
|
400
|
+
// (vitest/jest write coverage to coverage/coverage-summary.json)
|
|
401
|
+
if (parseResult.success && !parseResult.value.coverage) {
|
|
402
|
+
const diskCoverage = this.readCoverageFromDisk();
|
|
403
|
+
if (diskCoverage) {
|
|
404
|
+
parseResult.value.coverage = diskCoverage.summary;
|
|
405
|
+
if (diskCoverage.perFile.length > 0) {
|
|
406
|
+
parseResult.value.fileCoverages = diskCoverage.perFile;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
387
410
|
resolve(parseResult);
|
|
388
411
|
});
|
|
389
412
|
proc.on('error', (error) => {
|
|
@@ -400,14 +423,18 @@ Provide:
|
|
|
400
423
|
case 'vitest':
|
|
401
424
|
return {
|
|
402
425
|
command: 'npx',
|
|
403
|
-
args: ['vitest', 'run', file, '--reporter=json', '--no-color'
|
|
426
|
+
args: ['vitest', 'run', file, '--reporter=json', '--no-color',
|
|
427
|
+
'--coverage', '--coverage.reporter=json'],
|
|
404
428
|
};
|
|
405
429
|
case 'jest':
|
|
406
430
|
return {
|
|
407
431
|
command: 'npx',
|
|
408
|
-
args: ['jest', file, '--json', '--no-colors', '--testLocationInResults'
|
|
432
|
+
args: ['jest', file, '--json', '--no-colors', '--testLocationInResults',
|
|
433
|
+
'--coverage', '--coverageReporters=json'],
|
|
409
434
|
};
|
|
410
435
|
case 'mocha':
|
|
436
|
+
// Note: mocha has no built-in coverage — requires external nyc/c8 wrapper.
|
|
437
|
+
// Coverage data will be unavailable for mocha-based test runs.
|
|
411
438
|
return {
|
|
412
439
|
command: 'npx',
|
|
413
440
|
args: ['mocha', file, '--reporter=json'],
|
|
@@ -416,7 +443,8 @@ Provide:
|
|
|
416
443
|
// Default to vitest
|
|
417
444
|
return {
|
|
418
445
|
command: 'npx',
|
|
419
|
-
args: ['vitest', 'run', file, '--reporter=json', '--no-color'
|
|
446
|
+
args: ['vitest', 'run', file, '--reporter=json', '--no-color',
|
|
447
|
+
'--coverage', '--coverage.reporter=json'],
|
|
420
448
|
};
|
|
421
449
|
}
|
|
422
450
|
}
|
|
@@ -476,13 +504,15 @@ Provide:
|
|
|
476
504
|
}
|
|
477
505
|
}
|
|
478
506
|
}
|
|
507
|
+
const covData = this.extractCoverageFromJson(json);
|
|
479
508
|
return ok({
|
|
480
509
|
total: passed + failed + skipped,
|
|
481
510
|
passed,
|
|
482
511
|
failed,
|
|
483
512
|
skipped,
|
|
484
513
|
failedTests,
|
|
485
|
-
coverage:
|
|
514
|
+
coverage: covData.summary,
|
|
515
|
+
fileCoverages: covData.perFile.length > 0 ? covData.perFile : undefined,
|
|
486
516
|
});
|
|
487
517
|
}
|
|
488
518
|
catch {
|
|
@@ -525,13 +555,15 @@ Provide:
|
|
|
525
555
|
}
|
|
526
556
|
}
|
|
527
557
|
}
|
|
558
|
+
const covData = this.extractCoverageFromJson(json);
|
|
528
559
|
return ok({
|
|
529
560
|
total: passed + failed + skipped,
|
|
530
561
|
passed,
|
|
531
562
|
failed,
|
|
532
563
|
skipped,
|
|
533
564
|
failedTests,
|
|
534
|
-
coverage:
|
|
565
|
+
coverage: covData.summary,
|
|
566
|
+
fileCoverages: covData.perFile.length > 0 ? covData.perFile : undefined,
|
|
535
567
|
});
|
|
536
568
|
}
|
|
537
569
|
catch {
|
|
@@ -564,7 +596,7 @@ Provide:
|
|
|
564
596
|
failed,
|
|
565
597
|
skipped,
|
|
566
598
|
failedTests,
|
|
567
|
-
coverage: undefined,
|
|
599
|
+
coverage: undefined, // mocha has no built-in coverage
|
|
568
600
|
});
|
|
569
601
|
}
|
|
570
602
|
catch {
|
|
@@ -687,13 +719,20 @@ Provide:
|
|
|
687
719
|
duration: secureRandom() * 1000,
|
|
688
720
|
});
|
|
689
721
|
}
|
|
722
|
+
const fileCov = {
|
|
723
|
+
line: Math.round(secureRandom() * 4000 + 6000) / 100, // 60-100%
|
|
724
|
+
branch: Math.round(secureRandom() * 5000 + 4000) / 100, // 40-90%
|
|
725
|
+
function: Math.round(secureRandom() * 3000 + 7000) / 100, // 70-100%
|
|
726
|
+
statement: Math.round(secureRandom() * 4000 + 6000) / 100, // 60-100%
|
|
727
|
+
};
|
|
690
728
|
return {
|
|
691
729
|
total: testCount,
|
|
692
730
|
passed: testCount - failCount - skipCount,
|
|
693
731
|
failed: failCount,
|
|
694
732
|
skipped: skipCount,
|
|
695
733
|
failedTests,
|
|
696
|
-
coverage:
|
|
734
|
+
coverage: fileCov,
|
|
735
|
+
fileCoverages: [{ path: file, ...fileCov }],
|
|
697
736
|
};
|
|
698
737
|
}
|
|
699
738
|
shardTests(testFiles, workers, strategy) {
|
|
@@ -743,8 +782,21 @@ Provide:
|
|
|
743
782
|
aggregated.failedTests.push(...result.failedTests);
|
|
744
783
|
}
|
|
745
784
|
aggregated.coverage = this.aggregateCoverage(results.map(r => r.coverage).filter((c) => c !== undefined));
|
|
785
|
+
// Collect per-file coverages from all worker results
|
|
786
|
+
const allFileCoverages = results.flatMap(r => r.fileCoverages ?? []);
|
|
787
|
+
if (allFileCoverages.length > 0) {
|
|
788
|
+
aggregated.fileCoverages = allFileCoverages;
|
|
789
|
+
}
|
|
746
790
|
return aggregated;
|
|
747
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Aggregate coverage from multiple test file results.
|
|
794
|
+
*
|
|
795
|
+
* Note: uses unweighted average across entries. When coverage comes from
|
|
796
|
+
* readCoverageFromDisk() the runner's own weighted total is used directly
|
|
797
|
+
* (bypassing this method), so the unweighted average only applies when
|
|
798
|
+
* individual file JSON outputs each report their own summary.
|
|
799
|
+
*/
|
|
748
800
|
aggregateCoverage(coverages) {
|
|
749
801
|
if (coverages.length === 0) {
|
|
750
802
|
return undefined;
|
|
@@ -756,17 +808,188 @@ Provide:
|
|
|
756
808
|
statement: acc.statement + cov.statement,
|
|
757
809
|
}), { line: 0, branch: 0, function: 0, statement: 0 });
|
|
758
810
|
return {
|
|
759
|
-
line: sum.line / coverages.length,
|
|
760
|
-
branch: sum.branch / coverages.length,
|
|
761
|
-
function: sum.function / coverages.length,
|
|
762
|
-
statement: sum.statement / coverages.length,
|
|
811
|
+
line: Math.round((sum.line / coverages.length) * 100) / 100,
|
|
812
|
+
branch: Math.round((sum.branch / coverages.length) * 100) / 100,
|
|
813
|
+
function: Math.round((sum.function / coverages.length) * 100) / 100,
|
|
814
|
+
statement: Math.round((sum.statement / coverages.length) * 100) / 100,
|
|
763
815
|
};
|
|
764
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Extract coverage percentages from vitest/jest JSON output.
|
|
819
|
+
*
|
|
820
|
+
* Both runners include a `coverageMap` (or `coverageSummary`) object when
|
|
821
|
+
* --coverage is passed. The shape varies, so we handle the common cases:
|
|
822
|
+
* - Jest/vitest v8: json.coverageMap with per-file entries
|
|
823
|
+
* - Jest coverageSummary: json.coverageSummary.total with pct fields
|
|
824
|
+
*
|
|
825
|
+
* Returns both aggregate and per-file coverage data.
|
|
826
|
+
*/
|
|
827
|
+
extractCoverageFromJson(
|
|
828
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
829
|
+
json) {
|
|
830
|
+
const perFile = [];
|
|
831
|
+
try {
|
|
832
|
+
// Jest shape: { coverageSummary: { total: { lines: { pct }, branches: { pct }, ... } } }
|
|
833
|
+
const total = json?.coverageSummary?.total ?? json?.coverageMap?.total;
|
|
834
|
+
if (total?.lines?.pct !== undefined) {
|
|
835
|
+
return {
|
|
836
|
+
summary: {
|
|
837
|
+
line: typeof total.lines?.pct === 'number' ? total.lines.pct : 0,
|
|
838
|
+
branch: typeof total.branches?.pct === 'number' ? total.branches.pct : 0,
|
|
839
|
+
function: typeof total.functions?.pct === 'number' ? total.functions.pct : 0,
|
|
840
|
+
statement: typeof total.statements?.pct === 'number' ? total.statements.pct : 0,
|
|
841
|
+
},
|
|
842
|
+
perFile,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
// Vitest v8 shape: coverageMap is keyed by file path
|
|
846
|
+
const coverageMap = json?.coverageMap;
|
|
847
|
+
if (coverageMap && typeof coverageMap === 'object') {
|
|
848
|
+
const files = Object.keys(coverageMap).filter(k => k !== 'total');
|
|
849
|
+
if (files.length > 0) {
|
|
850
|
+
let lines = 0, branches = 0, functions = 0, statements = 0;
|
|
851
|
+
let count = 0;
|
|
852
|
+
for (const filePath of files) {
|
|
853
|
+
const entry = coverageMap[filePath];
|
|
854
|
+
const s = entry?.s ?? {};
|
|
855
|
+
const f = entry?.f ?? {};
|
|
856
|
+
const b = entry?.b ?? {};
|
|
857
|
+
const stmtTotal = Object.keys(s).length;
|
|
858
|
+
const stmtCovered = Object.values(s).filter((v) => v > 0).length;
|
|
859
|
+
const fnTotal = Object.keys(f).length;
|
|
860
|
+
const fnCovered = Object.values(f).filter((v) => v > 0).length;
|
|
861
|
+
const brTotal = Object.keys(b).length;
|
|
862
|
+
const brCovered = Object.values(b).filter((v) => Array.isArray(v) ? v.every(c => c > 0) : v > 0).length;
|
|
863
|
+
if (stmtTotal > 0) {
|
|
864
|
+
const fileLine = Math.round((stmtCovered / stmtTotal) * 10000) / 100;
|
|
865
|
+
const fileFn = fnTotal > 0 ? Math.round((fnCovered / fnTotal) * 10000) / 100 : 100;
|
|
866
|
+
const fileBr = brTotal > 0 ? Math.round((brCovered / brTotal) * 10000) / 100 : 100;
|
|
867
|
+
const fileStmt = fileLine;
|
|
868
|
+
perFile.push({ path: filePath, line: fileLine, branch: fileBr, function: fileFn, statement: fileStmt });
|
|
869
|
+
statements += fileStmt;
|
|
870
|
+
lines += fileLine;
|
|
871
|
+
functions += fileFn;
|
|
872
|
+
branches += fileBr;
|
|
873
|
+
count++;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (count > 0) {
|
|
877
|
+
return {
|
|
878
|
+
summary: {
|
|
879
|
+
line: Math.round((lines / count) * 100) / 100,
|
|
880
|
+
branch: Math.round((branches / count) * 100) / 100,
|
|
881
|
+
function: Math.round((functions / count) * 100) / 100,
|
|
882
|
+
statement: Math.round((statements / count) * 100) / 100,
|
|
883
|
+
},
|
|
884
|
+
perFile,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
// Coverage parsing is best-effort — never break test results for it
|
|
892
|
+
}
|
|
893
|
+
return { perFile };
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Read coverage from disk files written by vitest/jest.
|
|
897
|
+
* Both runners write coverage-summary.json to a coverage/ directory when
|
|
898
|
+
* --coverage is passed, rather than embedding it in stdout JSON.
|
|
899
|
+
*/
|
|
900
|
+
readCoverageFromDisk() {
|
|
901
|
+
try {
|
|
902
|
+
// Check common coverage output paths
|
|
903
|
+
const candidates = [
|
|
904
|
+
join(process.cwd(), 'coverage', 'coverage-summary.json'),
|
|
905
|
+
join(process.cwd(), 'coverage', 'coverage-final.json'),
|
|
906
|
+
];
|
|
907
|
+
for (const filePath of candidates) {
|
|
908
|
+
if (!existsSync(filePath))
|
|
909
|
+
continue;
|
|
910
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
911
|
+
const json = safeJsonParse(raw);
|
|
912
|
+
// coverage-summary.json shape: { total: { lines: { pct }, ... }, "/path": { lines: { pct }, ... } }
|
|
913
|
+
if (json?.total?.lines?.pct !== undefined) {
|
|
914
|
+
const perFile = [];
|
|
915
|
+
for (const [key, value] of Object.entries(json)) {
|
|
916
|
+
if (key === 'total')
|
|
917
|
+
continue;
|
|
918
|
+
// Normalize path: strip cwd prefix, reject traversal sequences
|
|
919
|
+
const normalizedPath = key.replace(process.cwd() + '/', '').replace(process.cwd() + '\\', '');
|
|
920
|
+
if (normalizedPath.includes('..'))
|
|
921
|
+
continue;
|
|
922
|
+
const entry = value;
|
|
923
|
+
if (entry?.lines?.pct !== undefined) {
|
|
924
|
+
perFile.push({
|
|
925
|
+
path: normalizedPath,
|
|
926
|
+
line: entry.lines.pct,
|
|
927
|
+
branch: entry.branches?.pct ?? 0,
|
|
928
|
+
function: entry.functions?.pct ?? 0,
|
|
929
|
+
statement: entry.statements?.pct ?? 0,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
summary: {
|
|
935
|
+
line: json.total.lines.pct,
|
|
936
|
+
branch: json.total.branches?.pct ?? 0,
|
|
937
|
+
function: json.total.functions?.pct ?? 0,
|
|
938
|
+
statement: json.total.statements?.pct ?? 0,
|
|
939
|
+
},
|
|
940
|
+
perFile,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// coverage-final.json shape: Istanbul raw map — extract from extractCoverageFromJson
|
|
944
|
+
if (typeof json === 'object' && !json.total) {
|
|
945
|
+
const result = this.extractCoverageFromJson({ coverageMap: json });
|
|
946
|
+
if (result.summary) {
|
|
947
|
+
return { summary: result.summary, perFile: result.perFile };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// Coverage from disk is best-effort
|
|
954
|
+
}
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
765
957
|
async storeResults(runId, result) {
|
|
766
958
|
await this.memory.set(`test-run:${runId}`, result, {
|
|
767
959
|
namespace: 'test-execution',
|
|
768
960
|
persist: true,
|
|
769
961
|
});
|
|
962
|
+
// Store coverage data so quality-assess and quality-gate can read it.
|
|
963
|
+
// Note: coverage:previous rotation is owned by coverage-analyzer (the
|
|
964
|
+
// canonical coverage service) to avoid double-rotation race conditions.
|
|
965
|
+
if (result.coverage) {
|
|
966
|
+
try {
|
|
967
|
+
// Write project-level summary using the same CoverageSummary shape
|
|
968
|
+
// as coverage-analyzer to avoid type mismatches
|
|
969
|
+
await this.memory.set('coverage:latest', {
|
|
970
|
+
line: result.coverage.line ?? 0,
|
|
971
|
+
branch: result.coverage.branch ?? 0,
|
|
972
|
+
function: result.coverage.function ?? 0,
|
|
973
|
+
statement: result.coverage.statement ?? 0,
|
|
974
|
+
files: result.fileCoverages?.length ?? 0,
|
|
975
|
+
}, { persist: true });
|
|
976
|
+
// Store per-file coverage via memory.set() (not storeVector) so that
|
|
977
|
+
// quality-analyzer's getStoredCoverage() can read it with memory.get()
|
|
978
|
+
if (result.fileCoverages) {
|
|
979
|
+
for (const fc of result.fileCoverages) {
|
|
980
|
+
await this.memory.set(`coverage:file:${fc.path}`, {
|
|
981
|
+
line: fc.line,
|
|
982
|
+
branch: fc.branch,
|
|
983
|
+
function: fc.function,
|
|
984
|
+
statement: fc.statement,
|
|
985
|
+
}, { persist: true });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
// Non-critical — don't break test storage if coverage store fails
|
|
991
|
+
}
|
|
992
|
+
}
|
|
770
993
|
}
|
|
771
994
|
}
|
|
772
995
|
// ============================================================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
2
2
|
import { safeJsonParse } from '../shared/safe-json.js';
|
|
3
3
|
/**
|
|
4
4
|
* Proof Envelope Integration for Agentic QE Fleet
|
|
@@ -48,17 +48,23 @@ export class ProofEnvelopeIntegration {
|
|
|
48
48
|
*
|
|
49
49
|
* @param signingKey - Key used for signing envelopes
|
|
50
50
|
*/
|
|
51
|
-
async initialize(signingKey
|
|
51
|
+
async initialize(signingKey) {
|
|
52
52
|
if (this.initialized)
|
|
53
53
|
return;
|
|
54
54
|
await this.kernel.initialize();
|
|
55
|
-
|
|
55
|
+
// Derive a deterministic per-installation signing key from the project
|
|
56
|
+
// directory. This is consistent across restarts (so envelopes remain
|
|
57
|
+
// verifiable) but unique per installation (unlike a global hardcoded key).
|
|
58
|
+
// For production use, pass an explicit key managed outside the process.
|
|
59
|
+
this.signingKey = signingKey || createHash('sha256')
|
|
60
|
+
.update(`aqe-proof-envelope:${process.cwd()}`)
|
|
61
|
+
.digest('hex');
|
|
56
62
|
// Try loading guidance ProofChain for parallel audit trail
|
|
57
63
|
try {
|
|
58
64
|
const modulePath = '@claude-flow/guidance/proof';
|
|
59
65
|
const mod = await import(/* @vite-ignore */ modulePath);
|
|
60
66
|
if (mod && typeof mod.createProofChain === 'function') {
|
|
61
|
-
this.guidanceProofChain = mod.createProofChain({ signingKey });
|
|
67
|
+
this.guidanceProofChain = mod.createProofChain({ signingKey: this.signingKey });
|
|
62
68
|
console.log('[ProofEnvelopeIntegration] Guidance ProofChain loaded');
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agentic QE v3 - Witness Engine Adapter
|
|
3
3
|
*
|
|
4
|
-
* Wraps the Prime Radiant WitnessEngine for
|
|
4
|
+
* Wraps the Prime Radiant WitnessEngine for SHA-256 witness chain operations.
|
|
5
5
|
* Used for creating tamper-evident audit trails of agent decisions.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - Each decision is hashed with
|
|
7
|
+
* Witness Chains:
|
|
8
|
+
* - Each decision is hashed with SHA-256
|
|
9
9
|
* - Hash includes reference to previous witness
|
|
10
10
|
* - Creates immutable audit trail
|
|
11
11
|
* - Enables deterministic replay
|
|
@@ -94,8 +94,8 @@ export declare class WitnessAdapter implements IWitnessAdapter {
|
|
|
94
94
|
*/
|
|
95
95
|
private createFallbackEngine;
|
|
96
96
|
/**
|
|
97
|
-
* Compute a hash for witness creation
|
|
98
|
-
*
|
|
97
|
+
* Compute a SHA-256 hash for witness creation.
|
|
98
|
+
* Chains the previous hash into the computation for tamper-evidence.
|
|
99
99
|
*/
|
|
100
100
|
private computeHash;
|
|
101
101
|
/**
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agentic QE v3 - Witness Engine Adapter
|
|
3
3
|
*
|
|
4
|
-
* Wraps the Prime Radiant WitnessEngine for
|
|
4
|
+
* Wraps the Prime Radiant WitnessEngine for SHA-256 witness chain operations.
|
|
5
5
|
* Used for creating tamper-evident audit trails of agent decisions.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - Each decision is hashed with
|
|
7
|
+
* Witness Chains:
|
|
8
|
+
* - Each decision is hashed with SHA-256
|
|
9
9
|
* - Hash includes reference to previous witness
|
|
10
10
|
* - Creates immutable audit trail
|
|
11
11
|
* - Enables deterministic replay
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @module integrations/coherence/engines/witness-adapter
|
|
14
14
|
*/
|
|
15
15
|
import { WasmNotLoadedError, DEFAULT_COHERENCE_LOGGER } from '../types';
|
|
16
|
-
import {
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// Witness Adapter Implementation
|
|
19
19
|
// ============================================================================
|
|
@@ -131,28 +131,16 @@ export class WitnessAdapter {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* Compute a hash for witness creation
|
|
135
|
-
*
|
|
134
|
+
* Compute a SHA-256 hash for witness creation.
|
|
135
|
+
* Chains the previous hash into the computation for tamper-evidence.
|
|
136
136
|
*/
|
|
137
137
|
computeHash(data, previousHash) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let hash = 0;
|
|
141
|
-
for (let i = 0; i < data.length; i++) {
|
|
142
|
-
hash = ((hash << 5) - hash + data[i]) | 0;
|
|
143
|
-
}
|
|
138
|
+
const hasher = createHash('sha256');
|
|
139
|
+
hasher.update(data);
|
|
144
140
|
if (previousHash) {
|
|
145
|
-
|
|
146
|
-
hash = ((hash << 5) - hash + previousHash.charCodeAt(i)) | 0;
|
|
147
|
-
}
|
|
141
|
+
hasher.update(previousHash, 'utf-8');
|
|
148
142
|
}
|
|
149
|
-
|
|
150
|
-
const unsignedHash = hash >>> 0;
|
|
151
|
-
return unsignedHash.toString(16).padStart(8, '0') +
|
|
152
|
-
'-' +
|
|
153
|
-
Date.now().toString(16) +
|
|
154
|
-
'-' +
|
|
155
|
-
secureRandom().toString(16).slice(2, 10);
|
|
143
|
+
return hasher.digest('hex');
|
|
156
144
|
}
|
|
157
145
|
/**
|
|
158
146
|
* Check if the adapter is initialized
|