@rigour-labs/core 2.9.4 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4 -0
- package/dist/pattern-index/embeddings.d.ts +19 -0
- package/dist/pattern-index/embeddings.js +78 -0
- package/dist/pattern-index/index.d.ts +11 -0
- package/dist/pattern-index/index.js +15 -0
- package/dist/pattern-index/indexer.d.ts +101 -0
- package/dist/pattern-index/indexer.js +573 -0
- package/dist/pattern-index/indexer.test.d.ts +6 -0
- package/dist/pattern-index/indexer.test.js +188 -0
- package/dist/pattern-index/matcher.d.ts +106 -0
- package/dist/pattern-index/matcher.js +376 -0
- package/dist/pattern-index/matcher.test.d.ts +6 -0
- package/dist/pattern-index/matcher.test.js +238 -0
- package/dist/pattern-index/overrides.d.ts +64 -0
- package/dist/pattern-index/overrides.js +196 -0
- package/dist/pattern-index/security.d.ts +25 -0
- package/dist/pattern-index/security.js +127 -0
- package/dist/pattern-index/staleness.d.ts +71 -0
- package/dist/pattern-index/staleness.js +381 -0
- package/dist/pattern-index/staleness.test.d.ts +6 -0
- package/dist/pattern-index/staleness.test.js +211 -0
- package/dist/pattern-index/types.d.ts +221 -0
- package/dist/pattern-index/types.js +7 -0
- package/package.json +14 -1
- package/src/index.ts +4 -0
- package/src/pattern-index/embeddings.ts +84 -0
- package/src/pattern-index/index.ts +53 -0
- package/src/pattern-index/indexer.test.ts +276 -0
- package/src/pattern-index/indexer.ts +693 -0
- package/src/pattern-index/matcher.test.ts +293 -0
- package/src/pattern-index/matcher.ts +493 -0
- package/src/pattern-index/overrides.ts +235 -0
- package/src/pattern-index/security.ts +151 -0
- package/src/pattern-index/staleness.test.ts +313 -0
- package/src/pattern-index/staleness.ts +438 -0
- package/src/pattern-index/types.ts +339 -0
- package/vitest.config.ts +7 -0
- package/vitest.setup.ts +30 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Indexer Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests for the pattern indexer.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { PatternIndexer, savePatternIndex, loadPatternIndex } from './indexer.js';
|
|
11
|
+
describe('PatternIndexer', () => {
|
|
12
|
+
let testDir;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
// Create a temporary test directory
|
|
15
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-test-'));
|
|
16
|
+
await fs.mkdir(path.join(testDir, 'src'), { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
// Clean up
|
|
20
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
describe('buildIndex', () => {
|
|
23
|
+
it('should index function declarations', async () => {
|
|
24
|
+
await fs.writeFile(path.join(testDir, 'src', 'utils.ts'), `
|
|
25
|
+
/**
|
|
26
|
+
* Format a date to a readable string.
|
|
27
|
+
*/
|
|
28
|
+
export function formatDate(date: Date): string {
|
|
29
|
+
return date.toISOString();
|
|
30
|
+
}
|
|
31
|
+
`);
|
|
32
|
+
const indexer = new PatternIndexer(testDir);
|
|
33
|
+
const index = await indexer.buildIndex();
|
|
34
|
+
expect(index.patterns).toHaveLength(1);
|
|
35
|
+
expect(index.patterns[0].name).toBe('formatDate');
|
|
36
|
+
expect(index.patterns[0].type).toBe('function');
|
|
37
|
+
expect(index.patterns[0].exported).toBe(true);
|
|
38
|
+
expect(index.patterns[0].signature).toContain('date: Date');
|
|
39
|
+
expect(index.patterns[0].description).toContain('Format a date');
|
|
40
|
+
});
|
|
41
|
+
it('should index arrow functions', async () => {
|
|
42
|
+
await fs.writeFile(path.join(testDir, 'src', 'helpers.ts'), `
|
|
43
|
+
export const slugify = (text: string): string => {
|
|
44
|
+
return text.toLowerCase().replace(/\\s+/g, '-');
|
|
45
|
+
};
|
|
46
|
+
`);
|
|
47
|
+
const indexer = new PatternIndexer(testDir);
|
|
48
|
+
const index = await indexer.buildIndex();
|
|
49
|
+
expect(index.patterns).toHaveLength(1);
|
|
50
|
+
expect(index.patterns[0].name).toBe('slugify');
|
|
51
|
+
expect(index.patterns[0].type).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
it('should detect React hooks', async () => {
|
|
54
|
+
await fs.writeFile(path.join(testDir, 'src', 'hooks.ts'), `
|
|
55
|
+
export const useAuth = () => {
|
|
56
|
+
return { user: null, login: () => {} };
|
|
57
|
+
};
|
|
58
|
+
`);
|
|
59
|
+
const indexer = new PatternIndexer(testDir);
|
|
60
|
+
const index = await indexer.buildIndex();
|
|
61
|
+
expect(index.patterns).toHaveLength(1);
|
|
62
|
+
expect(index.patterns[0].name).toBe('useAuth');
|
|
63
|
+
expect(index.patterns[0].type).toBe('hook');
|
|
64
|
+
});
|
|
65
|
+
it('should index classes', async () => {
|
|
66
|
+
await fs.writeFile(path.join(testDir, 'src', 'services.ts'), `
|
|
67
|
+
export class UserService {
|
|
68
|
+
getUser(id: string) {
|
|
69
|
+
return { id };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`);
|
|
73
|
+
const indexer = new PatternIndexer(testDir);
|
|
74
|
+
const index = await indexer.buildIndex();
|
|
75
|
+
expect(index.patterns.some(p => p.name === 'UserService')).toBe(true);
|
|
76
|
+
expect(index.patterns.find(p => p.name === 'UserService')?.type).toBe('class');
|
|
77
|
+
});
|
|
78
|
+
it('should detect error classes', async () => {
|
|
79
|
+
await fs.writeFile(path.join(testDir, 'src', 'errors.ts'), `
|
|
80
|
+
export class ValidationError extends Error {
|
|
81
|
+
constructor(message: string) {
|
|
82
|
+
super(message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`);
|
|
86
|
+
const indexer = new PatternIndexer(testDir);
|
|
87
|
+
const index = await indexer.buildIndex();
|
|
88
|
+
expect(index.patterns[0].type).toBe('error');
|
|
89
|
+
});
|
|
90
|
+
it('should index interfaces', async () => {
|
|
91
|
+
await fs.writeFile(path.join(testDir, 'src', 'types.ts'), `
|
|
92
|
+
export interface User {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
email: string;
|
|
96
|
+
}
|
|
97
|
+
`);
|
|
98
|
+
const indexer = new PatternIndexer(testDir);
|
|
99
|
+
const index = await indexer.buildIndex();
|
|
100
|
+
expect(index.patterns).toHaveLength(1);
|
|
101
|
+
expect(index.patterns[0].name).toBe('User');
|
|
102
|
+
expect(index.patterns[0].type).toBe('interface');
|
|
103
|
+
});
|
|
104
|
+
it('should index type aliases', async () => {
|
|
105
|
+
await fs.writeFile(path.join(testDir, 'src', 'types.ts'), `
|
|
106
|
+
export type UserId = string;
|
|
107
|
+
`);
|
|
108
|
+
const indexer = new PatternIndexer(testDir);
|
|
109
|
+
const index = await indexer.buildIndex();
|
|
110
|
+
expect(index.patterns).toHaveLength(1);
|
|
111
|
+
expect(index.patterns[0].type).toBe('type');
|
|
112
|
+
});
|
|
113
|
+
it('should index enums', async () => {
|
|
114
|
+
await fs.writeFile(path.join(testDir, 'src', 'constants.ts'), `
|
|
115
|
+
export enum Status {
|
|
116
|
+
Active = 'active',
|
|
117
|
+
Inactive = 'inactive'
|
|
118
|
+
}
|
|
119
|
+
`);
|
|
120
|
+
const indexer = new PatternIndexer(testDir);
|
|
121
|
+
const index = await indexer.buildIndex();
|
|
122
|
+
expect(index.patterns).toHaveLength(1);
|
|
123
|
+
expect(index.patterns[0].name).toBe('Status');
|
|
124
|
+
expect(index.patterns[0].type).toBe('enum');
|
|
125
|
+
});
|
|
126
|
+
it('should index constants (all caps)', async () => {
|
|
127
|
+
await fs.writeFile(path.join(testDir, 'src', 'config.ts'), `
|
|
128
|
+
export const API_URL = 'https://api.example.com';
|
|
129
|
+
export const MAX_RETRIES = 3;
|
|
130
|
+
`);
|
|
131
|
+
const indexer = new PatternIndexer(testDir);
|
|
132
|
+
const index = await indexer.buildIndex();
|
|
133
|
+
const constants = index.patterns.filter(p => p.type === 'constant');
|
|
134
|
+
expect(constants.length).toBeGreaterThanOrEqual(1);
|
|
135
|
+
expect(constants.some(c => c.name === 'API_URL')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
it('should track index statistics', async () => {
|
|
138
|
+
await fs.writeFile(path.join(testDir, 'src', 'utils.ts'), `
|
|
139
|
+
export function foo() {}
|
|
140
|
+
export function bar() {}
|
|
141
|
+
export interface Baz {}
|
|
142
|
+
`);
|
|
143
|
+
const indexer = new PatternIndexer(testDir);
|
|
144
|
+
const index = await indexer.buildIndex();
|
|
145
|
+
expect(index.stats.totalPatterns).toBe(3);
|
|
146
|
+
expect(index.stats.totalFiles).toBe(1);
|
|
147
|
+
expect(index.stats.indexDurationMs).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
it('should exclude test files by default', async () => {
|
|
150
|
+
await fs.writeFile(path.join(testDir, 'src', 'utils.ts'), 'export function main() {}');
|
|
151
|
+
await fs.writeFile(path.join(testDir, 'src', 'utils.test.ts'), 'export function testMain() {}');
|
|
152
|
+
const indexer = new PatternIndexer(testDir);
|
|
153
|
+
const index = await indexer.buildIndex();
|
|
154
|
+
expect(index.patterns.every(p => !p.file.includes('.test.'))).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('updateIndex (incremental)', () => {
|
|
158
|
+
it('should only reindex changed files', async () => {
|
|
159
|
+
// Initial index
|
|
160
|
+
await fs.writeFile(path.join(testDir, 'src', 'a.ts'), 'export function funcA() {}');
|
|
161
|
+
await fs.writeFile(path.join(testDir, 'src', 'b.ts'), 'export function funcB() {}');
|
|
162
|
+
const indexer = new PatternIndexer(testDir);
|
|
163
|
+
const initialIndex = await indexer.buildIndex();
|
|
164
|
+
expect(initialIndex.patterns).toHaveLength(2);
|
|
165
|
+
// Modify only one file
|
|
166
|
+
await fs.writeFile(path.join(testDir, 'src', 'a.ts'), 'export function funcA() {} export function funcA2() {}');
|
|
167
|
+
const updatedIndex = await indexer.updateIndex(initialIndex);
|
|
168
|
+
expect(updatedIndex.patterns).toHaveLength(3);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('savePatternIndex / loadPatternIndex', () => {
|
|
172
|
+
it('should save and load index correctly', async () => {
|
|
173
|
+
await fs.writeFile(path.join(testDir, 'src', 'utils.ts'), 'export function myFunc() {}');
|
|
174
|
+
const indexer = new PatternIndexer(testDir);
|
|
175
|
+
const index = await indexer.buildIndex();
|
|
176
|
+
const indexPath = path.join(testDir, '.rigour', 'patterns.json');
|
|
177
|
+
await savePatternIndex(index, indexPath);
|
|
178
|
+
const loaded = await loadPatternIndex(indexPath);
|
|
179
|
+
expect(loaded).not.toBeNull();
|
|
180
|
+
expect(loaded.patterns).toHaveLength(index.patterns.length);
|
|
181
|
+
expect(loaded.version).toBe(index.version);
|
|
182
|
+
});
|
|
183
|
+
it('should return null for non-existent index', async () => {
|
|
184
|
+
const loaded = await loadPatternIndex('/non/existent/path.json');
|
|
185
|
+
expect(loaded).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matcher
|
|
3
|
+
*
|
|
4
|
+
* Matches queries against the pattern index using multiple strategies:
|
|
5
|
+
* 1. Exact name match
|
|
6
|
+
* 2. Fuzzy name match (Levenshtein)
|
|
7
|
+
* 3. Signature match
|
|
8
|
+
* 4. Keyword/semantic match
|
|
9
|
+
*/
|
|
10
|
+
import type { PatternIndex, PatternMatchResult, PatternOverride } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for pattern matching.
|
|
13
|
+
*/
|
|
14
|
+
export interface MatcherConfig {
|
|
15
|
+
/** Minimum confidence threshold to include a match (0-100) */
|
|
16
|
+
minConfidence: number;
|
|
17
|
+
/** Maximum number of matches to return */
|
|
18
|
+
maxMatches: number;
|
|
19
|
+
/** Whether to use fuzzy matching */
|
|
20
|
+
useFuzzy: boolean;
|
|
21
|
+
/** Whether to use signature matching */
|
|
22
|
+
useSignature: boolean;
|
|
23
|
+
/** Whether to use keyword/semantic matching */
|
|
24
|
+
useKeywords: boolean;
|
|
25
|
+
/** Action when matches are found */
|
|
26
|
+
defaultAction: 'BLOCK' | 'WARN' | 'ALLOW';
|
|
27
|
+
/** Whether to use semantic embeddings for matching */
|
|
28
|
+
useEmbeddings: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pattern Matcher class.
|
|
32
|
+
* Finds similar patterns in the index.
|
|
33
|
+
*/
|
|
34
|
+
export declare class PatternMatcher {
|
|
35
|
+
private index;
|
|
36
|
+
private config;
|
|
37
|
+
private overrides;
|
|
38
|
+
private nameMap;
|
|
39
|
+
constructor(index: PatternIndex, config?: Partial<MatcherConfig>, overrides?: PatternOverride[]);
|
|
40
|
+
/**
|
|
41
|
+
* Find patterns similar to a query.
|
|
42
|
+
*/
|
|
43
|
+
match(query: {
|
|
44
|
+
name?: string;
|
|
45
|
+
signature?: string;
|
|
46
|
+
keywords?: string[];
|
|
47
|
+
type?: string;
|
|
48
|
+
intent?: string;
|
|
49
|
+
}): Promise<PatternMatchResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Check for exact name match.
|
|
52
|
+
*/
|
|
53
|
+
private exactNameMatch;
|
|
54
|
+
/**
|
|
55
|
+
* Check for fuzzy name match using Levenshtein distance.
|
|
56
|
+
*/
|
|
57
|
+
private fuzzyNameMatch;
|
|
58
|
+
/**
|
|
59
|
+
* Check for signature match.
|
|
60
|
+
*/
|
|
61
|
+
private signatureMatch;
|
|
62
|
+
/**
|
|
63
|
+
* Check for keyword/semantic match.
|
|
64
|
+
*/
|
|
65
|
+
private keywordMatch;
|
|
66
|
+
/**
|
|
67
|
+
* Check if an override exists for a pattern.
|
|
68
|
+
*/
|
|
69
|
+
private hasOverride;
|
|
70
|
+
/**
|
|
71
|
+
* Generate a suggestion message.
|
|
72
|
+
*/
|
|
73
|
+
private generateSuggestion;
|
|
74
|
+
/**
|
|
75
|
+
* Calculate Levenshtein distance between two strings.
|
|
76
|
+
*/
|
|
77
|
+
private levenshteinDistance;
|
|
78
|
+
/**
|
|
79
|
+
* Extract words from a camelCase/PascalCase name.
|
|
80
|
+
*/
|
|
81
|
+
private extractWords;
|
|
82
|
+
/**
|
|
83
|
+
* Calculate word overlap ratio.
|
|
84
|
+
*/
|
|
85
|
+
private calculateWordOverlap;
|
|
86
|
+
/**
|
|
87
|
+
* Normalize a signature for comparison.
|
|
88
|
+
*/
|
|
89
|
+
private normalizeSignature;
|
|
90
|
+
/**
|
|
91
|
+
* Extract parameters from a signature.
|
|
92
|
+
*/
|
|
93
|
+
private extractParameters;
|
|
94
|
+
/**
|
|
95
|
+
* Compare parameter types between two signatures.
|
|
96
|
+
*/
|
|
97
|
+
private compareParameterTypes;
|
|
98
|
+
/**
|
|
99
|
+
* Extract type from a parameter declaration.
|
|
100
|
+
*/
|
|
101
|
+
private extractType;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Quick helper to check for pattern duplicates.
|
|
105
|
+
*/
|
|
106
|
+
export declare function checkPatternDuplicate(index: PatternIndex, name: string, options?: Partial<MatcherConfig>): Promise<PatternMatchResult>;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matcher
|
|
3
|
+
*
|
|
4
|
+
* Matches queries against the pattern index using multiple strategies:
|
|
5
|
+
* 1. Exact name match
|
|
6
|
+
* 2. Fuzzy name match (Levenshtein)
|
|
7
|
+
* 3. Signature match
|
|
8
|
+
* 4. Keyword/semantic match
|
|
9
|
+
*/
|
|
10
|
+
import { generateEmbedding, cosineSimilarity } from './embeddings.js';
|
|
11
|
+
/** Default matcher configuration */
|
|
12
|
+
const DEFAULT_MATCHER_CONFIG = {
|
|
13
|
+
minConfidence: 60,
|
|
14
|
+
maxMatches: 5,
|
|
15
|
+
useFuzzy: true,
|
|
16
|
+
useSignature: true,
|
|
17
|
+
useKeywords: true,
|
|
18
|
+
defaultAction: 'WARN',
|
|
19
|
+
useEmbeddings: true
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Pattern Matcher class.
|
|
23
|
+
* Finds similar patterns in the index.
|
|
24
|
+
*/
|
|
25
|
+
export class PatternMatcher {
|
|
26
|
+
index;
|
|
27
|
+
config;
|
|
28
|
+
overrides;
|
|
29
|
+
nameMap;
|
|
30
|
+
constructor(index, config = {}, overrides = []) {
|
|
31
|
+
this.index = index;
|
|
32
|
+
this.config = { ...DEFAULT_MATCHER_CONFIG, ...config };
|
|
33
|
+
this.overrides = overrides;
|
|
34
|
+
// Build name map for O(1) lookups
|
|
35
|
+
this.nameMap = new Map();
|
|
36
|
+
for (const pattern of index.patterns) {
|
|
37
|
+
const normalized = pattern.name.toLowerCase();
|
|
38
|
+
const existing = this.nameMap.get(normalized) || [];
|
|
39
|
+
existing.push(pattern);
|
|
40
|
+
this.nameMap.set(normalized, existing);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Find patterns similar to a query.
|
|
45
|
+
*/
|
|
46
|
+
async match(query) {
|
|
47
|
+
const matches = [];
|
|
48
|
+
// Pre-calculate query embedding if semantic search is enabled
|
|
49
|
+
let queryEmbedding = null;
|
|
50
|
+
if (this.config.useEmbeddings && (query.intent || query.name)) {
|
|
51
|
+
queryEmbedding = await generateEmbedding(`${query.name || ''} ${query.intent || ''}`);
|
|
52
|
+
}
|
|
53
|
+
// Check for override first
|
|
54
|
+
if (query.name && this.hasOverride(query.name)) {
|
|
55
|
+
return {
|
|
56
|
+
query: query.name || '',
|
|
57
|
+
matches: [],
|
|
58
|
+
suggestion: 'Human override granted for this pattern.',
|
|
59
|
+
canOverride: false,
|
|
60
|
+
status: 'OVERRIDE_ALLOWED',
|
|
61
|
+
action: 'ALLOW'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Strategy 1: Fast Exact name match (O(1))
|
|
65
|
+
if (query.name) {
|
|
66
|
+
const exactPatterns = this.nameMap.get(query.name.toLowerCase()) || [];
|
|
67
|
+
for (const pattern of exactPatterns) {
|
|
68
|
+
const match = this.exactNameMatch(query.name, pattern);
|
|
69
|
+
if (match) {
|
|
70
|
+
matches.push(match);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// If we found exact matches, we might still want to find others,
|
|
74
|
+
// but we can skip checking these specific patterns again in the main loop.
|
|
75
|
+
}
|
|
76
|
+
for (const pattern of this.index.patterns) {
|
|
77
|
+
// Skip if we already matched this pattern via exact match
|
|
78
|
+
if (query.name && pattern.name.toLowerCase() === query.name.toLowerCase()) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let currentBest = null;
|
|
82
|
+
let maxConfidence = -1;
|
|
83
|
+
// Strategy 2: Fuzzy name match
|
|
84
|
+
if (query.name && this.config.useFuzzy) {
|
|
85
|
+
const match = this.fuzzyNameMatch(query.name, pattern);
|
|
86
|
+
if (match && match.confidence > maxConfidence) {
|
|
87
|
+
currentBest = match;
|
|
88
|
+
maxConfidence = match.confidence;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Strategy 3: Signature match
|
|
92
|
+
if (query.signature && this.config.useSignature) {
|
|
93
|
+
const match = this.signatureMatch(query.signature, pattern);
|
|
94
|
+
if (match && match.confidence > maxConfidence) {
|
|
95
|
+
currentBest = match;
|
|
96
|
+
maxConfidence = match.confidence;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Strategy 4: Keyword match
|
|
100
|
+
if (query.keywords && query.keywords.length > 0 && this.config.useKeywords) {
|
|
101
|
+
const match = this.keywordMatch(query.keywords, pattern);
|
|
102
|
+
if (match && match.confidence > maxConfidence) {
|
|
103
|
+
currentBest = match;
|
|
104
|
+
maxConfidence = match.confidence;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Strategy 5: Semantic embedding match
|
|
108
|
+
if (queryEmbedding && pattern.embedding && this.config.useEmbeddings) {
|
|
109
|
+
const similarity = cosineSimilarity(queryEmbedding, pattern.embedding);
|
|
110
|
+
const confidence = Math.round(similarity * 100);
|
|
111
|
+
if (confidence > maxConfidence) {
|
|
112
|
+
currentBest = {
|
|
113
|
+
pattern,
|
|
114
|
+
matchType: 'semantic',
|
|
115
|
+
confidence,
|
|
116
|
+
reason: `Semantic match (${confidence}%): similar intent to "${pattern.name}"`
|
|
117
|
+
};
|
|
118
|
+
maxConfidence = confidence;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (currentBest && maxConfidence >= this.config.minConfidence) {
|
|
122
|
+
matches.push(currentBest);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Sort by confidence and limit
|
|
126
|
+
matches.sort((a, b) => b.confidence - a.confidence);
|
|
127
|
+
const topMatches = matches.slice(0, this.config.maxMatches);
|
|
128
|
+
// Determine status and action
|
|
129
|
+
const hasExact = topMatches.some(m => m.matchType === 'exact');
|
|
130
|
+
const hasHighConfidence = topMatches.some(m => m.confidence >= 90);
|
|
131
|
+
let status = 'NO_MATCH';
|
|
132
|
+
let action = 'ALLOW';
|
|
133
|
+
let suggestion = '';
|
|
134
|
+
if (topMatches.length > 0) {
|
|
135
|
+
status = 'FOUND_SIMILAR';
|
|
136
|
+
action = hasExact || hasHighConfidence ? 'BLOCK' : this.config.defaultAction;
|
|
137
|
+
const best = topMatches[0];
|
|
138
|
+
suggestion = this.generateSuggestion(best);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
query: query.name || query.signature || query.keywords?.join(' ') || '',
|
|
142
|
+
matches: topMatches,
|
|
143
|
+
suggestion,
|
|
144
|
+
canOverride: true,
|
|
145
|
+
status,
|
|
146
|
+
action
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check for exact name match.
|
|
151
|
+
*/
|
|
152
|
+
exactNameMatch(queryName, pattern) {
|
|
153
|
+
const normalizedQuery = queryName.toLowerCase();
|
|
154
|
+
const normalizedPattern = pattern.name.toLowerCase();
|
|
155
|
+
if (normalizedQuery === normalizedPattern) {
|
|
156
|
+
return {
|
|
157
|
+
pattern,
|
|
158
|
+
matchType: 'exact',
|
|
159
|
+
confidence: 100,
|
|
160
|
+
reason: `Exact match: "${pattern.name}" already exists in ${pattern.file}`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check for fuzzy name match using Levenshtein distance.
|
|
167
|
+
*/
|
|
168
|
+
fuzzyNameMatch(queryName, pattern) {
|
|
169
|
+
const distance = this.levenshteinDistance(queryName.toLowerCase(), pattern.name.toLowerCase());
|
|
170
|
+
const maxLength = Math.max(queryName.length, pattern.name.length);
|
|
171
|
+
const similarity = 1 - (distance / maxLength);
|
|
172
|
+
const confidence = Math.round(similarity * 100);
|
|
173
|
+
// Also check word overlap
|
|
174
|
+
const queryWords = this.extractWords(queryName);
|
|
175
|
+
const patternWords = this.extractWords(pattern.name);
|
|
176
|
+
const wordOverlap = this.calculateWordOverlap(queryWords, patternWords);
|
|
177
|
+
// Combine word overlap with character similarity
|
|
178
|
+
const combinedConfidence = Math.round((confidence + wordOverlap * 100) / 2);
|
|
179
|
+
if (combinedConfidence >= 60) {
|
|
180
|
+
return {
|
|
181
|
+
pattern,
|
|
182
|
+
matchType: 'fuzzy',
|
|
183
|
+
confidence: combinedConfidence,
|
|
184
|
+
reason: `Similar name: "${pattern.name}" in ${pattern.file} (${combinedConfidence}% similar)`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check for signature match.
|
|
191
|
+
*/
|
|
192
|
+
signatureMatch(querySignature, pattern) {
|
|
193
|
+
if (!pattern.signature)
|
|
194
|
+
return null;
|
|
195
|
+
// Normalize signatures for comparison
|
|
196
|
+
const normalizedQuery = this.normalizeSignature(querySignature);
|
|
197
|
+
const normalizedPattern = this.normalizeSignature(pattern.signature);
|
|
198
|
+
if (normalizedQuery === normalizedPattern) {
|
|
199
|
+
return {
|
|
200
|
+
pattern,
|
|
201
|
+
matchType: 'signature',
|
|
202
|
+
confidence: 85,
|
|
203
|
+
reason: `Matching signature: ${pattern.name}${pattern.signature} in ${pattern.file}`
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Check parameter count and types
|
|
207
|
+
const queryParams = this.extractParameters(querySignature);
|
|
208
|
+
const patternParams = this.extractParameters(pattern.signature);
|
|
209
|
+
if (queryParams.length === patternParams.length && queryParams.length > 0) {
|
|
210
|
+
const typeMatch = this.compareParameterTypes(queryParams, patternParams);
|
|
211
|
+
if (typeMatch >= 0.7) {
|
|
212
|
+
const confidence = Math.round(60 + typeMatch * 20);
|
|
213
|
+
return {
|
|
214
|
+
pattern,
|
|
215
|
+
matchType: 'signature',
|
|
216
|
+
confidence,
|
|
217
|
+
reason: `Similar signature (${confidence}% match): ${pattern.name} in ${pattern.file}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Check for keyword/semantic match.
|
|
225
|
+
*/
|
|
226
|
+
keywordMatch(queryKeywords, pattern) {
|
|
227
|
+
if (!pattern.keywords || pattern.keywords.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
const normalizedQuery = queryKeywords.map(k => k.toLowerCase());
|
|
230
|
+
const normalizedPattern = pattern.keywords.map(k => k.toLowerCase());
|
|
231
|
+
const matches = normalizedQuery.filter(k => normalizedPattern.includes(k));
|
|
232
|
+
const overlap = matches.length / Math.max(normalizedQuery.length, normalizedPattern.length);
|
|
233
|
+
if (overlap >= 0.5) {
|
|
234
|
+
const confidence = Math.round(50 + overlap * 40);
|
|
235
|
+
return {
|
|
236
|
+
pattern,
|
|
237
|
+
matchType: 'semantic',
|
|
238
|
+
confidence,
|
|
239
|
+
reason: `Semantic match (keywords: ${matches.join(', ')}): ${pattern.name} in ${pattern.file}`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check if an override exists for a pattern.
|
|
246
|
+
*/
|
|
247
|
+
hasOverride(name) {
|
|
248
|
+
const now = new Date();
|
|
249
|
+
return this.overrides.some(override => {
|
|
250
|
+
// Check expiration
|
|
251
|
+
if (override.expiresAt && new Date(override.expiresAt) < now) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
// Check pattern match (supports globs)
|
|
255
|
+
if (override.pattern.includes('*')) {
|
|
256
|
+
const regex = new RegExp('^' + override.pattern.replace(/\*/g, '.*') + '$');
|
|
257
|
+
return regex.test(name);
|
|
258
|
+
}
|
|
259
|
+
return override.pattern === name;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Generate a suggestion message.
|
|
264
|
+
*/
|
|
265
|
+
generateSuggestion(match) {
|
|
266
|
+
const { pattern } = match;
|
|
267
|
+
if (pattern.exported) {
|
|
268
|
+
return `Import "${pattern.name}" from "${pattern.file}" instead of creating a new one.`;
|
|
269
|
+
}
|
|
270
|
+
return `A similar pattern "${pattern.name}" exists in "${pattern.file}". Consider reusing it or extracting it to a shared location.`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Calculate Levenshtein distance between two strings.
|
|
274
|
+
*/
|
|
275
|
+
levenshteinDistance(a, b) {
|
|
276
|
+
const matrix = [];
|
|
277
|
+
for (let i = 0; i <= b.length; i++) {
|
|
278
|
+
matrix[i] = [i];
|
|
279
|
+
}
|
|
280
|
+
for (let j = 0; j <= a.length; j++) {
|
|
281
|
+
matrix[0][j] = j;
|
|
282
|
+
}
|
|
283
|
+
for (let i = 1; i <= b.length; i++) {
|
|
284
|
+
for (let j = 1; j <= a.length; j++) {
|
|
285
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
286
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
|
|
290
|
+
matrix[i][j - 1] + 1, // insertion
|
|
291
|
+
matrix[i - 1][j] + 1 // deletion
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return matrix[b.length][a.length];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Extract words from a camelCase/PascalCase name.
|
|
300
|
+
*/
|
|
301
|
+
extractWords(name) {
|
|
302
|
+
return name
|
|
303
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
304
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
305
|
+
.toLowerCase()
|
|
306
|
+
.split(/[\s_-]+/)
|
|
307
|
+
.filter(w => w.length > 1);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Calculate word overlap ratio.
|
|
311
|
+
*/
|
|
312
|
+
calculateWordOverlap(words1, words2) {
|
|
313
|
+
const set1 = new Set(words1);
|
|
314
|
+
const set2 = new Set(words2);
|
|
315
|
+
let matches = 0;
|
|
316
|
+
for (const word of set1) {
|
|
317
|
+
if (set2.has(word)) {
|
|
318
|
+
matches++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return matches / Math.max(set1.size, set2.size);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Normalize a signature for comparison.
|
|
325
|
+
*/
|
|
326
|
+
normalizeSignature(sig) {
|
|
327
|
+
return sig
|
|
328
|
+
.replace(/\s+/g, '') // Remove whitespace
|
|
329
|
+
.replace(/:\s*\w+/g, '') // Remove type annotations
|
|
330
|
+
.replace(/\?/g, '') // Remove optional markers
|
|
331
|
+
.toLowerCase();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Extract parameters from a signature.
|
|
335
|
+
*/
|
|
336
|
+
extractParameters(sig) {
|
|
337
|
+
const match = sig.match(/\(([^)]*)\)/);
|
|
338
|
+
if (!match)
|
|
339
|
+
return [];
|
|
340
|
+
return match[1]
|
|
341
|
+
.split(',')
|
|
342
|
+
.map(p => p.trim())
|
|
343
|
+
.filter(p => p.length > 0);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Compare parameter types between two signatures.
|
|
347
|
+
*/
|
|
348
|
+
compareParameterTypes(params1, params2) {
|
|
349
|
+
let matches = 0;
|
|
350
|
+
for (let i = 0; i < params1.length; i++) {
|
|
351
|
+
const type1 = this.extractType(params1[i]);
|
|
352
|
+
const type2 = this.extractType(params2[i]);
|
|
353
|
+
if (type1 === type2) {
|
|
354
|
+
matches++;
|
|
355
|
+
}
|
|
356
|
+
else if (type1 && type2 && (type1.includes(type2) || type2.includes(type1))) {
|
|
357
|
+
matches += 0.5;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return matches / params1.length;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Extract type from a parameter declaration.
|
|
364
|
+
*/
|
|
365
|
+
extractType(param) {
|
|
366
|
+
const match = param.match(/:\s*(\w+)/);
|
|
367
|
+
return match ? match[1].toLowerCase() : null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Quick helper to check for pattern duplicates.
|
|
372
|
+
*/
|
|
373
|
+
export async function checkPatternDuplicate(index, name, options = {}) {
|
|
374
|
+
const matcher = new PatternMatcher(index, options);
|
|
375
|
+
return matcher.match({ name });
|
|
376
|
+
}
|