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.
Files changed (30) hide show
  1. package/.claude/skills/skills-manifest.json +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/dist/cli/bundle.js +731 -730
  4. package/dist/cli/commands/ruvector-commands.js +41 -1
  5. package/dist/coordination/protocols/defect-investigation.js +3 -3
  6. package/dist/domains/code-intelligence/services/knowledge-graph.js +3 -0
  7. package/dist/domains/coverage-analysis/services/coverage-analyzer.d.ts +6 -0
  8. package/dist/domains/coverage-analysis/services/coverage-analyzer.js +35 -1
  9. package/dist/domains/coverage-analysis/services/coverage-parser.d.ts +72 -4
  10. package/dist/domains/coverage-analysis/services/coverage-parser.js +559 -6
  11. package/dist/domains/defect-intelligence/services/defect-predictor.js +16 -6
  12. package/dist/domains/quality-assessment/coordinator.js +8 -1
  13. package/dist/domains/quality-assessment/plugin.js +8 -5
  14. package/dist/domains/quality-assessment/services/quality-analyzer.d.ts +0 -1
  15. package/dist/domains/quality-assessment/services/quality-analyzer.js +30 -17
  16. package/dist/domains/test-execution/interfaces.d.ts +11 -0
  17. package/dist/domains/test-execution/services/test-executor.d.ts +25 -0
  18. package/dist/domains/test-execution/services/test-executor.js +236 -13
  19. package/dist/governance/proof-envelope-integration.js +10 -4
  20. package/dist/integrations/coherence/engines/witness-adapter.d.ts +5 -5
  21. package/dist/integrations/coherence/engines/witness-adapter.js +10 -22
  22. package/dist/integrations/ruvector/coherence-gate.d.ts +14 -5
  23. package/dist/integrations/ruvector/coherence-gate.js +34 -6
  24. package/dist/learning/agent-routing.d.ts +7 -2
  25. package/dist/learning/agent-routing.js +17 -1
  26. package/dist/mcp/bundle.js +378 -377
  27. package/dist/mcp/tools/coverage-analysis/index.d.ts +12 -0
  28. package/dist/mcp/tools/coverage-analysis/index.js +27 -4
  29. package/dist/workers/workers/coverage-tracker.js +25 -30
  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
- // Use stored coverage or estimate from testability
384
+ // Only use real stored coverage never fabricate a number
385
385
  const storedCoverage = await this.getStoredCoverage(file);
386
- metrics.coverage = storedCoverage ?? Math.min(95, metrics.testability || 70);
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
- metrics.coverage = 70;
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
- // Try to get coverage from stored test results
413
- const key = `quality-analysis:coverage:${this.hashFilePath(file)}`;
414
- const coverage = await this.memory.get(key);
415
- return coverage ?? null;
416
- }
417
- hashFilePath(path) {
418
- // Simple hash for file path
419
- let hash = 0;
420
- for (let i = 0; i < path.length; i++) {
421
- hash = ((hash << 5) - hash) + path.charCodeAt(i);
422
- hash = hash & hash;
423
- }
424
- return hash.toString(16);
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: metrics.find((m) => m.name === 'coverage')?.value || 0,
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: undefined,
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: undefined,
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: undefined,
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 = 'agentic-qe-default-key') {
51
+ async initialize(signingKey) {
52
52
  if (this.initialized)
53
53
  return;
54
54
  await this.kernel.initialize();
55
- this.signingKey = signingKey;
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 Blake3 witness chain operations.
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
- * Blake3 Witness Chains:
8
- * - Each decision is hashed with Blake3
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
- * Uses a simple hash when crypto is not available
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 Blake3 witness chain operations.
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
- * Blake3 Witness Chains:
8
- * - Each decision is hashed with Blake3
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 { secureRandom } from '../../../shared/utils/crypto-random.js';
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
- * Uses a simple hash when crypto is not available
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
- // Simple hash implementation for environments without crypto
139
- // In production, this would use Blake3 or SHA-256
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
- for (let i = 0; i < previousHash.length; i++) {
146
- hash = ((hash << 5) - hash + previousHash.charCodeAt(i)) | 0;
147
- }
141
+ hasher.update(previousHash, 'utf-8');
148
142
  }
149
- // Convert to hex string
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