@getmikk/intent-engine 1.3.2 → 1.5.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/intent-engine",
3
- "version": "1.3.2",
3
+ "version": "1.5.1",
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.3.2",
24
+ "@getmikk/core": "^1.5.1",
25
25
  "zod": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
@@ -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
- if (matchedFunctions.length > 0) {
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
- return intents
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
- for (const fn of Object.values(this.lock.functions)) {
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
- if (fnNameLower.includes(kw)) score += 0.3
116
- if (fileLower.includes(kw)) score += 0.15
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 partial matches
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
- score += 0.2
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
- // Fall back to last meaningful word
190
- const words = prompt.split(/\s+/).filter(w => w.length > 2)
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. Generate suggestions
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