@getmikk/intent-engine 1.8.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/intent-engine",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,16 +21,9 @@
21
21
  "dev": "tsc --watch"
22
22
  },
23
23
  "dependencies": {
24
- "@getmikk/core": "^1.8.0",
25
- "zod": "^3.22.0"
26
- },
27
- "peerDependencies": {
28
- "@xenova/transformers": ""
29
- },
30
- "peerDependenciesMeta": {
31
- "@xenova/transformers": {
32
- "optional": true
33
- }
24
+ "@getmikk/core": "^1.9.0",
25
+ "zod": "^3.22.0",
26
+ "@xenova/transformers": "^2.17.2"
34
27
  },
35
28
  "devDependencies": {
36
29
  "@types/bun": "^1.3.10",
@@ -0,0 +1,69 @@
1
+ import type { ImpactResult, MikkContract } from '@getmikk/core'
2
+ import type { DecisionResult, DecisionStatus } from './types.js'
3
+
4
+ /**
5
+ * DecisionEngine — the "Brain" of Mikk 2.0.
6
+ * Evaluates quantitative impact analysis against project policies.
7
+ */
8
+ export class DecisionEngine {
9
+ constructor(private contract: MikkContract) {}
10
+
11
+ /**
12
+ * Evaluate an impact result against the defined policies.
13
+ */
14
+ evaluate(impact: ImpactResult): DecisionResult {
15
+ const policy = {
16
+ maxRiskScore: this.contract.policies?.maxRiskScore ?? 70,
17
+ maxImpactNodes: this.contract.policies?.maxImpactNodes ?? 10,
18
+ protectedModules: this.contract.policies?.protectedModules ?? [],
19
+ 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;
26
+
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}.`);
34
+ }
35
+
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}.`);
40
+ }
41
+
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);
49
+
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(', ')}.`);
54
+ }
55
+
56
+ // 4. Force BLOCKED for critical cross-boundary impacts if enforcement is on
57
+ 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.`);
60
+ }
61
+
62
+ return {
63
+ status,
64
+ reasons,
65
+ riskScore: maxRisk,
66
+ impactNodes: impactCount
67
+ };
68
+ }
69
+ }
@@ -0,0 +1,80 @@
1
+ import type { ImpactResult } from '@getmikk/core'
2
+ import type { DecisionResult, Explanation } from './types.js'
3
+
4
+ /**
5
+ * ExplanationEngine — generates human-readable reasoning for code modification decisions.
6
+ * Explains WHY a change is safe or risky based on the quantitative graph analysis.
7
+ */
8
+ export class ExplanationEngine {
9
+ /**
10
+ * Generate an explanation for the decision and impact.
11
+ */
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);
16
+
17
+ return {
18
+ summary,
19
+ details,
20
+ riskBreakdown
21
+ };
22
+ }
23
+
24
+ private getSummary(decision: DecisionResult): string {
25
+ 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.';
29
+ }
30
+ }
31
+
32
+ private getDetails(impact: ImpactResult, decision: DecisionResult): string[] {
33
+ const details: string[] = [];
34
+
35
+ if (impact.allImpacted.length === 0) {
36
+ details.push('No downstream symbols are affected by this change.');
37
+ } else {
38
+ details.push(`Affects ${impact.allImpacted.length} symbols across ${this.countModules(impact)} modules.`);
39
+ details.push(`Maximum risk score is ${impact.riskScore}/100.`);
40
+ }
41
+
42
+ if (impact.classified.critical.length > 0) {
43
+ details.push(`⚠ Critical: affects ${impact.classified.critical.length} symbols in separate modules.`);
44
+ }
45
+
46
+ if (decision.reasons.length > 0) {
47
+ decision.reasons.forEach(r => details.push(`Policy: ${r}`));
48
+ }
49
+
50
+ return details;
51
+ }
52
+
53
+ private getRiskBreakdown(impact: ImpactResult): { symbol: string; reason: string; score: number }[] {
54
+ // Return top 3 riskiest affected symbols
55
+ return impact.allImpacted
56
+ .sort((a, b) => b.riskScore - a.riskScore)
57
+ .slice(0, 3)
58
+ .map(node => ({
59
+ symbol: node.label,
60
+ score: node.riskScore,
61
+ reason: this.getRiskReason(node.riskScore)
62
+ }));
63
+ }
64
+
65
+ 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';
70
+ }
71
+
72
+ 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;
79
+ }
80
+ }
package/src/preflight.ts CHANGED
@@ -1,18 +1,23 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
1
+ import {
2
+ MikkContract, MikkLock, GraphBuilder, ImpactAnalyzer
3
+ } from '@getmikk/core'
2
4
  import { IntentInterpreter } from './interpreter.js'
