@rigour-labs/core 2.21.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +4 -2
- package/dist/gates/base.js +5 -1
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/content.js +1 -1
- package/dist/gates/context-window-artifacts.d.ts +34 -0
- package/dist/gates/context-window-artifacts.js +214 -0
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +4 -3
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +33 -0
- package/dist/gates/duplication-drift.js +190 -0
- package/dist/gates/environment.js +4 -4
- package/dist/gates/file.js +1 -1
- package/dist/gates/hallucinated-imports.d.ts +63 -0
- package/dist/gates/hallucinated-imports.js +406 -0
- package/dist/gates/inconsistent-error-handling.d.ts +39 -0
- package/dist/gates/inconsistent-error-handling.js +236 -0
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +62 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns.d.ts +2 -1
- package/dist/gates/security-patterns.js +2 -1
- package/dist/gates/structure.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +195 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +430 -0
- package/dist/types/index.js +57 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -27
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -50
- package/src/gates/context.ts +0 -267
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -42
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -156
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -305
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -312
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -159
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @rigour-labs/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rigour-labs/core)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**Deterministic quality gate engine for AI-generated code.**
|
|
7
|
+
|
|
8
|
+
The core library powering [Rigour](https://rigour.run) — AST analysis, AI drift detection, security scanning, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET.
|
|
9
|
+
|
|
10
|
+
> This package is the engine. For the CLI, use [`@rigour-labs/cli`](https://www.npmjs.com/package/@rigour-labs/cli). For MCP integration, use [`@rigour-labs/mcp`](https://www.npmjs.com/package/@rigour-labs/mcp).
|
|
11
|
+
|
|
12
|
+
## What's Inside
|
|
13
|
+
|
|
14
|
+
### 23 Quality Gates
|
|
15
|
+
|
|
16
|
+
**Structural:** File size, cyclomatic complexity, method count, parameter count, nesting depth, required docs, content hygiene.
|
|
17
|
+
|
|
18
|
+
**Security:** Hardcoded secrets, SQL injection, XSS, command injection, path traversal.
|
|
19
|
+
|
|
20
|
+
**AI-Native Drift Detection:** Duplication drift, hallucinated imports, inconsistent error handling, context window artifacts, async & error safety (promise safety).
|
|
21
|
+
|
|
22
|
+
**Agent Governance:** Multi-agent scope isolation, checkpoint supervision, context drift, retry loop breaker.
|
|
23
|
+
|
|
24
|
+
### Multi-Language Support
|
|
25
|
+
|
|
26
|
+
All gates support: TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET.
|
|
27
|
+
|
|
28
|
+
### Two-Score System
|
|
29
|
+
|
|
30
|
+
Every failure carries a **provenance tag** (`ai-drift`, `traditional`, `security`, `governance`) and contributes to two sub-scores:
|
|
31
|
+
|
|
32
|
+
- **AI Health Score** (0–100) — AI-specific failures
|
|
33
|
+
- **Structural Score** (0–100) — Traditional code quality
|
|
34
|
+
|
|
35
|
+
### Fix Packets (v2)
|
|
36
|
+
|
|
37
|
+
Machine-readable JSON diagnostics with severity, provenance, file, line number, and step-by-step remediation instructions that AI agents can consume directly.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { GateRunner } from '@rigour-labs/core';
|
|
43
|
+
|
|
44
|
+
const runner = new GateRunner(config, projectRoot);
|
|
45
|
+
const report = await runner.run();
|
|
46
|
+
|
|
47
|
+
console.log(report.pass); // true or false
|
|
48
|
+
console.log(report.score); // 0-100
|
|
49
|
+
console.log(report.failures); // Failure[]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
**[Full docs at docs.rigour.run](https://docs.rigour.run)**
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT © [Rigour Labs](https://github.com/rigour-labs)
|
package/dist/context.test.js
CHANGED
|
@@ -2,9 +2,8 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
|
2
2
|
import { GateRunner } from '../src/gates/runner.js';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import
|
|
6
|
-
const
|
|
7
|
-
const TEST_CWD = path.join(__dirname, '../temp-test-context');
|
|
5
|
+
import os from 'os';
|
|
6
|
+
const TEST_CWD = path.join(os.tmpdir(), 'rigour-temp-test-context-' + process.pid);
|
|
8
7
|
describe('Context Awareness Engine', () => {
|
|
9
8
|
beforeAll(async () => {
|
|
10
9
|
await fs.ensureDir(TEST_CWD);
|
package/dist/environment.test.js
CHANGED
|
@@ -3,8 +3,9 @@ import { GateRunner } from './gates/runner.js';
|
|
|
3
3
|
import { ConfigSchema } from './types/index.js';
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
describe('Environment Alignment Gate', () => {
|
|
7
|
-
const testDir = path.join(
|
|
8
|
+
const testDir = path.join(os.tmpdir(), 'rigour-temp-test-env-' + process.pid);
|
|
8
9
|
beforeEach(async () => {
|
|
9
10
|
await fs.ensureDir(testDir);
|
|
10
11
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @since v2.14.0
|
|
13
13
|
*/
|
|
14
14
|
import { Gate, GateContext } from './base.js';
|
|
15
|
-
import { Failure } from '../types/index.js';
|
|
15
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
16
16
|
export interface AgentRegistration {
|
|
17
17
|
agentId: string;
|
|
18
18
|
taskScope: string[];
|
|
@@ -46,5 +46,6 @@ export declare function clearSession(cwd: string): void;
|
|
|
46
46
|
export declare class AgentTeamGate extends Gate {
|
|
47
47
|
private config;
|
|
48
48
|
constructor(config?: AgentTeamConfig);
|
|
49
|
+
protected get provenance(): Provenance;
|
|
49
50
|
run(context: GateContext): Promise<Failure[]>;
|
|
50
51
|
}
|
package/dist/gates/agent-team.js
CHANGED
package/dist/gates/base.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GoldenRecord } from '../services/context-engine.js';
|
|
2
|
-
import { Failure } from '../types/index.js';
|
|
2
|
+
import { Failure, Severity, Provenance } from '../types/index.js';
|
|
3
3
|
export interface GateContext {
|
|
4
4
|
cwd: string;
|
|
5
5
|
record?: GoldenRecord;
|
|
@@ -11,5 +11,7 @@ export declare abstract class Gate {
|
|
|
11
11
|
readonly title: string;
|
|
12
12
|
constructor(id: string, title: string);
|
|
13
13
|
abstract run(context: GateContext): Promise<Failure[]>;
|
|
14
|
-
|
|
14
|
+
/** Default provenance for this gate — subclasses override */
|
|
15
|
+
protected get provenance(): Provenance;
|
|
16
|
+
protected createFailure(details: string, files?: string[], hint?: string, title?: string, line?: number, endLine?: number, severity?: Severity): Failure;
|
|
15
17
|
}
|
package/dist/gates/base.js
CHANGED
|
@@ -5,11 +5,15 @@ export class Gate {
|
|
|
5
5
|
this.id = id;
|
|
6
6
|
this.title = title;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/** Default provenance for this gate — subclasses override */
|
|
9
|
+
get provenance() { return 'traditional'; }
|
|
10
|
+
createFailure(details, files, hint, title, line, endLine, severity) {
|
|
9
11
|
return {
|
|
10
12
|
id: this.id,
|
|
11
13
|
title: title || this.title,
|
|
12
14
|
details,
|
|
15
|
+
severity: severity || 'medium',
|
|
16
|
+
provenance: this.provenance,
|
|
13
17
|
files,
|
|
14
18
|
line,
|
|
15
19
|
endLine,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @since v2.14.0
|
|
14
14
|
*/
|
|
15
15
|
import { Gate, GateContext } from './base.js';
|
|
16
|
-
import { Failure } from '../types/index.js';
|
|
16
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
17
17
|
export interface CheckpointEntry {
|
|
18
18
|
checkpointId: string;
|
|
19
19
|
timestamp: Date;
|
|
@@ -68,5 +68,6 @@ export declare function clearCheckpointSession(cwd: string): void;
|
|
|
68
68
|
export declare class CheckpointGate extends Gate {
|
|
69
69
|
private config;
|
|
70
70
|
constructor(config?: CheckpointConfig);
|
|
71
|
+
protected get provenance(): Provenance;
|
|
71
72
|
run(context: GateContext): Promise<Failure[]>;
|
|
72
73
|
}
|
package/dist/gates/checkpoint.js
CHANGED
|
@@ -198,6 +198,7 @@ export class CheckpointGate extends Gate {
|
|
|
198
198
|
auto_save_on_failure: config.auto_save_on_failure ?? true,
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
+
get provenance() { return 'governance'; }
|
|
201
202
|
async run(context) {
|
|
202
203
|
if (!this.config.enabled) {
|
|
203
204
|
return [];
|
|
@@ -217,13 +218,13 @@ export class CheckpointGate extends Gate {
|
|
|
217
218
|
// Check 2: Quality threshold
|
|
218
219
|
const lastCheckpoint = session.checkpoints[session.checkpoints.length - 1];
|
|
219
220
|
if (lastCheckpoint.qualityScore < (this.config.quality_threshold ?? 80)) {
|
|
220
|
-
failures.push(this.createFailure(`Quality score ${lastCheckpoint.qualityScore}% is below threshold ${this.config.quality_threshold}%`, lastCheckpoint.filesChanged, 'Review recent changes and address quality issues before continuing', 'Quality Below Threshold'));
|
|
221
|
+
failures.push(this.createFailure(`Quality score ${lastCheckpoint.qualityScore}% is below threshold ${this.config.quality_threshold}%`, lastCheckpoint.filesChanged, 'Review recent changes and address quality issues before continuing', 'Quality Below Threshold', undefined, undefined, 'high'));
|
|
221
222
|
}
|
|
222
223
|
// Check 3: Drift detection
|
|
223
224
|
if (this.config.drift_detection) {
|
|
224
225
|
const { hasDrift, trend } = detectDrift(session.checkpoints);
|
|
225
226
|
if (hasDrift && trend === 'degrading') {
|
|
226
|
-
failures.push(this.createFailure(`Quality drift detected: scores are degrading over time`, undefined, 'Agent performance is declining. Consider pausing and reviewing recent work.', 'Quality Drift Detected'));
|
|
227
|
+
failures.push(this.createFailure(`Quality drift detected: scores are degrading over time`, undefined, 'Agent performance is declining. Consider pausing and reviewing recent work.', 'Quality Drift Detected', undefined, undefined, 'high'));
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
return failures;
|
package/dist/gates/content.js
CHANGED
|
@@ -26,7 +26,7 @@ export class ContentGate extends Gate {
|
|
|
26
26
|
lines.forEach((line, index) => {
|
|
27
27
|
for (const pattern of patterns) {
|
|
28
28
|
if (pattern.test(line)) {
|
|
29
|
-
failures.push(this.createFailure(`Forbidden placeholder '${pattern.source}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1));
|
|
29
|
+
failures.push(this.createFailure(`Forbidden placeholder '${pattern.source}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1, 'info'));
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Artifacts Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects quality degradation patterns within a single file that emerge
|
|
5
|
+
* when AI loses context mid-generation. The telltale sign: clean,
|
|
6
|
+
* well-structured code at the top of a file that gradually degrades
|
|
7
|
+
* toward the bottom.
|
|
8
|
+
*
|
|
9
|
+
* Detection signals:
|
|
10
|
+
* 1. Comment density drops sharply (top half vs bottom half)
|
|
11
|
+
* 2. Function complexity increases toward end of file
|
|
12
|
+
* 3. Variable naming quality degrades (shorter names, more single-letter vars)
|
|
13
|
+
* 4. Error handling becomes sparser toward the bottom
|
|
14
|
+
* 5. Code style inconsistencies emerge (indentation, spacing)
|
|
15
|
+
*
|
|
16
|
+
* @since v2.16.0
|
|
17
|
+
*/
|
|
18
|
+
import { Gate, GateContext } from './base.js';
|
|
19
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
20
|
+
export interface ContextWindowArtifactsConfig {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
min_file_lines?: number;
|
|
23
|
+
degradation_threshold?: number;
|
|
24
|
+
signals_required?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class ContextWindowArtifactsGate extends Gate {
|
|
27
|
+
private config;
|
|
28
|
+
constructor(config?: ContextWindowArtifactsConfig);
|
|
29
|
+
protected get provenance(): Provenance;
|
|
30
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
31
|
+
private analyzeFile;
|
|
32
|
+
private measureHalf;
|
|
33
|
+
private measureFunctionLengths;
|
|
34
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Artifacts Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects quality degradation patterns within a single file that emerge
|
|
5
|
+
* when AI loses context mid-generation. The telltale sign: clean,
|
|
6
|
+
* well-structured code at the top of a file that gradually degrades
|
|
7
|
+
* toward the bottom.
|
|
8
|
+
*
|
|
9
|
+
* Detection signals:
|
|
10
|
+
* 1. Comment density drops sharply (top half vs bottom half)
|
|
11
|
+
* 2. Function complexity increases toward end of file
|
|
12
|
+
* 3. Variable naming quality degrades (shorter names, more single-letter vars)
|
|
13
|
+
* 4. Error handling becomes sparser toward the bottom
|
|
14
|
+
* 5. Code style inconsistencies emerge (indentation, spacing)
|
|
15
|
+
*
|
|
16
|
+
* @since v2.16.0
|
|
17
|
+
*/
|
|
18
|
+
import { Gate } from './base.js';
|
|
19
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
20
|
+
import { Logger } from '../utils/logger.js';
|
|
21
|
+
import fs from 'fs-extra';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
export class ContextWindowArtifactsGate extends Gate {
|
|
24
|
+
config;
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super('context-window-artifacts', 'Context Window Artifact Detection');
|
|
27
|
+
this.config = {
|
|
28
|
+
enabled: config.enabled ?? true,
|
|
29
|
+
min_file_lines: config.min_file_lines ?? 100,
|
|
30
|
+
degradation_threshold: config.degradation_threshold ?? 0.4,
|
|
31
|
+
signals_required: config.signals_required ?? 2,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
get provenance() { return 'ai-drift'; }
|
|
35
|
+
async run(context) {
|
|
36
|
+
if (!this.config.enabled)
|
|
37
|
+
return [];
|
|
38
|
+
const failures = [];
|
|
39
|
+
const files = await FileScanner.findFiles({
|
|
40
|
+
cwd: context.cwd,
|
|
41
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
42
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*'],
|
|
43
|
+
});
|
|
44
|
+
Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
if (lines.length < this.config.min_file_lines)
|
|
50
|
+
continue;
|
|
51
|
+
const metrics = this.analyzeFile(content, file);
|
|
52
|
+
if (metrics && metrics.signals.length >= this.config.signals_required &&
|
|
53
|
+
metrics.degradationScore >= this.config.degradation_threshold) {
|
|
54
|
+
const signalList = metrics.signals.map(s => ` • ${s}`).join('\n');
|
|
55
|
+
const midpoint = Math.floor(metrics.totalLines / 2);
|
|
56
|
+
failures.push(this.createFailure(`Context window artifact detected in ${file} (${metrics.totalLines} lines, degradation: ${(metrics.degradationScore * 100).toFixed(0)}%):\n${signalList}`, [file], `This file shows quality degradation from top to bottom, a pattern typical of AI context window exhaustion. Consider refactoring the bottom half or splitting the file. The quality drop begins around line ${midpoint}.`, 'Context Window Artifacts', midpoint, undefined, 'high'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (e) { }
|
|
60
|
+
}
|
|
61
|
+
return failures;
|
|
62
|
+
}
|
|
63
|
+
analyzeFile(content, file) {
|
|
64
|
+
const lines = content.split('\n');
|
|
65
|
+
const midpoint = Math.floor(lines.length / 2);
|
|
66
|
+
const topContent = lines.slice(0, midpoint).join('\n');
|
|
67
|
+
const bottomContent = lines.slice(midpoint).join('\n');
|
|
68
|
+
const topMetrics = this.measureHalf(topContent);
|
|
69
|
+
const bottomMetrics = this.measureHalf(bottomContent);
|
|
70
|
+
const signals = [];
|
|
71
|
+
let degradationScore = 0;
|
|
72
|
+
// Signal 1: Comment density drops (use threshold to avoid tiny-denominator noise)
|
|
73
|
+
if (topMetrics.commentDensity > 0.01) {
|
|
74
|
+
const commentRatio = bottomMetrics.commentDensity / topMetrics.commentDensity;
|
|
75
|
+
if (commentRatio < 0.5) {
|
|
76
|
+
signals.push(`Comment density drops ${((1 - commentRatio) * 100).toFixed(0)}% in bottom half`);
|
|
77
|
+
degradationScore += 0.25;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Signal 2: Function length increases
|
|
81
|
+
if (topMetrics.avgFunctionLength > 0 && bottomMetrics.avgFunctionLength > 0) {
|
|
82
|
+
const lengthRatio = bottomMetrics.avgFunctionLength / topMetrics.avgFunctionLength;
|
|
83
|
+
if (lengthRatio > 1.5) {
|
|
84
|
+
signals.push(`Average function length ${lengthRatio.toFixed(1)}x longer in bottom half`);
|
|
85
|
+
degradationScore += 0.2;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Signal 3: Variable naming quality degrades
|
|
89
|
+
if (bottomMetrics.singleCharVarCount > topMetrics.singleCharVarCount * 2 &&
|
|
90
|
+
bottomMetrics.singleCharVarCount >= 3) {
|
|
91
|
+
signals.push(`${bottomMetrics.singleCharVarCount} single-char variables in bottom half vs ${topMetrics.singleCharVarCount} in top`);
|
|
92
|
+
degradationScore += 0.2;
|
|
93
|
+
}
|
|
94
|
+
// Signal 3b: Average identifier length shrinks
|
|
95
|
+
if (topMetrics.avgIdentifierLength > 0 && bottomMetrics.avgIdentifierLength > 0) {
|
|
96
|
+
const nameRatio = bottomMetrics.avgIdentifierLength / topMetrics.avgIdentifierLength;
|
|
97
|
+
if (nameRatio < 0.7) {
|
|
98
|
+
signals.push(`Identifier names ${((1 - nameRatio) * 100).toFixed(0)}% shorter in bottom half`);
|
|
99
|
+
degradationScore += 0.15;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Signal 4: Error handling becomes sparser
|
|
103
|
+
if (topMetrics.errorHandlingDensity > 0) {
|
|
104
|
+
const errorRatio = bottomMetrics.errorHandlingDensity / topMetrics.errorHandlingDensity;
|
|
105
|
+
if (errorRatio < 0.3) {
|
|
106
|
+
signals.push(`Error handling ${((1 - errorRatio) * 100).toFixed(0)}% less frequent in bottom half`);
|
|
107
|
+
degradationScore += 0.2;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Signal 5: Empty blocks increase
|
|
111
|
+
if (bottomMetrics.emptyBlockCount > topMetrics.emptyBlockCount + 2) {
|
|
112
|
+
signals.push(`${bottomMetrics.emptyBlockCount} empty blocks in bottom half vs ${topMetrics.emptyBlockCount} in top`);
|
|
113
|
+
degradationScore += 0.15;
|
|
114
|
+
}
|
|
115
|
+
// Signal 6: TODO/FIXME/HACK density increases at bottom
|
|
116
|
+
if (bottomMetrics.todoCount > topMetrics.todoCount + 1) {
|
|
117
|
+
signals.push(`${bottomMetrics.todoCount} TODO/FIXME/HACK in bottom half vs ${topMetrics.todoCount} in top`);
|
|
118
|
+
degradationScore += 0.1;
|
|
119
|
+
}
|
|
120
|
+
// Cap at 1.0
|
|
121
|
+
degradationScore = Math.min(1.0, degradationScore);
|
|
122
|
+
return {
|
|
123
|
+
file,
|
|
124
|
+
totalLines: lines.length,
|
|
125
|
+
topHalf: topMetrics,
|
|
126
|
+
bottomHalf: bottomMetrics,
|
|
127
|
+
degradationScore,
|
|
128
|
+
signals,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
measureHalf(content) {
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
const codeLines = lines.filter(l => l.trim() && !l.trim().startsWith('//') && !l.trim().startsWith('#') && !l.trim().startsWith('*'));
|
|
134
|
+
// Only count inline comments (//), not JSDoc/block comments (/** ... */ or * ...)
|
|
135
|
+
// JSDoc tends to cluster at file top, skewing "degradation" unfairly
|
|
136
|
+
const commentLines = lines.filter(l => {
|
|
137
|
+
const trimmed = l.trim();
|
|
138
|
+
return trimmed.startsWith('//') || trimmed.startsWith('#');
|
|
139
|
+
});
|
|
140
|
+
// Comment density
|
|
141
|
+
const commentDensity = codeLines.length > 0 ? commentLines.length / codeLines.length : 0;
|
|
142
|
+
// Function lengths
|
|
143
|
+
const funcLengths = this.measureFunctionLengths(content);
|
|
144
|
+
const avgFunctionLength = funcLengths.length > 0
|
|
145
|
+
? funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length
|
|
146
|
+
: 0;
|
|
147
|
+
// Single-char variables (excluding common loop vars i, j, k in for loops)
|
|
148
|
+
const singleCharMatches = content.match(/\b(?:const|let|var)\s+([a-z])\b/g) || [];
|
|
149
|
+
const singleCharVarCount = singleCharMatches.length;
|
|
150
|
+
// Error handling density
|
|
151
|
+
const tryCount = (content.match(/\btry\s*\{/g) || []).length;
|
|
152
|
+
const funcCount = Math.max(1, funcLengths.length);
|
|
153
|
+
const errorHandlingDensity = tryCount / funcCount;
|
|
154
|
+
// Empty blocks
|
|
155
|
+
const emptyBlockCount = (content.match(/\{\s*\}/g) || []).length;
|
|
156
|
+
// TODO/FIXME/HACK count
|
|
157
|
+
const todoCount = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/gi) || []).length;
|
|
158
|
+
// Average identifier length
|
|
159
|
+
const identifiers = content.match(/\b(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g) || [];
|
|
160
|
+
const identNames = identifiers.map(m => {
|
|
161
|
+
const parts = m.split(/\s+/);
|
|
162
|
+
return parts[parts.length - 1];
|
|
163
|
+
});
|
|
164
|
+
const avgIdentifierLength = identNames.length > 0
|
|
165
|
+
? identNames.reduce((sum, n) => sum + n.length, 0) / identNames.length
|
|
166
|
+
: 0;
|
|
167
|
+
return {
|
|
168
|
+
commentDensity,
|
|
169
|
+
avgFunctionLength,
|
|
170
|
+
singleCharVarCount,
|
|
171
|
+
errorHandlingDensity,
|
|
172
|
+
emptyBlockCount,
|
|
173
|
+
todoCount,
|
|
174
|
+
avgIdentifierLength,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
measureFunctionLengths(content) {
|
|
178
|
+
const lines = content.split('\n');
|
|
179
|
+
const lengths = [];
|
|
180
|
+
const funcStarts = [
|
|
181
|
+
/^(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
182
|
+
/^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/,
|
|
183
|
+
/^\s+(?:async\s+)?\w+\s*\([^)]*\)\s*\{/,
|
|
184
|
+
];
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
for (const pattern of funcStarts) {
|
|
187
|
+
if (pattern.test(lines[i])) {
|
|
188
|
+
// Count function body length
|
|
189
|
+
let braceDepth = 0;
|
|
190
|
+
let started = false;
|
|
191
|
+
let bodyLines = 0;
|
|
192
|
+
for (let j = i; j < lines.length; j++) {
|
|
193
|
+
for (const ch of lines[j]) {
|
|
194
|
+
if (ch === '{') {
|
|
195
|
+
braceDepth++;
|
|
196
|
+
started = true;
|
|
197
|
+
}
|
|
198
|
+
if (ch === '}')
|
|
199
|
+
braceDepth--;
|
|
200
|
+
}
|
|
201
|
+
if (started)
|
|
202
|
+
bodyLines++;
|
|
203
|
+
if (started && braceDepth === 0)
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
if (bodyLines > 0)
|
|
207
|
+
lengths.push(bodyLines);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return lengths;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/gates/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure, Gates } from '../types/index.js';
|
|
2
|
+
import { Failure, Gates, Provenance } from '../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Extended Context Configuration (v2.14+)
|
|
5
5
|
* For 1M token frontier models like Opus 4.6
|
|
@@ -17,6 +17,7 @@ export declare class ContextGate extends Gate {
|
|
|
17
17
|
private config;
|
|
18
18
|
private extendedConfig;
|
|
19
19
|
constructor(config: Gates);
|
|
20
|
+
protected get provenance(): Provenance;
|
|
20
21
|
run(context: GateContext): Promise<Failure[]>;
|
|
21
22
|
private checkEnvDrift;
|
|
22
23
|
/**
|
package/dist/gates/context.js
CHANGED
|
@@ -18,6 +18,7 @@ export class ContextGate extends Gate {
|
|
|
18
18
|
max_cross_file_depth: 50,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
get provenance() { return 'ai-drift'; }
|
|
21
22
|
async run(context) {
|
|
22
23
|
const failures = [];
|
|
23
24
|
const record = context.record;
|
|
@@ -61,7 +62,7 @@ export class ContextGate extends Gate {
|
|
|
61
62
|
// it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
|
|
62
63
|
if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
|
|
63
64
|
const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
|
|
64
|
-
failures.push(this.createFailure(`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`, [file], `The project already uses '${anchor.id}' as a standard anchor. Avoid inventing variations like '${deviation}'. Reuse the existing anchor or align with established project patterns
|
|
65
|
+
failures.push(this.createFailure(`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`, [file], `The project already uses '${anchor.id}' as a standard anchor. Avoid inventing variations like '${deviation}'. Reuse the existing anchor or align with established project patterns.`, undefined, undefined, undefined, 'high'));
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -140,7 +141,7 @@ export class ContextGate extends Gate {
|
|
|
140
141
|
if (casing !== dominant && count > threshold) {
|
|
141
142
|
const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
|
|
142
143
|
const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
|
|
143
|
-
failures.push(this.createFailure(`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`, uniqueFiles, `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`, 'Naming Convention Drift'));
|
|
144
|
+
failures.push(this.createFailure(`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`, uniqueFiles, `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`, 'Naming Convention Drift', undefined, undefined, 'high'));
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -175,7 +176,7 @@ export class ContextGate extends Gate {
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
if (mixedFiles.length > 3) {
|
|
178
|
-
failures.push(this.createFailure(`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`, mixedFiles.slice(0, 5), 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.', 'Import Pattern Drift'));
|
|
179
|
+
failures.push(this.createFailure(`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`, mixedFiles.slice(0, 5), 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.', 'Import Pattern Drift', undefined, undefined, 'high'));
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
/**
|
package/dist/gates/coverage.js
CHANGED
|
@@ -35,7 +35,9 @@ export class CoverageGate extends Gate {
|
|
|
35
35
|
title: `Low coverage for high-risk file: ${file}`,
|
|
36
36
|
details: `Current coverage: ${coverage.toFixed(2)}%. Required: ${threshold}% due to structural risk.`,
|
|
37
37
|
files: [file],
|
|
38
|
-
hint: `Add dynamic tests to cover complex logical branches in this file
|
|
38
|
+
hint: `Add dynamic tests to cover complex logical branches in this file.`,
|
|
39
|
+
severity: 'medium',
|
|
40
|
+
provenance: 'traditional'
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
}
|
package/dist/gates/dependency.js
CHANGED
|
@@ -25,7 +25,7 @@ export class DependencyGate extends Gate {
|
|
|
25
25
|
};
|
|
26
26
|
for (const dep of forbidden) {
|
|
27
27
|
if (allDeps[dep]) {
|
|
28
|
-
failures.push(this.createFailure(`The package '${dep}' is forbidden by project standards.`, ['package.json'], `Remove '${dep}' from package.json and use approved alternatives.`, 'Forbidden Dependency'));
|
|
28
|
+
failures.push(this.createFailure(`The package '${dep}' is forbidden by project standards.`, ['package.json'], `Remove '${dep}' from package.json and use approved alternatives.`, 'Forbidden Dependency', undefined, undefined, 'medium'));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -37,7 +37,7 @@ export class DependencyGate extends Gate {
|
|
|
37
37
|
const content = await fs.readFile(reqPath, 'utf-8');
|
|
38
38
|
for (const dep of forbidden) {
|
|
39
39
|
if (new RegExp(`^${dep}([=<>! ]|$)`, 'm').test(content)) {
|
|
40
|
-
failures.push(this.createFailure(`The Python package '${dep}' is forbidden.`, ['requirements.txt'], `Remove '${dep}' from requirements.txt.`, 'Forbidden Dependency'));
|
|
40
|
+
failures.push(this.createFailure(`The Python package '${dep}' is forbidden.`, ['requirements.txt'], `Remove '${dep}' from requirements.txt.`, 'Forbidden Dependency', undefined, undefined, 'medium'));
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -46,7 +46,7 @@ export class DependencyGate extends Gate {
|
|
|
46
46
|
const content = await fs.readFile(pyprojPath, 'utf-8');
|
|
47
47
|
for (const dep of forbidden) {
|
|
48
48
|
if (new RegExp(`^${dep}\\s*=`, 'm').test(content)) {
|
|
49
|
-
failures.push(this.createFailure(`The Python package '${dep}' is forbidden in pyproject.toml.`, ['pyproject.toml'], `Remove '${dep}' from pyproject.toml dependencies.`, 'Forbidden Dependency'));
|
|
49
|
+
failures.push(this.createFailure(`The Python package '${dep}' is forbidden in pyproject.toml.`, ['pyproject.toml'], `Remove '${dep}' from pyproject.toml dependencies.`, 'Forbidden Dependency', undefined, undefined, 'medium'));
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -56,7 +56,7 @@ export class DependencyGate extends Gate {
|
|
|
56
56
|
const content = await fs.readFile(goModPath, 'utf-8');
|
|
57
57
|
for (const dep of forbidden) {
|
|
58
58
|
if (content.includes(dep)) {
|
|
59
|
-
failures.push(this.createFailure(`The Go module '${dep}' is forbidden.`, ['go.mod'], `Remove '${dep}' from go.mod.`, 'Forbidden Dependency'));
|
|
59
|
+
failures.push(this.createFailure(`The Go module '${dep}' is forbidden.`, ['go.mod'], `Remove '${dep}' from go.mod.`, 'Forbidden Dependency', undefined, undefined, 'medium'));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -66,7 +66,7 @@ export class DependencyGate extends Gate {
|
|
|
66
66
|
const content = await fs.readFile(pomPath, 'utf-8');
|
|
67
67
|
for (const dep of forbidden) {
|
|
68
68
|
if (content.includes(`<artifactId>${dep}</artifactId>`)) {
|
|
69
|
-
failures.push(this.createFailure(`The Java artifact '${dep}' is forbidden.`, ['pom.xml'], `Remove '${dep}' from pom.xml.`, 'Forbidden Dependency'));
|
|
69
|
+
failures.push(this.createFailure(`The Java artifact '${dep}' is forbidden.`, ['pom.xml'], `Remove '${dep}' from pom.xml.`, 'Forbidden Dependency', undefined, undefined, 'medium'));
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplication Drift Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects when AI generates near-identical functions across files because
|
|
5
|
+
* it doesn't remember what it already wrote. This is an AI-specific failure
|
|
6
|
+
* mode — humans reuse via copy-paste (same file), AI re-invents (cross-file).
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Extract all function bodies (normalized: strip whitespace, comments)
|
|
10
|
+
* 2. Compare function signatures + body hashes across files
|
|
11
|
+
* 3. Flag functions with >80% similarity in different files
|
|
12
|
+
*
|
|
13
|
+
* @since v2.16.0
|
|
14
|
+
*/
|
|
15
|
+
import { Gate, GateContext } from './base.js';
|
|
16
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
17
|
+
export interface DuplicationDriftConfig {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
similarity_threshold?: number;
|
|
20
|
+
min_body_lines?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class DuplicationDriftGate extends Gate {
|
|
23
|
+
private config;
|
|
24
|
+
constructor(config?: DuplicationDriftConfig);
|
|
25
|
+
protected get provenance(): Provenance;
|
|
26
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
27
|
+
private extractJSFunctions;
|
|
28
|
+
private extractPyFunctions;
|
|
29
|
+
private extractFunctionBody;
|
|
30
|
+
private normalizeBody;
|
|
31
|
+
private hash;
|
|
32
|
+
private findDuplicateGroups;
|
|
33
|
+
}
|