@getmikk/intent-engine 1.2.0 → 1.3.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/README.md +257 -0
- package/package.json +31 -27
- package/src/conflict-detector.ts +302 -302
- package/src/index.ts +6 -6
- package/src/interpreter.ts +216 -216
- package/src/preflight.ts +44 -44
- package/src/suggester.ts +104 -104
- package/src/types.ts +54 -54
- package/tests/intent-engine.test.ts +210 -210
- package/tests/smoke.test.ts +5 -5
- package/tsconfig.json +15 -15
package/src/conflict-detector.ts
CHANGED
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
-
import type { Intent, ConflictResult, Conflict } from './types.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Constraint types — classifies constraints for rule-based checking per Section 5.
|
|
6
|
-
* Rule-based matching is fast and doesn't require AI calls.
|
|
7
|
-
*/
|
|
8
|
-
type ConstraintType =
|
|
9
|
-
| 'no-import' // "No direct DB access outside db/"
|
|
10
|
-
| 'must-use' // "All auth must go through auth.middleware"
|
|
11
|
-
| 'no-call' // "Never call setTimeout in the payment flow"
|
|
12
|
-
| 'layer' // "Controllers cannot import from repositories directly"
|
|
13
|
-
| 'naming' // "All exported functions must be camelCase"
|
|
14
|
-
| 'complex' // Everything else
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* ConflictDetector — checks candidate intents against declared
|
|
18
|
-
* constraints and module boundaries. Uses rule-based pattern matching
|
|
19
|
-
* per spec Section 5 — no AI calls for deterministic constraint types.
|
|
20
|
-
*/
|
|
21
|
-
export class ConflictDetector {
|
|
22
|
-
constructor(
|
|
23
|
-
private contract: MikkContract,
|
|
24
|
-
private lock?: MikkLock
|
|
25
|
-
) { }
|
|
26
|
-
|
|
27
|
-
/** Check all intents for conflicts */
|
|
28
|
-
detect(intents: Intent[]): ConflictResult {
|
|
29
|
-
const conflicts: Conflict[] = []
|
|
30
|
-
|
|
31
|
-
for (const intent of intents) {
|
|
32
|
-
// Check constraint violations
|
|
33
|
-
for (const constraint of this.contract.declared.constraints) {
|
|
34
|
-
const conflict = this.checkConstraint(intent, constraint)
|
|
35
|
-
if (conflict) {
|
|
36
|
-
conflicts.push(conflict)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check boundary crossings for move/refactor actions
|
|
41
|
-
if (intent.target.moduleId && (intent.action === 'move' || intent.action === 'refactor')) {
|
|
42
|
-
// Check if the target module exists
|
|
43
|
-
const moduleExists = this.contract.declared.modules.some(
|
|
44
|
-
m => m.id === intent.target.moduleId
|
|
45
|
-
)
|
|
46
|
-
if (intent.action === 'move') {
|
|
47
|
-
conflicts.push({
|
|
48
|
-
type: 'boundary-crossing',
|
|
49
|
-
severity: 'warning',
|
|
50
|
-
message: `Moving ${intent.target.name} will cross module boundary from ${intent.target.moduleId}`,
|
|
51
|
-
relatedIntent: intent,
|
|
52
|
-
suggestedFix: moduleExists
|
|
53
|
-
? `Ensure all callers of ${intent.target.name} are updated after the move`
|
|
54
|
-
: `Module "${intent.target.moduleId}" not found — check mikk.json`,
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check for missing dependency: does the target exist in the lock?
|
|
60
|
-
if (this.lock && intent.action === 'modify') {
|
|
61
|
-
const fnExists = Object.values(this.lock.functions).some(
|
|
62
|
-
f => f.name === intent.target.name
|
|
63
|
-
)
|
|
64
|
-
const fileExists = intent.target.filePath
|
|
65
|
-
? !!this.lock.files[intent.target.filePath]
|
|
66
|
-
: true
|
|
67
|
-
if (!fnExists && intent.target.type === 'function') {
|
|
68
|
-
conflicts.push({
|
|
69
|
-
type: 'missing-dependency',
|
|
70
|
-
severity: 'warning',
|
|
71
|
-
message: `Function "${intent.target.name}" not found in lock file — it may not exist yet`,
|
|
72
|
-
relatedIntent: intent,
|
|
73
|
-
suggestedFix: `Did you mean "create" instead of "modify"?`,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
if (!fileExists) {
|
|
77
|
-
conflicts.push({
|
|
78
|
-
type: 'missing-dependency',
|
|
79
|
-
severity: 'warning',
|
|
80
|
-
message: `File "${intent.target.filePath}" not found in lock file`,
|
|
81
|
-
relatedIntent: intent,
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Ownership check: warn if modifying a module with explicit owners
|
|
87
|
-
if (intent.target.moduleId) {
|
|
88
|
-
const module = this.contract.declared.modules.find(
|
|
89
|
-
m => m.id === intent.target.moduleId
|
|
90
|
-
)
|
|
91
|
-
if (module?.owners && module.owners.length > 0) {
|
|
92
|
-
conflicts.push({
|
|
93
|
-
type: 'ownership-conflict',
|
|
94
|
-
severity: 'warning',
|
|
95
|
-
message: `Module "${module.name}" has designated owners: ${module.owners.join(', ')}`,
|
|
96
|
-
relatedIntent: intent,
|
|
97
|
-
suggestedFix: `Coordinate with ${module.owners[0]} before modifying this module`,
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
hasConflicts: conflicts.some(c => c.severity === 'error'),
|
|
105
|
-
conflicts,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Constraint Classification & Checking ─────────────────────
|
|
110
|
-
|
|
111
|
-
private classifyConstraint(text: string): ConstraintType {
|
|
112
|
-
const lower = text.toLowerCase()
|
|
113
|
-
if (lower.includes('no direct') || lower.includes('cannot import') ||
|
|
114
|
-
lower.includes('must not import')) return 'no-import'
|
|
115
|
-
if (lower.includes('must go through') || lower.includes('must use') ||
|
|
116
|
-
lower.includes('required')) return 'must-use'
|
|
117
|
-
if (lower.includes('never call') || lower.includes('do not call')) return 'no-call'
|
|
118
|
-
if (lower.includes('cannot import from') || lower.includes('layer')) return 'layer'
|
|
119
|
-
if (lower.includes('must be') && (lower.includes('case') || lower.includes('named')))
|
|
120
|
-
return 'naming'
|
|
121
|
-
return 'complex'
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private checkConstraint(intent: Intent, constraint: string): Conflict | null {
|
|
125
|
-
const type = this.classifyConstraint(constraint)
|
|
126
|
-
switch (type) {
|
|
127
|
-
case 'no-import': return this.checkNoImport(constraint, intent)
|
|
128
|
-
case 'must-use': return this.checkMustUse(constraint, intent)
|
|
129
|
-
case 'no-call': return this.checkNoCall(constraint, intent)
|
|
130
|
-
case 'layer': return this.checkLayer(constraint, intent)
|
|
131
|
-
case 'naming': return this.checkNaming(constraint, intent)
|
|
132
|
-
case 'complex': return this.checkComplex(constraint, intent)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** "No direct DB access outside db/" */
|
|
137
|
-
private checkNoImport(constraint: string, intent: Intent): Conflict | null {
|
|
138
|
-
const match = constraint.match(/no direct (\w+) (?:access|import) outside (.+)/i)
|
|
139
|
-
if (!match) {
|
|
140
|
-
// Fallback: "X cannot import Y" pattern
|
|
141
|
-
const alt = constraint.match(/(\w+) (?:cannot|must not) import (?:from )?(\w+)/i)
|
|
142
|
-
if (!alt) return null
|
|
143
|
-
const [, source, target] = alt
|
|
144
|
-
if (intent.target.moduleId?.toLowerCase().includes(target.toLowerCase())) {
|
|
145
|
-
return this.makeConflict(constraint, intent, 'error',
|
|
146
|
-
`Intent targets ${intent.target.moduleId} which conflicts with import restriction on ${target}`,
|
|
147
|
-
`Use the ${source} module's public API instead`)
|
|
148
|
-
}
|
|
149
|
-
return null
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const [, _accessType, allowedPath] = match
|
|
153
|
-
const allowed = allowedPath.trim().replace(/[/\\*]*/g, '')
|
|
154
|
-
const targetModule = intent.target.moduleId || ''
|
|
155
|
-
|
|
156
|
-
// If the intent's target is outside the allowed area and touches restricted module
|
|
157
|
-
if (targetModule && !targetModule.toLowerCase().includes(allowed.toLowerCase())) {
|
|
158
|
-
return this.makeConflict(constraint, intent, 'error',
|
|
159
|
-
`Intent "${intent.action} ${intent.target.name}" in module "${targetModule}" accesses a restricted area. Only "${allowed}" modules may access this.`,
|
|
160
|
-
`Route through the ${allowed} module's public API`)
|
|
161
|
-
}
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** "All auth must go through auth.middleware" */
|
|
166
|
-
private checkMustUse(constraint: string, intent: Intent): Conflict | null {
|
|
167
|
-
const match = constraint.match(/all (\w+) must (?:go through|use) (.+)/i)
|
|
168
|
-
if (!match) return null
|
|
169
|
-
const [, domain, requiredFn] = match
|
|
170
|
-
|
|
171
|
-
// Check if the intent touches this domain
|
|
172
|
-
const targetLower = intent.target.name.toLowerCase()
|
|
173
|
-
const moduleLower = intent.target.moduleId?.toLowerCase() || ''
|
|
174
|
-
if (!targetLower.includes(domain.toLowerCase()) &&
|
|
175
|
-
!moduleLower.includes(domain.toLowerCase())) {
|
|
176
|
-
return null
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// If creating or modifying in this domain, warn about required function
|
|
180
|
-
if (intent.action === 'create' || intent.action === 'modify') {
|
|
181
|
-
return this.makeConflict(constraint, intent, 'warning',
|
|
182
|
-
`${intent.action} in the "${domain}" domain requires using ${requiredFn.trim()}`,
|
|
183
|
-
`Ensure ${requiredFn.trim()} is called in the ${intent.target.name} flow`)
|
|
184
|
-
}
|
|
185
|
-
return null
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** "Never call setTimeout in the payment flow" */
|
|
189
|
-
private checkNoCall(constraint: string, intent: Intent): Conflict | null {
|
|
190
|
-
const match = constraint.match(/(?:never|do not) call (\w+)(?: in (?:the )?(.+))?/i)
|
|
191
|
-
if (!match) return null
|
|
192
|
-
const [, forbiddenCall, contextArea] = match
|
|
193
|
-
|
|
194
|
-
// If a context area is specified, only check intents in that area
|
|
195
|
-
if (contextArea) {
|
|
196
|
-
const area = contextArea.trim().replace(/\s*flow$/i, '')
|
|
197
|
-
const intentModule = intent.target.moduleId?.toLowerCase() || ''
|
|
198
|
-
const intentName = intent.target.name.toLowerCase()
|
|
199
|
-
if (!intentModule.includes(area.toLowerCase()) &&
|
|
200
|
-
!intentName.includes(area.toLowerCase())) {
|
|
201
|
-
return null
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Warn if creating code that might use the forbidden function
|
|
206
|
-
if (intent.action === 'create' || intent.action === 'modify') {
|
|
207
|
-
return this.makeConflict(constraint, intent, 'warning',
|
|
208
|
-
`Ensure "${forbiddenCall}" is not called in this context`,
|
|
209
|
-
`Avoid using ${forbiddenCall} — see constraint: "${constraint}"`)
|
|
210
|
-
}
|
|
211
|
-
return null
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** "Controllers cannot import from repositories directly" */
|
|
215
|
-
private checkLayer(constraint: string, intent: Intent): Conflict | null {
|
|
216
|
-
const match = constraint.match(/(\w+) cannot import (?:from )?(\w+)/i)
|
|
217
|
-
if (!match) return null
|
|
218
|
-
const [, sourceLayer, targetLayer] = match
|
|
219
|
-
|
|
220
|
-
const intentModule = intent.target.moduleId?.toLowerCase() || ''
|
|
221
|
-
if (intentModule.includes(sourceLayer.toLowerCase()) &&
|
|
222
|
-
(intent.action === 'create' || intent.action === 'modify')) {
|
|
223
|
-
return this.makeConflict(constraint, intent, 'warning',
|
|
224
|
-
`${sourceLayer} layer should not import directly from ${targetLayer}`,
|
|
225
|
-
`Use an intermediate service layer between ${sourceLayer} and ${targetLayer}`)
|
|
226
|
-
}
|
|
227
|
-
return null
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** "All exported functions must be camelCase" */
|
|
231
|
-
private checkNaming(constraint: string, intent: Intent): Conflict | null {
|
|
232
|
-
if (intent.action !== 'create') return null
|
|
233
|
-
|
|
234
|
-
const name = intent.target.name
|
|
235
|
-
if (constraint.toLowerCase().includes('camelcase')) {
|
|
236
|
-
// Check if name starts with lowercase and has no underscores/hyphens
|
|
237
|
-
if (!/^[a-z][a-zA-Z0-9]*$/.test(name) && name.length > 0) {
|
|
238
|
-
return this.makeConflict(constraint, intent, 'warning',
|
|
239
|
-
`Name "${name}" does not follow camelCase convention`,
|
|
240
|
-
`Rename to ${this.toCamelCase(name)}`)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return null
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Complex constraints — keyword overlap heuristic (no AI call) */
|
|
247
|
-
private checkComplex(constraint: string, intent: Intent): Conflict | null {
|
|
248
|
-
const constraintWords = this.extractKeywords(constraint)
|
|
249
|
-
const intentWords = [
|
|
250
|
-
...this.extractKeywords(intent.target.name),
|
|
251
|
-
...this.extractKeywords(intent.reason)
|
|
252
|
-
]
|
|
253
|
-
const overlap = constraintWords.filter(w =>
|
|
254
|
-
intentWords.some(iw => iw === w || iw.includes(w) || w.includes(iw))
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
// Only flag if significant keyword overlap suggests relevance
|
|
258
|
-
if (overlap.length >= 2) {
|
|
259
|
-
return this.makeConflict(constraint, intent, 'warning',
|
|
260
|
-
`Intent may conflict with constraint: "${constraint}" (keywords: ${overlap.join(', ')})`,
|
|
261
|
-
`Review the constraint before proceeding`)
|
|
262
|
-
}
|
|
263
|
-
return null
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── Helpers ───────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
private makeConflict(
|
|
269
|
-
constraint: string,
|
|
270
|
-
intent: Intent,
|
|
271
|
-
severity: 'error' | 'warning',
|
|
272
|
-
message: string,
|
|
273
|
-
suggestedFix?: string
|
|
274
|
-
): Conflict {
|
|
275
|
-
return {
|
|
276
|
-
type: 'constraint-violation',
|
|
277
|
-
severity,
|
|
278
|
-
message,
|
|
279
|
-
relatedIntent: intent,
|
|
280
|
-
suggestedFix,
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private extractKeywords(text: string): string[] {
|
|
285
|
-
const stopWords = new Set([
|
|
286
|
-
'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
|
|
287
|
-
'or', 'is', 'are', 'was', 'be', 'not', 'no', 'from', 'with',
|
|
288
|
-
'all', 'must', 'should', 'can', 'cannot', 'will', 'this', 'that',
|
|
289
|
-
])
|
|
290
|
-
return text
|
|
291
|
-
.toLowerCase()
|
|
292
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
293
|
-
.split(/\s+/)
|
|
294
|
-
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private toCamelCase(name: string): string {
|
|
298
|
-
return name
|
|
299
|
-
.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
|
|
300
|
-
.replace(/^[A-Z]/, c => c.toLowerCase())
|
|
301
|
-
}
|
|
302
|
-
}
|
|
1
|
+
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
+
import type { Intent, ConflictResult, Conflict } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Constraint types — classifies constraints for rule-based checking per Section 5.
|
|
6
|
+
* Rule-based matching is fast and doesn't require AI calls.
|
|
7
|
+
*/
|
|
8
|
+
type ConstraintType =
|
|
9
|
+
| 'no-import' // "No direct DB access outside db/"
|
|
10
|
+
| 'must-use' // "All auth must go through auth.middleware"
|
|
11
|
+
| 'no-call' // "Never call setTimeout in the payment flow"
|
|
12
|
+
| 'layer' // "Controllers cannot import from repositories directly"
|
|
13
|
+
| 'naming' // "All exported functions must be camelCase"
|
|
14
|
+
| 'complex' // Everything else
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ConflictDetector — checks candidate intents against declared
|
|
18
|
+
* constraints and module boundaries. Uses rule-based pattern matching
|
|
19
|
+
* per spec Section 5 — no AI calls for deterministic constraint types.
|
|
20
|
+
*/
|
|
21
|
+
export class ConflictDetector {
|
|
22
|
+
constructor(
|
|
23
|
+
private contract: MikkContract,
|
|
24
|
+
private lock?: MikkLock
|
|
25
|
+
) { }
|
|
26
|
+
|
|
27
|
+
/** Check all intents for conflicts */
|
|
28
|
+
detect(intents: Intent[]): ConflictResult {
|
|
29
|
+
const conflicts: Conflict[] = []
|
|
30
|
+
|
|
31
|
+
for (const intent of intents) {
|
|
32
|
+
// Check constraint violations
|
|
33
|
+
for (const constraint of this.contract.declared.constraints) {
|
|
34
|
+
const conflict = this.checkConstraint(intent, constraint)
|
|
35
|
+
if (conflict) {
|
|
36
|
+
conflicts.push(conflict)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check boundary crossings for move/refactor actions
|
|
41
|
+
if (intent.target.moduleId && (intent.action === 'move' || intent.action === 'refactor')) {
|
|
42
|
+
// Check if the target module exists
|
|
43
|
+
const moduleExists = this.contract.declared.modules.some(
|
|
44
|
+
m => m.id === intent.target.moduleId
|
|
45
|
+
)
|
|
46
|
+
if (intent.action === 'move') {
|
|
47
|
+
conflicts.push({
|
|
48
|
+
type: 'boundary-crossing',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
message: `Moving ${intent.target.name} will cross module boundary from ${intent.target.moduleId}`,
|
|
51
|
+
relatedIntent: intent,
|
|
52
|
+
suggestedFix: moduleExists
|
|
53
|
+
? `Ensure all callers of ${intent.target.name} are updated after the move`
|
|
54
|
+
: `Module "${intent.target.moduleId}" not found — check mikk.json`,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for missing dependency: does the target exist in the lock?
|
|
60
|
+
if (this.lock && intent.action === 'modify') {
|
|
61
|
+
const fnExists = Object.values(this.lock.functions).some(
|
|
62
|
+
f => f.name === intent.target.name
|
|
63
|
+
)
|
|
64
|
+
const fileExists = intent.target.filePath
|
|
65
|
+
? !!this.lock.files[intent.target.filePath]
|
|
66
|
+
: true
|
|
67
|
+
if (!fnExists && intent.target.type === 'function') {
|
|
68
|
+
conflicts.push({
|
|
69
|
+
type: 'missing-dependency',
|
|
70
|
+
severity: 'warning',
|
|
71
|
+
message: `Function "${intent.target.name}" not found in lock file — it may not exist yet`,
|
|
72
|
+
relatedIntent: intent,
|
|
73
|
+
suggestedFix: `Did you mean "create" instead of "modify"?`,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
if (!fileExists) {
|
|
77
|
+
conflicts.push({
|
|
78
|
+
type: 'missing-dependency',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
message: `File "${intent.target.filePath}" not found in lock file`,
|
|
81
|
+
relatedIntent: intent,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Ownership check: warn if modifying a module with explicit owners
|
|
87
|
+
if (intent.target.moduleId) {
|
|
88
|
+
const module = this.contract.declared.modules.find(
|
|
89
|
+
m => m.id === intent.target.moduleId
|
|
90
|
+
)
|
|
91
|
+
if (module?.owners && module.owners.length > 0) {
|
|
92
|
+
conflicts.push({
|
|
93
|
+
type: 'ownership-conflict',
|
|
94
|
+
severity: 'warning',
|
|
95
|
+
message: `Module "${module.name}" has designated owners: ${module.owners.join(', ')}`,
|
|
96
|
+
relatedIntent: intent,
|
|
97
|
+
suggestedFix: `Coordinate with ${module.owners[0]} before modifying this module`,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
hasConflicts: conflicts.some(c => c.severity === 'error'),
|
|
105
|
+
conflicts,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Constraint Classification & Checking ─────────────────────
|
|
110
|
+
|
|
111
|
+
private classifyConstraint(text: string): ConstraintType {
|
|
112
|
+
const lower = text.toLowerCase()
|
|
113
|
+
if (lower.includes('no direct') || lower.includes('cannot import') ||
|
|
114
|
+
lower.includes('must not import')) return 'no-import'
|
|
115
|
+
if (lower.includes('must go through') || lower.includes('must use') ||
|
|
116
|
+
lower.includes('required')) return 'must-use'
|
|
117
|
+
if (lower.includes('never call') || lower.includes('do not call')) return 'no-call'
|
|
118
|
+
if (lower.includes('cannot import from') || lower.includes('layer')) return 'layer'
|
|
119
|
+
if (lower.includes('must be') && (lower.includes('case') || lower.includes('named')))
|
|
120
|
+
return 'naming'
|
|
121
|
+
return 'complex'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private checkConstraint(intent: Intent, constraint: string): Conflict | null {
|
|
125
|
+
const type = this.classifyConstraint(constraint)
|
|
126
|
+
switch (type) {
|
|
127
|
+
case 'no-import': return this.checkNoImport(constraint, intent)
|
|
128
|
+
case 'must-use': return this.checkMustUse(constraint, intent)
|
|
129
|
+
case 'no-call': return this.checkNoCall(constraint, intent)
|
|
130
|
+
case 'layer': return this.checkLayer(constraint, intent)
|
|
131
|
+
case 'naming': return this.checkNaming(constraint, intent)
|
|
132
|
+
case 'complex': return this.checkComplex(constraint, intent)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** "No direct DB access outside db/" */
|
|
137
|
+
private checkNoImport(constraint: string, intent: Intent): Conflict | null {
|
|
138
|
+
const match = constraint.match(/no direct (\w+) (?:access|import) outside (.+)/i)
|
|
139
|
+
if (!match) {
|
|
140
|
+
// Fallback: "X cannot import Y" pattern
|
|
141
|
+
const alt = constraint.match(/(\w+) (?:cannot|must not) import (?:from )?(\w+)/i)
|
|
142
|
+
if (!alt) return null
|
|
143
|
+
const [, source, target] = alt
|
|
144
|
+
if (intent.target.moduleId?.toLowerCase().includes(target.toLowerCase())) {
|
|
145
|
+
return this.makeConflict(constraint, intent, 'error',
|
|
146
|
+
`Intent targets ${intent.target.moduleId} which conflicts with import restriction on ${target}`,
|
|
147
|
+
`Use the ${source} module's public API instead`)
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const [, _accessType, allowedPath] = match
|
|
153
|
+
const allowed = allowedPath.trim().replace(/[/\\*]*/g, '')
|
|
154
|
+
const targetModule = intent.target.moduleId || ''
|
|
155
|
+
|
|
156
|
+
// If the intent's target is outside the allowed area and touches restricted module
|
|
157
|
+
if (targetModule && !targetModule.toLowerCase().includes(allowed.toLowerCase())) {
|
|
158
|
+
return this.makeConflict(constraint, intent, 'error',
|
|
159
|
+
`Intent "${intent.action} ${intent.target.name}" in module "${targetModule}" accesses a restricted area. Only "${allowed}" modules may access this.`,
|
|
160
|
+
`Route through the ${allowed} module's public API`)
|
|
161
|
+
}
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** "All auth must go through auth.middleware" */
|
|
166
|
+
private checkMustUse(constraint: string, intent: Intent): Conflict | null {
|
|
167
|
+
const match = constraint.match(/all (\w+) must (?:go through|use) (.+)/i)
|
|
168
|
+
if (!match) return null
|
|
169
|
+
const [, domain, requiredFn] = match
|
|
170
|
+
|
|
171
|
+
// Check if the intent touches this domain
|
|
172
|
+
const targetLower = intent.target.name.toLowerCase()
|
|
173
|
+
const moduleLower = intent.target.moduleId?.toLowerCase() || ''
|
|
174
|
+
if (!targetLower.includes(domain.toLowerCase()) &&
|
|
175
|
+
!moduleLower.includes(domain.toLowerCase())) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If creating or modifying in this domain, warn about required function
|
|
180
|
+
if (intent.action === 'create' || intent.action === 'modify') {
|
|
181
|
+
return this.makeConflict(constraint, intent, 'warning',
|
|
182
|
+
`${intent.action} in the "${domain}" domain requires using ${requiredFn.trim()}`,
|
|
183
|
+
`Ensure ${requiredFn.trim()} is called in the ${intent.target.name} flow`)
|
|
184
|
+
}
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** "Never call setTimeout in the payment flow" */
|
|
189
|
+
private checkNoCall(constraint: string, intent: Intent): Conflict | null {
|
|
190
|
+
const match = constraint.match(/(?:never|do not) call (\w+)(?: in (?:the )?(.+))?/i)
|
|
191
|
+
if (!match) return null
|
|
192
|
+
const [, forbiddenCall, contextArea] = match
|
|
193
|
+
|
|
194
|
+
// If a context area is specified, only check intents in that area
|
|
195
|
+
if (contextArea) {
|
|
196
|
+
const area = contextArea.trim().replace(/\s*flow$/i, '')
|
|
197
|
+
const intentModule = intent.target.moduleId?.toLowerCase() || ''
|
|
198
|
+
const intentName = intent.target.name.toLowerCase()
|
|
199
|
+
if (!intentModule.includes(area.toLowerCase()) &&
|
|
200
|
+
!intentName.includes(area.toLowerCase())) {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Warn if creating code that might use the forbidden function
|
|
206
|
+
if (intent.action === 'create' || intent.action === 'modify') {
|
|
207
|
+
return this.makeConflict(constraint, intent, 'warning',
|
|
208
|
+
`Ensure "${forbiddenCall}" is not called in this context`,
|
|
209
|
+
`Avoid using ${forbiddenCall} — see constraint: "${constraint}"`)
|
|
210
|
+
}
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** "Controllers cannot import from repositories directly" */
|
|
215
|
+
private checkLayer(constraint: string, intent: Intent): Conflict | null {
|
|
216
|
+
const match = constraint.match(/(\w+) cannot import (?:from )?(\w+)/i)
|
|
217
|
+
if (!match) return null
|
|
218
|
+
const [, sourceLayer, targetLayer] = match
|
|
219
|
+
|
|
220
|
+
const intentModule = intent.target.moduleId?.toLowerCase() || ''
|
|
221
|
+
if (intentModule.includes(sourceLayer.toLowerCase()) &&
|
|
222
|
+
(intent.action === 'create' || intent.action === 'modify')) {
|
|
223
|
+
return this.makeConflict(constraint, intent, 'warning',
|
|
224
|
+
`${sourceLayer} layer should not import directly from ${targetLayer}`,
|
|
225
|
+
`Use an intermediate service layer between ${sourceLayer} and ${targetLayer}`)
|
|
226
|
+
}
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** "All exported functions must be camelCase" */
|
|
231
|
+
private checkNaming(constraint: string, intent: Intent): Conflict | null {
|
|
232
|
+
if (intent.action !== 'create') return null
|
|
233
|
+
|
|
234
|
+
const name = intent.target.name
|
|
235
|
+
if (constraint.toLowerCase().includes('camelcase')) {
|
|
236
|
+
// Check if name starts with lowercase and has no underscores/hyphens
|
|
237
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(name) && name.length > 0) {
|
|
238
|
+
return this.makeConflict(constraint, intent, 'warning',
|
|
239
|
+
`Name "${name}" does not follow camelCase convention`,
|
|
240
|
+
`Rename to ${this.toCamelCase(name)}`)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Complex constraints — keyword overlap heuristic (no AI call) */
|
|
247
|
+
private checkComplex(constraint: string, intent: Intent): Conflict | null {
|
|
248
|
+
const constraintWords = this.extractKeywords(constraint)
|
|
249
|
+
const intentWords = [
|
|
250
|
+
...this.extractKeywords(intent.target.name),
|
|
251
|
+
...this.extractKeywords(intent.reason)
|
|
252
|
+
]
|
|
253
|
+
const overlap = constraintWords.filter(w =>
|
|
254
|
+
intentWords.some(iw => iw === w || iw.includes(w) || w.includes(iw))
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Only flag if significant keyword overlap suggests relevance
|
|
258
|
+
if (overlap.length >= 2) {
|
|
259
|
+
return this.makeConflict(constraint, intent, 'warning',
|
|
260
|
+
`Intent may conflict with constraint: "${constraint}" (keywords: ${overlap.join(', ')})`,
|
|
261
|
+
`Review the constraint before proceeding`)
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
private makeConflict(
|
|
269
|
+
constraint: string,
|
|
270
|
+
intent: Intent,
|
|
271
|
+
severity: 'error' | 'warning',
|
|
272
|
+
message: string,
|
|
273
|
+
suggestedFix?: string
|
|
274
|
+
): Conflict {
|
|
275
|
+
return {
|
|
276
|
+
type: 'constraint-violation',
|
|
277
|
+
severity,
|
|
278
|
+
message,
|
|
279
|
+
relatedIntent: intent,
|
|
280
|
+
suggestedFix,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private extractKeywords(text: string): string[] {
|
|
285
|
+
const stopWords = new Set([
|
|
286
|
+
'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
|
|
287
|
+
'or', 'is', 'are', 'was', 'be', 'not', 'no', 'from', 'with',
|
|
288
|
+
'all', 'must', 'should', 'can', 'cannot', 'will', 'this', 'that',
|
|
289
|
+
])
|
|
290
|
+
return text
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
293
|
+
.split(/\s+/)
|
|
294
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private toCamelCase(name: string): string {
|
|
298
|
+
return name
|
|
299
|
+
.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
|
|
300
|
+
.replace(/^[A-Z]/, c => c.toLowerCase())
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { IntentInterpreter } from './interpreter.js'
|
|
2
|
-
export { ConflictDetector } from './conflict-detector.js'
|
|
3
|
-
export { Suggester } from './suggester.js'
|
|
4
|
-
export { PreflightPipeline } from './preflight.js'
|
|
5
|
-
export type { Intent, Conflict, ConflictResult, Suggestion, PreflightResult, AIProviderConfig } from './types.js'
|
|
6
|
-
export { IntentSchema } from './types.js'
|
|
1
|
+
export { IntentInterpreter } from './interpreter.js'
|
|
2
|
+
export { ConflictDetector } from './conflict-detector.js'
|
|
3
|
+
export { Suggester } from './suggester.js'
|
|
4
|
+
export { PreflightPipeline } from './preflight.js'
|
|
5
|
+
export type { Intent, Conflict, ConflictResult, Suggestion, PreflightResult, AIProviderConfig } from './types.js'
|
|
6
|
+
export { IntentSchema } from './types.js'
|