@getmikk/intent-engine 1.7.1 → 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/README.md CHANGED
@@ -1,312 +1,125 @@
1
- # @getmikk/intent-engine
1
+ # @getmikk/intent-engine
2
2
 
3
- > Architectural pre-flight check if your idea is safe before writing a single line.
3
+ > Parse developer intent, detect constraint conflicts, and find functions by meaning.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@getmikk/intent-engine)](https://www.npmjs.com/package/@getmikk/intent-engine)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](../../LICENSE)
7
7
 
8
- `@getmikk/intent-engine` is the pre-flight check layer. You describe what you want to build in plain English — *"add a caching layer to the auth module"* — and before any code is written, the engine interprets your intent into structured objects, checks it against every architectural constraint in `mikk.json`, detects conflicts and layer violations, and generates a concrete implementation plan with which files to touch and what to create.
8
+ Two capabilities in one package: a pre-flight pipeline that catches architectural conflicts before code is written, and a semantic search engine that finds functions by natural-language description using local vector embeddings.
9
9
 
10
- For AI coding agents, this is the guardrail that prevents architecturally unsafe code generation. For human developers, it's the equivalent of running your idea past a senior architect who knows every constraint in the codebase.
11
-
12
- > Part of [Mikk](../../README.md) — the codebase nervous system for AI-assisted development.
10
+ > Part of [Mikk](../../README.md) live architectural context for your AI agent.
13
11
 
14
12
  ---
15
13
 
16
- ## Installation
17
-
18
- ```bash
19
- npm install @getmikk/intent-engine
20
- # or
21
- bun add @getmikk/intent-engine
22
- ```
23
-
24
- **Peer dependency:** `@getmikk/core`
14
+ ## Pre-flight Pipeline
25
15
 
26
- ---
16
+ Takes a plain-English prompt describing a refactor or new feature, and returns a structured verdict before any code is written.
27
17
 
28
- ## Quick Start
18
+ ### Usage
29
19
 
30
- ```typescript
31
- import { PreflightPipeline } from '@getmikk/intent-engine'
32
- import { ContractReader, LockReader } from '@getmikk/core'
33
-
34
- const contract = await new ContractReader().read('./mikk.json')
35
- const lock = await new LockReader().read('./mikk.lock.json')
36
-
37
- const pipeline = new PreflightPipeline(contract, lock)
38
- const result = await pipeline.run('Add a Redis caching layer to the auth module')
39
-
40
- console.log(result.intents) // Parsed intent objects
41
- console.log(result.conflicts) // Constraint violations found
42
- console.log(result.suggestions) // File-level implementation plan
43
- console.log(result.approved) // true if no blocking conflicts
44
- ```
45
-
46
- ---
47
-
48
- ## Pipeline Architecture
49
-
50
- ```
51
- Natural Language Prompt
52
-
53
-
54
- ┌──────────────────┐
55
- │ IntentInterpreter │ → Intent[]
56
- └────────┬─────────┘
57
-
58
-
59
- ┌──────────────────┐
60
- │ ConflictDetector │ → ConflictResult
61
- └────────┬─────────┘
62
-
63
-
64
- ┌──────────────────┐
65
- │ Suggester │ → Suggestion[]
66
- └────────┬─────────┘
67
-
68
-
69
- PreflightResult
20
+ ```bash
21
+ mikk intent "Move user validation into a shared utils module"
22
+ mikk intent "Extract auth logic into middleware" --json
70
23
  ```
71
24
 
72
- ---
73
-
74
- ## API Reference
75
-
76
- ### PreflightPipeline
77
-
78
- The main entry point — orchestrates the full interpret → detect → suggest flow.
25
+ Or programmatically:
79
26
 
80
27
  ```typescript
81
28
  import { PreflightPipeline } from '@getmikk/intent-engine'
82
29
 
83
30
  const pipeline = new PreflightPipeline(contract, lock)
84
- const result = await pipeline.run('refactor the payment module to use Stripe')
85
- ```
86
-
87
- **`PreflightResult`:**
88
-
89
- | Field | Type | Description |
90
- |-------|------|-------------|
91
- | `intents` | `Intent[]` | Structured interpretation of the prompt |
92
- | `conflicts` | `ConflictResult` | Any constraint violations detected |
93
- | `suggestions` | `Suggestion[]` | Concrete implementation suggestions |
94
- | `approved` | `boolean` | `true` if no error-level conflicts |
95
-
96
- ---
97
-
98
- ### IntentInterpreter
99
-
100
- Parses natural-language prompts into structured intent objects using heuristic keyword matching and fuzzy matching against the lock file's function/module inventory.
101
-
102
- ```typescript
103
- import { IntentInterpreter } from '@getmikk/intent-engine'
31
+ const result = await pipeline.run("Add rate limiting to all API routes")
104
32
 
105
- const interpreter = new IntentInterpreter(contract, lock)
106
- const intents = await interpreter.interpret('add input validation to the signup form')
33
+ console.log(result.approved) // true | false
34
+ console.log(result.conflicts) // constraint violations found
35
+ console.log(result.suggestions) // implementation suggestions with affected files
107
36
  ```
108
37
 
109
- **How it works:**
110
-
111
- 1. **Action verb detection** — Scans for keywords like `create`, `add`, `modify`, `update`, `delete`, `remove`, `refactor`, `move`, `rename`
112
- 2. **Target resolution** — Matches mentioned names against lock file functions, classes, modules, and files using fuzzy matching
113
- 3. **Confidence scoring** — Higher confidence for exact matches, lower for fuzzy
114
-
115
- **`Intent`:**
116
-
117
- ```typescript
118
- type Intent = {
119
- action: 'create' | 'modify' | 'delete' | 'refactor' | 'move'
120
- target: {
121
- type: 'function' | 'class' | 'module' | 'file'
122
- name: string
123
- moduleId?: string // Which module contains the target
124
- filePath?: string // Resolved file path
125
- }
126
- reason: string // Why this intent was derived
127
- confidence: number // 0-1 confidence score
128
- }
129
- ```
130
-
131
- ---
132
-
133
- ### ConflictDetector
134
-
135
- Rule-based constraint checker that validates intents against the architectural rules in `mikk.json`.
38
+ ### What it returns
136
39
 
137
40
  ```typescript
138
- import { ConflictDetector } from '@getmikk/intent-engine'
139
-
140
- const detector = new ConflictDetector(contract, lock)
141
- const result = detector.detect(intents)
142
-
143
- if (result.hasConflicts) {
144
- for (const conflict of result.conflicts) {
145
- console.warn(`[${conflict.severity}] ${conflict.message}`)
146
- if (conflict.suggestedFix) {
147
- console.log(` Fix: ${conflict.suggestedFix}`)
41
+ {
42
+ intents: [
43
+ {
44
+ action: 'add' | 'move' | 'extract' | 'refactor' | 'remove' | ...,
45
+ target: { type: 'function' | 'module' | 'file', name: string, moduleId?: string },
46
+ confidence: number // 0-1
47
+ }
48
+ ],
49
+ conflicts: {
50
+ hasConflicts: boolean,
51
+ conflicts: [
52
+ {
53
+ type: string,
54
+ severity: 'error' | 'warning',
55
+ message: string,
56
+ suggestedFix: string
57
+ }
58
+ ]
59
+ },
60
+ suggestions: [
61
+ {
62
+ intent: Intent,
63
+ implementation: string,
64
+ affectedFiles: string[],
65
+ newFiles: string[],
66
+ estimatedImpact: number
148
67
  }
149
- }
68
+ ],
69
+ approved: boolean
150
70
  }
151
71
  ```
152
72
 
153
- **Constraint types checked:**
73
+ ### Constraint checks
154
74
 
155
- | Constraint | Description | Example |
156
- |-----------|-------------|---------|
157
- | `no-import` | Module A must not import from Module B | `"no-import": ["payments"]` in the auth module |
158
- | `must-use` | Module must use specified dependencies | `"must-use": ["@getmikk/core"]` |
159
- | `no-call` | Functions in module must not call specified targets | `"no-call": ["database.rawQuery"]` |
160
- | `layer` | Enforces layered architecture ordering | `"layer": 2` — can only import from lower layers |
161
- | `naming` | Enforces naming patterns for functions/files | `"naming": { "functions": "^handle|^use|^get" }` |
162
- | `max-files` | Limits the number of files in a module | `"max-files": 20` |
163
-
164
- **Additional checks:**
165
- - **Boundary crossing** — Detects when an intent would create a new cross-module dependency
166
- - **Missing dependencies** — Flags when a target module doesn't exist
167
- - **Ownership warnings** — Warns when modifying code owned by a different team/module
168
-
169
- **`Conflict`:**
170
-
171
- ```typescript
172
- type Conflict = {
173
- type: 'constraint-violation' | 'ownership-conflict' | 'boundary-crossing' | 'missing-dependency'
174
- severity: 'error' | 'warning'
175
- message: string
176
- relatedIntent: Intent
177
- suggestedFix?: string
178
- }
179
- ```
75
+ The pipeline checks against all 6 declared constraint types: `no-import`, `must-use`, `no-call`, `layer`, `naming`, `max-files`. If the proposed change would violate any of them, it surfaces as a conflict with a suggested fix.
180
76
 
181
77
  ---
182
78
 
183
- ### Suggester
79
+ ## Semantic Search
184
80
 
185
- Generates concrete implementation suggestions based on intents and the current codebase state.
186
-
187
- ```typescript
188
- import { Suggester } from '@getmikk/intent-engine'
81
+ Find functions by natural-language description using local vector embeddings. No external API — runs entirely on-device.
189
82
 
190
- const suggester = new Suggester(contract, lock)
191
- const suggestions = suggester.suggest(intents)
83
+ ### Setup
192
84
 
193
- for (const s of suggestions) {
194
- console.log(`Action: ${s.intent.action} ${s.intent.target.name}`)
195
- console.log(`Affected files: ${s.affectedFiles.join(', ')}`)
196
- console.log(`New files: ${s.newFiles.join(', ')}`)
197
- console.log(`Impact: ${s.estimatedImpact}`)
198
- }
85
+ ```bash
86
+ npm install @xenova/transformers
199
87
  ```
200
88
 
201
- **`Suggestion`:**
202
-
203
- | Field | Type | Description |
204
- |-------|------|-------------|
205
- | `intent` | `Intent` | The original intent this suggestion addresses |
206
- | `affectedFiles` | `string[]` | Existing files that would need changes |
207
- | `newFiles` | `string[]` | Files that would need to be created |
208
- | `estimatedImpact` | `'low' \| 'medium' \| 'high'` | Blast radius estimate |
209
- | `implementation` | `string` | Natural-language implementation guidance |
210
-
211
- ---
212
-
213
- ### SemanticSearcher
89
+ The model (`Xenova/all-MiniLM-L6-v2`, ~22MB) downloads once to `~/.cache/huggingface`.
214
90
 
215
- Finds functions semantically similar to a natural-language query using local embeddings via [`@xenova/transformers`](https://github.com/xenova/transformers.js). No API key required — the model runs entirely offline.
91
+ ### Usage
216
92
 
217
- **Model:** `Xenova/all-MiniLM-L6-v2` (~22 MB, downloaded once to `~/.cache/huggingface` on first use)
218
- **Optional peer dependency:** `@xenova/transformers >= 2`
219
-
220
- ```bash
221
- bun add @xenova/transformers # only needed if you use SemanticSearcher
222
- ```
93
+ Exposed via the MCP tool `mikk_semantic_search`, or directly:
223
94
 
224
95
  ```typescript
225
96
  import { SemanticSearcher } from '@getmikk/intent-engine'
226
97
 
227
- // Check if @xenova/transformers is installed before using
228
- if (await SemanticSearcher.isAvailable()) {
229
- const searcher = new SemanticSearcher(projectRoot)
98
+ const searcher = new SemanticSearcher(projectRoot)
230
99
 
231
- // index() builds embeddings; subsequent calls are O(1) cache hits
232
- await searcher.index(lock)
100
+ // Build (or load from cache) embeddings for the lock
101
+ await searcher.index(lock)
233
102
 
234
- // search() returns the topK most relevant functions
235
- const results = await searcher.search('validate JWT and return user payload', lock, 5)
236
- for (const r of results) {
237
- console.log(`${r.name} (${r.file}:${r.lines}) — score: ${r.score}`)
238
- console.log(` ${r.purpose}`)
239
- }
240
- }
103
+ // Find the 10 most semantically similar functions
104
+ const results = await searcher.search('validate a JWT token', lock, 10)
105
+ // Returns: [{ name, file, moduleId, purpose, lines, score }]
241
106
  ```
242
107
 
243
- **Cache behaviour:** Embeddings are persisted to `{projectRoot}/.mikk/embeddings.json` and fingerprinted by function count + first 20 sorted IDs. Re-indexing only re-embeds when the lock actually changes (e.g. after `mikk sync`). A cache hit costs a single disk read.
108
+ ### How it works
244
109
 
245
- **`SemanticMatch`:**
110
+ 1. For each function in the lock, concatenates: function name + purpose string (if present) + param names + return type
111
+ 2. Generates embeddings in batches of 64 using the pipeline
112
+ 3. Caches to `.mikk/embeddings.json` — fingerprinted by function count + first 20 sorted IDs
113
+ 4. Cache is valid until the lock changes; recomputes only what changed
114
+ 5. At search time, embeds the query and ranks all functions by cosine similarity
246
115
 
247
- | Field | Type | Description |
248
- |-------|------|-------------|
249
- | `id` | `string` | Function ID (`fn:module:name`) |
250
- | `name` | `string` | Function name |
251
- | `file` | `string` | Source file path |
252
- | `moduleId` | `string` | Owning module |
253
- | `purpose` | `string` | One-line purpose from the lock |
254
- | `lines` | `string` | Line range, e.g. `"12-34"` |
255
- | `score` | `number` | Cosine similarity `[0, 1]` — higher is more relevant |
116
+ All vectors are unit-normalized at generation time so similarity is a simple dot product.
256
117
 
257
- **API:**
258
-
259
- | Method | Description |
260
- |--------|-------------|
261
- | `SemanticSearcher.isAvailable()` | Returns `true` if `@xenova/transformers` is importable |
262
- | `new SemanticSearcher(projectRoot)` | Creates an instance scoped to a project root |
263
- | `.index(lock)` | Builds/loads embeddings for all functions in the lock |
264
- | `.search(query, lock, topK?)` | Returns top `topK` (default 10) semantically similar functions |
265
-
266
- > **Note:** Call `index()` before `search()`, otherwise `search()` throws `"Call index() before search()"`. The MCP server keeps a per-project singleton to avoid repeated model loads.
267
-
268
- ---
269
-
270
- ## Usage with AI Agents
271
-
272
- The intent engine is designed to be called by AI coding agents as a pre-flight check:
273
-
274
- ```typescript
275
- // In your AI agent's planning phase:
276
- const pipeline = new PreflightPipeline(contract, lock)
277
- const preflight = await pipeline.run(userPrompt)
278
-
279
- if (!preflight.approved) {
280
- // Show conflicts to user, ask for confirmation
281
- const errors = preflight.conflicts.conflicts.filter(c => c.severity === 'error')
282
- throw new Error(`Blocked: ${errors.map(e => e.message).join('; ')}`)
283
- }
284
-
285
- // Use suggestions to guide implementation
286
- for (const suggestion of preflight.suggestions) {
287
- // suggestion.affectedFiles — files to read/modify
288
- // suggestion.newFiles — files to create
289
- // suggestion.implementation — guidance text
290
- }
291
- ```
292
-
293
- ---
294
-
295
- ## Types
118
+ ### Check availability
296
119
 
297
120
  ```typescript
298
- import type {
299
- Intent,
300
- Conflict,
301
- ConflictResult,
302
- Suggestion,
303
- PreflightResult,
304
- AIProviderConfig,
305
- } from '@getmikk/intent-engine'
121
+ const available = await SemanticSearcher.isAvailable()
122
+ // true if @xenova/transformers is installed and importable
306
123
  ```
307
124
 
308
- ---
309
-
310
- ## License
311
-
312
- [Apache-2.0](../../LICENSE)
125
+ The MCP server calls `isAvailable()` before registering the tool — if the package is missing, the tool is not exposed and the response explains how to install it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/intent-engine",
3
- "version": "1.7.1",
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.7.1",
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",
@@ -106,7 +106,7 @@ export class ConflictDetector {
106
106
  }
107
107
  }
108
108
 
109
- // ── Constraint Classification & Checking ─────────────────────
109
+ // --- Constraint Classification & Checking ---------------------
110
110
 
111
111
  private classifyConstraint(text: string): ConstraintType {
112
112
  const lower = text.toLowerCase()
@@ -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
  }
@@ -5,7 +5,7 @@ import type { MikkLock } from '@getmikk/core'
5
5
  interface EmbeddingCache {
6
6
  lockFingerprint: string
7
7
  model: string
8
- embeddings: Record<string, number[]> // fnId unit-normed vector
8
+ embeddings: Record<string, number[]> // fnId -> unit-normed vector
9
9
  }
10
10
 
11
11
  export interface SemanticMatch {
@@ -19,7 +19,7 @@ export interface SemanticMatch {
19
19
  }
20
20
 
21
21
  /**
22
- * SemanticSearcher finds functions semantically similar to a natural-language
22
+ * SemanticSearcher -- finds functions semantically similar to a natural-language
23
23
  * query using local embeddings via @xenova/transformers.
24
24
  *
25
25
  * Model: Xenova/all-MiniLM-L6-v2 (~22 MB, downloads once to ~/.cache/huggingface).
@@ -57,12 +57,12 @@ export class SemanticSearcher {
57
57
 
58
58
  /**
59
59
  * Build (or load from cache) embeddings for every function in the lock.
60
- * Safe to call on every MCP request cache hit is O(1) disk read.
60
+ * Safe to call on every MCP request -- cache hit is O(1) disk read.
61
61
  */
62
62
  async index(lock: MikkLock): Promise<void> {
63
63
  const fingerprint = lockFingerprint(lock)
64
64
 
65
- // ── Cache hit ──────────────────────────────────────────────────────
65
+ // -- Cache hit --------------------------------------------------------
66
66
  try {
67
67
  const raw = await fs.readFile(this.cachePath, 'utf-8')
68
68
  const cached: EmbeddingCache = JSON.parse(raw)
@@ -77,9 +77,9 @@ export class SemanticSearcher {
77
77
  this.cache = cached
78
78
  return
79
79
  }
80
- } catch { /* miss or corrupt rebuild */ }
80
+ } catch { /* miss or corrupt -- rebuild */ }
81
81
 
82
- // ── Empty lock fast-path nothing to embed ────────────────────────
82
+ // -- Empty lock fast-path -- nothing to embed ------------------------
83
83
  const fns = Object.values(lock.functions)
84
84
  if (fns.length === 0) {
85
85
  this.cache = { lockFingerprint: fingerprint, model: SemanticSearcher.MODEL, embeddings: {} }
@@ -154,7 +154,7 @@ export class SemanticSearcher {
154
154
  }
155
155
  }
156
156
 
157
- // ─── Helpers ─────────────────────────────────────────────────────────────────
157
+ // --- Helpers -----------------------------------------------------------------
158
158
 
159
159
  /** Lightweight fingerprint: function count + first 20 sorted IDs */
160
160
  function lockFingerprint(lock: MikkLock): string {
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
  })