@getmikk/intent-engine 1.9.0 → 1.9.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.
@@ -2,68 +2,73 @@ import type { ImpactResult, MikkContract } from '@getmikk/core'
2
2
  import type { DecisionResult, DecisionStatus } from './types.js'
3
3
 
4
4
  /**
5
- * DecisionEngine — the "Brain" of Mikk 2.0.
6
- * Evaluates quantitative impact analysis against project policies.
5
+ * DecisionEngine — evaluates quantitative impact analysis against project policies.
6
+ *
7
+ * Policies live in contract.policies (all optional with safe defaults):
8
+ * - maxRiskScore (default 70) — WARNING above, BLOCKED at ≥ 90
9
+ * - maxImpactNodes (default 10) — WARNING/BLOCKED when exceeded
10
+ * - protectedModules (default []) — BLOCKED if a CRITICAL/HIGH-risk node's
11
+ * file path matches a protected module name
12
+ * - enforceStrictBoundaries (false) — BLOCKED on any critical cross-module hit
7
13
  */
8
14
  export class DecisionEngine {
9
15
  constructor(private contract: MikkContract) {}
10
16
 
11
- /**
12
- * Evaluate an impact result against the defined policies.
13
- */
14
17
  evaluate(impact: ImpactResult): DecisionResult {
15
18
  const policy = {
16
- maxRiskScore: this.contract.policies?.maxRiskScore ?? 70,
17
- maxImpactNodes: this.contract.policies?.maxImpactNodes ?? 10,
18
- protectedModules: this.contract.policies?.protectedModules ?? [],
19
+ maxRiskScore: this.contract.policies?.maxRiskScore ?? 70,
20
+ maxImpactNodes: this.contract.policies?.maxImpactNodes ?? 10,
21
+ protectedModules: this.contract.policies?.protectedModules ?? [] as string[],
19
22
  enforceStrictBoundaries: this.contract.policies?.enforceStrictBoundaries ?? false,
20
- };
21
- const reasons: string[] = [];
22
- let status: DecisionStatus = 'APPROVED';
23
-
24
- const maxRisk = impact.riskScore;
25
- const impactCount = impact.impacted.length;
23
+ }
26
24
 
27
- // 1. Check absolute risk threshold
28
- if (maxRisk >= 90) {
29
- status = 'BLOCKED';
30
- reasons.push(`Critical risk detected (${maxRisk}/100). Policy strictly blocks changes exceeding 90 risk.`);
31
- } else if (maxRisk > policy.maxRiskScore) {
32
- status = 'WARNING';
33
- reasons.push(`High risk (${maxRisk}) exceeds policy threshold of ${policy.maxRiskScore}.`);
25
+ const reasons: string[] = []
26
+ let status: DecisionStatus = 'APPROVED'
27
+ const promote = (next: DecisionStatus) => {
28
+ if (next === 'BLOCKED' || (next === 'WARNING' && status === 'APPROVED')) status = next
34
29
  }
35
30
 
36
- // 2. Check impact scale
37
- if (impactCount > policy.maxImpactNodes) {
38
- status = status === 'BLOCKED' ? 'BLOCKED' : 'WARNING';
39
- reasons.push(`Impact spread (${impactCount} symbols) exceeds propagation limit of ${policy.maxImpactNodes}.`);
31
+ // 1. Absolute risk threshold
32
+ if (impact.riskScore >= 90) {
33
+ promote('BLOCKED')
34
+ reasons.push(`Critical risk detected (${impact.riskScore}/100). Changes exceeding 90 are always blocked.`)
35
+ } else if (impact.riskScore > policy.maxRiskScore) {
36
+ promote('WARNING')
37
+ reasons.push(`High risk (${impact.riskScore}) exceeds policy threshold of ${policy.maxRiskScore}.`)
40
38
  }
41
39
 
42
- // 3. Check protected modules
43
- const touchedProtectedModules = impact.allImpacted
44
- .filter(node => node.risk === 'CRITICAL' || node.risk === 'HIGH')
45
- .map(node => {
46
- return policy.protectedModules.find(pm => node.file.toLowerCase().includes(pm.toLowerCase()));
47
- })
48
- .filter((m): m is string => !!m);
40
+ // 2. Impact scale
41
+ if (impact.impacted.length > policy.maxImpactNodes) {
42
+ promote('WARNING')
43
+ reasons.push(`Impact spread (${impact.impacted.length} symbols) exceeds limit of ${policy.maxImpactNodes}.`)
44
+ }
49
45
 
50
- const uniqueProtected = [...new Set(touchedProtectedModules)];
51
- if (uniqueProtected.length > 0) {
52
- status = status === 'BLOCKED' ? 'BLOCKED' : 'WARNING';
53
- reasons.push(`Change affects protected modules: ${uniqueProtected.join(', ')}.`);
46
+ // 3. Protected modules
47
+ // allImpacted entries are ClassifiedImpact objects — they have `nodeId`
48
+ // and `riskScore` (numeric), NOT a `.risk` string field.
49
+ const touched = new Set<string>()
50
+ for (const node of impact.allImpacted) {
51
+ // Only act on nodes above the HIGH threshold (riskScore ≥ 60)
52
+ if (node.riskScore < 60) continue
53
+ for (const pm of policy.protectedModules) {
54
+ if (node.file.toLowerCase().includes(pm.toLowerCase())) {
55
+ touched.add(pm)
56
+ }
57
+ }
58
+ }
59
+ if (touched.size > 0) {
60
+ promote('WARNING')
61
+ reasons.push(`Change affects protected modules: ${[...touched].join(', ')}.`)
54
62
  }
55
63
 
56
- // 4. Force BLOCKED for critical cross-boundary impacts if enforcement is on
64
+ // 4. Strict boundary enforcement
57
65
  if (policy.enforceStrictBoundaries && impact.classified.critical.length > 0) {
58
- status = 'BLOCKED';
59
- reasons.push(`Strict boundary enforcement: ${impact.classified.critical.length} critical cross-module impact(s) detected.`);
66
+ promote('BLOCKED')
67
+ reasons.push(
68
+ `Strict boundary enforcement: ${impact.classified.critical.length} critical cross-module impact(s) detected.`,
69
+ )
60
70
  }
61
71
 
62
- return {
63
- status,
64
- reasons,
65
- riskScore: maxRisk,
66
- impactNodes: impactCount
67
- };
72
+ return { status, reasons, riskScore: impact.riskScore, impactNodes: impact.impacted.length }
68
73
  }
69
74
  }
@@ -0,0 +1,305 @@
1
+ import type { MikkContract, MikkLock, DependencyGraph } from '@getmikk/core'
2
+ import { ImpactAnalyzer } from '@getmikk/core'
3
+
4
+ /**
5
+ * EnforcedSafetyGates — blocks unsafe edits at edit-time.
6
+ *
7
+ * Unlike warnings, blocking gates PREVENT edits from being applied
8
+ * unless explicitly bypassed.
9
+ *
10
+ * Integration points:
11
+ * - Pre-commit hooks
12
+ * - IDE save handlers
13
+ * - CI/CD gates
14
+ * - MCP tool validation (mikk_before_edit)
15
+ */
16
+ export interface SafetyGateResult {
17
+ canProceed: boolean
18
+ gate: string
19
+ reason: string
20
+ severity: 'BLOCKING' | 'WARNING'
21
+ bypassable: boolean
22
+ bypassCommand?: string
23
+ autoFixable: boolean
24
+ suggestedFix?: string
25
+ }
26
+
27
+ export interface SafetyGateConfig {
28
+ enforceOnSave: boolean
29
+ enforceOnCommit: boolean
30
+ enforceInCI: boolean
31
+ maxRiskScore: number
32
+ maxImpactNodes: number
33
+ requireTestsForChangedFiles: boolean
34
+ requireDocumentationForApiChanges: boolean
35
+ protectedModules: string[]
36
+ }
37
+
38
+ export class EnforcedSafetyGates {
39
+ private analyzer: ImpactAnalyzer
40
+
41
+ constructor(
42
+ private contract: MikkContract,
43
+ private lock: MikkLock,
44
+ graph: DependencyGraph,
45
+ private config: SafetyGateConfig,
46
+ ) {
47
+ this.analyzer = new ImpactAnalyzer(graph)
48
+ }
49
+
50
+ /** Run all safety gates before allowing edits. */
51
+ async validateEdits(
52
+ files: string[],
53
+ context?: { commitMessage?: string; branchName?: string; isTestRun?: boolean },
54
+ ): Promise<SafetyGateResult[]> {
55
+ return [
56
+ await this.checkRiskGate(files),
57
+ await this.checkScaleGate(files),
58
+ await this.checkProtectedModuleGate(files),
59
+ await this.checkBreakingChangeGate(files, context),
60
+ await this.checkTestCoverageGate(files),
61
+ await this.checkDocumentationGate(files),
62
+ ]
63
+ }
64
+
65
+ /** Returns whether all blocking gates passed. */
66
+ canProceed(results: SafetyGateResult[]): { allowed: boolean; blockingGates: string[] } {
67
+ const blocking = results.filter(r => !r.canProceed && r.severity === 'BLOCKING')
68
+ return { allowed: blocking.length === 0, blockingGates: blocking.map(r => r.gate) }
69
+ }
70
+
71
+ // ─── Gate implementations ──────────────────────────────────────────────
72
+
73
+ private async checkRiskGate(files: string[]): Promise<SafetyGateResult> {
74
+ const fileNodes = this.getFileNodes(files)
75
+
76
+ if (fileNodes.length === 0) {
77
+ return this.pass('RISK_SCORE', 'No tracked functions in modified files')
78
+ }
79
+
80
+ const impact = this.analyzer.analyze(fileNodes.map(n => n.id))
81
+ const risk = impact.riskScore
82
+
83
+ if (risk >= 90) {
84
+ return {
85
+ canProceed: false,
86
+ gate: 'RISK_SCORE',
87
+ reason: `Critical risk score: ${risk}/100. Changes could break significant portions of the system.`,
88
+ severity: 'BLOCKING',
89
+ bypassable: false,
90
+ autoFixable: false,
91
+ suggestedFix: 'Break changes into smaller increments or add comprehensive tests',
92
+ }
93
+ }
94
+ if (risk > this.config.maxRiskScore) {
95
+ return {
96
+ canProceed: false,
97
+ gate: 'RISK_SCORE',
98
+ reason: `High risk: ${risk}/100 exceeds threshold of ${this.config.maxRiskScore}`,
99
+ severity: 'BLOCKING',
100
+ bypassable: true,
101
+ bypassCommand: 'mikk safety bypass --gate=RISK_SCORE --reason="<explanation>"',
102
+ autoFixable: false,
103
+ }
104
+ }
105
+
106
+ return this.pass('RISK_SCORE', `Risk score ${risk} within acceptable limits`)
107
+ }
108
+
109
+ private async checkScaleGate(files: string[]): Promise<SafetyGateResult> {
110
+ const fileNodes = this.getFileNodes(files)
111
+
112
+ if (fileNodes.length === 0) {
113
+ return this.pass('IMPACT_SCALE', 'No tracked functions in modified files')
114
+ }
115
+
116
+ const impact = this.analyzer.analyze(fileNodes.map(n => n.id))
117
+ const count = impact.impacted.length
118
+ const hardLimit = this.config.maxImpactNodes * 2
119
+
120
+ if (count > hardLimit) {
121
+ return {
122
+ canProceed: false,
123
+ gate: 'IMPACT_SCALE',
124
+ reason: `Massive blast radius: ${count} functions affected. Hard limit: ${hardLimit}`,
125
+ severity: 'BLOCKING',
126
+ bypassable: false,
127
+ autoFixable: false,
128
+ suggestedFix: 'Split into multiple PRs or create a migration plan',
129
+ }
130
+ }
131
+ if (count > this.config.maxImpactNodes) {
132
+ return {
133
+ canProceed: false,
134
+ gate: 'IMPACT_SCALE',
135
+ reason: `Large impact: ${count} functions affected. Threshold: ${this.config.maxImpactNodes}`,
136
+ severity: 'BLOCKING',
137
+ bypassable: true,
138
+ bypassCommand: 'mikk safety bypass --gate=IMPACT_SCALE --reason="<explanation>"',
139
+ autoFixable: false,
140
+ }
141
+ }
142
+
143
+ return this.pass('IMPACT_SCALE', `Impact scale acceptable: ${count} functions`)
144
+ }
145
+
146
+ private async checkProtectedModuleGate(files: string[]): Promise<SafetyGateResult> {
147
+ const touchedProtected: string[] = []
148
+
149
+ for (const file of files) {
150
+ const norm = file.replace(/\\/g, '/')
151
+ const fileEntry = Object.values(this.lock.files).find(
152
+ f => f.path === norm || norm.endsWith(f.path),
153
+ )
154
+ // Guard: moduleId may be absent on file entries
155
+ if (fileEntry?.moduleId && this.config.protectedModules.includes(fileEntry.moduleId)) {
156
+ touchedProtected.push(fileEntry.moduleId)
157
+ }
158
+ }
159
+
160
+ const unique = [...new Set(touchedProtected)]
161
+ if (unique.length > 0) {
162
+ return {
163
+ canProceed: false,
164
+ gate: 'PROTECTED_MODULE',
165
+ reason: `Modified protected modules: ${unique.join(', ')}. These require architecture review.`,
166
+ severity: 'BLOCKING',
167
+ bypassable: false,
168
+ autoFixable: false,
169
+ suggestedFix: 'Request architecture review or schedule a change review meeting',
170
+ }
171
+ }
172
+
173
+ return this.pass('PROTECTED_MODULE', 'No protected modules touched')
174
+ }
175
+
176
+ private async checkBreakingChangeGate(
177
+ files: string[],
178
+ context?: { commitMessage?: string; branchName?: string },
179
+ ): Promise<SafetyGateResult> {
180
+ const fileNodes = this.getFileNodes(files)
181
+
182
+ // An exported function with callers is a breaking-change candidate
183
+ const exportedWithCallers = fileNodes.filter(n => {
184
+ const fn = this.lock.functions[n.id]
185
+ return fn?.isExported && fn.calledBy.length > 0
186
+ })
187
+
188
+ if (exportedWithCallers.length === 0) {
189
+ return this.pass('BREAKING_CHANGE', 'No exported API changes with existing callers detected')
190
+ }
191
+
192
+ const hasExplicitIntent =
193
+ context?.commitMessage?.toLowerCase().includes('breaking:') ||
194
+ context?.branchName?.toLowerCase().includes('breaking') ||
195
+ context?.commitMessage?.toLowerCase().includes('api change')
196
+
197
+ if (hasExplicitIntent) {
198
+ return this.pass(
199
+ 'BREAKING_CHANGE',
200
+ `Exported API changes (${exportedWithCallers.length} functions) with explicit breaking intent`,
201
+ )
202
+ }
203
+
204
+ return {
205
+ canProceed: false,
206
+ gate: 'BREAKING_CHANGE',
207
+ reason: `${exportedWithCallers.length} exported function(s) with callers modified without explicit intent marker. Add "BREAKING:" to commit message.`,
208
+ severity: 'BLOCKING',
209
+ bypassable: true,
210
+ bypassCommand: 'git commit -m "BREAKING: <your message>"',
211
+ autoFixable: false,
212
+ }
213
+ }
214
+
215
+ private async checkTestCoverageGate(files: string[]): Promise<SafetyGateResult> {
216
+ if (!this.config.requireTestsForChangedFiles) {
217
+ return this.pass('TEST_COVERAGE', 'Test coverage gate disabled')
218
+ }
219
+
220
+ const fileNodes = this.getFileNodes(files)
221
+ if (fileNodes.length === 0) return this.pass('TEST_COVERAGE', 'No tracked functions in modified files')
222
+
223
+ const impact = this.analyzer.analyze(fileNodes.map(n => n.id))
224
+
225
+ const highRisk = impact.impacted.filter(id => {
226
+ const fn = this.lock.functions[id]
227
+ return fn && (fn.calledBy.length > 10 || fn.isExported)
228
+ })
229
+
230
+ const hasTests = files.some(
231
+ f => f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__'),
232
+ )
233
+
234
+ if (highRisk.length > 0 && !hasTests) {
235
+ return {
236
+ canProceed: false,
237
+ gate: 'TEST_COVERAGE',
238
+ reason: `${highRisk.length} high-risk change(s) without test modifications`,
239
+ severity: 'BLOCKING',
240
+ bypassable: true,
241
+ bypassCommand: 'mikk safety bypass --gate=TEST_COVERAGE',
242
+ autoFixable: true,
243
+ suggestedFix: 'Add tests for changed functions or affected call paths',
244
+ }
245
+ }
246
+
247
+ return this.pass(
248
+ 'TEST_COVERAGE',
249
+ hasTests ? 'Tests modified alongside changes' : 'No high-risk changes requiring tests',
250
+ )
251
+ }
252
+
253
+ private async checkDocumentationGate(files: string[]): Promise<SafetyGateResult> {
254
+ if (!this.config.requireDocumentationForApiChanges) {
255
+ return this.pass('DOCUMENTATION', 'Documentation gate disabled')
256
+ }
257
+
258
+ const docFiles = ['README.md', 'API.md', 'CHANGELOG.md', 'AGENTS.md']
259
+ const docsUpdated = files.some(f => docFiles.some(d => f.endsWith(d)))
260
+
261
+ const fileNodes = this.getFileNodes(files)
262
+ const hasSignificantApiChanges = fileNodes.some(n => {
263
+ const fn = this.lock.functions[n.id]
264
+ return fn?.isExported && fn.calledBy.length > 5
265
+ })
266
+
267
+ if (hasSignificantApiChanges && !docsUpdated) {
268
+ return {
269
+ canProceed: false,
270
+ gate: 'DOCUMENTATION',
271
+ reason: 'Significant API changes detected without documentation updates',
272
+ severity: 'BLOCKING',
273
+ bypassable: true,
274
+ bypassCommand: 'mikk safety bypass --gate=DOCUMENTATION',
275
+ autoFixable: false,
276
+ suggestedFix: 'Update README.md, API.md, or AGENTS.md with the changes',
277
+ }
278
+ }
279
+
280
+ return this.pass(
281
+ 'DOCUMENTATION',
282
+ docsUpdated ? 'Documentation updated' : 'No significant API changes requiring docs',
283
+ )
284
+ }
285
+
286
+ // ─── Helpers ───────────────────────────────────────────────────────────
287
+
288
+ /** Collect graph-node IDs for every tracked function in the given files. */
289
+ private getFileNodes(files: string[]): Array<{ id: string }> {
290
+ const nodes: Array<{ id: string }> = []
291
+ for (const file of files) {
292
+ const norm = file.replace(/\\/g, '/')
293
+ for (const fn of Object.values(this.lock.functions)) {
294
+ if (fn.file === norm || fn.file.endsWith('/' + norm)) {
295
+ nodes.push({ id: fn.id })
296
+ }
297
+ }
298
+ }
299
+ return nodes
300
+ }
301
+
302
+ private pass(gate: string, reason: string): SafetyGateResult {
303
+ return { canProceed: true, gate, reason, severity: 'WARNING', bypassable: true, autoFixable: false }
304
+ }
305
+ }
@@ -1,4 +1,4 @@
1
- import type { ImpactResult } from '@getmikk/core'
1
+ import type { ImpactResult, MikkLock } from '@getmikk/core'
2
2
  import type { DecisionResult, Explanation } from './types.js'
3
3
 
4
4
  /**
@@ -7,74 +7,94 @@ import type { DecisionResult, Explanation } from './types.js'
7
7
  */
8
8
  export class ExplanationEngine {
9
9
  /**
10
- * Generate an explanation for the decision and impact.
10
+ * @param lock Optional lock reference used to resolve module names accurately.
11
+ * If omitted, module count is estimated from file paths.
11
12
  */
12
- explain(impact: ImpactResult, decision: DecisionResult): Explanation {
13
- const summary = this.getSummary(decision);
14
- const details = this.getDetails(impact, decision);
15
- const riskBreakdown = this.getRiskBreakdown(impact);
13
+ constructor(private lock?: MikkLock) {}
16
14
 
15
+ explain(impact: ImpactResult, decision: DecisionResult): Explanation {
17
16
  return {
18
- summary,
19
- details,
20
- riskBreakdown
21
- };
17
+ summary: this.getSummary(decision),
18
+ details: this.getDetails(impact, decision),
19
+ riskBreakdown: this.getRiskBreakdown(impact),
20
+ }
22
21
  }
23
22
 
24
23
  private getSummary(decision: DecisionResult): string {
25
24
  switch (decision.status) {
26
- case 'APPROVED': return 'This change is safe and conforms to project policies.';
27
- case 'WARNING': return 'Change detected with non-trivial impact — proceed with caution.';
28
- case 'BLOCKED': return 'MODIFICATION BLOCKED: This change violates safety or architectural policies.';
25
+ case 'APPROVED': return 'This change is safe and conforms to project policies.'
26
+ case 'WARNING': return 'Change detected with non-trivial impact — proceed with caution.'
27
+ case 'BLOCKED': return 'MODIFICATION BLOCKED: This change violates safety or architectural policies.'
29
28
  }
30
29
  }
31
30
 
32
31
  private getDetails(impact: ImpactResult, decision: DecisionResult): string[] {
33
- const details: string[] = [];
32
+ const details: string[] = []
34
33
 
35
34
  if (impact.allImpacted.length === 0) {
36
- details.push('No downstream symbols are affected by this change.');
35
+ details.push('No downstream symbols are affected by this change.')
37
36
  } else {
38
- details.push(`Affects ${impact.allImpacted.length} symbols across ${this.countModules(impact)} modules.`);
39
- details.push(`Maximum risk score is ${impact.riskScore}/100.`);
37
+ details.push(`Affects ${impact.allImpacted.length} symbols across ${this.countModules(impact)} modules.`)
38
+ details.push(`Maximum risk score is ${impact.riskScore}/100.`)
40
39
  }
41
40
 
42
41
  if (impact.classified.critical.length > 0) {
43
- details.push(`⚠ Critical: affects ${impact.classified.critical.length} symbols in separate modules.`);
42
+ details.push(`⚠ Critical: affects ${impact.classified.critical.length} symbol(s) in separate modules.`)
44
43
  }
45
44
 
46
- if (decision.reasons.length > 0) {
47
- decision.reasons.forEach(r => details.push(`Policy: ${r}`));
45
+ for (const r of decision.reasons) {
46
+ details.push(`Policy: ${r}`)
48
47
  }
49
48
 
50
- return details;
49
+ return details
51
50
  }
52
51
 
53
52
  private getRiskBreakdown(impact: ImpactResult): { symbol: string; reason: string; score: number }[] {
54
- // Return top 3 riskiest affected symbols
55
- return impact.allImpacted
53
+ return [...impact.allImpacted]
56
54
  .sort((a, b) => b.riskScore - a.riskScore)
57
55
  .slice(0, 3)
58
56
  .map(node => ({
59
57
  symbol: node.label,
60
- score: node.riskScore,
61
- reason: this.getRiskReason(node.riskScore)
62
- }));
58
+ score: node.riskScore,
59
+ reason: this.getRiskReason(node.riskScore),
60
+ }))
63
61
  }
64
62
 
65
63
  private getRiskReason(score: number): string {
66
- if (score >= 90) return 'Direct critical dependency in protected module';
67
- if (score >= 80) return 'Large downstream reach (potential side effects)';
68
- if (score >= 60) return 'Cross-module propagation';
69
- return 'Standard functional dependency';
64
+ if (score >= 90) return 'Direct critical dependency in protected module'
65
+ if (score >= 80) return 'Large downstream reach (potential side effects)'
66
+ if (score >= 60) return 'Cross-module propagation'
67
+ return 'Standard functional dependency'
70
68
  }
71
69
 
70
+ /**
71
+ * Count distinct modules touched by the impact set.
72
+ *
73
+ * Preference order:
74
+ * 1. lock.functions[nodeId].moduleId — accurate
75
+ * 2. heuristic from file path — fallback when lock is absent
76
+ */
72
77
  private countModules(impact: ImpactResult): number {
73
- const modules = new Set(impact.allImpacted.map(n => {
74
- // Heuristic to extract module from file path if moduleId not present
75
- const parts = n.file.split('/');
76
- return parts.length > 1 ? parts[0] : 'root';
77
- }));
78
- return modules.size;
78
+ const modules = new Set<string>()
79
+
80
+ for (const node of impact.allImpacted) {
81
+ let moduleId: string | undefined
82
+
83
+ // Try to resolve via lock first
84
+ if (this.lock) {
85
+ moduleId = this.lock.functions[node.nodeId]?.moduleId
86
+ }
87
+
88
+ if (!moduleId) {
89
+ // Fallback: first meaningful path segment after stripping common prefixes
90
+ const parts = node.file.replace(/\\/g, '/').split('/')
91
+ const segIdx = parts.findIndex(p => p !== 'src' && p !== 'packages' && p !== 'apps')
92
+ moduleId = segIdx >= 0 ? parts[segIdx] : (parts[0] ?? 'root')
93
+ }
94
+
95
+ modules.add(moduleId)
96
+ }
97
+
98
+ return modules.size
79
99
  }
80
100
  }
package/src/index.ts CHANGED
@@ -3,6 +3,16 @@ export { ConflictDetector } from './conflict-detector.js'
3
3
  export { Suggester } from './suggester.js'
4
4
  export { PreflightPipeline } from './preflight.js'
5
5
  export { SemanticSearcher } from './semantic-searcher.js'
6
+ export { IntentUnderstanding } from './intent-understanding.js'
7
+ export { AutoCorrectionEngine } from './auto-correction.js'
8
+ export { EnforcedSafetyGates } from './enforced-safety.js'
9
+ export { PreEditValidation } from './pre-edit-validation.js'
10
+
6
11
  export type { SemanticMatch } from './semantic-searcher.js'
12
+ export type { IntentContext, IntentAnalysis } from './intent-understanding.js'
13
+ export type { CorrectionIssue, CorrectionResult } from './auto-correction.js'
14
+ export type { SafetyGateResult, SafetyGateConfig } from './enforced-safety.js'
15
+ export type { EditProposal, ValidationResult } from './pre-edit-validation.js'
16
+
7
17
  export type { Intent, Conflict, ConflictResult, Suggestion, PreflightResult, AIProviderConfig } from './types.js'
8
18
  export { IntentSchema } from './types.js'