@getmikk/intent-engine 1.3.1 → 1.5.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/package.json +2 -2
- package/src/interpreter.ts +141 -14
- package/src/preflight.ts +17 -2
- package/src/suggester.ts +15 -0
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/intent-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dev": "tsc --watch"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@getmikk/core": "^1.
|
|
24
|
+
"@getmikk/core": "^1.5.0",
|
|
25
25
|
"zod": "^3.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
package/src/interpreter.ts
CHANGED
|
@@ -45,16 +45,51 @@ export class IntentInterpreter {
|
|
|
45
45
|
actions.push('modify')
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// Find the best matching target — try functions first, then modules
|
|
48
|
+
// Find the best matching target — try functions first, then classes, then modules
|
|
49
49
|
const matchedFunctions = this.findMatchingFunctions(prompt)
|
|
50
|
+
const matchedClasses = this.findMatchingClasses(prompt)
|
|
50
51
|
const matchedModule = this.findMatchingModule(prompt)
|
|
51
52
|
|
|
53
|
+
// If prompt mentions "class" and we have a class match, prefer class-level targeting
|
|
54
|
+
const prefersClass = promptLower.includes('class') && matchedClasses.length > 0
|
|
55
|
+
|
|
52
56
|
for (const action of actions) {
|
|
53
|
-
|
|
57
|
+
let resolvedAction = action
|
|
58
|
+
|
|
59
|
+
// Smart reclassification:
|
|
60
|
+
// - If action is "create" but a SPECIFIC FUNCTION already matches → "modify"
|
|
61
|
+
// (e.g., "add error handling to createZap" where createZap exists)
|
|
62
|
+
// - Module-level matches are NOT reclassified — creating new things
|
|
63
|
+
// in existing modules is valid (e.g., "create a new auth handler")
|
|
64
|
+
if (action === 'create') {
|
|
65
|
+
if (matchedFunctions.length > 0 && matchedFunctions[0].score >= 0.5) {
|
|
66
|
+
resolvedAction = 'modify'
|
|
67
|
+
}
|
|
68
|
+
if (matchedClasses.length > 0 && matchedClasses[0].score >= 0.5) {
|
|
69
|
+
resolvedAction = 'modify'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (prefersClass) {
|
|
74
|
+
// User explicitly mentioned "class" — target class-level
|
|
75
|
+
for (const cls of matchedClasses.slice(0, 3)) {
|
|
76
|
+
intents.push({
|
|
77
|
+
action: resolvedAction,
|
|
78
|
+
target: {
|
|
79
|
+
type: 'class',
|
|
80
|
+
name: cls.name,
|
|
81
|
+
moduleId: cls.moduleId,
|
|
82
|
+
filePath: cls.file,
|
|
83
|
+
},
|
|
84
|
+
reason: prompt,
|
|
85
|
+
confidence: cls.score,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
} else if (matchedFunctions.length > 0) {
|
|
54
89
|
// Create an intent for each matched function (up to 3)
|
|
55
90
|
for (const fn of matchedFunctions.slice(0, 3)) {
|
|
56
91
|
intents.push({
|
|
57
|
-
action,
|
|
92
|
+
action: resolvedAction,
|
|
58
93
|
target: {
|
|
59
94
|
type: 'function',
|
|
60
95
|
name: fn.name,
|
|
@@ -67,7 +102,7 @@ export class IntentInterpreter {
|
|
|
67
102
|
}
|
|
68
103
|
} else if (matchedModule) {
|
|
69
104
|
intents.push({
|
|
70
|
-
action,
|
|
105
|
+
action: resolvedAction,
|
|
71
106
|
target: {
|
|
72
107
|
type: 'module',
|
|
73
108
|
name: matchedModule.name,
|
|
@@ -79,7 +114,7 @@ export class IntentInterpreter {
|
|
|
79
114
|
} else {
|
|
80
115
|
// No match — use extracted name
|
|
81
116
|
intents.push({
|
|
82
|
-
action,
|
|
117
|
+
action: resolvedAction,
|
|
83
118
|
target: {
|
|
84
119
|
type: this.inferTargetType(prompt),
|
|
85
120
|
name: this.extractName(prompt),
|
|
@@ -90,7 +125,19 @@ export class IntentInterpreter {
|
|
|
90
125
|
}
|
|
91
126
|
}
|
|
92
127
|
|
|
93
|
-
|
|
128
|
+
// Deduplicate: if both "create" (reclassified to "modify") and "modify" exist
|
|
129
|
+
// for the same target, keep only one
|
|
130
|
+
const deduped: Intent[] = []
|
|
131
|
+
const seen = new Set<string>()
|
|
132
|
+
for (const intent of intents) {
|
|
133
|
+
const key = `${intent.action}:${intent.target.name}:${intent.target.moduleId || ''}`
|
|
134
|
+
if (!seen.has(key)) {
|
|
135
|
+
seen.add(key)
|
|
136
|
+
deduped.push(intent)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return deduped
|
|
94
141
|
}
|
|
95
142
|
|
|
96
143
|
// ── Fuzzy Matching ───────────────────────────────────────────
|
|
@@ -100,7 +147,19 @@ export class IntentInterpreter {
|
|
|
100
147
|
const keywords = this.extractKeywords(prompt)
|
|
101
148
|
const results: Array<{ name: string; file: string; moduleId: string; score: number }> = []
|
|
102
149
|
|
|
103
|
-
|
|
150
|
+
// Pre-compute keyword frequency across all function names for IDF-like penalization.
|
|
151
|
+
// A keyword that matches many functions is less discriminative.
|
|
152
|
+
const allFns = Object.values(this.lock.functions)
|
|
153
|
+
const keywordFreq = new Map<string, number>()
|
|
154
|
+
for (const kw of keywords) {
|
|
155
|
+
let count = 0
|
|
156
|
+
for (const fn of allFns) {
|
|
157
|
+
if (fn.name.toLowerCase().includes(kw)) count++
|
|
158
|
+
}
|
|
159
|
+
keywordFreq.set(kw, count)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const fn of allFns) {
|
|
104
163
|
let score = 0
|
|
105
164
|
const fnNameLower = fn.name.toLowerCase()
|
|
106
165
|
const fileLower = fn.file.toLowerCase()
|
|
@@ -110,19 +169,39 @@ export class IntentInterpreter {
|
|
|
110
169
|
score += 0.9
|
|
111
170
|
}
|
|
112
171
|
|
|
113
|
-
// Keyword matches in function name
|
|
172
|
+
// Keyword matches in function name — penalize if keyword is too common
|
|
114
173
|
for (const kw of keywords) {
|
|
115
|
-
|
|
116
|
-
if
|
|
174
|
+
const freq = keywordFreq.get(kw) || 0
|
|
175
|
+
// IDF-like penalization: if a keyword matches >40% of functions, reduce weight
|
|
176
|
+
const idfPenalty = freq > allFns.length * 0.4 ? 0.3 : 1.0
|
|
177
|
+
// Short keywords (3-4 chars) get reduced weight: "file" matches too many things
|
|
178
|
+
const lengthPenalty = kw.length <= 4 ? 0.5 : 1.0
|
|
179
|
+
|
|
180
|
+
const kwWeight = 0.3 * idfPenalty * lengthPenalty
|
|
181
|
+
|
|
182
|
+
if (fnNameLower.includes(kw)) score += kwWeight
|
|
183
|
+
if (fileLower.includes(kw)) score += 0.15 * idfPenalty * lengthPenalty
|
|
117
184
|
}
|
|
118
185
|
|
|
119
|
-
// CamelCase decomposition
|
|
186
|
+
// CamelCase decomposition — compound matches worth more
|
|
120
187
|
const fnWords = this.splitCamelCase(fn.name).map(w => w.toLowerCase())
|
|
188
|
+
let camelMatchCount = 0
|
|
121
189
|
for (const kw of keywords) {
|
|
122
190
|
if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
|
|
123
|
-
|
|
191
|
+
camelMatchCount++
|
|
124
192
|
}
|
|
125
193
|
}
|
|
194
|
+
// Multi-word compound matches are more meaningful
|
|
195
|
+
if (camelMatchCount >= 2) {
|
|
196
|
+
score += 0.4 // Strong multi-word match
|
|
197
|
+
} else if (camelMatchCount === 1) {
|
|
198
|
+
const freq = keywords.length > 0
|
|
199
|
+
? keywordFreq.get(keywords.find(kw =>
|
|
200
|
+
fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) || '') || 0
|
|
201
|
+
: 0
|
|
202
|
+
const idfPenalty = freq > allFns.length * 0.4 ? 0.3 : 1.0
|
|
203
|
+
score += 0.2 * idfPenalty
|
|
204
|
+
}
|
|
126
205
|
|
|
127
206
|
// Cap score at 1.0
|
|
128
207
|
if (score > 0.3) {
|
|
@@ -167,6 +246,47 @@ export class IntentInterpreter {
|
|
|
167
246
|
return bestMatch && bestMatch.score > 0.3 ? bestMatch : null
|
|
168
247
|
}
|
|
169
248
|
|
|
249
|
+
private findMatchingClasses(prompt: string): Array<{ name: string; file: string; moduleId: string; score: number }> {
|
|
250
|
+
if (!this.lock.classes) return []
|
|
251
|
+
const promptLower = prompt.toLowerCase()
|
|
252
|
+
const keywords = this.extractKeywords(prompt)
|
|
253
|
+
const results: Array<{ name: string; file: string; moduleId: string; score: number }> = []
|
|
254
|
+
|
|
255
|
+
for (const cls of Object.values(this.lock.classes)) {
|
|
256
|
+
let score = 0
|
|
257
|
+
const clsNameLower = cls.name.toLowerCase()
|
|
258
|
+
|
|
259
|
+
// Exact class name match in prompt → very high score
|
|
260
|
+
if (promptLower.includes(clsNameLower) && clsNameLower.length > 3) {
|
|
261
|
+
score += 0.95 // Classes get slightly higher score than functions when explicitly named
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Keyword matches against class name
|
|
265
|
+
for (const kw of keywords) {
|
|
266
|
+
if (clsNameLower.includes(kw)) score += 0.3
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// CamelCase decomposition
|
|
270
|
+
const clsWords = this.splitCamelCase(cls.name).map(w => w.toLowerCase())
|
|
271
|
+
for (const kw of keywords) {
|
|
272
|
+
if (clsWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
|
|
273
|
+
score += 0.2
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (score > 0.3) {
|
|
278
|
+
results.push({
|
|
279
|
+
name: cls.name,
|
|
280
|
+
file: cls.file,
|
|
281
|
+
moduleId: cls.moduleId,
|
|
282
|
+
score: Math.min(score, 1.0),
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return results.sort((a, b) => b.score - a.score)
|
|
288
|
+
}
|
|
289
|
+
|
|
170
290
|
// ── Helpers ───────────────────────────────────────────────────
|
|
171
291
|
|
|
172
292
|
private inferTargetType(prompt: string): Intent['target']['type'] {
|
|
@@ -186,8 +306,13 @@ export class IntentInterpreter {
|
|
|
186
306
|
const code = prompt.match(/`([^`]+)`/)
|
|
187
307
|
if (code) return code[1]
|
|
188
308
|
|
|
189
|
-
//
|
|
190
|
-
const
|
|
309
|
+
// Prefer CamelCase/PascalCase identifiers (likely code names)
|
|
310
|
+
const camelCase = prompt.match(/\b([a-z]+[A-Z][a-zA-Z]*|[A-Z][a-z]+[A-Z][a-zA-Z]*)\b/)
|
|
311
|
+
if (camelCase) return camelCase[1]
|
|
312
|
+
|
|
313
|
+
// Filter out structural words from the last word fallback
|
|
314
|
+
const structuralWords = new Set(['function', 'class', 'method', 'module', 'file', 'route', 'endpoint', 'handler', 'component', 'service', 'package'])
|
|
315
|
+
const words = prompt.split(/\s+/).filter(w => w.length > 2 && !structuralWords.has(w.toLowerCase()))
|
|
191
316
|
return words[words.length - 1] || 'unknown'
|
|
192
317
|
}
|
|
193
318
|
|
|
@@ -198,6 +323,8 @@ export class IntentInterpreter {
|
|
|
198
323
|
'add', 'create', 'modify', 'change', 'update', 'delete', 'remove',
|
|
199
324
|
'fix', 'move', 'refactor', 'new', 'old', 'all', 'this', 'that',
|
|
200
325
|
'should', 'can', 'will', 'must', 'need', 'want', 'please',
|
|
326
|
+
'function', 'method', 'class', 'module', 'file', 'package',
|
|
327
|
+
'endpoint', 'route', 'handler', 'component', 'service',
|
|
201
328
|
])
|
|
202
329
|
return text
|
|
203
330
|
.toLowerCase()
|
package/src/preflight.ts
CHANGED
|
@@ -31,14 +31,29 @@ export class PreflightPipeline {
|
|
|
31
31
|
// 2. Check for conflicts
|
|
32
32
|
const conflicts = this.conflictDetector.detect(intents)
|
|
33
33
|
|
|
34
|
-
// 3.
|
|
34
|
+
// 3. Low-confidence rejection: if the best intent has very low confidence,
|
|
35
|
+
// add a warning so the AI doesn't blindly proceed
|
|
36
|
+
const maxConfidence = intents.length > 0
|
|
37
|
+
? Math.max(...intents.map(i => i.confidence))
|
|
38
|
+
: 0
|
|
39
|
+
if (maxConfidence < 0.4 && intents.length > 0) {
|
|
40
|
+
conflicts.conflicts.push({
|
|
41
|
+
type: 'low-confidence',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
message: `Low confidence (${(maxConfidence * 100).toFixed(0)}%) — the intent could not be reliably matched to existing code. The suggestion may be inaccurate.`,
|
|
44
|
+
relatedIntent: intents[0],
|
|
45
|
+
suggestedFix: 'Be more specific about the function or module name in your prompt.',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Generate suggestions
|
|
35
50
|
const suggestions = this.suggester.suggest(intents)
|
|
36
51
|
|
|
37
52
|
return {
|
|
38
53
|
intents,
|
|
39
54
|
conflicts,
|
|
40
55
|
suggestions,
|
|
41
|
-
approved: !conflicts.hasConflicts,
|
|
56
|
+
approved: !conflicts.hasConflicts && maxConfidence >= 0.4,
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
}
|
package/src/suggester.ts
CHANGED
|
@@ -54,6 +54,21 @@ export class Suggester {
|
|
|
54
54
|
case 'delete': {
|
|
55
55
|
if (intent.target.filePath) {
|
|
56
56
|
affectedFiles.push(intent.target.filePath)
|
|
57
|
+
} else if (intent.target.type === 'function') {
|
|
58
|
+
// Only delete the specific function, not the whole module
|
|
59
|
+
const fn = Object.values(this.lock.functions).find(
|
|
60
|
+
f => f.name === intent.target.name
|
|
61
|
+
)
|
|
62
|
+
if (fn) {
|
|
63
|
+
affectedFiles.push(fn.file)
|
|
64
|
+
// Add callers that will need updating
|
|
65
|
+
for (const callerId of fn.calledBy) {
|
|
66
|
+
const caller = this.lock.functions[callerId]
|
|
67
|
+
if (caller && !affectedFiles.includes(caller.file)) {
|
|
68
|
+
affectedFiles.push(caller.file)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
57
72
|
}
|
|
58
73
|
break
|
|
59
74
|
}
|
package/src/types.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface ConflictResult {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface Conflict {
|
|
25
|
-
type: 'constraint-violation' | 'ownership-conflict' | 'boundary-crossing' | 'missing-dependency'
|
|
25
|
+
type: 'constraint-violation' | 'ownership-conflict' | 'boundary-crossing' | 'missing-dependency' | 'low-confidence'
|
|
26
26
|
severity: 'error' | 'warning'
|
|
27
27
|
message: string
|
|
28
28
|
relatedIntent: Intent
|