3
5
  import { ConflictDetector } from './conflict-detector.js'
4
6
  import { Suggester } from './suggester.js'
5
- import type { PreflightResult } from './types.js'
7
+ import { DecisionEngine } from './decision-engine.js'
8
+ import { ExplanationEngine } from './explanation-engine.js'
9
+ import type { PreflightResult, DecisionResult, Explanation } from './types.js'
6
10
 
7
11
  /**
8
12
  * PreflightPipeline — orchestrates the full intent pipeline:
9
- * interpret → conflict-detect → suggest.
10
- * Single function call for the CLI.
13
+ * interpret → analyze (impact/risk) → decide → explain → conflict-detect → suggest.
11
14
  */
12
15
  export class PreflightPipeline {
13
16
  private interpreter: IntentInterpreter
14
17
  private conflictDetector: ConflictDetector
15
18
  private suggester: Suggester
19
+ private decisionEngine: DecisionEngine
20
+ private explanationEngine: ExplanationEngine
16
21
 
17
22
  constructor(
18
23
  private contract: MikkContract,
@@ -21,6 +26,8 @@ export class PreflightPipeline {
21
26
  this.interpreter = new IntentInterpreter(contract, lock)
22
27
  this.conflictDetector = new ConflictDetector(contract, lock)
23
28
  this.suggester = new Suggester(contract, lock)
29
+ this.decisionEngine = new DecisionEngine(contract)
30
+ this.explanationEngine = new ExplanationEngine()
24
31
  }
25
32
 
26
33
  /** Run the full preflight pipeline */
@@ -28,11 +35,32 @@ export class PreflightPipeline {
28
35
  // 1. Interpret prompt into structured intents
29
36
  const intents = await this.interpreter.interpret(prompt)
30
37
 
31
- // 2. Check for conflicts
38
+ // 2. Perform Quantitative Impact Analysis
39
+ const graph = new GraphBuilder().buildFromLock(this.lock)
40
+ const analyzer = new ImpactAnalyzer(graph)
41
+
42
+ // Find node IDs for the intents
43
+ const targetIds: string[] = []
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)
50
+ )
51
+ if (matchedNode) targetIds.push(matchedNode.id)
52
+ }
53
+
54
+ const impact = analyzer.analyze(targetIds)
55
+
56
+ // 3. Decide and Explain
57
+ const decision = this.decisionEngine.evaluate(impact)
58
+ const explanation = this.explanationEngine.explain(impact, decision)
59
+
60
+ // 4. Check for Static Conflicts
32
61
  const conflicts = this.conflictDetector.detect(intents)
33
62
 
34
- // 3. Low-confidence rejection: if the best intent has very low confidence,
35
- // add a warning so the AI doesn't blindly proceed
63
+ // 5. Low-confidence rejection / Supplemental warnings
36
64
  const maxConfidence = intents.length > 0
37
65
  ? Math.max(...intents.map(i => i.confidence))
38
66
  : 0
@@ -40,20 +68,22 @@ export class PreflightPipeline {
40
68
  conflicts.conflicts.push({
41
69
  type: 'low-confidence',
42
70
  severity: 'warning',
43
- message: `Low confidence (${(maxConfidence * 100).toFixed(0)}%) — the intent could not be reliably matched to existing code. The suggestion may be inaccurate.`,
71
+ message: `Low confidence (${(maxConfidence * 100).toFixed(0)}%) — matching to existing code was ambiguous.`,
44
72
  relatedIntent: intents[0],
45
- suggestedFix: 'Be more specific about the function or module name in your prompt.',
73
+ suggestedFix: 'Be more specific about the function or module name.',
46
74
  })
47
75
  }
48
76
 
49
- // 4. Generate suggestions
77
+ // 6. Generate implementation suggestions
50
78
  const suggestions = this.suggester.suggest(intents)
51
79
 
52
80
  return {
53
81
  intents,
54
82
  conflicts,
55
83
  suggestions,
56
- approved: !conflicts.hasConflicts && maxConfidence >= 0.4,
84
+ decision,
85
+ explanation,
86
+ approved: !conflicts.hasConflicts && decision.status !== 'BLOCKED' && maxConfidence >= 0.4,
57
87
  }
58
88
  }
59
89
  }
package/src/types.ts CHANGED
@@ -38,6 +38,25 @@ export interface Suggestion {
38
38
  implementation: string
39
39
  }
40
40
 
41
+ export type DecisionStatus = 'APPROVED' | 'WARNING' | 'BLOCKED';
42
+
43
+ export interface DecisionResult {
44
+ status: DecisionStatus
45
+ reasons: string[]
46
+ riskScore: number
47
+ impactNodes: number
48
+ }
49
+
50
+ export interface Explanation {
51
+ summary: string
52
+ details: string[]
53
+ riskBreakdown: {
54
+ symbol: string
55
+ reason: string
56
+ score: number
57
+ }[]
58
+ }
59
+
41
60
  /** Configuration for the AI provider */
