@getmikk/intent-engine 1.2.0 → 1.3.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.
@@ -1,216 +1,216 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
- import { IntentSchema, type Intent } from './types.js'
3
-
4
- /**
5
- * IntentInterpreter — parses a natural-language prompt into structured
6
- * intents using heuristic keyword matching and fuzzy matching against
7
- * the lock file's function/module names.
8
- */
9
- export class IntentInterpreter {
10
- constructor(
11
- private contract: MikkContract,
12
- private lock: MikkLock
13
- ) { }
14
-
15
- async interpret(prompt: string): Promise<Intent[]> {
16
- const intents: Intent[] = []
17
- const promptLower = prompt.toLowerCase()
18
-
19
- // Detect action verbs
20
- const actions: Intent['action'][] = []
21
- if (promptLower.includes('add') || promptLower.includes('create') ||
22
- promptLower.includes('new') || promptLower.includes('implement')) {
23
- actions.push('create')
24
- }
25
- if (promptLower.includes('modify') || promptLower.includes('change') ||
26
- promptLower.includes('update') || promptLower.includes('fix') ||
27
- promptLower.includes('edit') || promptLower.includes('patch')) {
28
- actions.push('modify')
29
- }
30
- if (promptLower.includes('delete') || promptLower.includes('remove') ||
31
- promptLower.includes('drop')) {
32
- actions.push('delete')
33
- }
34
- if (promptLower.includes('refactor') || promptLower.includes('restructure') ||
35
- promptLower.includes('clean up') || promptLower.includes('reorganize')) {
36
- actions.push('refactor')
37
- }
38
- if (promptLower.includes('move') || promptLower.includes('migrate') ||
39
- promptLower.includes('relocate')) {
40
- actions.push('move')
41
- }
42
-
43
- // Default to modify if no action is detected
44
- if (actions.length === 0) {
45
- actions.push('modify')
46
- }
47
-
48
- // Find the best matching target — try functions first, then modules
49
- const matchedFunctions = this.findMatchingFunctions(prompt)
50
- const matchedModule = this.findMatchingModule(prompt)
51
-
52
- for (const action of actions) {
53
- if (matchedFunctions.length > 0) {
54
- // Create an intent for each matched function (up to 3)
55
- for (const fn of matchedFunctions.slice(0, 3)) {
56
- intents.push({
57
- action,
58
- target: {
59
- type: 'function',
60
- name: fn.name,
61
- moduleId: fn.moduleId,
62
- filePath: fn.file,
63
- },
64
- reason: prompt,
65
- confidence: fn.score,
66
- })
67
- }
68
- } else if (matchedModule) {
69
- intents.push({
70
- action,
71
- target: {
72
- type: 'module',
73
- name: matchedModule.name,
74
- moduleId: matchedModule.id,
75
- },
76
- reason: prompt,
77
- confidence: matchedModule.score,
78
- })
79
- } else {
80
- // No match — use extracted name
81
- intents.push({
82
- action,
83
- target: {
84
- type: this.inferTargetType(prompt),
85
- name: this.extractName(prompt),
86
- },
87
- reason: prompt,
88
- confidence: 0.3,
89
- })
90
- }
91
- }
92
-
93
- return intents
94
- }
95
-
96
- // ── Fuzzy Matching ───────────────────────────────────────────
97
-
98
- private findMatchingFunctions(prompt: string): Array<{ name: string; file: string; moduleId: string; score: number }> {
99
- const promptLower = prompt.toLowerCase()
100
- const keywords = this.extractKeywords(prompt)
101
- const results: Array<{ name: string; file: string; moduleId: string; score: number }> = []
102
-
103
- for (const fn of Object.values(this.lock.functions)) {
104
- let score = 0
105
- const fnNameLower = fn.name.toLowerCase()
106
- const fileLower = fn.file.toLowerCase()
107
-
108
- // Exact name match in prompt → very high score
109
- if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
110
- score += 0.9
111
- }
112
-
113
- // Keyword matches in function name
114
- for (const kw of keywords) {
115
- if (fnNameLower.includes(kw)) score += 0.3
116
- if (fileLower.includes(kw)) score += 0.15
117
- }
118
-
119
- // CamelCase decomposition partial matches
120
- const fnWords = this.splitCamelCase(fn.name).map(w => w.toLowerCase())
121
- for (const kw of keywords) {
122
- if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
123
- score += 0.2
124
- }
125
- }
126
-
127
- // Cap score at 1.0
128
- if (score > 0.3) {
129
- results.push({
130
- name: fn.name,
131
- file: fn.file,
132
- moduleId: fn.moduleId,
133
- score: Math.min(score, 1.0),
134
- })
135
- }
136
- }
137
-
138
- // Sort by score descending
139
- return results.sort((a, b) => b.score - a.score)
140
- }
141
-
142
- private findMatchingModule(prompt: string): { id: string; name: string; score: number } | null {
143
- const promptLower = prompt.toLowerCase()
144
- const keywords = this.extractKeywords(prompt)
145
- let bestMatch: { id: string; name: string; score: number } | null = null
146
-
147
- for (const module of this.contract.declared.modules) {
148
- let score = 0
149
- const moduleLower = module.name.toLowerCase()
150
- const moduleIdLower = module.id.toLowerCase()
151
-
152
- // Direct ID or name match
153
- if (promptLower.includes(moduleIdLower)) score += 0.8
154
- if (promptLower.includes(moduleLower)) score += 0.7
155
-
156
- // Keyword matches
157
- for (const kw of keywords) {
158
- if (moduleLower.includes(kw)) score += 0.2
159
- if (moduleIdLower.includes(kw)) score += 0.2
160
- }
161
-
162
- if (score > (bestMatch?.score || 0)) {
163
- bestMatch = { id: module.id, name: module.name, score: Math.min(score, 1.0) }
164
- }
165
- }
166
-
167
- return bestMatch && bestMatch.score > 0.3 ? bestMatch : null
168
- }
169
-
170
- // ── Helpers ───────────────────────────────────────────────────
171
-
172
- private inferTargetType(prompt: string): Intent['target']['type'] {
173
- const lower = prompt.toLowerCase()
174
- if (lower.includes('function') || lower.includes('method')) return 'function'
175
- if (lower.includes('class')) return 'class'
176
- if (lower.includes('module') || lower.includes('package')) return 'module'
177
- return 'file'
178
- }
179
-
180
- private extractName(prompt: string): string {
181
- // Try quoted strings first
182
- const quoted = prompt.match(/["'`]([^"'`]+)["'`]/)
183
- if (quoted) return quoted[1]
184
-
185
- // Try backtick code references
186
- const code = prompt.match(/`([^`]+)`/)
187
- if (code) return code[1]
188
-
189
- // Fall back to last meaningful word
190
- const words = prompt.split(/\s+/).filter(w => w.length > 2)
191
- return words[words.length - 1] || 'unknown'
192
- }
193
-
194
- private extractKeywords(text: string): string[] {
195
- const stopWords = new Set([
196
- 'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
197
- 'or', 'is', 'are', 'was', 'be', 'not', 'no', 'from', 'with',
198
- 'add', 'create', 'modify', 'change', 'update', 'delete', 'remove',
199
- 'fix', 'move', 'refactor', 'new', 'old', 'all', 'this', 'that',
200
- 'should', 'can', 'will', 'must', 'need', 'want', 'please',
201
- ])
202
- return text
203
- .toLowerCase()
204
- .replace(/[^a-z0-9\s]/g, ' ')
205
- .split(/\s+/)
206
- .filter(w => w.length > 2 && !stopWords.has(w))
207
- }
208
-
209
- private splitCamelCase(name: string): string[] {
210
- return name
211
- .replace(/([a-z])([A-Z])/g, '$1 $2')
212
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
213
- .split(/[\s_-]+/)
214
- .filter(w => w.length > 0)
215
- }
216
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+ import { IntentSchema, type Intent } from './types.js'
3
+
4
+ /**
5
+ * IntentInterpreter — parses a natural-language prompt into structured
6
+ * intents using heuristic keyword matching and fuzzy matching against
7
+ * the lock file's function/module names.
8
+ */
9
+ export class IntentInterpreter {
10
+ constructor(
11
+ private contract: MikkContract,
12
+ private lock: MikkLock
13
+ ) { }
14
+
15
+ async interpret(prompt: string): Promise<Intent[]> {
16
+ const intents: Intent[] = []
17
+ const promptLower = prompt.toLowerCase()
18
+
19
+ // Detect action verbs
20
+ const actions: Intent['action'][] = []
21
+ if (promptLower.includes('add') || promptLower.includes('create') ||
22
+ promptLower.includes('new') || promptLower.includes('implement')) {
23
+ actions.push('create')
24
+ }
25
+ if (promptLower.includes('modify') || promptLower.includes('change') ||
26
+ promptLower.includes('update') || promptLower.includes('fix') ||
27
+ promptLower.includes('edit') || promptLower.includes('patch')) {
28
+ actions.push('modify')
29
+ }
30
+ if (promptLower.includes('delete') || promptLower.includes('remove') ||
31
+ promptLower.includes('drop')) {
32
+ actions.push('delete')
33
+ }
34
+ if (promptLower.includes('refactor') || promptLower.includes('restructure') ||
35
+ promptLower.includes('clean up') || promptLower.includes('reorganize')) {
36
+ actions.push('refactor')
37
+ }
38
+ if (promptLower.includes('move') || promptLower.includes('migrate') ||
39
+ promptLower.includes('relocate')) {
40
+ actions.push('move')
41
+ }
42
+
43
+ // Default to modify if no action is detected
44
+ if (actions.length === 0) {
45
+ actions.push('modify')
46
+ }
47
+
48
+ // Find the best matching target — try functions first, then modules
49
+ const matchedFunctions = this.findMatchingFunctions(prompt)
50
+ const matchedModule = this.findMatchingModule(prompt)
51
+
52
+ for (const action of actions) {
53
+ if (matchedFunctions.length > 0) {
54
+ // Create an intent for each matched function (up to 3)
55
+ for (const fn of matchedFunctions.slice(0, 3)) {
56
+ intents.push({
57
+ action,
58
+ target: {
59
+ type: 'function',
60
+ name: fn.name,
61
+ moduleId: fn.moduleId,
62
+ filePath: fn.file,
63
+ },
64
+ reason: prompt,
65
+ confidence: fn.score,
66
+ })
67
+ }
68
+ } else if (matchedModule) {
69
+ intents.push({
70
+ action,
71
+ target: {
72
+ type: 'module',
73
+ name: matchedModule.name,
74
+ moduleId: matchedModule.id,
75
+ },
76
+ reason: prompt,
77
+ confidence: matchedModule.score,
78
+ })
79
+ } else {
80
+ // No match — use extracted name
81
+ intents.push({
82
+ action,
83
+ target: {
84
+ type: this.inferTargetType(prompt),
85
+ name: this.extractName(prompt),
86
+ },
87
+ reason: prompt,
88
+ confidence: 0.3,
89
+ })
90
+ }
91
+ }
92
+
93
+ return intents
94
+ }
95
+
96
+ // ── Fuzzy Matching ───────────────────────────────────────────
97
+
98
+ private findMatchingFunctions(prompt: string): Array<{ name: string; file: string; moduleId: string; score: number }> {
99
+ const promptLower = prompt.toLowerCase()
100
+ const keywords = this.extractKeywords(prompt)
101
+ const results: Array<{ name: string; file: string; moduleId: string; score: number }> = []
102
+
103
+ for (const fn of Object.values(this.lock.functions)) {
104
+ let score = 0
105
+ const fnNameLower = fn.name.toLowerCase()
106
+ const fileLower = fn.file.toLowerCase()
107
+
108
+ // Exact name match in prompt → very high score
109
+ if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
110
+ score += 0.9
111
+ }
112
+
113
+ // Keyword matches in function name
114
+ for (const kw of keywords) {
115
+ if (fnNameLower.includes(kw)) score += 0.3
116
+ if (fileLower.includes(kw)) score += 0.15
117
+ }
118
+
119
+ // CamelCase decomposition partial matches
120
+ const fnWords = this.splitCamelCase(fn.name).map(w => w.toLowerCase())
121
+ for (const kw of keywords) {
122
+ if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
123
+ score += 0.2
124
+ }
125
+ }
126
+
127
+ // Cap score at 1.0
128
+ if (score > 0.3) {
129
+ results.push({
130
+ name: fn.name,
131
+ file: fn.file,
132
+ moduleId: fn.moduleId,
133
+ score: Math.min(score, 1.0),
134
+ })
135
+ }
136
+ }
137
+
138
+ // Sort by score descending
139
+ return results.sort((a, b) => b.score - a.score)
140
+ }
141
+
142
+ private findMatchingModule(prompt: string): { id: string; name: string; score: number } | null {
143
+ const promptLower = prompt.toLowerCase()
144
+ const keywords = this.extractKeywords(prompt)
145
+ let bestMatch: { id: string; name: string; score: number } | null = null
146
+
147
+ for (const module of this.contract.declared.modules) {
148
+ let score = 0
149
+ const moduleLower = module.name.toLowerCase()
150
+ const moduleIdLower = module.id.toLowerCase()
151
+
152
+ // Direct ID or name match
153
+ if (promptLower.includes(moduleIdLower)) score += 0.8
154
+ if (promptLower.includes(moduleLower)) score += 0.7
155
+
156
+ // Keyword matches
157
+ for (const kw of keywords) {
158
+ if (moduleLower.includes(kw)) score += 0.2
159
+ if (moduleIdLower.includes(kw)) score += 0.2
160
+ }
161
+
162
+ if (score > (bestMatch?.score || 0)) {
163
+ bestMatch = { id: module.id, name: module.name, score: Math.min(score, 1.0) }
164
+ }
165
+ }
166
+
167
+ return bestMatch && bestMatch.score > 0.3 ? bestMatch : null
168
+ }
169
+
170
+ // ── Helpers ───────────────────────────────────────────────────
171
+
172
+ private inferTargetType(prompt: string): Intent['target']['type'] {
173
+ const lower = prompt.toLowerCase()
174
+ if (lower.includes('function') || lower.includes('method')) return 'function'
175
+ if (lower.includes('class')) return 'class'
176
+ if (lower.includes('module') || lower.includes('package')) return 'module'
177
+ return 'file'
178
+ }
179
+
180
+ private extractName(prompt: string): string {
181
+ // Try quoted strings first
182
+ const quoted = prompt.match(/["'`]([^"'`]+)["'`]/)
183
+ if (quoted) return quoted[1]
184
+
185
+ // Try backtick code references
186
+ const code = prompt.match(/`([^`]+)`/)
187
+ if (code) return code[1]
188
+
189
+ // Fall back to last meaningful word
190
+ const words = prompt.split(/\s+/).filter(w => w.length > 2)
191
+ return words[words.length - 1] || 'unknown'
192
+ }
193
+
194
+ private extractKeywords(text: string): string[] {
195
+ const stopWords = new Set([
196
+ 'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
197
+ 'or', 'is', 'are', 'was', 'be', 'not', 'no', 'from', 'with',
198
+ 'add', 'create', 'modify', 'change', 'update', 'delete', 'remove',
199
+ 'fix', 'move', 'refactor', 'new', 'old', 'all', 'this', 'that',
200
+ 'should', 'can', 'will', 'must', 'need', 'want', 'please',
201
+ ])
202
+ return text
203
+ .toLowerCase()
204
+ .replace(/[^a-z0-9\s]/g, ' ')
205
+ .split(/\s+/)
206
+ .filter(w => w.length > 2 && !stopWords.has(w))
207
+ }
208
+
209
+ private splitCamelCase(name: string): string[] {
210
+ return name
211
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
212
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
213
+ .split(/[\s_-]+/)
214
+ .filter(w => w.length > 0)
215
+ }
216
+ }
package/src/preflight.ts CHANGED
@@ -1,44 +1,44 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
- import { IntentInterpreter } from './interpreter.js'
3
- import { ConflictDetector } from './conflict-detector.js'
4
- import { Suggester } from './suggester.js'
5
- import type { PreflightResult } from './types.js'
6
-
7
- /**
8
- * PreflightPipeline — orchestrates the full intent pipeline:
9
- * interpret → conflict-detect → suggest.
10
- * Single function call for the CLI.
11
- */
12
- export class PreflightPipeline {
13
- private interpreter: IntentInterpreter
14
- private conflictDetector: ConflictDetector
15
- private suggester: Suggester
16
-
17
- constructor(
18
- private contract: MikkContract,
19
- private lock: MikkLock
20
- ) {
21
- this.interpreter = new IntentInterpreter(contract, lock)
22
- this.conflictDetector = new ConflictDetector(contract, lock)
23
- this.suggester = new Suggester(contract, lock)
24
- }
25
-
26
- /** Run the full preflight pipeline */
27
- async run(prompt: string): Promise<PreflightResult> {
28
- // 1. Interpret prompt into structured intents
29
- const intents = await this.interpreter.interpret(prompt)
30
-
31
- // 2. Check for conflicts
32
- const conflicts = this.conflictDetector.detect(intents)
33
-
34
- // 3. Generate suggestions
35
- const suggestions = this.suggester.suggest(intents)
36
-
37
- return {
38
- intents,
39
- conflicts,
40
- suggestions,
41
- approved: !conflicts.hasConflicts,
42
- }
43
- }
44
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+ import { IntentInterpreter } from './interpreter.js'
3
+ import { ConflictDetector } from './conflict-detector.js'
4
+ import { Suggester } from './suggester.js'
5
+ import type { PreflightResult } from './types.js'
6
+
7
+ /**
8
+ * PreflightPipeline — orchestrates the full intent pipeline:
9
+ * interpret → conflict-detect → suggest.
10
+ * Single function call for the CLI.
11
+ */
12
+ export class PreflightPipeline {
13
+ private interpreter: IntentInterpreter
14
+ private conflictDetector: ConflictDetector
15
+ private suggester: Suggester
16
+
17
+ constructor(
18
+ private contract: MikkContract,
19
+ private lock: MikkLock
20
+ ) {
21
+ this.interpreter = new IntentInterpreter(contract, lock)
22
+ this.conflictDetector = new ConflictDetector(contract, lock)
23
+ this.suggester = new Suggester(contract, lock)
24
+ }
25
+
26
+ /** Run the full preflight pipeline */
27
+ async run(prompt: string): Promise<PreflightResult> {
28
+ // 1. Interpret prompt into structured intents
29
+ const intents = await this.interpreter.interpret(prompt)
30
+
31
+ // 2. Check for conflicts
32
+ const conflicts = this.conflictDetector.detect(intents)
33
+
34
+ // 3. Generate suggestions
35
+ const suggestions = this.suggester.suggest(intents)
36
+
37
+ return {
38
+ intents,
39
+ conflicts,
40
+ suggestions,
41
+ approved: !conflicts.hasConflicts,
42
+ }
43
+ }
44
+ }