agentic-qe 3.8.8 → 3.8.9
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 +16 -0
- package/dist/cli/bundle.js +711 -710
- package/dist/cli/commands/ruvector-commands.js +41 -1
- package/dist/domains/code-intelligence/services/knowledge-graph.js +3 -0
- 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/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 +373 -372
- package/dist/mcp/tools/coverage-analysis/index.d.ts +12 -0
- package/dist/mcp/tools/coverage-analysis/index.js +27 -4
- package/package.json +1 -1
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* - LCOV format (from Istanbul, nyc, c8)
|
|
6
6
|
* - Cobertura XML format
|
|
7
7
|
* - JSON format (Istanbul/vitest)
|
|
8
|
+
* - JaCoCo XML format (Java/Kotlin)
|
|
9
|
+
* - dotcover XML format (C#/.NET)
|
|
10
|
+
* - Tarpaulin JSON format (Rust)
|
|
11
|
+
* - Go cover text format (Go)
|
|
12
|
+
* - Kover XML format (Kotlin/JVM)
|
|
13
|
+
* - xcresult JSON format (Swift/iOS)
|
|
8
14
|
*
|
|
9
15
|
* This is NOT a simulation - it reads and parses real coverage data.
|
|
10
16
|
*
|
|
@@ -319,29 +325,560 @@ export async function parseJSONCoverage(jsonPath, projectRoot) {
|
|
|
319
325
|
};
|
|
320
326
|
}
|
|
321
327
|
// ============================================================================
|
|
328
|
+
// JaCoCo XML Parser (Java/Kotlin)
|
|
329
|
+
// ============================================================================
|
|
330
|
+
/**
|
|
331
|
+
* Parse JaCoCo XML coverage data.
|
|
332
|
+
*
|
|
333
|
+
* JaCoCo XML structure:
|
|
334
|
+
* <report><package><class><method><counter type="LINE" missed="X" covered="Y"/></method></class></package></report>
|
|
335
|
+
*/
|
|
336
|
+
export async function parseJaCoCo(xmlPath, projectRoot) {
|
|
337
|
+
const content = await fs.readFile(xmlPath, 'utf-8');
|
|
338
|
+
return parseJaCoCoContent(content, projectRoot || path.dirname(xmlPath));
|
|
339
|
+
}
|
|
340
|
+
export function parseJaCoCoContent(content, projectRoot) {
|
|
341
|
+
const files = new Map();
|
|
342
|
+
// Parse <package> blocks
|
|
343
|
+
const packagePattern = /<package\s+name="([^"]*)">([\s\S]*?)<\/package>/g;
|
|
344
|
+
let pkgMatch;
|
|
345
|
+
while ((pkgMatch = packagePattern.exec(content)) !== null) {
|
|
346
|
+
const pkgName = pkgMatch[1].replace(/\//g, path.sep);
|
|
347
|
+
const pkgBody = pkgMatch[2];
|
|
348
|
+
// Parse <sourcefile> blocks within package
|
|
349
|
+
const sfPattern = /<sourcefile\s+name="([^"]*)">([\s\S]*?)<\/sourcefile>/g;
|
|
350
|
+
let sfMatch;
|
|
351
|
+
while ((sfMatch = sfPattern.exec(pkgBody)) !== null) {
|
|
352
|
+
const fileName = sfMatch[1];
|
|
353
|
+
const sfBody = sfMatch[2];
|
|
354
|
+
const filePath = path.join(projectRoot, pkgName, fileName);
|
|
355
|
+
// Parse <counter> elements
|
|
356
|
+
const counters = parseJaCoCoCounters(sfBody);
|
|
357
|
+
const lineDetails = new Map();
|
|
358
|
+
// Parse <line> elements for detailed line data
|
|
359
|
+
const linePattern = /<line\s+nr="(\d+)"\s+mi="(\d+)"\s+ci="(\d+)"/g;
|
|
360
|
+
let lineMatch;
|
|
361
|
+
while ((lineMatch = linePattern.exec(sfBody)) !== null) {
|
|
362
|
+
const lineNum = parseInt(lineMatch[1], 10);
|
|
363
|
+
const ci = parseInt(lineMatch[3], 10);
|
|
364
|
+
lineDetails.set(lineNum, ci);
|
|
365
|
+
}
|
|
366
|
+
const uncoveredLines = Array.from(lineDetails.entries())
|
|
367
|
+
.filter(([, hits]) => hits === 0)
|
|
368
|
+
.map(([line]) => line)
|
|
369
|
+
.sort((a, b) => a - b);
|
|
370
|
+
files.set(filePath, {
|
|
371
|
+
path: filePath,
|
|
372
|
+
relativePath: path.join(pkgName, fileName),
|
|
373
|
+
lines: {
|
|
374
|
+
total: counters.LINE.total,
|
|
375
|
+
covered: counters.LINE.covered,
|
|
376
|
+
percentage: counters.LINE.total > 0 ? (counters.LINE.covered / counters.LINE.total) * 100 : 0,
|
|
377
|
+
details: lineDetails,
|
|
378
|
+
uncoveredLines,
|
|
379
|
+
},
|
|
380
|
+
branches: {
|
|
381
|
+
total: counters.BRANCH.total,
|
|
382
|
+
covered: counters.BRANCH.covered,
|
|
383
|
+
percentage: counters.BRANCH.total > 0 ? (counters.BRANCH.covered / counters.BRANCH.total) * 100 : 0,
|
|
384
|
+
uncoveredBranches: [],
|
|
385
|
+
},
|
|
386
|
+
functions: {
|
|
387
|
+
total: counters.METHOD.total,
|
|
388
|
+
covered: counters.METHOD.covered,
|
|
389
|
+
percentage: counters.METHOD.total > 0 ? (counters.METHOD.covered / counters.METHOD.total) * 100 : 0,
|
|
390
|
+
details: [],
|
|
391
|
+
uncoveredFunctions: [],
|
|
392
|
+
},
|
|
393
|
+
statements: {
|
|
394
|
+
total: counters.INSTRUCTION.total,
|
|
395
|
+
covered: counters.INSTRUCTION.covered,
|
|
396
|
+
percentage: counters.INSTRUCTION.total > 0 ? (counters.INSTRUCTION.covered / counters.INSTRUCTION.total) * 100 : 0,
|
|
397
|
+
},
|
|
398
|
+
coveragePercentage: calculateOverallPercentage(counters.LINE.covered, counters.LINE.total, counters.BRANCH.covered, counters.BRANCH.total),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
timestamp: new Date(),
|
|
404
|
+
format: 'jacoco',
|
|
405
|
+
language: 'java',
|
|
406
|
+
files,
|
|
407
|
+
summary: calculateSummary(files),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function parseJaCoCoCounters(xml) {
|
|
411
|
+
const result = {
|
|
412
|
+
LINE: { covered: 0, total: 0 },
|
|
413
|
+
BRANCH: { covered: 0, total: 0 },
|
|
414
|
+
METHOD: { covered: 0, total: 0 },
|
|
415
|
+
INSTRUCTION: { covered: 0, total: 0 },
|
|
416
|
+
};
|
|
417
|
+
const counterPattern = /<counter\s+type="(\w+)"\s+missed="(\d+)"\s+covered="(\d+)"\s*\/>/g;
|
|
418
|
+
let m;
|
|
419
|
+
while ((m = counterPattern.exec(xml)) !== null) {
|
|
420
|
+
const type = m[1];
|
|
421
|
+
const missed = parseInt(m[2], 10);
|
|
422
|
+
const covered = parseInt(m[3], 10);
|
|
423
|
+
if (result[type]) {
|
|
424
|
+
result[type].covered += covered;
|
|
425
|
+
result[type].total += missed + covered;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// dotcover XML Parser (C#/.NET)
|
|
432
|
+
// ============================================================================
|
|
433
|
+
/**
|
|
434
|
+
* Parse dotcover XML coverage data.
|
|
435
|
+
*
|
|
436
|
+
* dotcover XML structure:
|
|
437
|
+
* <Root CoveredStatements="X" TotalStatements="Y"><Assembly><Namespace><Type><Member Statement="..."/></Type></Namespace></Assembly></Root>
|
|
438
|
+
*/
|
|
439
|
+
export async function parseDotcover(xmlPath, projectRoot) {
|
|
440
|
+
const content = await fs.readFile(xmlPath, 'utf-8');
|
|
441
|
+
return parseDotcoverContent(content, projectRoot || path.dirname(xmlPath));
|
|
442
|
+
}
|
|
443
|
+
export function parseDotcoverContent(content, projectRoot) {
|
|
444
|
+
const files = new Map();
|
|
445
|
+
// Parse <File> elements to build file path index
|
|
446
|
+
const fileIndex = new Map();
|
|
447
|
+
const filePattern = /<File\s+Index="(\d+)"\s+Name="([^"]*)"\s*\/>/g;
|
|
448
|
+
let fm;
|
|
449
|
+
while ((fm = filePattern.exec(content)) !== null) {
|
|
450
|
+
fileIndex.set(fm[1], fm[2]);
|
|
451
|
+
}
|
|
452
|
+
// Parse <Statement> elements to build per-file coverage
|
|
453
|
+
const stmtPattern = /<Statement\s+FileIndex="(\d+)"\s+Line="(\d+)"\s+Column="\d+"\s+EndLine="\d+"\s+EndColumn="\d+"\s+Covered="(\w+)"\s*\/>/g;
|
|
454
|
+
const fileData = new Map();
|
|
455
|
+
let sm;
|
|
456
|
+
while ((sm = stmtPattern.exec(content)) !== null) {
|
|
457
|
+
const fileIdx = sm[1];
|
|
458
|
+
const lineNum = parseInt(sm[2], 10);
|
|
459
|
+
const isCovered = sm[3] === 'True';
|
|
460
|
+
const filePath = fileIndex.get(fileIdx) || `file-${fileIdx}`;
|
|
461
|
+
if (!fileData.has(filePath)) {
|
|
462
|
+
fileData.set(filePath, { lineDetails: new Map(), total: 0, covered: 0 });
|
|
463
|
+
}
|
|
464
|
+
const data = fileData.get(filePath);
|
|
465
|
+
data.lineDetails.set(lineNum, isCovered ? 1 : 0);
|
|
466
|
+
data.total++;
|
|
467
|
+
if (isCovered)
|
|
468
|
+
data.covered++;
|
|
469
|
+
}
|
|
470
|
+
// If no Statement elements found, parse from CoveredStatements/TotalStatements attributes
|
|
471
|
+
if (fileData.size === 0) {
|
|
472
|
+
const rootPattern = /CoveredStatements="(\d+)"\s+TotalStatements="(\d+)"/;
|
|
473
|
+
const rootMatch = rootPattern.exec(content);
|
|
474
|
+
if (rootMatch) {
|
|
475
|
+
const covered = parseInt(rootMatch[1], 10);
|
|
476
|
+
const total = parseInt(rootMatch[2], 10);
|
|
477
|
+
const filePath = path.join(projectRoot, 'aggregate');
|
|
478
|
+
files.set(filePath, {
|
|
479
|
+
path: filePath,
|
|
480
|
+
relativePath: 'aggregate',
|
|
481
|
+
lines: { total, covered, percentage: total > 0 ? (covered / total) * 100 : 0, details: new Map(), uncoveredLines: [] },
|
|
482
|
+
branches: { total: 0, covered: 0, percentage: 0, uncoveredBranches: [] },
|
|
483
|
+
functions: { total: 0, covered: 0, percentage: 0, details: [], uncoveredFunctions: [] },
|
|
484
|
+
statements: { total, covered, percentage: total > 0 ? (covered / total) * 100 : 0 },
|
|
485
|
+
coveragePercentage: total > 0 ? (covered / total) * 100 : 0,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const [filePath, data] of fileData.entries()) {
|
|
490
|
+
const uncoveredLines = Array.from(data.lineDetails.entries())
|
|
491
|
+
.filter(([, hits]) => hits === 0)
|
|
492
|
+
.map(([line]) => line)
|
|
493
|
+
.sort((a, b) => a - b);
|
|
494
|
+
files.set(filePath, {
|
|
495
|
+
path: filePath,
|
|
496
|
+
relativePath: path.relative(projectRoot, filePath),
|
|
497
|
+
lines: {
|
|
498
|
+
total: data.lineDetails.size,
|
|
499
|
+
covered: Array.from(data.lineDetails.values()).filter(h => h > 0).length,
|
|
500
|
+
percentage: data.lineDetails.size > 0 ? (Array.from(data.lineDetails.values()).filter(h => h > 0).length / data.lineDetails.size) * 100 : 0,
|
|
501
|
+
details: data.lineDetails,
|
|
502
|
+
uncoveredLines,
|
|
503
|
+
},
|
|
504
|
+
branches: { total: 0, covered: 0, percentage: 0, uncoveredBranches: [] },
|
|
505
|
+
functions: { total: 0, covered: 0, percentage: 0, details: [], uncoveredFunctions: [] },
|
|
506
|
+
statements: {
|
|
507
|
+
total: data.total,
|
|
508
|
+
covered: data.covered,
|
|
509
|
+
percentage: data.total > 0 ? (data.covered / data.total) * 100 : 0,
|
|
510
|
+
},
|
|
511
|
+
coveragePercentage: data.total > 0 ? (data.covered / data.total) * 100 : 0,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
timestamp: new Date(),
|
|
516
|
+
format: 'dotcover',
|
|
517
|
+
language: 'csharp',
|
|
518
|
+
files,
|
|
519
|
+
summary: calculateSummary(files),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// Tarpaulin JSON Parser (Rust)
|
|
524
|
+
// ============================================================================
|
|
525
|
+
/**
|
|
526
|
+
* Parse Tarpaulin JSON coverage data.
|
|
527
|
+
*
|
|
528
|
+
* Tarpaulin JSON structure (--output-dir with --out Json):
|
|
529
|
+
* { "files": [ { "path": "...", "content": "...", "traces": [ { "line": N, "stats": { "Line": N } } ] } ] }
|
|
530
|
+
*
|
|
531
|
+
* Also handles tarpaulin LCOV output by delegating to parseLCOVContent.
|
|
532
|
+
*/
|
|
533
|
+
export async function parseTarpaulin(filePath, projectRoot) {
|
|
534
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
535
|
+
const root = projectRoot || path.dirname(filePath);
|
|
536
|
+
// If it looks like LCOV, delegate
|
|
537
|
+
if (content.includes('SF:') && content.includes('end_of_record')) {
|
|
538
|
+
const report = parseLCOVContent(content, root);
|
|
539
|
+
report.format = 'tarpaulin';
|
|
540
|
+
report.language = 'rust';
|
|
541
|
+
return report;
|
|
542
|
+
}
|
|
543
|
+
return parseTarpaulinContent(content, root);
|
|
544
|
+
}
|
|
545
|
+
export function parseTarpaulinContent(content, projectRoot) {
|
|
546
|
+
const files = new Map();
|
|
547
|
+
const data = safeJsonParse(content);
|
|
548
|
+
for (const fileEntry of data?.files || []) {
|
|
549
|
+
const filePath = fileEntry.path || '';
|
|
550
|
+
const lineDetails = new Map();
|
|
551
|
+
let linesTotal = 0;
|
|
552
|
+
let linesCovered = 0;
|
|
553
|
+
for (const trace of fileEntry.traces || []) {
|
|
554
|
+
const lineNum = trace.line;
|
|
555
|
+
const hits = trace.stats?.Line ?? (trace.stats?.line ?? 0);
|
|
556
|
+
if (typeof lineNum === 'number') {
|
|
557
|
+
lineDetails.set(lineNum, hits);
|
|
558
|
+
linesTotal++;
|
|
559
|
+
if (hits > 0)
|
|
560
|
+
linesCovered++;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const uncoveredLines = Array.from(lineDetails.entries())
|
|
564
|
+
.filter(([, hits]) => hits === 0)
|
|
565
|
+
.map(([line]) => line)
|
|
566
|
+
.sort((a, b) => a - b);
|
|
567
|
+
files.set(filePath, {
|
|
568
|
+
path: filePath,
|
|
569
|
+
relativePath: path.relative(projectRoot, filePath),
|
|
570
|
+
lines: {
|
|
571
|
+
total: linesTotal,
|
|
572
|
+
covered: linesCovered,
|
|
573
|
+
percentage: linesTotal > 0 ? (linesCovered / linesTotal) * 100 : 0,
|
|
574
|
+
details: lineDetails,
|
|
575
|
+
uncoveredLines,
|
|
576
|
+
},
|
|
577
|
+
branches: { total: 0, covered: 0, percentage: 0, uncoveredBranches: [] },
|
|
578
|
+
functions: { total: 0, covered: 0, percentage: 0, details: [], uncoveredFunctions: [] },
|
|
579
|
+
statements: {
|
|
580
|
+
total: linesTotal,
|
|
581
|
+
covered: linesCovered,
|
|
582
|
+
percentage: linesTotal > 0 ? (linesCovered / linesTotal) * 100 : 0,
|
|
583
|
+
},
|
|
584
|
+
coveragePercentage: linesTotal > 0 ? (linesCovered / linesTotal) * 100 : 0,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
timestamp: new Date(),
|
|
589
|
+
format: 'tarpaulin',
|
|
590
|
+
language: 'rust',
|
|
591
|
+
files,
|
|
592
|
+
summary: calculateSummary(files),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// Go cover Parser
|
|
597
|
+
// ============================================================================
|
|
598
|
+
/**
|
|
599
|
+
* Parse Go coverage profile text format.
|
|
600
|
+
*
|
|
601
|
+
* Format:
|
|
602
|
+
* mode: set|count|atomic
|
|
603
|
+
* pkg/file.go:startLine.startCol,endLine.endCol numStatements count
|
|
604
|
+
*/
|
|
605
|
+
export async function parseGoCover(coverPath, projectRoot) {
|
|
606
|
+
const content = await fs.readFile(coverPath, 'utf-8');
|
|
607
|
+
return parseGoCoverContent(content, projectRoot || path.dirname(coverPath));
|
|
608
|
+
}
|
|
609
|
+
export function parseGoCoverContent(content, projectRoot) {
|
|
610
|
+
const files = new Map();
|
|
611
|
+
const fileData = new Map();
|
|
612
|
+
const lines = content.split('\n');
|
|
613
|
+
for (const line of lines) {
|
|
614
|
+
const trimmed = line.trim();
|
|
615
|
+
if (!trimmed || trimmed.startsWith('mode:'))
|
|
616
|
+
continue;
|
|
617
|
+
// Format: file:startLine.startCol,endLine.endCol numStatements count
|
|
618
|
+
const match = trimmed.match(/^(.+):(\d+)\.\d+,(\d+)\.\d+\s+(\d+)\s+(\d+)$/);
|
|
619
|
+
if (!match)
|
|
620
|
+
continue;
|
|
621
|
+
const filePath = match[1];
|
|
622
|
+
const startLine = parseInt(match[2], 10);
|
|
623
|
+
const endLine = parseInt(match[3], 10);
|
|
624
|
+
const numStmts = parseInt(match[4], 10);
|
|
625
|
+
const count = parseInt(match[5], 10);
|
|
626
|
+
if (!fileData.has(filePath)) {
|
|
627
|
+
fileData.set(filePath, { lineDetails: new Map(), stmtTotal: 0, stmtCovered: 0 });
|
|
628
|
+
}
|
|
629
|
+
const data = fileData.get(filePath);
|
|
630
|
+
// Mark each line in the range
|
|
631
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
632
|
+
const existing = data.lineDetails.get(l) || 0;
|
|
633
|
+
data.lineDetails.set(l, existing + count);
|
|
634
|
+
}
|
|
635
|
+
data.stmtTotal += numStmts;
|
|
636
|
+
if (count > 0)
|
|
637
|
+
data.stmtCovered += numStmts;
|
|
638
|
+
}
|
|
639
|
+
for (const [filePath, data] of fileData.entries()) {
|
|
640
|
+
const linesTotal = data.lineDetails.size;
|
|
641
|
+
const linesCovered = Array.from(data.lineDetails.values()).filter(h => h > 0).length;
|
|
642
|
+
const uncoveredLines = Array.from(data.lineDetails.entries())
|
|
643
|
+
.filter(([, hits]) => hits === 0)
|
|
644
|
+
.map(([line]) => line)
|
|
645
|
+
.sort((a, b) => a - b);
|
|
646
|
+
files.set(filePath, {
|
|
647
|
+
path: filePath,
|
|
648
|
+
relativePath: path.relative(projectRoot, filePath),
|
|
649
|
+
lines: {
|
|
650
|
+
total: linesTotal,
|
|
651
|
+
covered: linesCovered,
|
|
652
|
+
percentage: linesTotal > 0 ? (linesCovered / linesTotal) * 100 : 0,
|
|
653
|
+
details: data.lineDetails,
|
|
654
|
+
uncoveredLines,
|
|
655
|
+
},
|
|
656
|
+
branches: { total: 0, covered: 0, percentage: 0, uncoveredBranches: [] },
|
|
657
|
+
functions: { total: 0, covered: 0, percentage: 0, details: [], uncoveredFunctions: [] },
|
|
658
|
+
statements: {
|
|
659
|
+
total: data.stmtTotal,
|
|
660
|
+
covered: data.stmtCovered,
|
|
661
|
+
percentage: data.stmtTotal > 0 ? (data.stmtCovered / data.stmtTotal) * 100 : 0,
|
|
662
|
+
},
|
|
663
|
+
coveragePercentage: data.stmtTotal > 0 ? (data.stmtCovered / data.stmtTotal) * 100 : 0,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
timestamp: new Date(),
|
|
668
|
+
format: 'gocover',
|
|
669
|
+
language: 'go',
|
|
670
|
+
files,
|
|
671
|
+
summary: calculateSummary(files),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// Kover XML Parser (Kotlin/JVM)
|
|
676
|
+
// ============================================================================
|
|
677
|
+
/**
|
|
678
|
+
* Parse Kover XML coverage data.
|
|
679
|
+
*
|
|
680
|
+
* Kover outputs JaCoCo-compatible XML. This delegates to the JaCoCo parser
|
|
681
|
+
* but sets the format and language appropriately.
|
|
682
|
+
*/
|
|
683
|
+
export async function parseKover(xmlPath, projectRoot) {
|
|
684
|
+
const content = await fs.readFile(xmlPath, 'utf-8');
|
|
685
|
+
const report = parseJaCoCoContent(content, projectRoot || path.dirname(xmlPath));
|
|
686
|
+
report.format = 'kover';
|
|
687
|
+
report.language = 'kotlin';
|
|
688
|
+
return report;
|
|
689
|
+
}
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// xcresult JSON Parser (Swift/iOS)
|
|
692
|
+
// ============================================================================
|
|
693
|
+
/**
|
|
694
|
+
* Parse xcresult coverage data exported via:
|
|
695
|
+
* xcrun xccov view --report --json result.xcresult > coverage.json
|
|
696
|
+
*
|
|
697
|
+
* Structure:
|
|
698
|
+
* { "targets": [ { "files": [ { "path": "...", "lineCoverage": 0.85, "coveredLines": N, "executableLines": N, "functions": [...] } ] } ] }
|
|
699
|
+
*/
|
|
700
|
+
export async function parseXcresult(jsonPath, projectRoot) {
|
|
701
|
+
const content = await fs.readFile(jsonPath, 'utf-8');
|
|
702
|
+
return parseXcresultContent(content, projectRoot || path.dirname(jsonPath));
|
|
703
|
+
}
|
|
704
|
+
export function parseXcresultContent(content, projectRoot) {
|
|
705
|
+
const files = new Map();
|
|
706
|
+
const data = safeJsonParse(content);
|
|
707
|
+
const targets = data?.targets || [];
|
|
708
|
+
for (const target of targets) {
|
|
709
|
+
for (const fileEntry of target.files || []) {
|
|
710
|
+
const filePath = fileEntry.path || '';
|
|
711
|
+
const executableLines = fileEntry.executableLines || 0;
|
|
712
|
+
const coveredLines = fileEntry.coveredLines || 0;
|
|
713
|
+
const lineCoverage = fileEntry.lineCoverage || 0;
|
|
714
|
+
// Parse function data if available
|
|
715
|
+
const functions = [];
|
|
716
|
+
let fnTotal = 0;
|
|
717
|
+
let fnCovered = 0;
|
|
718
|
+
for (const fn of fileEntry.functions || []) {
|
|
719
|
+
const hits = fn.coveredLines > 0 ? 1 : 0;
|
|
720
|
+
functions.push({
|
|
721
|
+
name: fn.name || 'unknown',
|
|
722
|
+
line: fn.lineNumber || 0,
|
|
723
|
+
hits,
|
|
724
|
+
});
|
|
725
|
+
fnTotal++;
|
|
726
|
+
if (hits > 0)
|
|
727
|
+
fnCovered++;
|
|
728
|
+
}
|
|
729
|
+
files.set(filePath, {
|
|
730
|
+
path: filePath,
|
|
731
|
+
relativePath: path.relative(projectRoot, filePath),
|
|
732
|
+
lines: {
|
|
733
|
+
total: executableLines,
|
|
734
|
+
covered: coveredLines,
|
|
735
|
+
percentage: lineCoverage * 100,
|
|
736
|
+
details: new Map(),
|
|
737
|
+
uncoveredLines: [],
|
|
738
|
+
},
|
|
739
|
+
branches: { total: 0, covered: 0, percentage: 0, uncoveredBranches: [] },
|
|
740
|
+
functions: {
|
|
741
|
+
total: fnTotal,
|
|
742
|
+
covered: fnCovered,
|
|
743
|
+
percentage: fnTotal > 0 ? (fnCovered / fnTotal) * 100 : 0,
|
|
744
|
+
details: functions,
|
|
745
|
+
uncoveredFunctions: functions.filter(f => f.hits === 0).map(f => f.name),
|
|
746
|
+
},
|
|
747
|
+
statements: {
|
|
748
|
+
total: executableLines,
|
|
749
|
+
covered: coveredLines,
|
|
750
|
+
percentage: lineCoverage * 100,
|
|
751
|
+
},
|
|
752
|
+
coveragePercentage: lineCoverage * 100,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
timestamp: new Date(),
|
|
758
|
+
format: 'xcresult',
|
|
759
|
+
language: 'swift',
|
|
760
|
+
files,
|
|
761
|
+
summary: calculateSummary(files),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
// ============================================================================
|
|
322
765
|
// Auto-Detect Parser
|
|
323
766
|
// ============================================================================
|
|
324
767
|
/**
|
|
325
|
-
*
|
|
768
|
+
* Language-to-format mapping for inference when format is not specified
|
|
769
|
+
*/
|
|
770
|
+
const LANGUAGE_FORMAT_HINTS = {
|
|
771
|
+
java: ['jacoco', 'cobertura', 'lcov'],
|
|
772
|
+
kotlin: ['kover', 'jacoco'],
|
|
773
|
+
csharp: ['dotcover', 'cobertura'],
|
|
774
|
+
rust: ['tarpaulin', 'lcov'],
|
|
775
|
+
go: ['gocover'],
|
|
776
|
+
swift: ['xcresult'],
|
|
777
|
+
dart: ['lcov'],
|
|
778
|
+
typescript: ['lcov', 'json'],
|
|
779
|
+
javascript: ['lcov', 'json'],
|
|
780
|
+
python: ['lcov', 'cobertura', 'json'],
|
|
781
|
+
};
|
|
782
|
+
/**
|
|
783
|
+
* Auto-detect coverage format and parse accordingly.
|
|
784
|
+
*
|
|
785
|
+
* @param coveragePath - Path to the coverage file
|
|
786
|
+
* @param projectRoot - Project root for relative path calculation
|
|
787
|
+
* @param format - Explicit format override
|
|
788
|
+
* @param language - Language hint for format inference
|
|
326
789
|
*/
|
|
327
|
-
export async function parseCoverage(coveragePath, projectRoot) {
|
|
790
|
+
export async function parseCoverage(coveragePath, projectRoot, format, language) {
|
|
791
|
+
// If format is explicitly specified, use it directly
|
|
792
|
+
if (format) {
|
|
793
|
+
switch (format) {
|
|
794
|
+
case 'lcov': return parseLCOV(coveragePath, projectRoot);
|
|
795
|
+
case 'json': return parseJSONCoverage(coveragePath, projectRoot);
|
|
796
|
+
case 'jacoco': return parseJaCoCo(coveragePath, projectRoot);
|
|
797
|
+
case 'dotcover': return parseDotcover(coveragePath, projectRoot);
|
|
798
|
+
case 'tarpaulin': return parseTarpaulin(coveragePath, projectRoot);
|
|
799
|
+
case 'gocover': return parseGoCover(coveragePath, projectRoot);
|
|
800
|
+
case 'kover': return parseKover(coveragePath, projectRoot);
|
|
801
|
+
case 'xcresult': return parseXcresult(coveragePath, projectRoot);
|
|
802
|
+
default: break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
328
805
|
const ext = path.extname(coveragePath).toLowerCase();
|
|
329
806
|
const basename = path.basename(coveragePath).toLowerCase();
|
|
330
|
-
//
|
|
807
|
+
// Detect by filename/extension
|
|
331
808
|
if (ext === '.json' || basename.includes('coverage-final')) {
|
|
332
809
|
return parseJSONCoverage(coveragePath, projectRoot);
|
|
333
810
|
}
|
|
334
811
|
if (basename === 'lcov.info' || ext === '.info' || basename.includes('lcov')) {
|
|
335
812
|
return parseLCOV(coveragePath, projectRoot);
|
|
336
813
|
}
|
|
337
|
-
|
|
814
|
+
if (basename.includes('jacoco') && ext === '.xml') {
|
|
815
|
+
return parseJaCoCo(coveragePath, projectRoot);
|
|
816
|
+
}
|
|
817
|
+
if (basename.includes('dotcover') && ext === '.xml') {
|
|
818
|
+
return parseDotcover(coveragePath, projectRoot);
|
|
819
|
+
}
|
|
820
|
+
if (basename.includes('tarpaulin')) {
|
|
821
|
+
return parseTarpaulin(coveragePath, projectRoot);
|
|
822
|
+
}
|
|
823
|
+
if (basename.includes('kover') && ext === '.xml') {
|
|
824
|
+
return parseKover(coveragePath, projectRoot);
|
|
825
|
+
}
|
|
826
|
+
if (basename.includes('coverage') && ext === '.out') {
|
|
827
|
+
return parseGoCover(coveragePath, projectRoot);
|
|
828
|
+
}
|
|
829
|
+
if (basename.includes('xcresult') || basename.includes('xccov')) {
|
|
830
|
+
return parseXcresult(coveragePath, projectRoot);
|
|
831
|
+
}
|
|
832
|
+
// Content-based detection
|
|
338
833
|
const content = await fs.readFile(coveragePath, 'utf-8');
|
|
339
|
-
|
|
834
|
+
const trimmed = content.trim();
|
|
835
|
+
if (trimmed.startsWith('{')) {
|
|
836
|
+
// Check for tarpaulin JSON structure
|
|
837
|
+
if (trimmed.includes('"files"') && trimmed.includes('"traces"')) {
|
|
838
|
+
return parseTarpaulinContent(content, projectRoot || path.dirname(coveragePath));
|
|
839
|
+
}
|
|
840
|
+
// Check for xcresult JSON structure
|
|
841
|
+
if (trimmed.includes('"targets"') && trimmed.includes('"lineCoverage"')) {
|
|
842
|
+
return parseXcresultContent(content, projectRoot || path.dirname(coveragePath));
|
|
843
|
+
}
|
|
340
844
|
return parseJSONCoverage(coveragePath, projectRoot);
|
|
341
845
|
}
|
|
342
846
|
if (content.includes('SF:') && content.includes('end_of_record')) {
|
|
343
847
|
return parseLCOV(coveragePath, projectRoot);
|
|
344
848
|
}
|
|
849
|
+
// Go cover format: starts with "mode:"
|
|
850
|
+
if (trimmed.startsWith('mode:')) {
|
|
851
|
+
return parseGoCoverContent(content, projectRoot || path.dirname(coveragePath));
|
|
852
|
+
}
|
|
853
|
+
// XML-based detection
|
|
854
|
+
if (trimmed.startsWith('<?xml') || trimmed.startsWith('<')) {
|
|
855
|
+
if (content.includes('<counter type="') || content.includes('<report ')) {
|
|
856
|
+
// Differentiate JaCoCo vs Kover using language hint
|
|
857
|
+
if (language === 'kotlin') {
|
|
858
|
+
const report = parseJaCoCoContent(content, projectRoot || path.dirname(coveragePath));
|
|
859
|
+
report.format = 'kover';
|
|
860
|
+
report.language = 'kotlin';
|
|
861
|
+
return report;
|
|
862
|
+
}
|
|
863
|
+
return parseJaCoCoContent(content, projectRoot || path.dirname(coveragePath));
|
|
864
|
+
}
|
|
865
|
+
if (content.includes('CoveredStatements=') || content.includes('<Root')) {
|
|
866
|
+
return parseDotcoverContent(content, projectRoot || path.dirname(coveragePath));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Language-based fallback: try the first format associated with the language
|
|
870
|
+
if (language) {
|
|
871
|
+
const langLower = language.toLowerCase();
|
|
872
|
+
const hints = LANGUAGE_FORMAT_HINTS[langLower];
|
|
873
|
+
if (hints && hints.length > 0) {
|
|
874
|
+
try {
|
|
875
|
+
return await parseCoverage(coveragePath, projectRoot, hints[0]);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
// Language hint didn't help, fall through to error
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
345
882
|
throw new Error(`Unknown coverage format: ${coveragePath}`);
|
|
346
883
|
}
|
|
347
884
|
/**
|
|
@@ -349,12 +886,28 @@ export async function parseCoverage(coveragePath, projectRoot) {
|
|
|
349
886
|
*/
|
|
350
887
|
export async function findAndParseCoverage(searchDir, projectRoot) {
|
|
351
888
|
const root = projectRoot || searchDir;
|
|
352
|
-
// Common coverage file locations
|
|
889
|
+
// Common coverage file locations (multi-language)
|
|
353
890
|
const candidates = [
|
|
891
|
+
// JS/TS (Istanbul, nyc, c8, vitest)
|
|
354
892
|
path.join(searchDir, 'coverage', 'lcov.info'),
|
|
355
893
|
path.join(searchDir, 'coverage', 'coverage-final.json'),
|
|
356
894
|
path.join(searchDir, 'lcov.info'),
|
|
357
895
|
path.join(searchDir, '.nyc_output', 'coverage.json'),
|
|
896
|
+
// Java (JaCoCo)
|
|
897
|
+
path.join(searchDir, 'target', 'site', 'jacoco', 'jacoco.xml'),
|
|
898
|
+
path.join(searchDir, 'build', 'reports', 'jacoco', 'test', 'jacocoTestReport.xml'),
|
|
899
|
+
// C# (dotcover)
|
|
900
|
+
path.join(searchDir, 'dotcover-report.xml'),
|
|
901
|
+
// Rust (tarpaulin)
|
|
902
|
+
path.join(searchDir, 'tarpaulin-report.json'),
|
|
903
|
+
path.join(searchDir, 'cobertura.xml'),
|
|
904
|
+
// Go
|
|
905
|
+
path.join(searchDir, 'coverage.out'),
|
|
906
|
+
path.join(searchDir, 'cover.out'),
|
|
907
|
+
// Kotlin (Kover)
|
|
908
|
+
path.join(searchDir, 'build', 'reports', 'kover', 'report.xml'),
|
|
909
|
+
// Swift (xcresult)
|
|
910
|
+
path.join(searchDir, 'coverage.json'),
|
|
358
911
|
];
|
|
359
912
|
for (const candidate of candidates) {
|
|
360
913
|
try {
|
|
@@ -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
|
/**
|