@rigour-labs/core 6.5.2 → 6.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +1 -1
  2. package/dist/context/automatic-index.d.ts +5 -0
  3. package/dist/context/automatic-index.js +24 -3
  4. package/dist/context/index-status.test.d.ts +1 -0
  5. package/dist/context/index-status.test.js +26 -0
  6. package/dist/gates/dedupe-failures.test.d.ts +1 -0
  7. package/dist/gates/dedupe-failures.test.js +12 -0
  8. package/dist/gates/deprecated-dependencies.d.ts +27 -0
  9. package/dist/gates/deprecated-dependencies.js +104 -0
  10. package/dist/gates/deprecated-dependencies.test.d.ts +1 -0
  11. package/dist/gates/deprecated-dependencies.test.js +55 -0
  12. package/dist/gates/runner.d.ts +7 -1
  13. package/dist/gates/runner.js +25 -11
  14. package/dist/gates/security-patterns-data.js +2 -1
  15. package/dist/gates/security-patterns-owasp.test.js +15 -0
  16. package/dist/gates/semantic-bugs.js +1 -1
  17. package/dist/gates/unindexed-reads/coverage.d.ts +15 -0
  18. package/dist/gates/unindexed-reads/coverage.js +24 -0
  19. package/dist/gates/unindexed-reads/index.d.ts +20 -0
  20. package/dist/gates/unindexed-reads/index.js +98 -0
  21. package/dist/gates/unindexed-reads/queries.d.ts +17 -0
  22. package/dist/gates/unindexed-reads/queries.js +147 -0
  23. package/dist/gates/unindexed-reads/queries.test.d.ts +1 -0
  24. package/dist/gates/unindexed-reads/queries.test.js +54 -0
  25. package/dist/gates/unindexed-reads/schema.d.ts +38 -0
  26. package/dist/gates/unindexed-reads/schema.js +256 -0
  27. package/dist/gates/unindexed-reads/schema.test.d.ts +1 -0
  28. package/dist/gates/unindexed-reads/schema.test.js +81 -0
  29. package/dist/gates/unindexed-reads/sql.d.ts +21 -0
  30. package/dist/gates/unindexed-reads/sql.js +131 -0
  31. package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +1 -0
  32. package/dist/gates/unindexed-reads/unindexed-reads.test.js +90 -0
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +4 -1
  35. package/dist/inference/brain-placeholder.test.d.ts +1 -0
  36. package/dist/inference/brain-placeholder.test.js +28 -0
  37. package/dist/inference/llama-engine-checksum.test.d.ts +1 -0
  38. package/dist/inference/llama-engine-checksum.test.js +27 -0
  39. package/dist/inference/llama-engine.d.ts +4 -1
  40. package/dist/inference/llama-engine.js +15 -8
  41. package/dist/memory/recall.d.ts +37 -0
  42. package/dist/memory/recall.js +93 -0
  43. package/dist/memory/recall.test.d.ts +1 -0
  44. package/dist/memory/recall.test.js +36 -0
  45. package/dist/pattern-index/assess.d.ts +16 -0
  46. package/dist/pattern-index/assess.js +37 -0
  47. package/dist/pattern-index/embeddings.d.ts +19 -0
  48. package/dist/pattern-index/embeddings.js +16 -0
  49. package/dist/pattern-index/index.d.ts +3 -2
  50. package/dist/pattern-index/index.js +4 -2
  51. package/dist/pattern-index/indexer.js +15 -7
  52. package/dist/pattern-index/matcher.d.ts +6 -0
  53. package/dist/pattern-index/matcher.js +13 -3
  54. package/dist/pattern-index/pattern-reuse.test.d.ts +1 -0
  55. package/dist/pattern-index/pattern-reuse.test.js +76 -0
  56. package/dist/pattern-index/staleness.d.ts +2 -15
  57. package/dist/pattern-index/staleness.js +1 -78
  58. package/dist/pattern-index/types.d.ts +2 -0
  59. package/dist/review/agent-fixes.d.ts +4 -0
  60. package/dist/review/agent-fixes.js +10 -7
  61. package/dist/review/agent-fixes.test.js +4 -4
  62. package/dist/review/changed-lines.d.ts +2 -1
  63. package/dist/review/changed-lines.js +12 -2
  64. package/dist/review/check-outcomes.d.ts +31 -0
  65. package/dist/review/check-outcomes.js +81 -0
  66. package/dist/review/check-outcomes.test.d.ts +1 -0
  67. package/dist/review/check-outcomes.test.js +56 -0
  68. package/dist/review/migration-order.d.ts +5 -0
  69. package/dist/review/migration-order.js +70 -0
  70. package/dist/review/migration-order.test.d.ts +1 -0
  71. package/dist/review/migration-order.test.js +62 -0
  72. package/dist/review/quiet.d.ts +2 -0
  73. package/dist/review/quiet.js +12 -3
  74. package/dist/review/review.d.ts +2 -0
  75. package/dist/review/review.js +7 -2
  76. package/dist/review/review.test.js +11 -0
  77. package/dist/semantic/ast.d.ts +1 -0
  78. package/dist/semantic/ast.js +4 -0
  79. package/dist/semantic/learn/compile.js +3 -1
  80. package/dist/semantic/learn/learn.js +40 -5
  81. package/dist/semantic/types.d.ts +2 -0
  82. package/dist/services/context-scope-summary.test.d.ts +1 -0
  83. package/dist/services/context-scope-summary.test.js +17 -0
  84. package/dist/services/context-telemetry-service.js +2 -1
  85. package/dist/storage/context-telemetry.js +33 -19
  86. package/dist/storage/db.js +52 -1
  87. package/dist/storage/findings.js +1 -1
  88. package/dist/storage/fix-lessons.d.ts +13 -0
  89. package/dist/storage/fix-lessons.js +84 -0
  90. package/dist/storage/fix-lessons.test.d.ts +1 -0
  91. package/dist/storage/fix-lessons.test.js +61 -0
  92. package/dist/storage/index.d.ts +2 -1
  93. package/dist/storage/index.js +2 -1
  94. package/dist/storage/lessons.d.ts +16 -2
  95. package/dist/storage/lessons.js +27 -61
  96. package/dist/storage/lessons.test.js +1 -15
  97. package/dist/storage/local-memory.js +39 -5
  98. package/dist/storage/local-memory.test.d.ts +1 -0
  99. package/dist/storage/local-memory.test.js +55 -0
  100. package/dist/storage/patterns.d.ts +1 -1
  101. package/dist/storage/patterns.js +1 -1
  102. package/dist/storage/share-memory.test.d.ts +1 -0
  103. package/dist/storage/share-memory.test.js +34 -0
  104. package/dist/storage/team-vector-store.js +2 -1
  105. package/dist/storage/telemetry-scope.test.d.ts +1 -0
  106. package/dist/storage/telemetry-scope.test.js +38 -0
  107. package/dist/templates/universal-config.js +11 -0
  108. package/dist/types/index.d.ts +132 -0
  109. package/dist/types/index.js +15 -0
  110. package/package.json +7 -7
