@getmikk/intent-engine 2.0.13 → 2.0.15

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": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -27,7 +27,8 @@
27
27
  "lint": "bunx eslint --config ../../eslint.config.mjs ."
28
28
  },
29
29
  "dependencies": {
30
- "@getmikk/core": "^2.0.11",
30
+ "@getmikk/core": "^2.0.15",
31
+ "@xenova/transformers": "^2.17.2",
31
32
  "zod": "^3.22.0"
32
33
  },
33
34
  "devDependencies": {
@@ -60,8 +60,8 @@ export class AutoCorrectionEngine {
60
60
  } else {
61
61
  failedFixes.push(`${file}:${issue.line} — ${issue.message}`)
62
62
  }
63
- } catch {
64
- failedFixes.push(`${file}:${issue.line} — ${issue.message}`)
63
+ } catch (err) {
64
+ failedFixes.push(`${file}:${issue.line} — ${issue.message}: ${err instanceof Error ? err.message : String(err)}`)
65
65
  }
66
66
  }
67
67
  }
@@ -149,7 +149,7 @@ export class ConflictDetector {
149
149
  return null
150
150
  }
151
151
 
152
- const [, _accessType, allowedPath] = match
152
+ const [, , allowedPath] = match
153
153
  const allowed = allowedPath.trim().replace(/[/\\*]*/g, '')
154
154
  const targetModule = intent.target.moduleId || ''
155
155
 
@@ -1,5 +1,5 @@
1
1
  import type { MikkContract, MikkLock } from '@getmikk/core'
2
- import { IntentSchema, type Intent } from './types.js'
2
+ import type { Intent } from './types.js'
3
3
 
4
4
  /**
5
5
  * IntentInterpreter — parses a natural-language prompt into structured
@@ -209,7 +209,7 @@ export class PreEditValidation {
209
209
  }
210
210
 
211
211
  /** Build a zero-impact result when no tracked functions exist. */
