@rigour-labs/core 5.2.1 → 5.2.3
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/dist/gates/coverage.d.ts +3 -0
- package/dist/gates/coverage.js +73 -9
- package/dist/gates/coverage.test.d.ts +1 -0
- package/dist/gates/coverage.test.js +53 -0
- package/dist/gates/runner.js +12 -6
- package/dist/services/adaptive-thresholds.d.ts +2 -0
- package/dist/services/adaptive-thresholds.js +2 -2
- package/dist/storage/local-memory.js +10 -12
- package/package.json +6 -6
package/dist/gates/coverage.d.ts
CHANGED
package/dist/gates/coverage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import readline from 'node:readline';
|
|
3
4
|
import { Gate } from './base.js';
|
|
4
5
|
import { globby } from 'globby';
|
|
5
6
|
export class CoverageGate extends Gate {
|
|
@@ -11,22 +12,21 @@ export class CoverageGate extends Gate {
|
|
|
11
12
|
async run(context) {
|
|
12
13
|
const failures = [];
|
|
13
14
|
// 1. Locate coverage report (lcov.info is standard)
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
ignore: ['node_modules/**']
|
|
17
|
-
});
|
|
18
|
-
if (reports.length === 0) {
|
|
15
|
+
const report = await this.findCoverageReport(context);
|
|
16
|
+
if (!report) {
|
|
19
17
|
// If no reports found, and coverage is required, we could flag it.
|
|
20
18
|
// But for now, we'll just skip silently if not configured.
|
|
21
19
|
return [];
|
|
22
20
|
}
|
|
23
|
-
// 2. Parse coverage (
|
|
24
|
-
const coverageData = await this.
|
|
21
|
+
// 2. Parse coverage (LCOV or Istanbul JSON)
|
|
22
|
+
const coverageData = await this.parseCoverage(path.join(context.cwd, report));
|
|
25
23
|
// 3. Quality Handshake: SME SME LOGIC
|
|
26
24
|
// We look for files that have high complexity but low coverage.
|
|
27
25
|
// In a real implementation, we would share data between ASTGate and CoverageGate.
|
|
28
26
|
// For this demo, we'll implement a standalone check.
|
|
29
27
|
for (const [file, stats] of Object.entries(coverageData)) {
|
|
28
|
+
if (!stats.found)
|
|
29
|
+
continue;
|
|
30
30
|
const coverage = (stats.hit / stats.found) * 100;
|
|
31
31
|
const threshold = stats.isComplex ? 80 : 50; // SME logic: Complex files need higher coverage
|
|
32
32
|
if (coverage < threshold) {
|
|
@@ -43,16 +43,66 @@ export class CoverageGate extends Gate {
|
|
|
43
43
|
}
|
|
44
44
|
return failures;
|
|
45
45
|
}
|
|
46
|
+
async findCoverageReport(context) {
|
|
47
|
+
const candidates = [
|
|
48
|
+
'lcov.info',
|
|
49
|
+
'coverage/lcov.info',
|
|
50
|
+
'coverage/coverage-final.json',
|
|
51
|
+
'coverage-final.json',
|
|
52
|
+
'.nyc_output/coverage-final.json',
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
if (await fs.pathExists(path.join(context.cwd, candidate))) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const reports = await globby(['**/lcov.info', '**/coverage-final.json'], {
|
|
60
|
+
cwd: context.cwd,
|
|
61
|
+
gitignore: true,
|
|
62
|
+
followSymbolicLinks: false,
|
|
63
|
+
onlyFiles: true,
|
|
64
|
+
deep: 8,
|
|
65
|
+
ignore: [...new Set([
|
|
66
|
+
...(context.ignore || []),
|
|
67
|
+
'**/node_modules/**',
|
|
68
|
+
'**/.git/**',
|
|
69
|
+
'**/dist/**',
|
|
70
|
+
'**/build/**',
|
|
71
|
+
'**/coverage/**',
|
|
72
|
+
'**/.next/**',
|
|
73
|
+
'**/out/**',
|
|
74
|
+
'**/target/**',
|
|
75
|
+
'**/vendor/**',
|
|
76
|
+
'**/.venv/**',
|
|
77
|
+
'**/venv/**',
|
|
78
|
+
])],
|
|
79
|
+
});
|
|
80
|
+
if (reports.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
reports.sort();
|
|
83
|
+
return reports[0];
|
|
84
|
+
}
|
|
85
|
+
async parseCoverage(reportPath) {
|
|
86
|
+
if (reportPath.endsWith('.json')) {
|
|
87
|
+
return this.parseCoverageFinalJson(reportPath);
|
|
88
|
+
}
|
|
89
|
+
return this.parseLcov(reportPath);
|
|
90
|
+
}
|
|
46
91
|
async parseLcov(reportPath) {
|
|
47
|
-
const content = await fs.readFile(reportPath, 'utf-8');
|
|
48
92
|
const results = {};
|
|
49
93
|
let currentFile = '';
|
|
50
|
-
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: fs.createReadStream(reportPath, { encoding: 'utf-8' }),
|
|
96
|
+
crlfDelay: Infinity,
|
|
97
|
+
});
|
|
98
|
+
for await (const line of rl) {
|
|
51
99
|
if (line.startsWith('SF:')) {
|
|
52
100
|
currentFile = line.substring(3);
|
|
53
101
|
results[currentFile] = { found: 0, hit: 0, isComplex: false };
|
|
54
102
|
}
|
|
55
103
|
else if (line.startsWith('LF:')) {
|
|
104
|
+
if (!currentFile)
|
|
105
|
+
continue;
|
|
56
106
|
const found = parseInt(line.substring(3));
|
|
57
107
|
results[currentFile].found = found;
|
|
58
108
|
// SME Logic: If a file has > 100 logical lines, it's considered "Complex"
|
|
@@ -61,9 +111,23 @@ export class CoverageGate extends Gate {
|
|
|
61
111
|
results[currentFile].isComplex = true;
|
|
62
112
|
}
|
|
63
113
|
else if (line.startsWith('LH:')) {
|
|
114
|
+
if (!currentFile)
|
|
115
|
+
continue;
|
|
64
116
|
results[currentFile].hit = parseInt(line.substring(3));
|
|
65
117
|
}
|
|
66
118
|
}
|
|
67
119
|
return results;
|
|
68
120
|
}
|
|
121
|
+
async parseCoverageFinalJson(reportPath) {
|
|
122
|
+
const raw = await fs.readJson(reportPath);
|
|
123
|
+
const results = {};
|
|
124
|
+
for (const [file, data] of Object.entries(raw || {})) {
|
|
125
|
+
const statements = data?.s || {};
|
|
126
|
+
const statementHits = Object.values(statements).map((count) => Number(count) || 0);
|
|
127
|
+
const found = statementHits.length;
|
|
128
|
+
const hit = statementHits.filter((count) => count > 0).length;
|
|
129
|
+
results[file] = { found, hit, isComplex: found > 100 };
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
69
133
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { CoverageGate } from './coverage.js';
|
|
6
|
+
describe('CoverageGate', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'coverage-gate-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('flags low coverage from lcov report', async () => {
|
|
15
|
+
fs.mkdirSync(path.join(testDir, 'coverage'), { recursive: true });
|
|
16
|
+
fs.writeFileSync(path.join(testDir, 'coverage', 'lcov.info'), [
|
|
17
|
+
'TN:',
|
|
18
|
+
'SF:src/alpha.ts',
|
|
19
|
+
'LF:10',
|
|
20
|
+
'LH:3',
|
|
21
|
+
'end_of_record',
|
|
22
|
+
].join('\n'), 'utf-8');
|
|
23
|
+
const gate = new CoverageGate({});
|
|
24
|
+
const failures = await gate.run({ cwd: testDir });
|
|
25
|
+
expect(failures).toHaveLength(1);
|
|
26
|
+
expect(failures[0].id).toBe('DYNAMIC_COVERAGE_LOW');
|
|
27
|
+
expect(failures[0].files).toEqual(['src/alpha.ts']);
|
|
28
|
+
});
|
|
29
|
+
it('parses coverage-final.json reports', async () => {
|
|
30
|
+
fs.writeFileSync(path.join(testDir, 'coverage-final.json'), JSON.stringify({
|
|
31
|
+
'src/beta.ts': {
|
|
32
|
+
s: { '1': 1, '2': 0, '3': 0, '4': 1 },
|
|
33
|
+
},
|
|
34
|
+
}), 'utf-8');
|
|
35
|
+
const gate = new CoverageGate({});
|
|
36
|
+
const failures = await gate.run({ cwd: testDir });
|
|
37
|
+
expect(failures).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
it('ignores coverage reports under node_modules', async () => {
|
|
40
|
+
fs.mkdirSync(path.join(testDir, 'node_modules', 'pkg', 'coverage'), { recursive: true });
|
|
41
|
+
fs.writeFileSync(path.join(testDir, 'node_modules', 'pkg', 'coverage', 'lcov.info'), ['TN:', 'SF:src/dep.ts', 'LF:10', 'LH:0', 'end_of_record'].join('\n'), 'utf-8');
|
|
42
|
+
const gate = new CoverageGate({});
|
|
43
|
+
const failures = await gate.run({ cwd: testDir });
|
|
44
|
+
expect(failures).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
it('respects context.ignore for coverage report discovery', async () => {
|
|
47
|
+
fs.mkdirSync(path.join(testDir, 'generated', 'reports'), { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(testDir, 'generated', 'reports', 'lcov.info'), ['TN:', 'SF:src/generated.ts', 'LF:10', 'LH:0', 'end_of_record'].join('\n'), 'utf-8');
|
|
49
|
+
const gate = new CoverageGate({});
|
|
50
|
+
const failures = await gate.run({ cwd: testDir, ignore: ['generated/**'] });
|
|
51
|
+
expect(failures).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/dist/gates/runner.js
CHANGED
|
@@ -340,13 +340,17 @@ export class GateRunner {
|
|
|
340
340
|
...(deepStats ? { deep: deepStats } : {}),
|
|
341
341
|
},
|
|
342
342
|
};
|
|
343
|
-
// Store findings + reinforce patterns in local SQLite
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
343
|
+
// Store findings + reinforce patterns in local SQLite before returning.
|
|
344
|
+
// CLI commands call process.exit(), so fire-and-forget writes can be dropped.
|
|
345
|
+
try {
|
|
346
|
+
await persistAndReinforce(cwd, report, deepStats ? {
|
|
347
|
+
deepTier: deepStats.tier,
|
|
348
|
+
deepModel: deepStats.model,
|
|
349
|
+
} : undefined);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
348
352
|
// Silent — local memory is advisory, never blocks scans
|
|
349
|
-
}
|
|
353
|
+
}
|
|
350
354
|
// v5: Record per-provenance data for adaptive thresholds + temporal drift
|
|
351
355
|
try {
|
|
352
356
|
const passedCount = Object.values(summary).filter(s => s === 'PASS').length;
|
|
@@ -355,6 +359,8 @@ export class GateRunner {
|
|
|
355
359
|
aiDriftFailures: provenanceCounts['ai-drift'],
|
|
356
360
|
structuralFailures: provenanceCounts['traditional'],
|
|
357
361
|
securityFailures: provenanceCounts['security'],
|
|
362
|
+
governanceFailures: provenanceCounts['governance'],
|
|
363
|
+
deepAnalysisFailures: provenanceCounts['deep-analysis'],
|
|
358
364
|
};
|
|
359
365
|
recordGateRun(cwd, passedCount, failedCount, failures.length, provenanceData);
|
|
360
366
|
}
|
|
@@ -44,6 +44,8 @@ export interface ProvenanceRunData {
|
|
|
44
44
|
aiDriftFailures: number;
|
|
45
45
|
structuralFailures: number;
|
|
46
46
|
securityFailures: number;
|
|
47
|
+
governanceFailures?: number;
|
|
48
|
+
deepAnalysisFailures?: number;
|
|
47
49
|
}
|
|
48
50
|
export interface ProvenanceTrends {
|
|
49
51
|
aiDrift: QualityTrend;
|
|
@@ -164,9 +164,9 @@ export function getProvenanceTrends(cwd) {
|
|
|
164
164
|
const recent = withProvenance.slice(-RECENT_WINDOW);
|
|
165
165
|
const baseline = withProvenance.slice(0, -RECENT_WINDOW);
|
|
166
166
|
const computeForField = (field) => {
|
|
167
|
-
const baselineValues = baseline.map(r => r.provenance[field]);
|
|
167
|
+
const baselineValues = baseline.map(r => r.provenance[field] ?? 0);
|
|
168
168
|
const { mean, std } = meanAndStd(baselineValues);
|
|
169
|
-
const recentAvg = recent.reduce((sum, r) => sum + r.provenance[field], 0) / recent.length;
|
|
169
|
+
const recentAvg = recent.reduce((sum, r) => sum + (r.provenance[field] ?? 0), 0) / recent.length;
|
|
170
170
|
const z = zScore(recentAvg, mean, std);
|
|
171
171
|
return { trend: trendFromZScore(z), z: Math.round(z * 100) / 100 };
|
|
172
172
|
};
|
|
@@ -97,18 +97,16 @@ export async function persistAndReinforce(cwd, report, meta) {
|
|
|
97
97
|
return;
|
|
98
98
|
const repoName = path.basename(cwd);
|
|
99
99
|
try {
|
|
100
|
-
await db
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await decayPatterns(tx, 30);
|
|
111
|
-
});
|
|
100
|
+
const scanId = await insertScan(db, repoName, report, meta);
|
|
101
|
+
if (report.failures.length > 0) {
|
|
102
|
+
await insertFindings(db, scanId, report.failures);
|
|
103
|
+
}
|
|
104
|
+
for (const f of report.failures) {
|
|
105
|
+
const category = f.category || f.id;
|
|
106
|
+
const source = f.source === 'llm' ? 'llm' : 'ast';
|
|
107
|
+
await reinforcePattern(db, repoName, category, `${f.title}: ${f.details?.substring(0, 120)}`, source);
|
|
108
|
+
}
|
|
109
|
+
await decayPatterns(db, 30);
|
|
112
110
|
Logger.info(`Local memory: stored ${report.failures.length} findings, ` +
|
|
113
111
|
`reinforced ${report.failures.length} patterns for ${repoName}`);
|
|
114
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.3",
|
|
4
4
|
"description": "Deterministic quality gate engine for AI-generated code. AST analysis, drift detection, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@xenova/transformers": "^2.17.2",
|
|
60
60
|
"sqlite3": "^5.1.7",
|
|
61
61
|
"openai": "^4.104.0",
|
|
62
|
-
"@rigour-labs/brain-darwin-arm64": "5.2.
|
|
63
|
-
"@rigour-labs/brain-darwin-x64": "5.2.
|
|
64
|
-
"@rigour-labs/brain-linux-arm64": "5.2.
|
|
65
|
-
"@rigour-labs/brain-
|
|
66
|
-
"@rigour-labs/brain-
|
|
62
|
+
"@rigour-labs/brain-darwin-arm64": "5.2.3",
|
|
63
|
+
"@rigour-labs/brain-darwin-x64": "5.2.3",
|
|
64
|
+
"@rigour-labs/brain-linux-arm64": "5.2.3",
|
|
65
|
+
"@rigour-labs/brain-win-x64": "5.2.3",
|
|
66
|
+
"@rigour-labs/brain-linux-x64": "5.2.3"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/fs-extra": "^11.0.4",
|