@@ -56,6 +56,22 @@ export async function generateEmbedding(text) {
56
56
  return [];
57
57
  }
58
58
  }
59
+ /**
60
+ * Version of the text patterns are embedded by. An index embedded with an older text is
61
+ * re-embedded on its next enrichment.
62
+ * 2: the name split into words (`retryWithBackoff` → "retry with backoff"). Measured by
63
+ * scripts/eval/pattern-intent.mjs: recall by intent 87% at 0.30 with no false matches,
64
+ * against 13% for the raw name at the old 0.60 bar.
65
+ */
66
+ export const EMBEDDING_TEXT_VERSION = 2;
67
+ export function patternEmbeddingText(pattern) {
68
+ const words = pattern.name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').toLowerCase();
69
+ return `${words} ${pattern.type} ${pattern.description ?? ''}`.trim();
70
+ }
71
+ /** A pattern's embedding, rounded to 4 decimals: a third of the size on disk, no change to ranking. */
72
+ export async function embedPattern(pattern) {
73
+ return (await generateEmbedding(patternEmbeddingText(pattern))).map(x => Math.round(x * 1e4) / 1e4);
74
+ }
59
75
  /**
60
76
  * Calculate cosine similarity between two vectors.
61
77
  */
@@ -5,8 +5,9 @@
5
5
  */
6
6
  export type { PatternType, PatternEntry, PatternIndex, PatternIndexConfig, PatternIndexStats, IndexedFile, PatternMatchResult, PatternMatch, PatternOverride, StalenessResult, StalenessIssue, DeprecationEntry } from './types.js';
7
7
  export { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath } from './indexer.js';
8
- export { PatternMatcher, checkPatternDuplicate, type MatcherConfig } from './matcher.js';
8
+ export { PatternMatcher, checkPatternDuplicate, SEMANTIC_MATCH_FLOOR, type MatcherConfig } from './matcher.js';
9
+ export { assessPattern, type PatternQuery, type PatternAssessment } from './assess.js';
9
10
  export { StalenessDetector, checkCodeStaleness } from './staleness.js';
10
11
  export { SecurityDetector } from './security.js';
11
12
  export { OverrideManager, loadConfigOverrides } from './overrides.js';
12
- export { generateEmbedding, semanticSearch, cosineSimilarity } from './embeddings.js';
13
+ export { generateEmbedding, semanticSearch, cosineSimilarity, embedPattern, patternEmbeddingText, EMBEDDING_TEXT_VERSION } from './embeddings.js';
@@ -6,7 +6,9 @@
6
6
  // Indexer
7
7
  export { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath } from './indexer.js';
8
8
  // Matcher
9
- export { PatternMatcher, checkPatternDuplicate } from './matcher.js';
9
+ export { PatternMatcher, checkPatternDuplicate, SEMANTIC_MATCH_FLOOR } from './matcher.js';
10
+ // Reuse check before writing
11
+ export { assessPattern } from './assess.js';
10
12
  // Staleness Detection
11
13
  export { StalenessDetector, checkCodeStaleness } from './staleness.js';
12
14
  // Security Detection