212
- private emptyImpact(files: string[]): ImpactResult {
212
+ private emptyImpact(_files: string[]): ImpactResult {
213
213
  return {
214
214
  changed: [],
215
215
  impacted: [],
@@ -251,6 +251,7 @@ export class PreEditValidation {
251
251
  }
252
252
  }
253
253
 
254
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
255
  private buildRecommendations(intent: any, impact: ImpactResult, gates: any[], corrections: any): string[] {
255
256
  const recs: string[] = []
256
257
 
@@ -268,6 +269,7 @@ export class PreEditValidation {
268
269
 
269
270
  const warnings = gates.filter(g => g.severity === 'WARNING' && !g.canProceed)
270
271
  if (warnings.length > 0) {
272
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
273
  recs.push(`Address warnings: ${warnings.map((w: any) => w.gate).join(', ')}`)
272
274
  }
273
275
  if (corrections.issues.length > 0) {
@@ -1,6 +1,8 @@
1
1
  import * as path from 'node:path'
2
2
  import * as fs from 'node:fs/promises'
3
- import type { MikkLock } from '@getmikk/core'
3
+ import type { MikkLock, MikkLockFunction } from '@getmikk/core'
4
+
5
+ const MAX_BODY_TOKENS = 150
4
6
 
5
7
  interface EmbeddingCache {
6
8
  lockFingerprint: string
@@ -35,6 +37,7 @@ export class SemanticSearcher {
35
37
  static readonly MODEL = 'Xenova/all-MiniLM-L6-v2'
36
38
 
37
39
  private readonly cachePath: string
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
41
  private pipeline: any = null
39
42
  private cache: EmbeddingCache | null = null
40
43
 
@@ -77,7 +80,9 @@ export class SemanticSearcher {
77
80
  this.cache = cached
78
81
  return
79
82
  }
80
- } catch { /* miss or corrupt -- rebuild */ }
83
+ } catch (err) {
84
+ console.warn(`[mikk] Semantic search cache miss/rebuild: ${err instanceof Error ? err.message : String(err)}`)
85
+ }
81
86
 
82
87
  // -- Empty lock fast-path -- nothing to embed ------------------------
83
88
  const fns = Object.values(lock.functions)
@@ -86,16 +91,8 @@ export class SemanticSearcher {
86
91
  return
87
92
  }
88
93
 
89
- // Text representation: name + purpose + param names (no bodies, keeps it fast)
90
- const texts = fns.map(fn => {
91
- const parts: string[] = [fn.name]
92
- if (fn.purpose) parts.push(fn.purpose)
93
- if (fn.params?.length) parts.push(fn.params.map((p: any) => p.name).join(' '))
94
- if (fn.returnType && fn.returnType !== 'void' && fn.returnType !== 'any') {
95
- parts.push('returns ' + fn.returnType)
96
- }
97
- return parts.join(' ')
98
- })
94
+ // Text representation: name + purpose + params + types + return type + body snippet
95
+ const texts = await Promise.all(fns.map(fn => buildRichText(fn, this.projectRoot)))
99
96
 
100
97
  await this.ensurePipeline()
101
98
  const embeddings: Record<string, number[]> = {}
@@ -109,8 +106,12 @@ export class SemanticSearcher {
109
106
  }
110
107
 
111
108
  this.cache = { lockFingerprint: fingerprint, model: SemanticSearcher.MODEL, embeddings }
112
- await fs.mkdir(path.dirname(this.cachePath), { recursive: true })
113
- await fs.writeFile(this.cachePath, JSON.stringify(this.cache))
109
+ try {
110
+ await fs.mkdir(path.dirname(this.cachePath), { recursive: true })
111
+ await fs.writeFile(this.cachePath, JSON.stringify(this.cache))
112
+ } catch (err) {
113
+ console.warn(`[mikk] Failed to write semantic search cache: ${err instanceof Error ? err.message : String(err)}`)
114
+ }
114
115
  }
115
116
 
116
117
  /**
@@ -154,12 +155,97 @@ export class SemanticSearcher {
154
155
  }
155
156
  }
156
157
 
157
- // --- Helpers -----------------------------------------------------------------
158
+ async function buildRichText(fn: MikkLockFunction, projectRoot: string): Promise<string> {
159
+ const parts: string[] = [fn.name]
160
+
161
+ if (fn.purpose) {
162
+ parts.push(fn.purpose)
163
+ }
164
+
165
+ if (fn.params?.length) {
166
+ const paramStr = fn.params.map((p) => p.name).join(' ')
167
+ const typeStr = fn.params.map((p) => p.type || '').filter(Boolean).join(' ')
168
+ parts.push(paramStr, typeStr)
169
+ }
170
+
171
+ if (fn.returnType && fn.returnType !== 'void' && fn.returnType !== 'any') {
172
+ parts.push('returns', fn.returnType)
173
+ }
174
+
175
+ const body = await getFunctionBodySnippet(fn, projectRoot)
176
+ if (body) {
177
+ parts.push(body)
178
+ }
179
+
180
+ return parts.join(' ')
181
+ }
182
+
183
+ async function getFunctionBodySnippet(fn: MikkLockFunction, projectRoot: string): Promise<string> {
184
+ try {
185
+ const fullPath = path.join(projectRoot, fn.file)
186
+ const content = await fs.readFile(fullPath, 'utf-8')
187
+ const lines = content.split('\n')
188
+ const start = Math.max(0, fn.startLine - 1)
189
+ const end = Math.min(lines.length, fn.endLine)
190
+ const bodyLines = lines.slice(start, end)
191
+ const bodyText = bodyLines.join(' ')
158
192
 
159
- /** Lightweight fingerprint: function count + first 20 sorted IDs */
193
+ const cleaned = cleanCodeForEmbedding(bodyText)
194
+
195
+ const tokens = cleaned.split(/\s+/).filter(Boolean)
196
+ if (tokens.length <= MAX_BODY_TOKENS) {
197
+ return cleaned
198
+ }
199
+
200
+ const truncated = tokens.slice(0, MAX_BODY_TOKENS).join(' ')
201
+ return truncated + ' ...'
202
+ } catch {
203
+ return ''
204
+ }
205
+ }
206
+
207
+ function cleanCodeForEmbedding(code: string): string {
208
+ return code
209
+ .replace(/\/\*\*[\s\S]*?\*\//g, ' ')
210
+ .replace(/\/\/[^\n]*/g, ' ')
211
+ .replace(/#.*$/gm, ' ')
212
+ .replace(/['"`][^'"`]*['"`]/g, ' ')
213
+ .replace(/\{[^}]*\}/g, ' ')
214
+ .replace(/\s+/g, ' ')
215
+ .trim()
216
+ }
217
+
218
+ async function _readFileCached(filePath: string, cache: Map<string, string>): Promise<string> {
219
+ if (cache.has(filePath)) {
220
+ return cache.get(filePath)!
221
+ }
222
+ try {
223
+ const content = await fs.readFile(filePath, 'utf-8')
224
+ cache.set(filePath, content)
225
+ return content
226
+ } catch {
227
+ return ''
228
+ }
229
+ }
230
+
231
+ /** Improved fingerprint: function count + all sorted IDs + metadata */
160
232
  function lockFingerprint(lock: MikkLock): string {
161
- const ids = Object.keys(lock.functions).sort().slice(0, 20).join('|')
162
- return `${Object.keys(lock.functions).length}:${ids}`
233
+ const ids = Object.keys(lock.functions).sort().join('|')
234
+ const fnCount = Object.keys(lock.functions).length
235
+ const fileCount = Object.keys(lock.files ?? {}).length
236
+ const moduleCount = Object.keys(lock.modules ?? {}).length
237
+ const hash = hashContent(`${fnCount}:${fileCount}:${moduleCount}:${ids}`)
238
+ return hash.slice(0, 32)
239
+ }
240
+
241
+ function hashContent(content: string): string {
242
+ let hash = 0
243
+ for (let i = 0; i < content.length; i++) {
244
+ const char = content.charCodeAt(i)
245
+ hash = ((hash << 5) - hash) + char
246
+ hash = hash & hash
247
+ }
248
+ return Math.abs(hash).toString(16)
163
249
  }
164
250
 
165
251
  function cosineSimilarity(a: number[], b: number[]): number {
@@ -119,7 +119,7 @@ describe('ConflictDetector', () => {
119
119
  confidence: 0.8,
120
120
  }])
121
121
  // The "No direct DB access outside db/" constraint should fire
122
- const dbConflict = result.conflicts.find(c =>
122
+ result.conflicts.find(c =>
123
123
  c.message.toLowerCase().includes('db') || c.message.toLowerCase().includes('restricted')
124
124
  )
125
125
  // This may or may not fire depending on exact matching — test the shape