@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
@@ -1,311 +0,0 @@
1
- /**
2
- * Checkpoint Supervision Gate
3
- *
4
- * Monitors agent quality during extended execution for frontier models
5
- * like GPT-5.3-Codex "coworking mode" that run autonomously for long periods.
6
- *
7
- * Features:
8
- * - Time-based checkpoint triggers
9
- * - Quality score tracking
10
- * - Drift detection (quality degradation over time)
11
- * - Auto-save on failure
12
- *
13
- * @since v2.14.0
14
- */
15
-
16
- import { Gate, GateContext } from './base.js';
17
- import { Failure } from '../types/index.js';
18
- import { Logger } from '../utils/logger.js';
19
- import * as fs from 'fs';
20
- import * as path from 'path';
21
-
22
- export interface CheckpointEntry {
23
- checkpointId: string;
24
- timestamp: Date;
25
- progressPct: number;
26
- filesChanged: string[];
27
- summary: string;
28
- qualityScore: number;
29
- warnings: string[];
30
- }
31
-
32
- export interface CheckpointSession {
33
- sessionId: string;
34
- startedAt: Date;
35
- lastCheckpoint?: Date;
36
- checkpoints: CheckpointEntry[];
37
- status: 'active' | 'completed' | 'aborted';
38
- }
39
-
40
- export interface CheckpointConfig {
41
- enabled?: boolean;
42
- interval_minutes?: number;
43
- quality_threshold?: number;
44
- drift_detection?: boolean;
45
- auto_save_on_failure?: boolean;
46
- }
47
-
48
- // In-memory checkpoint store (persisted to .rigour/checkpoint-session.json)
49
- let currentCheckpointSession: CheckpointSession | null = null;
50
-
51
- /**
52
- * Generate unique checkpoint ID
53
- */
54
- function generateCheckpointId(): string {
55
- return `cp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
56
- }
57
-
58
- /**
59
- * Get or create checkpoint session
60
- */
61
- export function getOrCreateCheckpointSession(cwd: string): CheckpointSession {
62
- if (!currentCheckpointSession) {
63
- loadCheckpointSession(cwd);
64
- }
65
- if (!currentCheckpointSession) {
66
- currentCheckpointSession = {
67
- sessionId: `chk-session-${Date.now()}`,
68
- startedAt: new Date(),
69
- checkpoints: [],
70
- status: 'active',
71
- };
72
- persistCheckpointSession(cwd);
73
- }
74
- return currentCheckpointSession;
75
- }
76
-
77
- /**
78
- * Record a checkpoint with quality evaluation
79
- */
80
- export function recordCheckpoint(
81
- cwd: string,
82
- progressPct: number,
83
- filesChanged: string[],
84
- summary: string,
85
- qualityScore: number
86
- ): { continue: boolean; warnings: string[]; checkpoint: CheckpointEntry } {
87
- const session = getOrCreateCheckpointSession(cwd);
88
- const warnings: string[] = [];
89
-
90
- // Default threshold
91
- const qualityThreshold = 80;
92
-
93
- // Check if quality is below threshold
94
- const shouldContinue = qualityScore >= qualityThreshold;
95
- if (!shouldContinue) {
96
- warnings.push(`Quality score ${qualityScore}% is below threshold ${qualityThreshold}%`);
97
- }
98
-
99
- // Detect drift (quality degradation over recent checkpoints)
100
- if (session.checkpoints.length >= 2) {
101
- const recentScores = session.checkpoints.slice(-3).map(cp => cp.qualityScore);
102
- const avgRecent = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
103
-
104
- if (qualityScore < avgRecent - 10) {
105
- warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
106
- }
107
- }
108
-
109
- const checkpoint: CheckpointEntry = {
110
- checkpointId: generateCheckpointId(),
111
- timestamp: new Date(),
112
- progressPct,
113
- filesChanged,
114
- summary,
115
- qualityScore,
116
- warnings,
117
- };
118
-
119
- session.checkpoints.push(checkpoint);
120
- session.lastCheckpoint = new Date();
121
- persistCheckpointSession(cwd);
122
-
123
- return { continue: shouldContinue, warnings, checkpoint };
124
- }
125
-
126
- /**
127
- * Get current checkpoint session
128
- */
129
- export function getCheckpointSession(cwd: string): CheckpointSession | null {
130
- if (!currentCheckpointSession) {
131
- loadCheckpointSession(cwd);
132
- }
133
- return currentCheckpointSession;
134
- }
135
-
136
- /**
137
- * Complete checkpoint session
138
- */
139
- export function completeCheckpointSession(cwd: string): void {
140
- if (currentCheckpointSession) {
141
- currentCheckpointSession.status = 'completed';
142
- persistCheckpointSession(cwd);
143
- }
144
- }
145
-
146
- /**
147
- * Abort checkpoint session (quality too low)
148
- */
149
- export function abortCheckpointSession(cwd: string, reason: string): void {
150
- if (currentCheckpointSession) {
151
- currentCheckpointSession.status = 'aborted';
152
- // Add final checkpoint with abort reason
153
- currentCheckpointSession.checkpoints.push({
154
- checkpointId: generateCheckpointId(),
155
- timestamp: new Date(),
156
- progressPct: currentCheckpointSession.checkpoints.length > 0
157
- ? currentCheckpointSession.checkpoints[currentCheckpointSession.checkpoints.length - 1].progressPct
158
- : 0,
159
- filesChanged: [],
160
- summary: `Session aborted: ${reason}`,
161
- qualityScore: 0,
162
- warnings: [reason],
163
- });
164
- persistCheckpointSession(cwd);
165
- }
166
- }
167
-
168
- /**
169
- * Clear checkpoint session
170
- */
171
- export function clearCheckpointSession(cwd: string): void {
172
- currentCheckpointSession = null;
173
- const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
174
- if (fs.existsSync(sessionPath)) {
175
- fs.unlinkSync(sessionPath);
176
- }
177
- }
178
-
179
- function persistCheckpointSession(cwd: string): void {
180
- const rigourDir = path.join(cwd, '.rigour');
181
- if (!fs.existsSync(rigourDir)) {
182
- fs.mkdirSync(rigourDir, { recursive: true });
183
- }
184
- const sessionPath = path.join(rigourDir, 'checkpoint-session.json');
185
- fs.writeFileSync(sessionPath, JSON.stringify(currentCheckpointSession, null, 2));
186
- }
187
-
188
- function loadCheckpointSession(cwd: string): void {
189
- const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
190
- if (fs.existsSync(sessionPath)) {
191
- try {
192
- const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
193
- currentCheckpointSession = {
194
- ...data,
195
- startedAt: new Date(data.startedAt),
196
- lastCheckpoint: data.lastCheckpoint ? new Date(data.lastCheckpoint) : undefined,
197
- checkpoints: data.checkpoints.map((cp: any) => ({
198
- ...cp,
199
- timestamp: new Date(cp.timestamp),
200
- })),
201
- };
202
- } catch (err) {
203
- Logger.warn('Failed to load checkpoint session, starting fresh');
204
- currentCheckpointSession = null;
205
- }
206
- }
207
- }
208
-
209
- /**
210
- * Calculate time since last checkpoint
211
- */
212
- function timeSinceLastCheckpoint(session: CheckpointSession): number {
213
- const lastTime = session.lastCheckpoint || session.startedAt;
214
- return (Date.now() - lastTime.getTime()) / 1000 / 60; // minutes
215
- }
216
-
217
- /**
218
- * Detect quality drift pattern
219
- */
220
- function detectDrift(checkpoints: CheckpointEntry[]): { hasDrift: boolean; trend: 'improving' | 'stable' | 'degrading' } {
221
- if (checkpoints.length < 3) {
222
- return { hasDrift: false, trend: 'stable' };
223
- }
224
-
225
- const recent = checkpoints.slice(-5);
226
- const scores = recent.map(cp => cp.qualityScore);
227
-
228
- // Calculate trend using simple linear regression
229
- const n = scores.length;
230
- const sumX = (n * (n - 1)) / 2;
231
- const sumY = scores.reduce((a, b) => a + b, 0);
232
- const sumXY = scores.reduce((sum, y, x) => sum + x * y, 0);
233
- const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
234
-
235
- const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
236
-
237
- if (slope < -2) {
238
- return { hasDrift: true, trend: 'degrading' };
239
- } else if (slope > 2) {
240
- return { hasDrift: false, trend: 'improving' };
241
- }
242
- return { hasDrift: false, trend: 'stable' };
243
- }
244
-
245
- export class CheckpointGate extends Gate {
246
- private config: CheckpointConfig;
247
-
248
- constructor(config: CheckpointConfig = {}) {
249
- super('checkpoint', 'Checkpoint Supervision');
250
- this.config = {
251
- enabled: config.enabled ?? false,
252
- interval_minutes: config.interval_minutes ?? 15,
253
- quality_threshold: config.quality_threshold ?? 80,
254
- drift_detection: config.drift_detection ?? true,
255
- auto_save_on_failure: config.auto_save_on_failure ?? true,
256
- };
257
- }
258
-
259
- async run(context: GateContext): Promise<Failure[]> {
260
- if (!this.config.enabled) {
261
- return [];
262
- }
263
-
264
- const failures: Failure[] = [];
265
- const session = getCheckpointSession(context.cwd);
266
-
267
- if (!session || session.checkpoints.length === 0) {
268
- // No checkpoints yet, skip
269
- return [];
270
- }
271
-
272
- Logger.info(`Checkpoint Gate: ${session.checkpoints.length} checkpoints in session`);
273
-
274
- // Check 1: Time since last checkpoint
275
- const minutesSinceLast = timeSinceLastCheckpoint(session);
276
- if (minutesSinceLast > (this.config.interval_minutes ?? 15) * 2) {
277
- failures.push(this.createFailure(
278
- `No checkpoint in ${minutesSinceLast.toFixed(0)} minutes (expected every ${this.config.interval_minutes} min)`,
279
- undefined,
280
- 'Ensure agent is reporting checkpoints via rigour_checkpoint MCP tool',
281
- 'Missing Checkpoint'
282
- ));
283
- }
284
-
285
- // Check 2: Quality threshold
286
- const lastCheckpoint = session.checkpoints[session.checkpoints.length - 1];
287
- if (lastCheckpoint.qualityScore < (this.config.quality_threshold ?? 80)) {
288
- failures.push(this.createFailure(
289
- `Quality score ${lastCheckpoint.qualityScore}% is below threshold ${this.config.quality_threshold}%`,
290
- lastCheckpoint.filesChanged,
291
- 'Review recent changes and address quality issues before continuing',
292
- 'Quality Below Threshold'
293
- ));
294
- }
295
-
296
- // Check 3: Drift detection
297
- if (this.config.drift_detection) {
298
- const { hasDrift, trend } = detectDrift(session.checkpoints);
299
- if (hasDrift && trend === 'degrading') {
300
- failures.push(this.createFailure(
301
- `Quality drift detected: scores are degrading over time`,
302
- undefined,
303
- 'Agent performance is declining. Consider pausing and reviewing recent work.',
304
- 'Quality Drift Detected'
305
- ));
306
- }
307
- }
308
-
309
- return failures;
310
- }
311
- }
@@ -1,50 +0,0 @@
1
- import { Gate, GateContext } from './base.js';
2
- import { Failure } from '../types/index.js';
3
- import { FileScanner } from '../utils/scanner.js';
4
-
5
- export interface ContentGateConfig {
6
- forbidTodos: boolean;
7
- forbidFixme: boolean;
8
- }
9
-
10
- export class ContentGate extends Gate {
11
- constructor(private config: ContentGateConfig) {
12
- super('content-check', 'Forbidden Content');
13
- }
14
-
15
- async run(context: GateContext): Promise<Failure[]> {
16
- const patterns: RegExp[] = [];
17
- if (this.config.forbidTodos) patterns.push(/TODO/i);
18
- if (this.config.forbidFixme) patterns.push(/FIXME/i);
19
-
20
- if (patterns.length === 0) return [];
21
-
22
- const files = await FileScanner.findFiles({
23
- cwd: context.cwd,
24
- ignore: context.ignore,
25
- patterns: context.patterns
26
- });
27
- const contents = await FileScanner.readFiles(context.cwd, files);
28
-
29
- const failures: Failure[] = [];
30
- for (const [file, content] of contents) {
31
- const lines = content.split('\n');
32
- lines.forEach((line, index) => {
33
- for (const pattern of patterns) {
34
- if (pattern.test(line)) {
35
- failures.push(this.createFailure(
36
- `Forbidden placeholder '${pattern.source}' found`,
37
- [file],
38
- 'Remove forbidden comments. address the root cause or create a tracked issue.',
39
- undefined,
40
- index + 1,
41
- index + 1
42
- ));
43
- }
44
- }
45
- });
46
- }
47
-
48
- return failures;
49
- }
50
- }
@@ -1,267 +0,0 @@
1
- import { Gate, GateContext } from './base.js';
2
- import { Failure, Gates } from '../types/index.js';
3
- import { FileScanner } from '../utils/scanner.js';
4
- import { Logger } from '../utils/logger.js';
5
- import fs from 'fs-extra';
6
- import path from 'path';
7
-
8
- /**
9
- * Extended Context Configuration (v2.14+)
10
- * For 1M token frontier models like Opus 4.6
11
- */
12
- export interface ExtendedContextConfig {
13
- enabled?: boolean;
14
- sensitivity?: number;
15
- mining_depth?: number;
16
- cross_file_patterns?: boolean; // NEW: Enable cross-file pattern analysis
17
- naming_consistency?: boolean; // NEW: Check naming convention drift
18
- import_relationships?: boolean; // NEW: Validate import patterns
19
- max_cross_file_depth?: number; // NEW: How many related files to analyze
20
- }
21
-
22
- export class ContextGate extends Gate {
23
- private extendedConfig: ExtendedContextConfig;
24
-
25
- constructor(private config: Gates) {
26
- super('context-drift', 'Context Awareness & Drift Detection');
27
- this.extendedConfig = {
28
- enabled: config.context?.enabled ?? false,
29
- sensitivity: config.context?.sensitivity ?? 0.8,
30
- mining_depth: config.context?.mining_depth ?? 100,
31
- cross_file_patterns: true, // Default ON for frontier model support
32
- naming_consistency: true,
33
- import_relationships: true,
34
- max_cross_file_depth: 50,
35
- };
36
- }
37
-
38
- async run(context: GateContext): Promise<Failure[]> {
39
- const failures: Failure[] = [];
40
- const record = context.record;
41
- if (!record || !this.extendedConfig.enabled) return [];
42
-
43
- const files = await FileScanner.findFiles({ cwd: context.cwd });
44
- const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
45
-
46
- // Collect all patterns across files for cross-file analysis
47
- const namingPatterns: Map<string, { casing: string; file: string; count: number }[]> = new Map();
48
- const importPatterns: Map<string, string[]> = new Map();
49
-
50
- for (const file of files) {
51
- try {
52
- const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
53
-
54
- // 1. Original: Detect Redundant Suffixes (The Golden Example)
55
- this.checkEnvDrift(content, file, envAnchors, failures);
56
-
57
- // 2. NEW: Cross-file pattern collection
58
- if (this.extendedConfig.cross_file_patterns) {
59
- this.collectNamingPatterns(content, file, namingPatterns);
60
- this.collectImportPatterns(content, file, importPatterns);
61
- }
62
-
63
- } catch (e) { }
64
- }
65
-
66
- // 3. NEW: Analyze naming consistency across files
67
- if (this.extendedConfig.naming_consistency) {
68
- this.analyzeNamingConsistency(namingPatterns, failures);
69
- }
70
-
71
- // 4. NEW: Analyze import relationship patterns
72
- if (this.extendedConfig.import_relationships) {
73
- this.analyzeImportPatterns(importPatterns, failures);
74
- }
75
-
76
- return failures;
77
- }
78
-
79
- private checkEnvDrift(content: string, file: string, anchors: any[], failures: Failure[]) {
80
- // Find all environment variable accesses in the content
81
- const matches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
82
-
83
- for (const match of matches) {
84
- const accessedVar = match[1] || match[2];
85
-
86
- for (const anchor of anchors) {
87
- // If the accessed variable contains the anchor but is not equal to it,
88
- // it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
89
- if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
90
- const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
91
-
92
- failures.push(this.createFailure(
93
- `Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`,
94
- [file],
95
- `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.`
96
- ));
97
- }
98
- }
99
- }
100
- }
101
-
102
- /**
103
- * Collect naming patterns (function names, class names, variable names)
104
- */
105
- private collectNamingPatterns(
106
- content: string,
107
- file: string,
108
- patterns: Map<string, { casing: string; file: string; count: number }[]>
109
- ) {
110
- // Named function declarations: function fetchData() { ... }
111
- const namedFuncMatches = content.matchAll(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
112
- for (const match of namedFuncMatches) {
113
- const casing = this.detectCasing(match[1]);
114
- this.addPattern(patterns, 'function', { casing, file, count: 1 });
115
- }
116
-
117
- // Arrow function expressions: (export) const fetchData = (async) (...) => { ... }
118
- const arrowFuncMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g);
119
- for (const match of arrowFuncMatches) {
120
- const casing = this.detectCasing(match[1]);
121
- this.addPattern(patterns, 'function', { casing, file, count: 1 });
122
- }
123
-
124
- // Function expressions: (export) const fetchData = (async) function(...) { ... }
125
- const funcExprMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function\s*\(/g);
126
- for (const match of funcExprMatches) {
127
- const casing = this.detectCasing(match[1]);
128
- this.addPattern(patterns, 'function', { casing, file, count: 1 });
129
- }
130
-
131
- // Class declarations
132
- const classMatches = content.matchAll(/class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
133
- for (const match of classMatches) {
134
- const casing = this.detectCasing(match[1]);
135
- this.addPattern(patterns, 'class', { casing, file, count: 1 });
136
- }
137
-
138
- // Interface declarations (TypeScript)
139
- const interfaceMatches = content.matchAll(/interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
140
- for (const match of interfaceMatches) {
141
- const casing = this.detectCasing(match[1]);
142
- this.addPattern(patterns, 'interface', { casing, file, count: 1 });
143
- }
144
- }
145
-
146
- /**
147
- * Collect import patterns
148
- */
149
- private collectImportPatterns(content: string, file: string, patterns: Map<string, string[]>) {
150
- // ES6 imports
151
- const importMatches = content.matchAll(/import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g);
152
- for (const match of importMatches) {
153
- const importPath = match[1];
154
- if (!patterns.has(file)) {
155
- patterns.set(file, []);
156
- }
157
- patterns.get(file)!.push(importPath);
158
- }
159
- }
160
-
161
- /**
162
- * Analyze naming consistency across files
163
- */
164
- private analyzeNamingConsistency(
165
- patterns: Map<string, { casing: string; file: string; count: number }[]>,
166
- failures: Failure[]
167
- ) {
168
- for (const [type, entries] of patterns) {
169
- const casingCounts = new Map<string, number>();
170
- for (const entry of entries) {
171
- casingCounts.set(entry.casing, (casingCounts.get(entry.casing) || 0) + entry.count);
172
- }
173
-
174
- // Find dominant casing
175
- let dominant = '';
176
- let maxCount = 0;
177
- for (const [casing, count] of casingCounts) {
178
- if (count > maxCount) {
179
- dominant = casing;
180
- maxCount = count;
181
- }
182
- }
183
-
184
- // Report violations (non-dominant casing with significant usage)
185
- const total = entries.reduce((sum, e) => sum + e.count, 0);
186
- const threshold = total * (1 - (this.extendedConfig.sensitivity ?? 0.8));
187
-
188
- for (const [casing, count] of casingCounts) {
189
- if (casing !== dominant && count > threshold) {
190
- const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
191
- const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
192
-
193
- failures.push(this.createFailure(
194
- `Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`,
195
- uniqueFiles,
196
- `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`,
197
- 'Naming Convention Drift'
198
- ));
199
- }
200
- }
201
- }
202
- }
203
-
204
- /**
205
- * Analyze import patterns for consistency
206
- */
207
- private analyzeImportPatterns(patterns: Map<string, string[]>, failures: Failure[]) {
208
- // Check for mixed import styles (relative vs absolute)
209
- const relativeCount = new Map<string, number>();
210
- const absoluteCount = new Map<string, number>();
211
-
212
- for (const [file, imports] of patterns) {
213
- for (const imp of imports) {
214
- if (imp.startsWith('.') || imp.startsWith('..')) {
215
- relativeCount.set(file, (relativeCount.get(file) || 0) + 1);
216
- } else if (!imp.startsWith('@') && !imp.includes('/')) {
217
- // Skip external packages
218
- } else {
219
- absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
220
- }
221
- }
222
- }
223
-
224
- // Detect files with both relative AND absolute local imports
225
- const mixedFiles: string[] = [];
226
- for (const file of patterns.keys()) {
227
- const hasRelative = (relativeCount.get(file) || 0) > 0;
228
- const hasAbsolute = (absoluteCount.get(file) || 0) > 0;
229
- if (hasRelative && hasAbsolute) {
230
- mixedFiles.push(file);
231
- }
232
- }
233
-
234
- if (mixedFiles.length > 3) {
235
- failures.push(this.createFailure(
236
- `Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`,
237
- mixedFiles.slice(0, 5),
238
- 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.',
239
- 'Import Pattern Drift'
240
- ));
241
- }
242
- }
243
-
244
- /**
245
- * Detect casing convention of an identifier
246
- */
247
- private detectCasing(name: string): string {
248
- if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
249
- if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
250
- if (/^[a-z][a-zA-Z0-9]*$/.test(name)) return 'camelCase'; // single-word lowercase (e.g. fetch, use, get)
251
- if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
252
- if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
253
- if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';
254
- return 'unknown';
255
- }
256
-
257
- private addPattern(
258
- patterns: Map<string, { casing: string; file: string; count: number }[]>,
259
- type: string,
260
- entry: { casing: string; file: string; count: number }
261
- ) {
262
- if (!patterns.has(type)) {
263
- patterns.set(type, []);
264
- }
265
- patterns.get(type)!.push(entry);
266
- }
267
- }