@@ -14,4 +16,4 @@ export { SecurityDetector } from './security.js';
14
16
  // Override Management
15
17
  export { OverrideManager, loadConfigOverrides } from './overrides.js';
16
18
  // Embeddings
17
- export { generateEmbedding, semanticSearch, cosineSimilarity } from './embeddings.js';
19
+ export { generateEmbedding, semanticSearch, cosineSimilarity, embedPattern, patternEmbeddingText, EMBEDDING_TEXT_VERSION } from './embeddings.js';
@@ -13,7 +13,7 @@ import * as path from 'path';
13
13
  import { globby } from 'globby';
14
14
  import ts from 'typescript';
15
15
  import micromatch from 'micromatch';
16
- import { generateEmbedding } from './embeddings.js';
16
+ import { EMBEDDING_TEXT_VERSION, embedPattern } from './embeddings.js';
17
17
  import { hashContent } from './indexer-helpers.js';
18
18
  import { extractGoPatterns, extractRustPatterns, extractJVMStylePatterns, extractPythonPatterns, extractGenericCPatterns, } from './indexer-lang.js';
19
19
  import { nodeToPattern } from './indexer-ts.js';
@@ -88,7 +88,7 @@ export class PatternIndexer {
88
88
  for (let i = 0; i < patterns.length; i += BATCH_SIZE) {
89
89
  const batch = patterns.slice(i, i + BATCH_SIZE);
90
90
  await Promise.all(batch.map(async (pattern) => {
91
- pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
91
+ pattern.embedding = await embedPattern(pattern);
92
92
  }));
93
93
  }
94
94
  }