42
61
  export interface AIProviderConfig {
43
62
  provider: 'anthropic' | 'openai' | 'local'
@@ -50,5 +69,7 @@ export interface PreflightResult {
50
69
  intents: Intent[]
51
70
  conflicts: ConflictResult
52
71
  suggestions: Suggestion[]
72
+ decision: DecisionResult
73
+ explanation: Explanation
53
74
  approved: boolean
54
75
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Ambient stub for the optional peer dependency @xenova/transformers.
3
- * The real types are only available when the package is installed.
4
- * We use dynamic import + `any` everywhere so this stub is sufficient.
2
+ * Ambient stub for the direct dependency @xenova/transformers.
3
+ * Provides basic types for the library and documents that SemanticSearcher.isAvailable()
4
+ * is used to test runtime loadability (e.g. WASM support) for graceful error handling.
5
5
  */
6
6
  declare module '@xenova/transformers' {
7
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,6 +1,9 @@
1
1
  import { describe, test, expect, beforeAll } from 'bun:test'
2
2
  import { SemanticSearcher } from '../src/semantic-searcher'
3
3
  import type { MikkLock } from '@getmikk/core'
4
+ import * as path from 'node:path'
5
+ import * as os from 'node:os'
6
+ import * as fs from 'node:fs/promises'
4
7
 
5
8
  // ── Minimal mock lock ─────────────────────────────────────────────────────────
6
9
  const mockLock: MikkLock = {
@@ -72,9 +75,10 @@ describe('SemanticSearcher', () => {
72
75
 
73
76
  describe('with indexed lock', () => {
74
77
  beforeAll(async () => {
75
- searcher = new SemanticSearcher('/tmp/mikk-test-' + Date.now())
78
+ const uniquePath = path.join(os.tmpdir(), 'mikk-test-' + Math.random().toString(36).slice(2))
79
+ searcher = new SemanticSearcher(uniquePath)
76
80
  await searcher.index(mockLock)
77
- }, 60_000) // model download can take time on first run
81
+ }, 120_000) // Increase timeout for CI model download
78
82
 
79
83
  test('returns results for any query', async () => {
80
84
  const results = await searcher.search('authenticate user', mockLock, 4)
@@ -154,25 +158,30 @@ describe('SemanticSearcher', () => {
154
158
  ...mockLock,
155
159
  functions: { ...mockLock.functions, 'fn:new:brandNewFn': newFn },
156
160
  }
157
- const freshSearcher = new SemanticSearcher('/tmp/mikk-reindex-' + Date.now())
161
+ const uniquePath = path.join(os.tmpdir(), 'mikk-reindex-' + Math.random().toString(36).slice(2))
162
+ const freshSearcher = new SemanticSearcher(uniquePath)
158
163
  await freshSearcher.index(changedLock) // fingerprint differs → full recompute
159
164
  const results = await freshSearcher.search('unique purpose', changedLock, 5)
160
165
  expect(results.some(r => r.name === 'brandNewFn')).toBe(true)
161
- }, 30_000)
166
+ await fs.rm(uniquePath, { recursive: true, force: true })
167
+ }, 120_000)
162
168
  })
163
169
 
164
170
  describe('edge cases', () => {
165
171
  test('search() before index() throws', async () => {
166
- const fresh = new SemanticSearcher('/tmp/mikk-never-indexed-' + Date.now())
172
+ const uniquePath = path.join(os.tmpdir(), 'mikk-never-' + Math.random().toString(36).slice(2))
173
+ const fresh = new SemanticSearcher(uniquePath)
167
174
  await expect(fresh.search('query', mockLock)).rejects.toThrow('Call index() before search()')
168
175
  })
169
176
 
170
177
  test('empty lock: index() and search() succeed, return empty array', async () => {
171
178
  const emptyLock: MikkLock = { ...mockLock, functions: {} }
172
- const s = new SemanticSearcher('/tmp/mikk-empty-' + Date.now())
179
+ const uniquePath = path.join(os.tmpdir(), 'mikk-empty-' + Math.random().toString(36).slice(2))
180
+ const s = new SemanticSearcher(uniquePath)
173
181
  await s.index(emptyLock)
174
182
  const results = await s.search('anything', emptyLock, 5)
175
183
  expect(results).toEqual([])
176
- })
184
+ await fs.rm(uniquePath, { recursive: true, force: true })
185
+ }, 120_000)
177
186
  })
178
187
  })