@getmikk/intent-engine 1.9.0 → 2.0.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 +158 -69
- package/package.json +2 -2
- package/src/auto-correction.ts +284 -0
- package/src/decision-engine.ts +50 -45
- package/src/enforced-safety.ts +305 -0
- package/src/explanation-engine.ts +56 -36
- package/src/index.ts +10 -0
- package/src/intent-understanding.ts +227 -0
- package/src/pre-edit-validation.ts +295 -0
- package/src/preflight.ts +34 -35
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ImpactResult, MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IntentUnderstanding - Analyzes if breaking changes are intentional.
|
|
5
|
+
*
|
|
6
|
+
* Understands developer intent through:
|
|
7
|
+
* 1. Explicit intent declarations ("REFACTOR:", "BREAKING:" in commit messages)
|
|
8
|
+
* 2. Change pattern analysis (rename vs bugfix vs new feature)
|
|
9
|
+
* 3. Context clues (branch name, PR description, file patterns)
|
|
10
|
+
*/
|
|
11
|
+
export interface IntentContext {
|
|
12
|
+
commitMessage?: string
|
|
13
|
+
branchName?: string
|
|
14
|
+
prDescription?: string
|
|
15
|
+
author?: string
|
|
16
|
+
filesChanged: string[]
|
|
17
|
+
changeType: 'refactor' | 'feature' | 'bugfix' | 'breaking' | 'unknown'
|
|
18
|
+
confidence: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IntentAnalysis {
|
|
22
|
+
isIntentionalBreakingChange: boolean
|
|
23
|
+
confidence: number
|
|
24
|
+
reasoning: string[]
|
|
25
|
+
suggestedActions: string[]
|
|
26
|
+
riskAcceptance: 'none' | 'low' | 'medium' | 'high'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class IntentUnderstanding {
|
|
30
|
+
constructor(private contract: MikkContract, private lock: MikkLock) {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analyze if breaking changes are intentional based on context.
|
|
34
|
+
*
|
|
35
|
+
* NOTE: `impact` is typed as ImpactResult. ClassifiedImpact nodes expose
|
|
36
|
+
* `nodeId` (not `id` or `functionId`) — all node lookups use that field.
|
|
37
|
+
*/
|
|
38
|
+
analyzeIntent(impact: ImpactResult, context: Partial<IntentContext> = {}): IntentAnalysis {
|
|
39
|
+
const reasoning: string[] = []
|
|
40
|
+
let confidence = 0.5
|
|
41
|
+
let isIntentional = false
|
|
42
|
+
let riskAcceptance: IntentAnalysis['riskAcceptance'] = 'none'
|
|
43
|
+
|
|
44
|
+
// 1. Check explicit markers in commit message
|
|
45
|
+
if (context.commitMessage) {
|
|
46
|
+
const msg = context.commitMessage.toLowerCase()
|
|
47
|
+
const breakingMarkers = ['breaking:', 'breaking change', 'refactor:', 'migration:', 'api change']
|
|
48
|
+
const found = breakingMarkers.find(m => msg.includes(m))
|
|
49
|
+
|
|
50
|
+
if (found) {
|
|
51
|
+
isIntentional = true
|
|
52
|
+
confidence += 0.3
|
|
53
|
+
reasoning.push(`Explicit breaking change marker found: "${found}"`)
|
|
54
|
+
riskAcceptance = 'high'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const safetyMarkers = ['fix:', 'hotfix:', 'patch:', 'bugfix:']
|
|
58
|
+
if (safetyMarkers.some(m => msg.includes(m))) {
|
|
59
|
+
confidence += 0.2
|
|
60
|
+
reasoning.push('Safety fix detected — changes should be minimal and safe')
|
|
61
|
+
if (riskAcceptance === 'none') riskAcceptance = 'low'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Analyze branch naming patterns
|
|
66
|
+
if (context.branchName) {
|
|
67
|
+
const branch = context.branchName.toLowerCase()
|
|
68
|
+
if (branch.includes('refactor') || branch.includes('breaking') || branch.includes('v2')) {
|
|
69
|
+
isIntentional = true
|
|
70
|
+
confidence += 0.2
|
|
71
|
+
reasoning.push(`Branch name "${context.branchName}" suggests intentional restructuring`)
|
|
72
|
+
if (riskAcceptance === 'none') riskAcceptance = 'medium'
|
|
73
|
+
}
|
|
74
|
+
if (branch.includes('hotfix') || branch.includes('patch')) {
|
|
75
|
+
reasoning.push('Hotfix branch — expect minimal, safe changes')
|
|
76
|
+
if (riskAcceptance === 'none') riskAcceptance = 'low'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Analyze change patterns
|
|
81
|
+
const changePattern = this.analyzeChangePattern(impact, context.filesChanged ?? [])
|
|
82
|
+
if (changePattern.isRename) {
|
|
83
|
+
isIntentional = true
|
|
84
|
+
confidence += 0.15
|
|
85
|
+
reasoning.push('Pattern suggests systematic rename/refactor')
|
|
86
|
+
}
|
|
87
|
+
if (changePattern.isSignatureChange) {
|
|
88
|
+
reasoning.push('Function signature changes detected')
|
|
89
|
+
if (!isIntentional) confidence -= 0.1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. Check against contract migration policies
|
|
93
|
+
const migrationPhase = this.detectMigrationPhase(impact)
|
|
94
|
+
if (migrationPhase === 'planned') {
|
|
95
|
+
isIntentional = true
|
|
96
|
+
confidence += 0.25
|
|
97
|
+
reasoning.push('Changes align with planned migration in contract')
|
|
98
|
+
riskAcceptance = 'high'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 5. Historical analysis
|
|
102
|
+
if (context.author) {
|
|
103
|
+
const pattern = this.analyzeAuthorPattern(context.author)
|
|
104
|
+
if (pattern === 'careful') {
|
|
105
|
+
confidence += 0.1
|
|
106
|
+
reasoning.push(`${context.author} has history of careful, safe changes`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
confidence = Math.max(0, Math.min(1, confidence))
|
|
111
|
+
|
|
112
|
+
const suggestedActions = this.generateSuggestions(impact, isIntentional, confidence, riskAcceptance)
|
|
113
|
+
|
|
114
|
+
return { isIntentionalBreakingChange: isIntentional, confidence, reasoning, suggestedActions, riskAcceptance }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Private helpers ───────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
private detectMigrationPhase(impact: ImpactResult): 'none' | 'planned' {
|
|
120
|
+
const decisions = this.contract.declared?.decisions ?? []
|
|
121
|
+
|
|
122
|
+
for (const decision of decisions) {
|
|
123
|
+
const text = `${decision.title ?? ''} ${decision.reason ?? ''}`.toLowerCase()
|
|
124
|
+
if (!text.includes('migration') && !text.includes('deprecat')) continue
|
|
125
|
+
|
|
126
|
+
// Use `nodeId` — the correct field on ClassifiedImpact
|
|
127
|
+
const affectedModules = new Set<string>()
|
|
128
|
+
for (const node of impact.allImpacted) {
|
|
129
|
+
const fn = this.lock.functions[node.nodeId]
|
|
130
|
+
if (fn?.moduleId) affectedModules.add(fn.moduleId)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const decisionModules = this.extractModulesFromDecision(`${decision.title ?? ''} ${decision.reason ?? ''}`)
|
|
134
|
+
if ([...affectedModules].some(m => decisionModules.includes(m))) return 'planned'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return 'none'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private extractModulesFromDecision(decision: string): string[] {
|
|
141
|
+
const modules = this.contract.declared?.modules ?? []
|
|
142
|
+
const mentioned: string[] = []
|
|
143
|
+
for (const mod of modules) {
|
|
144
|
+
if (
|
|
145
|
+
decision.toLowerCase().includes(mod.id.toLowerCase()) ||
|
|
146
|
+
decision.toLowerCase().includes(mod.name.toLowerCase())
|
|
147
|
+
) {
|
|
148
|
+
mentioned.push(mod.id)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return mentioned
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private analyzeChangePattern(
|
|
155
|
+
impact: ImpactResult,
|
|
156
|
+
filesChanged: string[],
|
|
157
|
+
): { isRename: boolean; isSignatureChange: boolean; isNewFeature: boolean } {
|
|
158
|
+
const result = { isRename: false, isSignatureChange: false, isNewFeature: false }
|
|
159
|
+
|
|
160
|
+
const lockFilePaths = new Set(Object.keys(this.lock.files))
|
|
161
|
+
const norm = (f: string) => f.replace(/\\/g, '/')
|
|
162
|
+
const deletedFiles = filesChanged.filter(f => !lockFilePaths.has(norm(f)))
|
|
163
|
+
const addedFiles = filesChanged.filter(f => lockFilePaths.has(norm(f)))
|
|
164
|
+
|
|
165
|
+
if (deletedFiles.length > 0 && addedFiles.length > 0) {
|
|
166
|
+
for (const deleted of deletedFiles) {
|
|
167
|
+
const base = deleted.split('/').pop()?.split('.')[0]
|
|
168
|
+
if (base && addedFiles.some(a => norm(a).includes(base))) {
|
|
169
|
+
result.isRename = true
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Use `nodeId` — correct field on ClassifiedImpact
|
|
176
|
+
for (const node of impact.allImpacted) {
|
|
177
|
+
const fn = this.lock.functions[node.nodeId]
|
|
178
|
+
if (fn?.params && fn.params.length > 0) {
|
|
179
|
+
result.isSignatureChange = true
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const newExports = filesChanged.flatMap(file => {
|
|
185
|
+
const n = norm(file)
|
|
186
|
+
return Object.values(this.lock.functions).filter(
|
|
187
|
+
f => (f.file === n || f.file.endsWith('/' + n)) && f.isExported && f.calledBy.length === 0,
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
if (newExports.length > 3) result.isNewFeature = true
|
|
191
|
+
|
|
192
|
+
return result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private analyzeAuthorPattern(_author: string): 'careful' | 'normal' | 'aggressive' {
|
|
196
|
+
return 'normal'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private generateSuggestions(
|
|
200
|
+
impact: ImpactResult,
|
|
201
|
+
isIntentional: boolean,
|
|
202
|
+
confidence: number,
|
|
203
|
+
riskAcceptance: IntentAnalysis['riskAcceptance'],
|
|
204
|
+
): string[] {
|
|
205
|
+
const out: string[] = []
|
|
206
|
+
|
|
207
|
+
if (isIntentional && confidence > 0.7) {
|
|
208
|
+
out.push('✓ Breaking change appears intentional — proceeding with caution')
|
|
209
|
+
if (impact.impacted.length > 10) out.push('Consider breaking this into smaller PRs for easier review')
|
|
210
|
+
out.push('Ensure tests exist for all impacted call paths')
|
|
211
|
+
out.push('Update relevant documentation for API changes')
|
|
212
|
+
if (riskAcceptance === 'high') out.push('Schedule deployment during low-traffic period')
|
|
213
|
+
} else if (impact.riskScore > 80) {
|
|
214
|
+
out.push('⚠ HIGH RISK: Breaking changes without explicit intent detected')
|
|
215
|
+
out.push('Add "BREAKING:" prefix to commit message if intentional')
|
|
216
|
+
out.push('Run full test suite before committing')
|
|
217
|
+
out.push('Consider creating a migration guide for consumers')
|
|
218
|
+
} else if (impact.impacted.length > 5) {
|
|
219
|
+
out.push('Review impacted functions to ensure changes are necessary')
|
|
220
|
+
out.push('Check if any changes can be made backward-compatible')
|
|
221
|
+
} else {
|
|
222
|
+
out.push('Changes appear low-risk — standard review process recommended')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return out
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { MikkContract, MikkLock, DependencyGraph, ImpactResult } from '@getmikk/core'
|
|
2
|
+
import { ImpactAnalyzer } from '@getmikk/core'
|
|
3
|
+
import { IntentUnderstanding, type IntentContext } from './intent-understanding.js'
|
|
4
|
+
import { AutoCorrectionEngine } from './auto-correction.js'
|
|
5
|
+
import { EnforcedSafetyGates, type SafetyGateConfig } from './enforced-safety.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PreEditValidation — intercepts edits before they are applied.
|
|
9
|
+
*
|
|
10
|
+
* Combines:
|
|
11
|
+
* 1. Intent understanding (is this intentional?)
|
|
12
|
+
* 2. Real impact analysis (what breaks?)
|
|
13
|
+
* 3. Auto-correction (can we fix issues automatically?)
|
|
14
|
+
* 4. Safety gates (should we block this?)
|
|
15
|
+
*
|
|
16
|
+
* This is the single entry point for mikk_before_edit.
|
|
17
|
+
*/
|
|
18
|
+
export interface EditProposal {
|
|
19
|
+
files: string[]
|
|
20
|
+
description: string
|
|
21
|
+
author: string
|
|
22
|
+
intent?: Partial<IntentContext>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ValidationResult {
|
|
26
|
+
allowed: boolean
|
|
27
|
+
confidence: number
|
|
28
|
+
|
|
29
|
+
intent: {
|
|
30
|
+
isIntentionalBreakingChange: boolean
|
|
31
|
+
confidence: number
|
|
32
|
+
reasoning: string[]
|
|
33
|
+
riskAcceptance: 'none' | 'low' | 'medium' | 'high'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impact: {
|
|
37
|
+
totalFiles: number
|
|
38
|
+
totalFunctions: number
|
|
39
|
+
riskScore: number
|
|
40
|
+
criticalPaths: string[]
|
|
41
|
+
blastRadius: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
gates: Array<{
|
|
45
|
+
name: string
|
|
46
|
+
passed: boolean
|
|
47
|
+
severity: 'BLOCKING' | 'WARNING'
|
|
48
|
+
message: string
|
|
49
|
+
bypassable: boolean
|
|
50
|
+
}>
|
|
51
|
+
|
|
52
|
+
corrections: {
|
|
53
|
+
available: boolean
|
|
54
|
+
issuesFound: number
|
|
55
|
+
autoFixable: number
|
|
56
|
+
applied: string[]
|
|
57
|
+
suggested: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
recommendations: string[]
|
|
61
|
+
nextSteps: string[]
|
|
62
|
+
tokenSavings: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class PreEditValidation {
|
|
66
|
+
private intentEngine: IntentUnderstanding
|
|
67
|
+
private autoCorrection: AutoCorrectionEngine
|
|
68
|
+
private safetyGates: EnforcedSafetyGates
|
|
69
|
+
private impactAnalyzer: ImpactAnalyzer
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
private contract: MikkContract,
|
|
73
|
+
private lock: MikkLock,
|
|
74
|
+
graph: DependencyGraph,
|
|
75
|
+
private projectRoot: string,
|
|
76
|
+
safetyConfig?: Partial<SafetyGateConfig>,
|
|
77
|
+
) {
|
|
78
|
+
this.intentEngine = new IntentUnderstanding(contract, lock)
|
|
79
|
+
this.impactAnalyzer = new ImpactAnalyzer(graph)
|
|
80
|
+
|
|
81
|
+
// Build protected modules list from constraints that mention "protected".
|
|
82
|
+
// Constraints may be strings OR objects — guard both cases.
|
|
83
|
+
const protectedModules = (contract.declared?.constraints ?? [])
|
|
84
|
+
.filter(c => {
|
|
85
|
+
if (typeof c === 'string') return c.toLowerCase().includes('protected')
|
|
86
|
+
return false // object-style constraints don't auto-map to module names
|
|
87
|
+
})
|
|
88
|
+
.flatMap(c => {
|
|
89
|
+
const parts = (c as string).split('::')
|
|
90
|
+
return parts[0] ? [parts[0]] : []
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const defaultConfig: SafetyGateConfig = {
|
|
94
|
+
enforceOnSave: true,
|
|
95
|
+
enforceOnCommit: true,
|
|
96
|
+
enforceInCI: true,
|
|
97
|
+
maxRiskScore: 70,
|
|
98
|
+
maxImpactNodes: 10,
|
|
99
|
+
requireTestsForChangedFiles: true,
|
|
100
|
+
requireDocumentationForApiChanges: true,
|
|
101
|
+
protectedModules,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.safetyGates = new EnforcedSafetyGates(contract, lock, graph, { ...defaultConfig, ...safetyConfig })
|
|
105
|
+
this.autoCorrection = new AutoCorrectionEngine(contract, lock, graph, projectRoot)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Main validation method — call this BEFORE any edit. */
|
|
109
|
+
async validate(proposal: EditProposal): Promise<ValidationResult> {
|
|
110
|
+
const { files } = proposal
|
|
111
|
+
|
|
112
|
+
// 1. Real impact analysis from the graph
|
|
113
|
+
const fileNodeIds = this.collectFileNodeIds(files)
|
|
114
|
+
const impact = fileNodeIds.length > 0
|
|
115
|
+
? this.impactAnalyzer.analyze(fileNodeIds)
|
|
116
|
+
: this.emptyImpact(files)
|
|
117
|
+
|
|
118
|
+
// 2. Intent analysis — receives the real ImpactResult so field shapes match
|
|
119
|
+
const intentAnalysis = this.intentEngine.analyzeIntent(impact, {
|
|
120
|
+
commitMessage: proposal.intent?.commitMessage,
|
|
121
|
+
branchName: proposal.intent?.branchName,
|
|
122
|
+
author: proposal.author,
|
|
123
|
+
filesChanged: files,
|
|
124
|
+
changeType: this.inferChangeType(proposal),
|
|
125
|
+
confidence: 0.7,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 3. Safety gates
|
|
129
|
+
const gateResults = await this.safetyGates.validateEdits(files, {
|
|
130
|
+
commitMessage: proposal.intent?.commitMessage,
|
|
131
|
+
branchName: proposal.intent?.branchName,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// 4. Auto-correction
|
|
135
|
+
const corrections = await this.autoCorrection.analyzeAndFix(files)
|
|
136
|
+
|
|
137
|
+
// 5. Allowed?
|
|
138
|
+
const { allowed, blockingGates } = this.safetyGates.canProceed(gateResults)
|
|
139
|
+
|
|
140
|
+
// 6. Recommendations
|
|
141
|
+
const recommendations = this.buildRecommendations(intentAnalysis, impact, gateResults, corrections)
|
|
142
|
+
|
|
143
|
+
// 7. Token savings estimate
|
|
144
|
+
const tokenSavings = this.calculateTokenSavings(files, impact)
|
|
145
|
+
|
|
146
|
+
// 8. Impact summary for the response
|
|
147
|
+
const impactSummary = this.summariseImpact(files, impact)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
allowed,
|
|
151
|
+
confidence: intentAnalysis.confidence,
|
|
152
|
+
|
|
153
|
+
intent: {
|
|
154
|
+
isIntentionalBreakingChange: intentAnalysis.isIntentionalBreakingChange,
|
|
155
|
+
confidence: intentAnalysis.confidence,
|
|
156
|
+
reasoning: intentAnalysis.reasoning,
|
|
157
|
+
riskAcceptance: intentAnalysis.riskAcceptance,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
impact: impactSummary,
|
|
161
|
+
|
|
162
|
+
gates: gateResults.map(g => ({
|
|
163
|
+
name: g.gate,
|
|
164
|
+
passed: g.canProceed,
|
|
165
|
+
severity: g.severity,
|
|
166
|
+
message: g.reason,
|
|
167
|
+
bypassable: g.bypassable,
|
|
168
|
+
})),
|
|
169
|
+
|
|
170
|
+
corrections: {
|
|
171
|
+
available: corrections.issues.length > 0,
|
|
172
|
+
issuesFound: corrections.issues.length,
|
|
173
|
+
autoFixable: corrections.appliedFixes.length,
|
|
174
|
+
applied: corrections.appliedFixes.slice(0, 5),
|
|
175
|
+
suggested: corrections.issues
|
|
176
|
+
.filter(i => !i.autoFixable)
|
|
177
|
+
.map(i => `${i.file}:${i.line} — ${i.message}`)
|
|
178
|
+
.slice(0, 5),
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
recommendations,
|
|
182
|
+
|
|
183
|
+
nextSteps: allowed
|
|
184
|
+
? ['Proceed with edit', 'Run tests after changes']
|
|
185
|
+
: [
|
|
186
|
+
`Address blocking gates: ${blockingGates.join(', ')}`,
|
|
187
|
+
...gateResults
|
|
188
|
+
.filter(g => !g.canProceed && g.suggestedFix)
|
|
189
|
+
.map(g => g.suggestedFix!)
|
|
190
|
+
.slice(0, 3),
|
|
191
|
+
],
|
|
192
|
+
|
|
193
|
+
tokenSavings,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Private helpers ────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** Collect graph IDs for every tracked function in the given files. */
|
|
200
|
+
private collectFileNodeIds(files: string[]): string[] {
|
|
201
|
+
const ids: string[] = []
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
const norm = file.replace(/\\/g, '/')
|
|
204
|
+
for (const fn of Object.values(this.lock.functions)) {
|
|
205
|
+
if (fn.file === norm || fn.file.endsWith('/' + norm)) ids.push(fn.id)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return ids
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Build a zero-impact result when no tracked functions exist. */
|
|
212
|
+
private emptyImpact(files: string[]): ImpactResult {
|
|
213
|
+
return {
|
|
214
|
+
changed: [],
|
|
215
|
+
impacted: [],
|
|
216
|
+
allImpacted: [],
|
|
217
|
+
depth: 0,
|
|
218
|
+
entryPoints: [],
|
|
219
|
+
criticalModules: [],
|
|
220
|
+
paths: [],
|
|
221
|
+
confidence: 1.0,
|
|
222
|
+
riskScore: 0,
|
|
223
|
+
classified: { critical: [], high: [], medium: [], low: [] },
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private summariseImpact(files: string[], impact: ImpactResult) {
|
|
228
|
+
const fileFunctions = this.collectFileNodeIds(files)
|
|
229
|
+
.map(id => this.lock.functions[id])
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
|
|
232
|
+
const criticalPaths = fileFunctions
|
|
233
|
+
.filter(f => f.calledBy.length > 10)
|
|
234
|
+
.map(f => `${f.moduleId}::${f.name}`)
|
|
235
|
+
.slice(0, 5)
|
|
236
|
+
|
|
237
|
+
const blastRadius = [...new Set(
|
|
238
|
+
fileFunctions
|
|
239
|
+
.flatMap(f => f.calledBy)
|
|
240
|
+
.map(id => this.lock.functions[id])
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.map(f => `${f.moduleId}::${f.name}`),
|
|
243
|
+
)].slice(0, 10)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
totalFiles: files.length,
|
|
247
|
+
totalFunctions: fileFunctions.length,
|
|
248
|
+
riskScore: impact.riskScore,
|
|
249
|
+
criticalPaths,
|
|
250
|
+
blastRadius,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private buildRecommendations(intent: any, impact: ImpactResult, gates: any[], corrections: any): string[] {
|
|
255
|
+
const recs: string[] = []
|
|
256
|
+
|
|
257
|
+
if (intent.isIntentionalBreakingChange && intent.confidence > 0.7) {
|
|
258
|
+
recs.push('✓ Breaking change appears intentional — ensure migration guide exists')
|
|
259
|
+
}
|
|
260
|
+
if (impact.riskScore > 70) {
|
|
261
|
+
recs.push('⚠ High risk — consider breaking into smaller changes')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const summaryImpact = this.summariseImpact([], impact)
|
|
265
|
+
if (summaryImpact.criticalPaths.length > 0) {
|
|
266
|
+
recs.push(`Critical paths affected: ${summaryImpact.criticalPaths.join(', ')}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const warnings = gates.filter(g => g.severity === 'WARNING' && !g.canProceed)
|
|
270
|
+
if (warnings.length > 0) {
|
|
271
|
+
recs.push(`Address warnings: ${warnings.map((w: any) => w.gate).join(', ')}`)
|
|
272
|
+
}
|
|
273
|
+
if (corrections.issues.length > 0) {
|
|
274
|
+
recs.push(`Found ${corrections.issues.length} issue(s) — ${corrections.appliedFixes.length} auto-fixed`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return recs
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private calculateTokenSavings(files: string[], impact: ImpactResult): number {
|
|
281
|
+
// Naive estimate: without Mikk the AI reads all impacted + changed files
|
|
282
|
+
const fileCount = impact.impacted.length + files.length
|
|
283
|
+
const naiveCost = fileCount * 500
|
|
284
|
+
return Math.max(0, naiveCost - 500) // 500 tokens for the validation result itself
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private inferChangeType(proposal: EditProposal): IntentContext['changeType'] {
|
|
288
|
+
const desc = `${proposal.description} ${proposal.intent?.commitMessage ?? ''}`.toLowerCase()
|
|
289
|
+
if (desc.includes('refactor') || desc.includes('restructure')) return 'refactor'
|
|
290
|
+
if (desc.includes('feat') || desc.includes('add') || desc.includes('new')) return 'feature'
|
|
291
|
+
if (desc.includes('fix') || desc.includes('bug') || desc.includes('patch')) return 'bugfix'
|
|
292
|
+
if (desc.includes('breaking') || desc.includes('api change')) return 'breaking'
|
|
293
|
+
return 'unknown'
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/preflight.ts
CHANGED
|
@@ -6,75 +6,74 @@ import { ConflictDetector } from './conflict-detector.js'
|
|
|
6
6
|
import { Suggester } from './suggester.js'
|
|
7
7
|
import { DecisionEngine } from './decision-engine.js'
|
|
8
8
|
import { ExplanationEngine } from './explanation-engine.js'
|
|
9
|
-
import type { PreflightResult
|
|
9
|
+
import type { PreflightResult } from './types.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* PreflightPipeline — orchestrates the full intent pipeline:
|
|
13
|
-
* interpret →
|
|
13
|
+
* interpret → impact/risk analysis → decide → explain → conflict-detect → suggest.
|
|
14
14
|
*/
|
|
15
15
|
export class PreflightPipeline {
|
|
16
|
-
private interpreter:
|
|
16
|
+
private interpreter: IntentInterpreter
|
|
17
17
|
private conflictDetector: ConflictDetector
|
|
18
|
-
private suggester:
|
|
19
|
-
private decisionEngine:
|
|
18
|
+
private suggester: Suggester
|
|
19
|
+
private decisionEngine: DecisionEngine
|
|
20
20
|
private explanationEngine: ExplanationEngine
|
|
21
21
|
|
|
22
22
|
constructor(
|
|
23
23
|
private contract: MikkContract,
|
|
24
|
-
private lock: MikkLock
|
|
24
|
+
private lock: MikkLock,
|
|
25
25
|
) {
|
|
26
|
-
this.interpreter
|
|
26
|
+
this.interpreter = new IntentInterpreter(contract, lock)
|
|
27
27
|
this.conflictDetector = new ConflictDetector(contract, lock)
|
|
28
|
-
this.suggester
|
|
29
|
-
this.decisionEngine
|
|
30
|
-
|
|
28
|
+
this.suggester = new Suggester(contract, lock)
|
|
29
|
+
this.decisionEngine = new DecisionEngine(contract)
|
|
30
|
+
// Pass lock so ExplanationEngine can resolve module names accurately
|
|
31
|
+
this.explanationEngine = new ExplanationEngine(lock)
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** Run the full preflight pipeline */
|
|
34
|
+
/** Run the full preflight pipeline for a natural-language prompt. */
|
|
34
35
|
async run(prompt: string): Promise<PreflightResult> {
|
|
35
|
-
// 1. Interpret prompt
|
|
36
|
+
// 1. Interpret prompt → structured intents
|
|
36
37
|
const intents = await this.interpreter.interpret(prompt)
|
|
37
38
|
|
|
38
|
-
// 2.
|
|
39
|
-
const graph
|
|
39
|
+
// 2. Build graph from lock (no file re-parsing) and run impact analysis
|
|
40
|
+
const graph = new GraphBuilder().buildFromLock(this.lock)
|
|
40
41
|
const analyzer = new ImpactAnalyzer(graph)
|
|
41
|
-
|
|
42
|
-
// Find node IDs for the intents
|
|
42
|
+
|
|
43
43
|
const targetIds: string[] = []
|
|
44
44
|
for (const intent of intents) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
n.name.toLowerCase() === intent.target.name.toLowerCase() &&
|
|
49
|
-
(intent.target.type === 'function' ? n.type === 'function' : true)
|
|
45
|
+
const match = [...graph.nodes.values()].find(n =>
|
|
46
|
+
n.name.toLowerCase() === intent.target.name.toLowerCase() &&
|
|
47
|
+
(intent.target.type === 'function' ? n.type === 'function' : true),
|
|
50
48
|
)
|
|
51
|
-
if (
|
|
49
|
+
if (match) targetIds.push(match.id)
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
const impact = analyzer.analyze(targetIds)
|
|
55
53
|
|
|
56
|
-
// 3. Decide and
|
|
57
|
-
const decision
|
|
54
|
+
// 3. Decide and explain
|
|
55
|
+
const decision = this.decisionEngine.evaluate(impact)
|
|
58
56
|
const explanation = this.explanationEngine.explain(impact, decision)
|
|
59
57
|
|
|
60
|
-
// 4.
|
|
58
|
+
// 4. Static conflict detection
|
|
61
59
|
const conflicts = this.conflictDetector.detect(intents)
|
|
62
60
|
|
|
63
|
-
// 5. Low-confidence rejection
|
|
64
|
-
const
|
|
61
|
+
// 5. Low-confidence rejection
|
|
62
|
+
const maxConf = intents.length > 0
|
|
65
63
|
? Math.max(...intents.map(i => i.confidence))
|
|
66
64
|
: 0
|
|
67
|
-
|
|
65
|
+
|
|
66
|
+
if (maxConf < 0.4 && intents.length > 0) {
|
|
68
67
|
conflicts.conflicts.push({
|
|
69
|
-
type:
|
|
70
|
-
severity:
|
|
71
|
-
message:
|
|
72
|
-
relatedIntent:
|
|
73
|
-
suggestedFix:
|
|
68
|
+
type: 'low-confidence',
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
message: `Low confidence (${(maxConf * 100).toFixed(0)}%) — matching to existing code was ambiguous.`,
|
|
71
|
+
relatedIntent: intents[0],
|
|
72
|
+
suggestedFix: 'Be more specific about the function or module name.',
|
|
74
73
|
})
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
// 6.
|
|
76
|
+
// 6. Implementation suggestions
|
|
78
77
|
const suggestions = this.suggester.suggest(intents)
|
|
79
78
|
|
|
80
79
|
return {
|
|
@@ -83,7 +82,7 @@ export class PreflightPipeline {
|
|
|
83
82
|
suggestions,
|
|
84
83
|
decision,
|
|
85
84
|
explanation,
|
|
86
|
-
approved: !conflicts.hasConflicts && decision.status !== 'BLOCKED' &&
|
|
85
|
+
approved: !conflicts.hasConflicts && decision.status !== 'BLOCKED' && maxConf >= 0.4,
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
}
|