@@ -96,6 +96,8 @@ export class PatternIndexer {
96
96
  const stats = this.calculateStats(patterns, indexedFiles, endTime - startTime);
97
97
  return {
98
98
  version: INDEX_VERSION,
99
+ // Set only when this build embedded the patterns.
100
+ ...(this.config.useEmbeddings ? { embeddingTextVersion: EMBEDDING_TEXT_VERSION } : {}),
99
101
  lastUpdated: new Date().toISOString(),
100
102
  rootDir: this.rootDir,
101
103
  patterns,
@@ -142,7 +144,7 @@ export class PatternIndexer {
142
144
  const batch = updatedPatterns.slice(i, i + BATCH_SIZE);
143
145
  await Promise.all(batch.map(async (pattern) => {
144
146
  if (!pattern.embedding) {
145
- pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
147
+ pattern.embedding = await embedPattern(pattern);
146
148
  }
147
149
  }));
148
150
  }
@@ -151,6 +153,8 @@ export class PatternIndexer {
151
153
  const stats = this.calculateStats(updatedPatterns, updatedFiles, endTime - startTime);
152
154
  return {
153
155
  version: INDEX_VERSION,
156
+ // Kept from the index being updated, so an incremental update never looks stale.
157
+ embeddingTextVersion: this.config.useEmbeddings ? EMBEDDING_TEXT_VERSION : existingIndex.embeddingTextVersion,
154
158
  lastUpdated: new Date().toISOString(),
155
159
  rootDir: this.rootDir,
156
160
  patterns: updatedPatterns,
@@ -182,7 +186,7 @@ export class PatternIndexer {
182
186
  const filePatterns = await this.extractPatterns(absolutePath, content);
183
187
  if (this.config.useEmbeddings) {
184
188
  await Promise.all(filePatterns.map(async (pattern) => {
185
- pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
189
+ pattern.embedding = await embedPattern(pattern);
186
190
  }));
187
191
  }
188
192
  patterns.push(...filePatterns);
@@ -200,6 +204,7 @@ export class PatternIndexer {
200
204
  }
201
205
  return {
202
206
  version: INDEX_VERSION,
207
+ embeddingTextVersion: this.config.useEmbeddings ? EMBEDDING_TEXT_VERSION : existingIndex.embeddingTextVersion,
203
208
  lastUpdated: new Date().toISOString(),
204
209
  rootDir: this.rootDir,
205
210
  patterns,
@@ -210,20 +215,23 @@ export class PatternIndexer {
210
215
  async enrichIndex(existingIndex) {
211
216
  const startTime = Date.now();
212
217
  const patterns = existingIndex.patterns.map(pattern => ({ ...pattern }));
213
- const pending = patterns.filter(pattern => !pattern.embedding?.length);
218
+ // An index embedded from an older text is re-embedded whole: mixed vectors would not compare.
219
+ const stale = existingIndex.embeddingTextVersion !== EMBEDDING_TEXT_VERSION;
220
+ const pending = patterns.filter(pattern => stale || !pattern.embedding?.length);
214
221
  if (pending.length > 0) {
215
- pending[0].embedding = await generateEmbedding(`${pending[0].name} ${pending[0].type} ${pending[0].description}`);
222
+ pending[0].embedding = await embedPattern(pending[0]);
216
223
  if (!pending[0].embedding.length)
217
224
  return existingIndex;
218
225
  }
219
226
  const batchSize = 10;
220
227
  for (let i = 1; i < pending.length; i += batchSize) {
221
228
  await Promise.all(pending.slice(i, i + batchSize).map(async (pattern) => {
222
- pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
229
+ pattern.embedding = await embedPattern(pattern);
223
230
  }));
224
231
  }
225
232
  return {
226
233
  ...existingIndex,
234
+ embeddingTextVersion: EMBEDDING_TEXT_VERSION,
227
235
  lastUpdated: new Date().toISOString(),
228
236
  patterns,
229
237
  stats: this.calculateStats(patterns, existingIndex.files, Date.now() - startTime),
@@ -8,6 +8,12 @@
8
8
  * 4. Keyword/semantic match
9
9
  */
10
10
  import type { PatternIndex, PatternMatchResult, PatternOverride } from './types.js';
11
+ /**
12
+ * The similarity a match by meaning needs. Measured by scripts/eval/pattern-intent.mjs: at 0.30,
13
+ * 87% of intents found the existing function with no false matches; name-based strategies keep
14
+ * minConfidence, which this cosine scale does not share.
15
+ */
16
+ export declare const SEMANTIC_MATCH_FLOOR = 0.3;
11
17
  /**
12
18
  * Configuration for pattern matching.
13
19
  */
@@ -7,7 +7,13 @@
7
7
  * 3. Signature match
8
8
  * 4. Keyword/semantic match
9
9
  */
10
- import { generateEmbedding, cosineSimilarity } from './embeddings.js';
10
+ import { generateEmbedding, cosineSimilarity, patternEmbeddingText } from './embeddings.js';
11
+ /**
12
+ * The similarity a match by meaning needs. Measured by scripts/eval/pattern-intent.mjs: at 0.30,
13
+ * 87% of intents found the existing function with no false matches; name-based strategies keep
14
+ * minConfidence, which this cosine scale does not share.
15
+ */
16
+ export const SEMANTIC_MATCH_FLOOR = 0.3;
11
17
  /** Default matcher configuration */
12
18
  const DEFAULT_MATCHER_CONFIG = {
13
19
  minConfidence: 60,
@@ -48,7 +54,8 @@ export class PatternMatcher {
48
54
  // Pre-calculate query embedding if semantic search is enabled
49
55
  let queryEmbedding = null;
50
56
  if (this.config.useEmbeddings && (query.intent || query.name)) {
51
- queryEmbedding = await generateEmbedding(`${query.name || ''} ${query.intent || ''}`);
57
+ // The same text shape patterns are embedded by: the name split into words, then the intent.
58
+ queryEmbedding = await generateEmbedding(`${patternEmbeddingText({ name: query.name || '', type: '' })} ${query.intent || ''}`.trim());
52
59
  }
53
60
  // Check for override first
54
61
  if (query.name && this.hasOverride(query.name)) {
@@ -118,7 +125,10 @@ export class PatternMatcher {
118
125
  maxConfidence = confidence;
119
126
  }
120
127
  }
121
- if (currentBest && maxConfidence >= this.config.minConfidence) {
128
+ const accepted = currentBest?.matchType === 'semantic'
129
+ ? maxConfidence >= SEMANTIC_MATCH_FLOOR * 100
130
+ : maxConfidence >= this.config.minConfidence;
131
+ if (currentBest && accepted) {
122
132
  matches.push(currentBest);
123
133
  }
124
134
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ /** Topic vectors: one dimension per word, so similarity between texts is predictable. */
6
+ const TOPICS = ['retry', 'backoff', 'request', 'date', 'format', 'chart'];
7
+ const topicVector = (text) => TOPICS.map(t => (text.toLowerCase().includes(t) ? 1 : 0));
8
+ vi.mock('./embeddings.js', async (importOriginal) => {
9
+ const real = await importOriginal();
10
+ return {
11
+ ...real,
12
+ generateEmbedding: async (text) => topicVector(text),
13
+ embedPattern: async (p) => topicVector(real.patternEmbeddingText(p)),
14
+ };
15
+ });
16
+ const { patternEmbeddingText, EMBEDDING_TEXT_VERSION } = await import('./embeddings.js');
17
+ const { PatternMatcher, SEMANTIC_MATCH_FLOOR } = await import('./matcher.js');
18
+ const { PatternIndexer } = await import('./indexer.js');
19
+ const { assessPattern } = await import('./assess.js');
20
+ const pattern = (name, extra = {}) => ({
21
+ id: name, type: 'function', name, file: 'src/util.ts', line: 1, endLine: 3, signature: '', description: '',
22
+ keywords: [], hash: name, exported: true, usageCount: 0, indexedAt: '2026-10-02T00:00:00Z', ...extra,
23
+ });
24
+ const index = (patterns, version) => ({
25
+ version: '1.0.0', lastUpdated: '', rootDir: '/x', patterns, files: [],
26
+ stats: { totalPatterns: patterns.length, totalFiles: 1, byType: {}, indexDurationMs: 0 },
27
+ ...(version ? { embeddingTextVersion: version } : {}),
28
+ });
29
+ describe('pattern embedding text', () => {
30
+ it('reads a name as words, which is what lets an intent find it', () => {
31
+ expect(patternEmbeddingText({ name: 'retryWithBackoff', type: 'function' })).toBe('retry with backoff function');
32
+ expect(patternEmbeddingText({ name: 'parse_csv-rows', type: 'function', description: 'Rows from a file' })).toBe('parse csv rows function Rows from a file');
33
+ });
34
+ });
35
+ describe('matching by meaning', () => {
36
+ it(`accepts a match by meaning at ${SEMANTIC_MATCH_FLOOR}, below the bar name matching keeps`, async () => {
37
+ // The query shares 1 of 3 topics with the pattern: cosine 0.58, under the old 0.60.
38
+ const retry = pattern('retryWithBackoff', { embedding: topicVector('retry backoff request') });
39
+ const result = await new PatternMatcher(index([retry]), { useFuzzy: false }).match({ name: 'callAgain', intent: 'make the request work on a flaky network' });
40
+ expect(result.matches.map(m => [m.pattern.name, m.matchType])).toEqual([['retryWithBackoff', 'semantic']]);
41
+ expect(result.action).toBe('WARN');
42
+ });
43
+ it('returns nothing for an unrelated intent', async () => {
44
+ const retry = pattern('retryWithBackoff', { embedding: topicVector('retry backoff request') });
45
+ const result = await new PatternMatcher(index([retry]), { useFuzzy: false }).match({ name: 'drawPie', intent: 'render a pie chart' });
46
+ expect(result.status).toBe('NO_MATCH');
47
+ });
48
+ });
49
+ describe('enrichIndex', () => {
50
+ it('re-embeds an index built from an older embedding text, and marks the version', async () => {
51
+ const stale = index([pattern('formatDate', { embedding: [9, 9, 9, 9, 9, 9] })], 1);
52
+ const enriched = await new PatternIndexer('/x', { useEmbeddings: true }).enrichIndex(stale);
53
+ expect(enriched.embeddingTextVersion).toBe(EMBEDDING_TEXT_VERSION);
54
+ expect(enriched.patterns[0].embedding).toEqual(topicVector('format date function'));
55
+ });
56
+ });
57
+ describe('assessPattern', () => {
58
+ let cwd;
59
+ beforeEach(() => {
60
+ cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'assess-'));
61
+ fs.mkdirSync(path.join(cwd, 'src'));
62
+ fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { 'react-dom': '18.2.0' } }));
63
+ fs.writeFileSync(path.join(cwd, 'src/mount.ts'), 'export function mountApp(el) {\n ReactDOM.render(<App />, el);\n}\n');
64
+ });
65
+ afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
66
+ it('blocks an exact duplicate and reports what is deprecated inside the function it would reuse', async () => {
67
+ const mount = pattern('mountApp', { file: 'src/mount.ts', line: 1, endLine: 3 });
68
+ const result = await assessPattern(cwd, index([mount]), { name: 'mountApp' });
69
+ expect(result.action).toBe('BLOCK');
70
+ expect(result.deprecations.map(d => d.replacement)).toEqual(['createRoot(container).render(<App />)']);
71
+ });
72
+ it('allows a name nothing resembles', async () => {
73
+ const result = await assessPattern(cwd, index([pattern('mountApp', { file: 'src/mount.ts' })]), { name: 'exportPdfReport' });
74
+ expect(result).toEqual({ action: 'ALLOW', suggestion: '', deprecations: [] });
75
+ });
76
+ });
@@ -4,7 +4,7 @@
4
4
  * Detects when AI is suggesting deprecated or outdated patterns.
5
5
  * Uses package.json analysis and a deprecation database.
6
6
  */
7
- import type { StalenessResult, StalenessIssue, DeprecationEntry } from './types.js';
7
+ import type { StalenessResult, DeprecationEntry } from './types.js';
8
8
  /**
9
9
  * Project context extracted from package.json and other config files.
10
10
  */
@@ -21,18 +21,7 @@ export declare class StalenessDetector {
21
21
  private deprecations;
22
22
  private projectContext;
23
23
  private rootDir;
24
- private remoteRulesUrl;
25
24
  constructor(rootDir: string, customDeprecations?: DeprecationEntry[]);
26
- /**
27
- * Fetch latest deprecation rules from Rigour's remote registry.
28
- * This ensures the tool stays up-to-date even without a package update.
29
- */
30
- syncRemoteRules(): Promise<number>;
31
- /**
32
- * Check NPM registry for live deprecation status of project dependencies.
33
- * This is the ultimate "up-to-date" check.
34
- */
35
- checkLiveRegistry(context: ProjectContext): Promise<StalenessIssue[]>;
36
25
  /**
37
26
  * Load project context from package.json.
38
27
  */
@@ -40,9 +29,7 @@ export declare class StalenessDetector {
40
29
  /**
41
30
  * Check code for staleness issues.
42
31
  */
43
- checkStaleness(code: string, filePath?: string, options?: {
44
- live?: boolean;
45
- }): Promise<StalenessResult>;
32
+ checkStaleness(code: string, filePath?: string): Promise<StalenessResult>;
46
33
  /**
47
34
  * Check if project has a library.
48
35
  */
@@ -19,82 +19,10 @@ export class StalenessDetector {
19
19
  deprecations;
20
20
  projectContext = null;
21
21
  rootDir;
22
- remoteRulesUrl = 'https://raw.githubusercontent.com/rigour-labs/rules/main/deprecations.json';
23
22
  constructor(rootDir, customDeprecations = []) {
24
23
  this.rootDir = rootDir;
25
24
  this.deprecations = [...BUILT_IN_DEPRECATIONS, ...customDeprecations];
26
25
  }
27
- /**
28
- * Fetch latest deprecation rules from Rigour's remote registry.
29
- * This ensures the tool stays up-to-date even without a package update.
30
- */
31
- async syncRemoteRules() {
32
- try {
33
- // Using dynamic import for fetch to avoid Node < 18 issues
34
- const response = await fetch(this.remoteRulesUrl);
35
- if (!response.ok)
36
- throw new Error(`HTTP error! status: ${response.status}`);
37
- const data = await response.json();
38
- if (data.deprecations && Array.isArray(data.deprecations)) {
39
- // Merge remote rules, avoiding duplicates
40
- const existingPatterns = new Set(this.deprecations.map(d => d.pattern));
41
- const newRules = data.deprecations.filter((d) => !existingPatterns.has(d.pattern));
42
- this.deprecations.push(...newRules);
43
- return newRules.length;
44
- }
45
- return 0;
46
- }
47
- catch (error) {
48
- console.warn('Failed to sync remote rules, using built-in database:', error);
49
- return 0;
50
- }
51
- }
52
- /**
53
- * Check NPM registry for live deprecation status of project dependencies.
54
- * This is the ultimate "up-to-date" check.
55
- */
56
- async checkLiveRegistry(context) {
57
- const issues = [];
58
- const { execa } = await import('execa');
59
- // We only check top-level dependencies to avoid noise/performance hits
60
- const deps = Object.keys(context.dependencies);
61
- for (const dep of deps) {
62
- try {
63
- // Run 'npm info <package> --json' to get metadata
64
- const { stdout } = await execa('npm', ['info', dep, '--json']);
65
- const info = JSON.parse(stdout);
66
- // 1. Check if package is deprecated
67
- if (info.deprecated) {
68
- issues.push({
69
- line: 0, // Package-level
70
- pattern: dep,
71
- severity: 'error',
72
- reason: `Package "${dep}" is marked as DEPRECATED in NPM registry: ${info.deprecated}`,
73
- replacement: 'Check package README for suggested alternatives',
74
- docs: `https://www.npmjs.com/package/${dep}`
75
- });
76
- }
77
- // 2. Check for latest version staleness
78
- const current = context.dependencies[dep].replace(/^[\^~>=<]+/, '');
79
- const latest = info['dist-tags']?.latest;
80
- if (latest && semver.major(latest) > semver.major(current)) {
81
- issues.push({
82
- line: 0,
83
- pattern: dep,
84
- severity: 'info',
85
- reason: `Package "${dep}" has a new major version available (${latest}). Your version: ${current}`,
86
- replacement: `npm install ${dep}@latest`,
87
- docs: `https://www.npmjs.com/package/${dep}`
88
- });
89
- }
90
- }
91
- catch (error) {
92
- // Silently skip if npm check fails
93
- continue;
94
- }
95
- }
96
- return issues;
97
- }
98
26
  /**
99
27
  * Load project context from package.json.
100
28
  */
@@ -125,7 +53,7 @@ export class StalenessDetector {
125
53
  /**
126
54
  * Check code for staleness issues.
127
55
  */
128
- async checkStaleness(code, filePath, options = {}) {
56
+ async checkStaleness(code, filePath) {
129
57
  const context = await this.loadProjectContext();
130
58
  const issues = [];
131
59
  // 1. Check built-in/remote rules
@@ -161,11 +89,6 @@ export class StalenessDetector {
161
89
  regex.lastIndex = 0;
162
90
  }
163
91
  }
164
- // 2. Perform live registry check if requested (only once per run/file usually)
165
- if (options.live) {
166
- const liveIssues = await this.checkLiveRegistry(context);
167
- issues.push(...liveIssues);
168
- }
169
92
  // Determine overall status
170
93
  let status = 'FRESH';
171
94
  if (issues.some(i => i.severity === 'error')) {
@@ -49,6 +49,8 @@ export interface PatternEntry {
49
49
  export interface PatternIndex {
50
50
  /** Index format version */
51
51
  version: string;
52
+ /** Version of the text its embeddings were built from (embeddings.ts EMBEDDING_TEXT_VERSION). */
53
+ embeddingTextVersion?: number;
52
54
  /** When the index was last updated */
53
55
  lastUpdated: string;
54
56
  /** Root directory that was indexed */
@@ -3,6 +3,8 @@ export interface ResolvedFix {
3
3
  id: string;
4
4
  file: string;
5
5
  rule: string;
6
+ title?: string;
7
+ details?: string;
6
8
  before: string;
7
9
  after: string;
8
10
  resolvedAt: string;
@@ -10,6 +12,8 @@ export interface ResolvedFix {
10
12
  export interface FixCapture {
11
13
  opened: number;
12
14
  resolved: number;
15
+ /** The fixes this review resolved, for turning into lessons (storage/fix-lessons.ts). */
16
+ fixes: ResolvedFix[];
13
17
  }
14
18
  /** Record a review's findings: open new ones, resolve open ones the review no longer reports. */
15
19
  export declare function recordReviewOutcome(cwd: string, findings: Failure[], reviewedFiles: string[]): FixCapture;
@@ -13,6 +13,7 @@
13
13
  import crypto from 'crypto';
14
14
  import fs from 'fs';
15
15
  import path from 'path';
16
+ import { checkId, recordOutcome } from './check-outcomes.js';
16
17
  const DIR = path.join('.rigour', 'agent-fixes');
17
18
  const MAX_FILE_BYTES = 200_000;
18
19
  const OPEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -20,13 +21,14 @@ const OPEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
20
21
  export function recordReviewOutcome(cwd, findings, reviewedFiles) {
21
22
  const open = readOpen(cwd);
22
23
  const current = new Map(findings.flatMap(f => (f.files?.[0] ? [[`${f.id}:${f.files[0]}`, f]] : [])));
23
- const capture = { resolved: resolveGone(cwd, open, current, new Set(reviewedFiles)), opened: openNew(cwd, open, current) };
24
+ const fixes = resolveGone(cwd, open, current, new Set(reviewedFiles));
25
+ const opened = openNew(cwd, open, current);
24
26
  writeOpen(cwd, open);
25
- return capture;
27
+ return { opened, resolved: fixes.length, fixes };
26
28
  }
27
29
  /** Open findings this review no longer reports: resolved when their file was reviewed and changed. */
28
30
  function resolveGone(cwd, open, current, reviewed) {
29
- let resolved = 0;
31
+ const resolved = [];
30
32
  for (const [key, entry] of Object.entries(open)) {
31
33
  if (current.has(key))
32
34
  continue;
@@ -37,8 +39,8 @@ function resolveGone(cwd, open, current, reviewed) {
37
39
  }
38
40
  const after = readSmall(cwd, entry.file);
39
41
  if (after !== null && after !== entry.before) {
40
- writeResolved(cwd, entry, after);
41
- resolved++;
42
+ resolved.push(writeResolved(cwd, entry, after));
43
+ recordOutcome(cwd, checkId({ rule: entry.rule, title: entry.title }), 'fixed');
42
44
  }
43
45
  delete open[key];
44
46
  }
@@ -53,7 +55,7 @@ function openNew(cwd, open, current) {
53
55
  const before = readSmall(cwd, file);
54
56
  if (before === null)
55
57
  continue;
56
- open[key] = { file, rule: finding.id, before, openedAt: new Date().toISOString() };
58
+ open[key] = { file, rule: finding.id, title: finding.title, details: finding.details, before, openedAt: new Date().toISOString() };
57
59
  opened++;
58
60
  }
59
61
  return opened;
@@ -96,7 +98,7 @@ function writeOpen(cwd, open) {
96
98
  }
97
99
  function writeResolved(cwd, entry, after) {
98
100
  const id = crypto.createHash('sha256').update(`${entry.rule}\0${entry.file}\0${entry.before}\0${after}`).digest('hex').slice(0, 16);
99
- const fix = { id, file: entry.file, rule: entry.rule, before: entry.before, after, resolvedAt: new Date().toISOString() };
101
+ const fix = { id, file: entry.file, rule: entry.rule, title: entry.title, details: entry.details, before: entry.before, after, resolvedAt: new Date().toISOString() };
100
102
  try {
101
103
  fs.mkdirSync(path.join(cwd, DIR, 'resolved'), { recursive: true });
102
104
  fs.writeFileSync(path.join(cwd, DIR, 'resolved', `${id}.json`), JSON.stringify(fix));
@@ -104,6 +106,7 @@ function writeResolved(cwd, entry, after) {
104
106
  catch {
105
107
  // Best-effort, as above.
106
108
  }
109
+ return fix;
107
110
  }
108
111
  function readSmall(cwd, file) {
109
112
  try {
@@ -12,21 +12,21 @@ describe('recordReviewOutcome', () => {
12
12
  });
13
13
  afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
14
14
  it('stores the before and after once a reviewed file no longer has the finding', () => {
15
- expect(recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts'])).toEqual({ opened: 1, resolved: 0 });
15
+ expect(recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts'])).toMatchObject({ opened: 1, resolved: 0 });
16
16
  fs.writeFileSync(path.join(cwd, 'a.ts'), 'after\n');
17
- expect(recordReviewOutcome(cwd, [], ['a.ts'])).toEqual({ opened: 0, resolved: 1 });
17
+ expect(recordReviewOutcome(cwd, [], ['a.ts'])).toMatchObject({ opened: 0, resolved: 1 });
18
18
  const [fix] = listResolvedFixes(cwd);
19
19
  expect(fix).toMatchObject({ file: 'a.ts', rule: 'semantic-bugs', before: 'before\n', after: 'after\n' });
20
20
  expect(openFindingCount(cwd)).toBe(0);
21
21
  });
22
22
  it('keeps a finding open when its file was not part of the review (committed, not fixed)', () => {
23
23
  recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
24
- expect(recordReviewOutcome(cwd, [], ['other.ts'])).toEqual({ opened: 0, resolved: 0 });
24
+ expect(recordReviewOutcome(cwd, [], ['other.ts'])).toMatchObject({ opened: 0, resolved: 0 });
25
25
  expect(openFindingCount(cwd)).toBe(1);
26
26
  });
27
27
  it('does not count a finding that vanished without an edit as a fix', () => {
28
28
  recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
29
- expect(recordReviewOutcome(cwd, [], ['a.ts'])).toEqual({ opened: 0, resolved: 0 });
29
+ expect(recordReviewOutcome(cwd, [], ['a.ts'])).toMatchObject({ opened: 0, resolved: 0 });
30
30
  expect(listResolvedFixes(cwd)).toEqual([]);
31
31
  });
32
32
  });
@@ -14,6 +14,7 @@
14
14
  * change gets the same verdict everywhere.
15
15
  */
16
16
  import type { Failure } from '../types/index.js';
17
+ import type { RemovedBlock } from '../utils/diff.js';
17
18
  import { type LineSpan } from './changed-function-spans.js';
18
19
  export interface ChangedLineSplit {
19
20
  findings: Failure[];
@@ -22,4 +23,4 @@ export interface ChangedLineSplit {
22
23
  unlocated: number;
23
24
  outside: number;
24
25
  }
25
- export declare function splitByChangedLines(failures: Failure[], changedLines: Record<string, Set<number>>, spans?: Record<string, LineSpan[]>): ChangedLineSplit;
26
+ export declare function splitByChangedLines(failures: Failure[], changedLines: Record<string, Set<number>>, spans?: Record<string, LineSpan[]>, removed?: Record<string, RemovedBlock[]>): ChangedLineSplit;
@@ -1,5 +1,5 @@
1
1
  import { anchorInChangedFunction } from './changed-function-spans.js';
2
- export function splitByChangedLines(failures, changedLines, spans = {}) {
2
+ export function splitByChangedLines(failures, changedLines, spans = {}, removed = {}) {
3
3
  const split = { findings: [], fileFindings: [], contextFindings: [], unlocated: 0, outside: 0 };
4
4
  for (const failure of failures) {
5
5
  const files = failure.files ?? [];
@@ -12,7 +12,7 @@ export function splitByChangedLines(failures, changedLines, spans = {}) {
12
12
  else
13
13
  split.outside++;
14
14
  }
15
- else if (files.some(file => changedLines[file]?.has(failure.line))) {
15
+ else if (files.some(file => changedLines[file]?.has(failure.line) || removedInside(failure, removed[file]))) {
16
16
  split.findings.push(failure);
17
17
  }
18
18
  else if (failure.provenance === 'deep-analysis') {
@@ -37,3 +37,13 @@ function placeDeepFinding(failure, files, changedLines, spans, split) {
37
37
  else
38
38
  split.outside++;
39
39
  }
40
+ /**
41
+ * Whether the change deleted lines inside a multi-line finding (its line to endLine), such as an
42
+ * option removed from a call whose first line did not change: that call is part of the change.
43
+ */
44
+ function removedInside(failure, blocks) {
45
+ const end = failure.endLine;
46
+ if (!blocks?.length || end === undefined || failure.line === undefined || end <= failure.line)
47
+ return false;
48
+ return blocks.some(block => block.line > failure.line && block.line <= end);
49
+ }
@@ -0,0 +1,31 @@
1
+ /** Outcomes a check needs before it can be muted, and the posterior mean below which it is. */
2
+ export declare const MUTE_MIN_OUTCOMES = 5;
3
+ export declare const MUTE_BELOW = 0.25;
4
+ export interface CheckOutcome {
5
+ fixed: number;
6
+ dismissed: number;
7
+ }
8
+ export interface CheckPrecision extends CheckOutcome {
9
+ check: string;
10
+ /** Posterior mean of Beta(fixed + 1, dismissed + 1). */
11
+ precision: number;
12
+ muted: boolean;
13
+ }
14
+ /** A check is a gate and the rule within it: `ast` alone would mute complexity and parameter count together. */
15
+ export declare function checkId(finding: {
16
+ id?: string;
17
+ rule?: string;
18
+ title?: string;
19
+ }): string;
20
+ export declare function precisionOf(outcome: CheckOutcome): number;
21
+ export declare function isMuted(outcome: CheckOutcome | undefined): boolean;
22
+ export declare function readOutcomes(cwd: string): Record<string, CheckOutcome>;
23
+ export declare function recordOutcome(cwd: string, check: string, kind: keyof CheckOutcome): void;
24
+ /** Every check with an outcome, most dismissed first. */
25
+ export declare function checkPrecisions(cwd: string): CheckPrecision[];
26
+ /** Remember which check each reported finding key came from (most recent kept). */
27
+ export declare function rememberReported(cwd: string, entries: Array<{
28
+ key: string;
29
+ check: string;
30
+ }>): void;
31
+ export declare function reportedCheck(cwd: string, key: string): string | undefined;