@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.
Files changed (101) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +4 -2
  7. package/dist/gates/base.js +5 -1
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/content.js +1 -1
  11. package/dist/gates/context-window-artifacts.d.ts +34 -0
  12. package/dist/gates/context-window-artifacts.js +214 -0
  13. package/dist/gates/context.d.ts +2 -1
  14. package/dist/gates/context.js +4 -3
  15. package/dist/gates/coverage.js +3 -1
  16. package/dist/gates/dependency.js +5 -5
  17. package/dist/gates/duplication-drift.d.ts +33 -0
  18. package/dist/gates/duplication-drift.js +190 -0
  19. package/dist/gates/environment.js +4 -4
  20. package/dist/gates/file.js +1 -1
  21. package/dist/gates/hallucinated-imports.d.ts +63 -0
  22. package/dist/gates/hallucinated-imports.js +406 -0
  23. package/dist/gates/inconsistent-error-handling.d.ts +39 -0
  24. package/dist/gates/inconsistent-error-handling.js +236 -0
  25. package/dist/gates/promise-safety.d.ts +68 -0
  26. package/dist/gates/promise-safety.js +509 -0
  27. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  28. package/dist/gates/retry-loop-breaker.js +2 -1
  29. package/dist/gates/runner.js +62 -1
  30. package/dist/gates/safety.d.ts +2 -1
  31. package/dist/gates/safety.js +2 -1
  32. package/dist/gates/security-patterns.d.ts +2 -1
  33. package/dist/gates/security-patterns.js +2 -1
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/services/fix-packet-service.d.ts +0 -1
  38. package/dist/services/fix-packet-service.js +9 -14
  39. package/dist/services/score-history.d.ts +54 -0
  40. package/dist/services/score-history.js +122 -0
  41. package/dist/templates/index.js +195 -0
  42. package/dist/types/fix-packet.d.ts +5 -5
  43. package/dist/types/fix-packet.js +1 -1
  44. package/dist/types/index.d.ts +430 -0
  45. package/dist/types/index.js +57 -0
  46. package/package.json +21 -1
  47. package/src/context.test.ts +0 -256
  48. package/src/discovery.test.ts +0 -88
  49. package/src/discovery.ts +0 -112
  50. package/src/environment.test.ts +0 -115
  51. package/src/gates/agent-team.test.ts +0 -134
  52. package/src/gates/agent-team.ts +0 -210
  53. package/src/gates/ast-handlers/base.ts +0 -13
  54. package/src/gates/ast-handlers/python.ts +0 -145
  55. package/src/gates/ast-handlers/python_parser.py +0 -181
  56. package/src/gates/ast-handlers/typescript.ts +0 -264
  57. package/src/gates/ast-handlers/universal.ts +0 -184
  58. package/src/gates/ast.ts +0 -54
  59. package/src/gates/base.ts +0 -27
  60. package/src/gates/checkpoint.test.ts +0 -135
  61. package/src/gates/checkpoint.ts +0 -311
  62. package/src/gates/content.ts +0 -50
  63. package/src/gates/context.ts +0 -267
  64. package/src/gates/coverage.ts +0 -74
  65. package/src/gates/dependency.ts +0 -108
  66. package/src/gates/environment.ts +0 -94
  67. package/src/gates/file.ts +0 -42
  68. package/src/gates/retry-loop-breaker.ts +0 -151
  69. package/src/gates/runner.ts +0 -156
  70. package/src/gates/safety.ts +0 -56
  71. package/src/gates/security-patterns.test.ts +0 -162
  72. package/src/gates/security-patterns.ts +0 -305
  73. package/src/gates/structure.ts +0 -36
  74. package/src/index.ts +0 -13
  75. package/src/pattern-index/embeddings.ts +0 -84
  76. package/src/pattern-index/index.ts +0 -59
  77. package/src/pattern-index/indexer.test.ts +0 -276
  78. package/src/pattern-index/indexer.ts +0 -1023
  79. package/src/pattern-index/matcher.test.ts +0 -293
  80. package/src/pattern-index/matcher.ts +0 -493
  81. package/src/pattern-index/overrides.ts +0 -235
  82. package/src/pattern-index/security.ts +0 -151
  83. package/src/pattern-index/staleness.test.ts +0 -313
  84. package/src/pattern-index/staleness.ts +0 -568
  85. package/src/pattern-index/types.ts +0 -339
  86. package/src/safety.test.ts +0 -53
  87. package/src/services/adaptive-thresholds.test.ts +0 -189
  88. package/src/services/adaptive-thresholds.ts +0 -275
  89. package/src/services/context-engine.ts +0 -104
  90. package/src/services/fix-packet-service.ts +0 -42
  91. package/src/services/state-service.ts +0 -138
  92. package/src/smoke.test.ts +0 -18
  93. package/src/templates/index.ts +0 -312
  94. package/src/types/fix-packet.ts +0 -32
  95. package/src/types/index.ts +0 -159
  96. package/src/utils/logger.ts +0 -43
  97. package/src/utils/scanner.test.ts +0 -37
  98. package/src/utils/scanner.ts +0 -43
  99. package/tsconfig.json +0 -10
  100. package/vitest.config.ts +0 -7
  101. package/vitest.setup.ts +0 -30
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @rigour-labs/core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@rigour-labs/core?color=cyan)](https://www.npmjs.com/package/@rigour-labs/core)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)
@@ -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 { fileURLToPath } from 'url';
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
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);
@@ -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(process.cwd(), 'temp-test-env');
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
  }
@@ -120,6 +120,7 @@ export class AgentTeamGate extends Gate {
120
120
  task_ownership: config.task_ownership ?? 'strict',
121
121
  };
122
122
  }
123
+ get provenance() { return 'governance'; }
123
124
  async run(context) {
124
125
  if (!this.config.enabled) {
125
126
  return [];
@@ -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
- protected createFailure(details: string, files?: string[], hint?: string, title?: string, line?: number, endLine?: number): Failure;
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
  }
@@ -5,11 +5,15 @@ export class Gate {
5
5
  this.id = id;
6
6
  this.title = title;
7
7
  }
8
- createFailure(details, files, hint, title, line, endLine) {
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
  }
@@ -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;
@@ -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
+ }
@@ -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
  /**
@@ -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
  /**
@@ -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
  }
@@ -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
+ }