@rigour-labs/core 5.0.0 → 5.1.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 (139) hide show
  1. package/README.md +9 -1
  2. package/dist/gates/agent-team.d.ts +0 -1
  3. package/dist/gates/agent-team.js +0 -1
  4. package/dist/gates/checkpoint.d.ts +0 -2
  5. package/dist/gates/checkpoint.js +0 -2
  6. package/dist/gates/context-window-artifacts.d.ts +6 -2
  7. package/dist/gates/context-window-artifacts.js +107 -31
  8. package/dist/gates/deep-analysis.d.ts +2 -0
  9. package/dist/gates/deep-analysis.js +41 -11
  10. package/dist/gates/dependency.d.ts +0 -2
  11. package/dist/gates/dependency.js +23 -5
  12. package/dist/gates/deprecated-apis.d.ts +0 -2
  13. package/dist/gates/deprecated-apis.js +33 -20
  14. package/dist/gates/duplication-drift/index.d.ts +61 -0
  15. package/dist/gates/duplication-drift/index.js +240 -0
  16. package/dist/gates/duplication-drift/similarity.d.ts +68 -0
  17. package/dist/gates/duplication-drift/similarity.js +177 -0
  18. package/dist/gates/duplication-drift/tokenizer.d.ts +55 -0
  19. package/dist/gates/duplication-drift/tokenizer.js +195 -0
  20. package/dist/gates/frontend-secret-exposure.d.ts +0 -3
  21. package/dist/gates/frontend-secret-exposure.js +1 -114
  22. package/dist/gates/frontend-secret-patterns.d.ts +33 -0
  23. package/dist/gates/frontend-secret-patterns.js +119 -0
  24. package/dist/gates/{hallucinated-imports.d.ts → hallucinated-imports/index.d.ts} +2 -29
  25. package/dist/gates/hallucinated-imports/index.js +174 -0
  26. package/dist/gates/hallucinated-imports/js-resolver.d.ts +45 -0
  27. package/dist/gates/hallucinated-imports/js-resolver.js +320 -0
  28. package/dist/gates/hallucinated-imports/manifest-discovery.d.ts +28 -0
  29. package/dist/gates/hallucinated-imports/manifest-discovery.js +114 -0
  30. package/dist/gates/hallucinated-imports/python-resolver.d.ts +24 -0
  31. package/dist/gates/hallucinated-imports/python-resolver.js +306 -0
  32. package/dist/gates/hallucinated-imports-lang.d.ts +2 -2
  33. package/dist/gates/hallucinated-imports-lang.js +269 -34
  34. package/dist/gates/hallucinated-imports.test.js +1 -2
  35. package/dist/gates/inconsistent-error-handling.d.ts +0 -5
  36. package/dist/gates/inconsistent-error-handling.js +15 -144
  37. package/dist/gates/language-adapters/csharp-adapter.d.ts +16 -0
  38. package/dist/gates/language-adapters/csharp-adapter.js +211 -0
  39. package/dist/gates/language-adapters/go-adapter.d.ts +26 -0
  40. package/dist/gates/language-adapters/go-adapter.js +195 -0
  41. package/dist/gates/language-adapters/index.d.ts +15 -0
  42. package/dist/gates/language-adapters/index.js +16 -0
  43. package/dist/gates/language-adapters/java-adapter.d.ts +16 -0
  44. package/dist/gates/language-adapters/java-adapter.js +237 -0
  45. package/dist/gates/language-adapters/js-adapter.d.ts +26 -0
  46. package/dist/gates/language-adapters/js-adapter.js +279 -0
  47. package/dist/gates/language-adapters/python-adapter.d.ts +25 -0
  48. package/dist/gates/language-adapters/python-adapter.js +183 -0
  49. package/dist/gates/language-adapters/registry.d.ts +26 -0
  50. package/dist/gates/language-adapters/registry.js +65 -0
  51. package/dist/gates/language-adapters/ruby-adapter.d.ts +25 -0
  52. package/dist/gates/language-adapters/ruby-adapter.js +217 -0
  53. package/dist/gates/language-adapters/rust-adapter.d.ts +27 -0
  54. package/dist/gates/language-adapters/rust-adapter.js +235 -0
  55. package/dist/gates/language-adapters/types.d.ts +60 -0
  56. package/dist/gates/language-adapters/types.js +22 -0
  57. package/dist/gates/logic-drift-extractors.d.ts +15 -0
  58. package/dist/gates/logic-drift-extractors.js +34 -0
  59. package/dist/gates/logic-drift.d.ts +0 -30
  60. package/dist/gates/logic-drift.js +39 -129
  61. package/dist/gates/phantom-apis.d.ts +0 -2
  62. package/dist/gates/phantom-apis.js +49 -20
  63. package/dist/gates/promise-safety.d.ts +0 -1
  64. package/dist/gates/promise-safety.js +14 -2
  65. package/dist/gates/runner.js +51 -22
  66. package/dist/gates/security-patterns-data.d.ts +14 -0
  67. package/dist/gates/security-patterns-data.js +235 -0
  68. package/dist/gates/security-patterns.d.ts +17 -3
  69. package/dist/gates/security-patterns.js +80 -211
  70. package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
  71. package/dist/gates/side-effect-analysis/categorizer.js +83 -0
  72. package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
  73. package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
  74. package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
  75. package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
  76. package/dist/gates/side-effect-helpers/index.d.ts +4 -0
  77. package/dist/gates/side-effect-helpers/index.js +4 -0
  78. package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
  79. package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
  80. package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
  81. package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
  82. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
  83. package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
  84. package/dist/gates/side-effect-helpers/types.d.ts +38 -0
  85. package/dist/gates/side-effect-helpers/types.js +41 -0
  86. package/dist/gates/side-effect-rules.d.ts +0 -1
  87. package/dist/gates/side-effect-rules.js +0 -1
  88. package/dist/gates/style-drift-rules.d.ts +86 -0
  89. package/dist/gates/style-drift-rules.js +103 -0
  90. package/dist/gates/style-drift.d.ts +7 -16
  91. package/dist/gates/style-drift.js +101 -119
  92. package/dist/gates/test-quality-matchers.d.ts +53 -0
  93. package/dist/gates/test-quality-matchers.js +86 -0
  94. package/dist/gates/test-quality.d.ts +0 -3
  95. package/dist/gates/test-quality.js +47 -44
  96. package/dist/hooks/checker.d.ts +0 -1
  97. package/dist/hooks/checker.js +1 -3
  98. package/dist/hooks/dlp-templates.d.ts +0 -1
  99. package/dist/hooks/dlp-templates.js +0 -4
  100. package/dist/hooks/index.d.ts +0 -2
  101. package/dist/hooks/index.js +0 -2
  102. package/dist/hooks/input-validator.d.ts +0 -1
  103. package/dist/hooks/input-validator.js +0 -1
  104. package/dist/hooks/input-validator.test.js +0 -1
  105. package/dist/hooks/standalone-checker.d.ts +0 -1
  106. package/dist/hooks/standalone-checker.js +0 -1
  107. package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
  108. package/dist/hooks/standalone-dlp-checker.js +0 -1
  109. package/dist/hooks/templates.d.ts +6 -1
  110. package/dist/hooks/templates.js +6 -1
  111. package/dist/hooks/types.d.ts +1 -2
  112. package/dist/hooks/types.js +1 -1
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.js +1 -1
  115. package/dist/services/adaptive-thresholds.d.ts +0 -2
  116. package/dist/services/adaptive-thresholds.js +0 -2
  117. package/dist/services/filesystem-cache.d.ts +0 -1
  118. package/dist/services/filesystem-cache.js +0 -1
  119. package/dist/services/score-history.d.ts +0 -1
  120. package/dist/services/score-history.js +0 -1
  121. package/dist/services/temporal-drift.d.ts +1 -2
  122. package/dist/services/temporal-drift.js +7 -8
  123. package/dist/storage/db.d.ts +23 -7
  124. package/dist/storage/db.js +116 -55
  125. package/dist/storage/findings.d.ts +4 -3
  126. package/dist/storage/findings.js +13 -20
  127. package/dist/storage/local-memory.d.ts +4 -4
  128. package/dist/storage/local-memory.js +20 -22
  129. package/dist/storage/patterns.d.ts +5 -5
  130. package/dist/storage/patterns.js +20 -26
  131. package/dist/storage/scans.d.ts +6 -6
  132. package/dist/storage/scans.js +12 -21
  133. package/dist/types/index.d.ts +1 -0
  134. package/dist/utils/scanner.js +1 -1
  135. package/package.json +7 -8
  136. package/dist/gates/duplication-drift.d.ts +0 -128
  137. package/dist/gates/duplication-drift.js +0 -585
  138. package/dist/gates/hallucinated-imports.js +0 -641
  139. package/dist/gates/side-effect-helpers.d.ts +0 -260
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Style Drift Detection Gate — Naming Convention Rules and Regexes
3
+ *
4
+ * Contains per-language naming convention rules and naming pattern regexes.
5
+ */
6
+ /**
7
+ * Casing classification rules
8
+ */
9
+ export function classifyCasing(name) {
10
+ if (name.startsWith('_') || name.length <= 1)
11
+ return null;
12
+ if (/^[A-Z][A-Z0-9_]+$/.test(name)) {
13
+ return 'SCREAMING_SNAKE';
14
+ }
15
+ else if (/^[A-Z]/.test(name)) {
16
+ return 'PascalCase';
17
+ }
18
+ else if (name.includes('_')) {
19
+ return 'snake_case';
20
+ }
21
+ else {
22
+ return 'camelCase';
23
+ }
24
+ }
25
+ /**
26
+ * Function name pattern for JavaScript
27
+ */
28
+ export const JS_FUNCTION_PATTERN = /(?:function|async\s+function)\s+(\w+)/;
29
+ /**
30
+ * Method definition pattern for all languages
31
+ */
32
+ export const METHOD_PATTERN = /^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*[{:]/;
33
+ /**
34
+ * Arrow function assignment pattern
35
+ */
36
+ export const ARROW_FUNCTION_PATTERN = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/;
37
+ /**
38
+ * Variable declaration pattern (non-function)
39
+ */
40
+ export const VAR_DECLARATION_PATTERN = /(?:const|let|var)\s+(\w+)\s*=/;
41
+ /**
42
+ * Python function pattern
43
+ */
44
+ export const PYTHON_FUNCTION_PATTERN = /def\s+(\w+)/;
45
+ /**
46
+ * Python variable pattern
47
+ */
48
+ export const PYTHON_VAR_PATTERN = /^(\w+)\s*=/;
49
+ /**
50
+ * Go function pattern
51
+ */
52
+ export const GO_FUNCTION_PATTERN = /^func\s+(?:\([^)]+\)\s+)?(\w+)/;
53
+ /**
54
+ * Go variable pattern
55
+ */
56
+ export const GO_VAR_PATTERN = /^\s*(\w+)\s*:?=/;
57
+ /**
58
+ * Rust function pattern
59
+ */
60
+ export const RUST_FUNCTION_PATTERN = /fn\s+(\w+)/;
61
+ /**
62
+ * Rust variable pattern
63
+ */
64
+ export const RUST_VAR_PATTERN = /let\s+(?:mut\s+)?(\w+)/;
65
+ /**
66
+ * Ruby method pattern
67
+ */
68
+ export const RUBY_METHOD_PATTERN = /def\s+(?:self\.)?(\w+)/;
69
+ /**
70
+ * Ruby variable pattern
71
+ */
72
+ export const RUBY_VAR_PATTERN = /^\s*(\w+)\s*=/;
73
+ /**
74
+ * Java/Kotlin/C# method pattern
75
+ */
76
+ export const JAVA_METHOD_PATTERN = /(?:public|private|protected|internal|static|override|virtual|abstract)\s+(?:\w+\s+)*(\w+)\s*\(/;
77
+ /**
78
+ * Java/Kotlin/C# variable pattern
79
+ */
80
+ export const JAVA_VAR_PATTERN = /(?:var|val|final)?\s*\w+\s+(\w+)\s*[=;]/;
81
+ /**
82
+ * Error handling patterns
83
+ */
84
+ export const TRY_CATCH_PATTERN = /\btry\s*\{|\btry\s*:/;
85
+ export const CATCH_PATTERN = /\.catch\s*\(|\bexcept\b|\brescue\b/;
86
+ export const RESULT_TYPE_PATTERN = /Result<|Result\[|Err\(|Ok\(|Either<|\bif\s+err\s*!=\s*nil\b/;
87
+ /**
88
+ * Import style patterns
89
+ */
90
+ export const NAMED_IMPORT_PATTERN = /^import\s+\{/;
91
+ export const WILDCARD_IMPORT_PATTERN = /^import\s+\*\s+as/;
92
+ export const SIDE_EFFECT_IMPORT_PATTERN = /^import\s+['"]/;
93
+ export const DEFAULT_IMPORT_PATTERN = /^import\s+\w/;
94
+ /**
95
+ * Quote style detection
96
+ */
97
+ export function countQuotes(line) {
98
+ return {
99
+ single: (line.match(/'/g) || []).length,
100
+ double: (line.match(/"/g) || []).length,
101
+ backtick: (line.match(/`/g) || []).length,
102
+ };
103
+ }
@@ -7,19 +7,17 @@
7
7
  *
8
8
  * What it checks:
9
9
  * 1. Naming conventions — camelCase vs snake_case vs PascalCase consistency
10
- * 2. Error handling patterns — try-catch vs .catch() vs Result type consistency
10
+ * 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
11
11
  * 3. Import style — named vs default vs wildcard import consistency
12
12
  * 4. Quote style — single vs double quote consistency
13
13
  *
14
14
  * How it works:
15
- * 1. First scan: sample source files → compute a style fingerprint → store baseline
16
- * 2. Subsequent scans: compare new/changed files against baseline
15
+ * 1. First scan: sample source files → compute per-language style fingerprints → store baseline
16
+ * 2. Subsequent scans: compare new/changed files against their language's baseline
17
17
  * 3. If a file deviates >25% on any dimension → flag as style drift
18
18
  *
19
- * The baseline is stored in .rigour/style-baseline.json and evolves with
20
- * human-approved changes (not AI drift).
21
- *
22
- * @since v5.0.0
19
+ * Baselines are per-language to avoid cross-language contamination
20
+ * (e.g., Python snake_case shouldn't flag JS camelCase).
23
21
  */
24
22
  import { Gate, GateContext } from './base.js';
25
23
  import { Failure, Provenance } from '../types/index.js';
@@ -34,20 +32,13 @@ export declare class StyleDriftGate extends Gate {
34
32
  constructor(config?: StyleDriftConfig);
35
33
  protected get provenance(): Provenance;
36
34
  run(context: GateContext): Promise<Failure[]>;
35
+ private computePerLanguageBaseline;
37
36
  private computeFingerprint;
37
+ private emptyFingerprint;
38
38
  private analyzeFile;
39
- private classifyCasing;
40
39
  private mergeIntoFingerprint;
41
40
  private compareToBaseline;
42
- /**
43
- * Compare two distributions and return a deviation score (0-1).
44
- * 0 = perfect match, 1 = completely different predominant style.
45
- *
46
- * Method: find the predominant category in each distribution.
47
- * If they differ, score = how far the file is from the baseline's predominant category.
48
- */
49
41
  private distributionDeviation;
50
42
  private hasSignificantData;
51
- /** Convert a typed distribution to a generic Record for comparison */
52
43
  private toRecord;
53
44
  }
@@ -7,23 +7,23 @@
7
7
  *
8
8
  * What it checks:
9
9
  * 1. Naming conventions — camelCase vs snake_case vs PascalCase consistency
10
- * 2. Error handling patterns — try-catch vs .catch() vs Result type consistency
10
+ * 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
11
11
  * 3. Import style — named vs default vs wildcard import consistency
12
12
  * 4. Quote style — single vs double quote consistency
13
13
  *
14
14
  * How it works:
15
- * 1. First scan: sample source files → compute a style fingerprint → store baseline
16
- * 2. Subsequent scans: compare new/changed files against baseline
15
+ * 1. First scan: sample source files → compute per-language style fingerprints → store baseline
16
+ * 2. Subsequent scans: compare new/changed files against their language's baseline
17
17
  * 3. If a file deviates >25% on any dimension → flag as style drift
18
18
  *
19
- * The baseline is stored in .rigour/style-baseline.json and evolves with
20
- * human-approved changes (not AI drift).
21
- *
22
- * @since v5.0.0
19
+ * Baselines are per-language to avoid cross-language contamination
20
+ * (e.g., Python snake_case shouldn't flag JS camelCase).
23
21
  */
24
22
  import { Gate } from './base.js';
25
23
  import { FileScanner } from '../utils/scanner.js';
26
24
  import { Logger } from '../utils/logger.js';
25
+ import { languageAdapters } from './language-adapters/index.js';
26
+ import { TRY_CATCH_PATTERN, CATCH_PATTERN, RESULT_TYPE_PATTERN, NAMED_IMPORT_PATTERN, WILDCARD_IMPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DEFAULT_IMPORT_PATTERN, countQuotes, } from './style-drift-rules.js';
27
27
  import fs from 'fs-extra';
28
28
  import path from 'path';
29
29
  export class StyleDriftGate extends Gate {
@@ -33,7 +33,7 @@ export class StyleDriftGate extends Gate {
33
33
  this.config = {
34
34
  enabled: config.enabled ?? true,
35
35
  deviation_threshold: config.deviation_threshold ?? 0.25,
36
- sample_size: config.sample_size ?? 100,
36
+ sample_size: config.sample_size ?? 50,
37
37
  baseline_path: config.baseline_path ?? '.rigour/style-baseline.json',
38
38
  };
39
39
  }
@@ -46,43 +46,63 @@ export class StyleDriftGate extends Gate {
46
46
  // Find source files
47
47
  const files = await FileScanner.findFiles({
48
48
  cwd: context.cwd,
49
- patterns: context.patterns || ['**/*.{ts,tsx,js,jsx,py}'],
49
+ patterns: context.patterns || languageAdapters.getScanPatterns(),
50
50
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'],
51
51
  });
52
52
  if (files.length === 0)
53
53
  return [];
54
+ // Group files by language
55
+ const filesByLang = new Map();
56
+ for (const file of files) {
57
+ const adapter = languageAdapters.getAdapter(file);
58
+ if (!adapter)
59
+ continue;
60
+ const langFiles = filesByLang.get(adapter.id) || [];
61
+ langFiles.push(file);
62
+ filesByLang.set(adapter.id, langFiles);
63
+ }
54
64
  // Load or create baseline
55
65
  let baseline = null;
56
66
  if (await fs.pathExists(baselinePath)) {
57
67
  try {
58
- baseline = await fs.readJson(baselinePath);
68
+ const raw = await fs.readJson(baselinePath);
69
+ // Handle migration from old single-fingerprint format
70
+ if (raw.version === 2) {
71
+ baseline = raw;
72
+ }
73
+ else {
74
+ Logger.debug('Old style baseline format detected, creating new per-language baseline');
75
+ }
59
76
  }
60
77
  catch {
61
78
  Logger.debug('Failed to load style baseline, will create new one');
62
79
  }
63
80
  }
64
81
  if (!baseline) {
65
- // First scan: create baseline from sampled files
66
- const sampled = files.slice(0, this.config.sample_size);
67
- baseline = await this.computeFingerprint(context, sampled);
68
- baseline.createdAt = new Date().toISOString();
69
- // Ensure directory exists and save baseline
82
+ // First scan: create per-language baseline
83
+ baseline = await this.computePerLanguageBaseline(context, filesByLang);
70
84
  await fs.ensureDir(path.dirname(baselinePath));
71
85
  await fs.writeJson(baselinePath, baseline, { spaces: 2 });
72
- Logger.info(`Style Drift: Created baseline from ${sampled.length} files → ${baselinePath}`);
86
+ const langSummary = Object.entries(baseline.languages)
87
+ .map(([lang, fp]) => `${lang}:${fp.totalFilesAnalyzed}`)
88
+ .join(', ');
89
+ Logger.info(`Style Drift: Created baseline (${langSummary}) → ${baselinePath}`);
73
90
  return []; // No failures on first scan
74
91
  }
75
- // Subsequent scan: compare each file against baseline
92
+ // Subsequent scan: compare each file against its own language's baseline
76
93
  const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
77
94
  for (const [file, content] of contents) {
78
- const ext = path.extname(file);
79
- if (!['.ts', '.tsx', '.js', '.jsx', '.py'].includes(ext))
95
+ const adapter = languageAdapters.getAdapter(file);
96
+ if (!adapter)
80
97
  continue;
81
- const fileFingerprint = this.analyzeFile(content, ext);
82
- const deviations = this.compareToBaseline(fileFingerprint, baseline);
98
+ const langBaseline = baseline.languages[adapter.id];
99
+ if (!langBaseline)
100
+ continue; // No baseline for this language yet
101
+ const fileFingerprint = this.analyzeFile(content, file);
102
+ const deviations = this.compareToBaseline(fileFingerprint, langBaseline);
83
103
  for (const deviation of deviations) {
84
104
  if (deviation.score > this.config.deviation_threshold) {
85
- failures.push(this.createFailure(`Style drift in ${file}: ${deviation.dimension} deviates ${(deviation.score * 100).toFixed(0)}% from project baseline (${deviation.detail}).`, [file], `This file's ${deviation.dimension} doesn't match the project's established convention. ${deviation.suggestion}`, 'Style Drift', undefined, undefined, 'low'));
105
+ failures.push(this.createFailure(`Style drift in ${file}: ${deviation.dimension} deviates ${(deviation.score * 100).toFixed(0)}% from ${adapter.id} baseline (${deviation.detail}).`, [file], `This file's ${deviation.dimension} doesn't match the ${adapter.id} project convention. ${deviation.suggestion}`, 'Style Drift', undefined, undefined, 'low'));
86
106
  }
87
107
  }
88
108
  }
@@ -91,130 +111,105 @@ export class StyleDriftGate extends Gate {
91
111
  }
92
112
  return failures;
93
113
  }
94
- // ─── Fingerprint Computation ─────────────────────────────────────
95
- async computeFingerprint(context, files) {
96
- const fingerprint = {
97
- naming: {
98
- functions: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
99
- variables: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
100
- },
101
- errorHandling: { tryCatch: 0, promiseCatch: 0, resultType: 0 },
102
- importStyle: { named: 0, default: 0, wildcard: 0, sideEffect: 0 },
103
- quoteStyle: { single: 0, double: 0, backtick: 0 },
104
- totalFilesAnalyzed: 0,
105
- createdAt: '',
114
+ // ─── Per-Language Baseline Computation ────────────────────────────
115
+ async computePerLanguageBaseline(context, filesByLang) {
116
+ const baseline = {
117
+ languages: {},
118
+ createdAt: new Date().toISOString(),
119
+ version: 2,
106
120
  };
121
+ for (const [langId, langFiles] of filesByLang) {
122
+ // Sample up to sample_size files per language
123
+ const sampled = langFiles.slice(0, this.config.sample_size);
124
+ const fingerprint = await this.computeFingerprint(context, sampled);
125
+ fingerprint.createdAt = baseline.createdAt;
126
+ baseline.languages[langId] = fingerprint;
127
+ }
128
+ return baseline;
129
+ }
130
+ async computeFingerprint(context, files) {
131
+ const fingerprint = this.emptyFingerprint();
107
132
  const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
108
133
  for (const [file, content] of contents) {
109
- const ext = path.extname(file);
110
- const fileAnalysis = this.analyzeFile(content, ext);
134
+ const fileAnalysis = this.analyzeFile(content, file);
111
135
  this.mergeIntoFingerprint(fingerprint, fileAnalysis);
112
136
  fingerprint.totalFilesAnalyzed++;
113
137
  }
114
138
  return fingerprint;
115
139
  }
116
- analyzeFile(content, ext) {
117
- const fp = {
140
+ emptyFingerprint() {
141
+ return {
118
142
  naming: {
119
- functions: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
120
- variables: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0 },
143
+ functions: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0, 'kebab-case': 0, other: 0 },
144
+ variables: { camelCase: 0, snake_case: 0, PascalCase: 0, SCREAMING_SNAKE: 0, 'kebab-case': 0, other: 0 },
121
145
  },
122
146
  errorHandling: { tryCatch: 0, promiseCatch: 0, resultType: 0 },
123
147
  importStyle: { named: 0, default: 0, wildcard: 0, sideEffect: 0 },
124
148
  quoteStyle: { single: 0, double: 0, backtick: 0 },
125
- totalFilesAnalyzed: 1,
149
+ totalFilesAnalyzed: 0,
126
150
  createdAt: '',
127
151
  };
152
+ }
153
+ analyzeFile(content, filePath) {
154
+ const fp = this.emptyFingerprint();
155
+ fp.totalFilesAnalyzed = 1;
128
156
  const lines = content.split('\n');
129
- for (const line of lines) {
130
- // ── Naming conventions ──
131
- // Function declarations
132
- const fnMatch = line.match(/(?:function|async\s+function)\s+(\w+)/);
133
- if (fnMatch)
134
- this.classifyCasing(fnMatch[1], fp.naming.functions);
135
- // Method definitions
136
- const methodMatch = line.match(/^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*[{:]/);
137
- if (methodMatch && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodMatch[1])) {
138
- this.classifyCasing(methodMatch[1], fp.naming.functions);
139
- }
140
- // Arrow function assignments
141
- const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/);
142
- if (arrowMatch)
143
- this.classifyCasing(arrowMatch[1], fp.naming.functions);
144
- // Variable declarations (non-function)
145
- const varMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=/);
146
- if (varMatch && !arrowMatch)
147
- this.classifyCasing(varMatch[1], fp.naming.variables);
148
- // Python function/variable
149
- if (ext === '.py') {
150
- const pyFn = line.match(/def\s+(\w+)/);
151
- if (pyFn)
152
- this.classifyCasing(pyFn[1], fp.naming.functions);
153
- const pyVar = line.match(/^(\w+)\s*=/);
154
- if (pyVar && !pyFn)
155
- this.classifyCasing(pyVar[1], fp.naming.variables);
157
+ const adapter = languageAdapters.getAdapter(filePath);
158
+ // ── Naming conventions (via adapter) ──
159
+ if (adapter) {
160
+ const namingPatterns = adapter.extractNamingPatterns(content);
161
+ for (const pattern of namingPatterns) {
162
+ if (pattern.kind === 'function' || pattern.kind === 'method') {
163
+ fp.naming.functions[pattern.convention]++;
164
+ }
165
+ else if (pattern.kind === 'variable' || pattern.kind === 'constant') {
166
+ fp.naming.variables[pattern.convention]++;
167
+ }
156
168
  }
169
+ }
170
+ for (const line of lines) {
157
171
  // ── Error handling ──
158
- if (/\btry\s*\{/.test(line) || /\btry\s*:/.test(line))
172
+ if (TRY_CATCH_PATTERN.test(line))
159
173
  fp.errorHandling.tryCatch++;
160
- if (/\.catch\s*\(/.test(line))
174
+ if (CATCH_PATTERN.test(line))
161
175
  fp.errorHandling.promiseCatch++;
162
- if (/Result<|Result\[|Err\(|Ok\(|Either</.test(line))
176
+ if (RESULT_TYPE_PATTERN.test(line))
163
177
  fp.errorHandling.resultType++;
164
178
  // ── Import style ──
165
- if (/^import\s+\{/.test(line.trim()))
179
+ if (NAMED_IMPORT_PATTERN.test(line.trim()))
166
180
  fp.importStyle.named++;
167
- else if (/^import\s+\*\s+as/.test(line.trim()))
181
+ else if (WILDCARD_IMPORT_PATTERN.test(line.trim()))
168
182
  fp.importStyle.wildcard++;
169
- else if (/^import\s+['"]/.test(line.trim()))
183
+ else if (SIDE_EFFECT_IMPORT_PATTERN.test(line.trim()))
170
184
  fp.importStyle.sideEffect++;
171
- else if (/^import\s+\w/.test(line.trim()))
185
+ else if (DEFAULT_IMPORT_PATTERN.test(line.trim()))
172
186
  fp.importStyle.default++;
173
187
  // ── Quote style ──
174
- // Count quotes in non-import lines (imports are already counted above)
175
188
  if (!line.trim().startsWith('import')) {
176
- const singles = (line.match(/'/g) || []).length;
177
- const doubles = (line.match(/"/g) || []).length;
178
- const backticks = (line.match(/`/g) || []).length;
179
- fp.quoteStyle.single += singles;
180
- fp.quoteStyle.double += doubles;
181
- fp.quoteStyle.backtick += backticks;
189
+ const quotes = countQuotes(line);
190
+ fp.quoteStyle.single += quotes.single;
191
+ fp.quoteStyle.double += quotes.double;
192
+ fp.quoteStyle.backtick += quotes.backtick;
182
193
  }
183
194
  }
184
195
  return fp;
185
196
  }
186
- classifyCasing(name, dist) {
187
- if (name.startsWith('_') || name.length <= 1)
188
- return; // Skip private/single char
189
- if (/^[A-Z][A-Z0-9_]+$/.test(name)) {
190
- dist.SCREAMING_SNAKE++;
191
- }
192
- else if (/^[A-Z]/.test(name)) {
193
- dist.PascalCase++;
194
- }
195
- else if (name.includes('_')) {
196
- dist.snake_case++;
197
- }
198
- else {
199
- dist.camelCase++;
200
- }
201
- }
202
197
  mergeIntoFingerprint(target, source) {
203
- // Naming
204
198
  for (const key of Object.keys(target.naming.functions)) {
205
- target.naming.functions[key] += source.naming.functions[key];
206
- target.naming.variables[key] += source.naming.variables[key];
199
+ const targetVal = target.naming.functions[key] ?? 0;
200
+ const sourceVal = source.naming.functions[key] ?? 0;
201
+ target.naming.functions[key] = targetVal + sourceVal;
202
+ const targetVarVal = target.naming.variables[key] ?? 0;
203
+ const sourceVarVal = source.naming.variables[key] ?? 0;
204
+ target.naming.variables[key] = targetVarVal + sourceVarVal;
207
205
  }
208
- // Error handling
209
206
  target.errorHandling.tryCatch += source.errorHandling.tryCatch;
210
207
  target.errorHandling.promiseCatch += source.errorHandling.promiseCatch;
211
208
  target.errorHandling.resultType += source.errorHandling.resultType;
212
- // Import style
213
209
  target.importStyle.named += source.importStyle.named;
214
210
  target.importStyle.default += source.importStyle.default;
215
211
  target.importStyle.wildcard += source.importStyle.wildcard;
216
212
  target.importStyle.sideEffect += source.importStyle.sideEffect;
217
- // Quote style
218
213
  target.quoteStyle.single += source.quoteStyle.single;
219
214
  target.quoteStyle.double += source.quoteStyle.double;
220
215
  target.quoteStyle.backtick += source.quoteStyle.backtick;
@@ -222,7 +217,6 @@ export class StyleDriftGate extends Gate {
222
217
  // ─── Baseline Comparison ─────────────────────────────────────────
223
218
  compareToBaseline(file, baseline) {
224
219
  const deviations = [];
225
- // Compare function naming
226
220
  const fnDev = this.distributionDeviation(this.toRecord(file.naming.functions), this.toRecord(baseline.naming.functions));
227
221
  if (fnDev.score > 0) {
228
222
  deviations.push({
@@ -232,7 +226,6 @@ export class StyleDriftGate extends Gate {
232
226
  suggestion: `Use ${fnDev.baselinePredominant} for function names to match project conventions.`,
233
227
  });
234
228
  }
235
- // Compare variable naming
236
229
  const varDev = this.distributionDeviation(this.toRecord(file.naming.variables), this.toRecord(baseline.naming.variables));
237
230
  if (varDev.score > 0) {
238
231
  deviations.push({
@@ -242,7 +235,6 @@ export class StyleDriftGate extends Gate {
242
235
  suggestion: `Use ${varDev.baselinePredominant} for variable names to match project conventions.`,
243
236
  });
244
237
  }
245
- // Compare error handling
246
238
  const errDev = this.distributionDeviation(file.errorHandling, baseline.errorHandling);
247
239
  if (errDev.score > 0 && this.hasSignificantData(file.errorHandling)) {
248
240
  deviations.push({
@@ -252,7 +244,6 @@ export class StyleDriftGate extends Gate {
252
244
  suggestion: `Use ${errDev.baselinePredominant} error handling pattern to match project conventions.`,
253
245
  });
254
246
  }
255
- // Compare import style
256
247
  const impDev = this.distributionDeviation(file.importStyle, baseline.importStyle);
257
248
  if (impDev.score > 0 && this.hasSignificantData(file.importStyle)) {
258
249
  deviations.push({
@@ -264,42 +255,33 @@ export class StyleDriftGate extends Gate {
264
255
  }
265
256
  return deviations;
266
257
  }
267
- /**
268
- * Compare two distributions and return a deviation score (0-1).
269
- * 0 = perfect match, 1 = completely different predominant style.
270
- *
271
- * Method: find the predominant category in each distribution.
272
- * If they differ, score = how far the file is from the baseline's predominant category.
273
- */
274
258
  distributionDeviation(file, baseline) {
275
259
  const fileTotal = Object.values(file).reduce((a, b) => a + b, 0);
276
260
  const baselineTotal = Object.values(baseline).reduce((a, b) => a + b, 0);
277
261
  if (fileTotal < 3 || baselineTotal < 5) {
278
262
  return { score: 0, filePredominant: 'N/A', baselinePredominant: 'N/A' };
279
263
  }
280
- // Find predominant category
281
264
  const filePredominant = Object.entries(file).sort((a, b) => b[1] - a[1])[0][0];
282
265
  const baselinePredominant = Object.entries(baseline).sort((a, b) => b[1] - a[1])[0][0];
283
266
  if (filePredominant === baselinePredominant) {
284
267
  return { score: 0, filePredominant, baselinePredominant };
285
268
  }
286
- // Calculate how much the file uses the baseline's predominant style
287
269
  const fileUseOfBaseline = (file[baselinePredominant] || 0) / fileTotal;
288
270
  const baselineUseOfBaseline = (baseline[baselinePredominant] || 0) / baselineTotal;
289
- // Score = how far the file deviates from baseline's predominant ratio
290
271
  const deviation = Math.max(0, baselineUseOfBaseline - fileUseOfBaseline);
291
272
  return { score: deviation, filePredominant, baselinePredominant };
292
273
  }
293
274
  hasSignificantData(obj) {
294
275
  return Object.values(obj).reduce((a, b) => a + b, 0) >= 3;
295
276
  }
296
- /** Convert a typed distribution to a generic Record for comparison */
297
277
  toRecord(dist) {
298
278
  return {
299
279
  camelCase: dist.camelCase,
300
280
  snake_case: dist.snake_case,
301
281
  PascalCase: dist.PascalCase,
302
282
  SCREAMING_SNAKE: dist.SCREAMING_SNAKE,
283
+ 'kebab-case': dist['kebab-case'] || 0,
284
+ other: dist.other || 0,
303
285
  };
304
286
  }
305
287
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Test Quality Gate — Assertion Pattern Matchers and Test Anti-Patterns
3
+ *
4
+ * Contains assertion pattern matchers and test anti-pattern regexes.
5
+ */
6
+ /**
7
+ * Regex patterns for detecting assertions in JavaScript/TypeScript tests
8
+ */
9
+ export declare const JS_ASSERTION_PATTERNS: RegExp[];
10
+ /**
11
+ * Regex patterns for detecting mocks in JavaScript/TypeScript tests
12
+ */
13
+ export declare const JS_MOCK_PATTERNS: RegExp[];
14
+ /**
15
+ * Regex patterns for detecting assertions in Python tests
16
+ */
17
+ export declare const PYTHON_ASSERTION_PATTERNS: RegExp[];
18
+ /**
19
+ * Regex patterns for detecting mocks in Python tests
20
+ */
21
+ export declare const PYTHON_MOCK_PATTERNS: RegExp[];
22
+ /**
23
+ * Test block pattern for JavaScript/TypeScript
24
+ */
25
+ export declare const JS_TEST_START_PATTERN: RegExp;
26
+ /**
27
+ * Python test function pattern
28
+ */
29
+ export declare const PYTHON_TEST_FUNC_PATTERN: RegExp;
30
+ /**
31
+ * Tautological assertion patterns for JavaScript
32
+ */
33
+ export declare const JS_TAUTOLOGICAL_PATTERNS: RegExp[];
34
+ /**
35
+ * Variable tautology pattern for JavaScript
36
+ */
37
+ export declare const JS_VAR_TAUTOLOGY_PATTERN: RegExp;
38
+ /**
39
+ * Snapshot test patterns
40
+ */
41
+ export declare const SNAPSHOT_PATTERNS: RegExp[];
42
+ /**
43
+ * Python tautological patterns
44
+ */
45
+ export declare const PYTHON_TAUTOLOGICAL_PATTERNS: RegExp[];
46
+ /**
47
+ * Python fixture decorator pattern
48
+ */
49
+ export declare const PYTHON_FIXTURE_PATTERN: RegExp;
50
+ /**
51
+ * Python conftest file name
52
+ */
53
+ export declare const PYTHON_CONFTEST_NAME = "conftest.py";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Test Quality Gate — Assertion Pattern Matchers and Test Anti-Patterns
3
+ *
4
+ * Contains assertion pattern matchers and test anti-pattern regexes.
5
+ */
6
+ /**
7
+ * Regex patterns for detecting assertions in JavaScript/TypeScript tests
8
+ */
9
+ export const JS_ASSERTION_PATTERNS = [
10
+ /expect\s*\(/,
11
+ /assert\s*[.(]/,
12
+ /\.toEqual|\.toBe|\.toContain|\.toMatch|\.toThrow|\.toHaveBeenCalled|\.toHaveLength|\.toBeTruthy|\.toBeFalsy|\.toBeDefined|\.toBeNull|\.toBeUndefined|\.toBeGreaterThan|\.toBeLessThan|\.toHaveProperty|\.toStrictEqual|\.rejects|\.resolves/,
13
+ ];
14
+ /**
15
+ * Regex patterns for detecting mocks in JavaScript/TypeScript tests
16
+ */
17
+ export const JS_MOCK_PATTERNS = [
18
+ /jest\.fn\(/,
19
+ /vi\.fn\(/,
20
+ /jest\.mock\(/,
21
+ /vi\.mock\(/,
22
+ /jest\.spyOn\(/,
23
+ /vi\.spyOn\(/,
24
+ /sinon\.(stub|mock|spy)\(/,
25
+ ];
26
+ /**
27
+ * Regex patterns for detecting assertions in Python tests
28
+ */
29
+ export const PYTHON_ASSERTION_PATTERNS = [
30
+ /\bassert\s+/,
31
+ /self\.assert\w+\s*\(/,
32
+ /pytest\.raises\s*\(/,
33
+ /\.assert_called|\.assert_any_call/,
34
+ ];
35
+ /**
36
+ * Regex patterns for detecting mocks in Python tests
37
+ */
38
+ export const PYTHON_MOCK_PATTERNS = [
39
+ /mock\./,
40
+ /Mock\(/,
41
+ /patch\(/,
42
+ /MagicMock\(/,
43
+ ];
44
+ /**
45
+ * Test block pattern for JavaScript/TypeScript
46
+ */
47
+ export const JS_TEST_START_PATTERN = /^(?:it|test)\s*\(\s*['"`].*['"`]\s*,\s*(async\s+)?(?:\(\s*\)|function\s*\(\s*\)|\(\s*\{[^}]*\}\s*\))\s*(?:=>)?\s*\{/;
48
+ /**
49
+ * Python test function pattern
50
+ */
51
+ export const PYTHON_TEST_FUNC_PATTERN = /^(\s*)(?:def|async\s+def)\s+(test_\w+)\s*\(/;
52
+ /**
53
+ * Tautological assertion patterns for JavaScript
54
+ */
55
+ export const JS_TAUTOLOGICAL_PATTERNS = [
56
+ /expect\s*\(\s*true\s*\)\s*\.toBe\s*\(\s*true\s*\)/,
57
+ /expect\s*\(\s*false\s*\)\s*\.toBe\s*\(\s*false\s*\)/,
58
+ /expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)/,
59
+ ];
60
+ /**
61
+ * Variable tautology pattern for JavaScript
62
+ */
63
+ export const JS_VAR_TAUTOLOGY_PATTERN = /expect\s*\(\s*(\w+)\s*\)\s*\.(?:toBe|toEqual|toStrictEqual)\s*\(\s*(\w+)\s*\)/;
64
+ /**
65
+ * Snapshot test patterns
66
+ */
67
+ export const SNAPSHOT_PATTERNS = [
68
+ /\.toMatchSnapshot\s*\(/,
69
+ /\.toMatchInlineSnapshot\s*\(/,
70
+ ];
71
+ /**
72
+ * Python tautological patterns
73
+ */
74
+ export const PYTHON_TAUTOLOGICAL_PATTERNS = [
75
+ /\bassert\s+True\s*$/,
76
+ /\bassert\s+1\s*==\s*1/,
77
+ /self\.assertTrue\s*\(\s*True\s*\)/,
78
+ ];
79
+ /**
80
+ * Python fixture decorator pattern
81
+ */
82
+ export const PYTHON_FIXTURE_PATTERN = /^@pytest\.fixture/;
83
+ /**
84
+ * Python conftest file name
85
+ */
86
+ export const PYTHON_CONFTEST_NAME = 'conftest.py';