@getmikk/intent-engine 1.9.0 → 1.9.2

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.
@@ -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, DecisionResult, Explanation } from './types.js'
9
+ import type { PreflightResult } from './types.js'
10
10
 
11
11
  /**
12
12
  * PreflightPipeline — orchestrates the full intent pipeline:
13
- * interpret → analyze (impact/risk) → decide → explain → conflict-detect → suggest.
13
+ * interpret → impact/risk analysis → decide → explain → conflict-detect → suggest.
14
14
  */
15
15
  export class PreflightPipeline {
16
- private interpreter: IntentInterpreter
16
+ private interpreter: IntentInterpreter
17
17
  private conflictDetector: ConflictDetector
18
- private suggester: Suggester
19
- private decisionEngine: 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 = new IntentInterpreter(contract, lock)
26
+ this.interpreter = new IntentInterpreter(contract, lock)
27
27
  this.conflictDetector = new ConflictDetector(contract, lock)
28
- this.suggester = new Suggester(contract, lock)
29
- this.decisionEngine = new DecisionEngine(contract)
30
- this.explanationEngine = new ExplanationEngine()
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 into structured intents
36
+ // 1. Interpret prompt structured intents
36
37
  const intents = await this.interpreter.interpret(prompt)
37
38
 
38
- // 2. Perform Quantitative Impact Analysis
39
- const graph = new GraphBuilder().buildFromLock(this.lock)
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
- // Find better match by checking both name and type
46
- // In Mikk 2.0, we have IDs like fn:src/file.ts:name
47
- const matchedNode = [...graph.nodes.values()].find(n =>
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 (matchedNode) targetIds.push(matchedNode.id)
49
+ if (match) targetIds.push(match.id)
52
50
  }
53
51
 
54
52
  const impact = analyzer.analyze(targetIds)
55
53
 
56
- // 3. Decide and Explain
57
- const decision = this.decisionEngine.evaluate(impact)
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. Check for Static Conflicts
58
+ // 4. Static conflict detection
61
59
  const conflicts = this.conflictDetector.detect(intents)
62
60
 
63
- // 5. Low-confidence rejection / Supplemental warnings
64
- const maxConfidence = intents.length > 0
61
+ // 5. Low-confidence rejection
62
+ const maxConf = intents.length > 0
65
63
  ? Math.max(...intents.map(i => i.confidence))
66
64
  : 0
67
- if (maxConfidence < 0.4 && intents.length > 0) {
65
+
66
+ if (maxConf < 0.4 && intents.length > 0) {
68
67
  conflicts.conflicts.push({
69
- type: 'low-confidence',
70
- severity: 'warning',
71
- message: `Low confidence (${(maxConfidence * 100).toFixed(0)}%) — matching to existing code was ambiguous.`,
72
- relatedIntent: intents[0],
73
- suggestedFix: 'Be more specific about the function or module name.',
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. Generate implementation suggestions
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' && maxConfidence >= 0.4,
85
+ approved: !conflicts.hasConflicts && decision.status !== 'BLOCKED' && maxConf >= 0.4,
87
86
  }
88
87
  }
89
88
  }