@getmikk/core 1.8.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +6 -4
  2. package/src/constants.ts +285 -0
  3. package/src/contract/contract-generator.ts +7 -0
  4. package/src/contract/index.ts +2 -3
  5. package/src/contract/lock-compiler.ts +66 -35
  6. package/src/contract/lock-reader.ts +30 -5
  7. package/src/contract/schema.ts +21 -0
  8. package/src/error-handler.ts +432 -0
  9. package/src/graph/cluster-detector.ts +52 -22
  10. package/src/graph/confidence-engine.ts +85 -0
  11. package/src/graph/graph-builder.ts +298 -255
  12. package/src/graph/impact-analyzer.ts +132 -119
  13. package/src/graph/index.ts +4 -0
  14. package/src/graph/memory-manager.ts +186 -0
  15. package/src/graph/query-engine.ts +76 -0
  16. package/src/graph/risk-engine.ts +86 -0
  17. package/src/graph/types.ts +89 -65
  18. package/src/index.ts +2 -0
  19. package/src/parser/change-detector.ts +99 -0
  20. package/src/parser/go/go-extractor.ts +18 -8
  21. package/src/parser/go/go-parser.ts +2 -0
  22. package/src/parser/index.ts +86 -36
  23. package/src/parser/javascript/js-extractor.ts +1 -1
  24. package/src/parser/javascript/js-parser.ts +2 -0
  25. package/src/parser/oxc-parser.ts +708 -0
  26. package/src/parser/oxc-resolver.ts +83 -0
  27. package/src/parser/tree-sitter/parser.ts +19 -10
  28. package/src/parser/types.ts +100 -73
  29. package/src/parser/typescript/ts-extractor.ts +229 -589
  30. package/src/parser/typescript/ts-parser.ts +16 -171
  31. package/src/parser/typescript/ts-resolver.ts +11 -1
  32. package/src/search/bm25.ts +16 -4
  33. package/src/utils/minimatch.ts +1 -1
  34. package/tests/contract.test.ts +2 -2
  35. package/tests/dead-code.test.ts +7 -7
  36. package/tests/esm-resolver.test.ts +75 -0
  37. package/tests/graph.test.ts +20 -20
  38. package/tests/helpers.ts +11 -6
  39. package/tests/impact-classified.test.ts +37 -41
  40. package/tests/parser.test.ts +7 -5
  41. package/tests/ts-parser.test.ts +27 -52
  42. package/test-output.txt +0 -373
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/core",
3
- "version": "1.8.3",
3
+ "version": "2.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,13 +24,15 @@
24
24
  "@types/better-sqlite3": "^7.6.13",
25
25
  "better-sqlite3": "^12.6.2",
26
26
  "fast-glob": "^3.3.0",
27
+ "oxc-parser": "^0.121.0",
28
+ "oxc-resolver": "^11.19.1",
27
29
  "tree-sitter-wasms": "^0.1.13",
28
30
  "web-tree-sitter": "0.20.8",
29
- "zod": "^3.22.0"
31
+ "zod": "^3.22.0",
32
+ "typescript": "^5.7.0"
30
33
  },
31
34
  "devDependencies": {
32
35
  "@types/bun": "^1.3.10",
33
- "@types/node": "^22.0.0",
34
- "typescript": "^5.7.0"
36
+ "@types/node": "^22.0.0"
35
37
  }
36
38
  }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Centralized constants for the Mikk codebase
3
+ *
4
+ * This file contains all magic numbers, thresholds, and configuration values
5
+ * that were previously scattered throughout the codebase.
6
+ */
7
+
8
+ // ─── Memory Management ───────────────────────────────────────────────────────
9
+
10
+ export const MEMORY_LIMITS = {
11
+ WARNING: 100 * 1024 * 1024, // 100MB
12
+ CRITICAL: 200 * 1024 * 1024, // 200MB
13
+ EMERGENCY: 400 * 1024 * 1024, // 400MB
14
+ } as const
15
+
16
+ export const MEMORY_CONFIG = {
17
+ MAX_AGE: 30 * 60 * 1000, // 30 minutes
18
+ MAX_NODES: 10000, // Maximum nodes to keep in memory
19
+ GC_INTERVAL: 60 * 1000, // GC check interval (1 minute)
20
+ LARGE_GRAPH_THRESHOLD: 5000, // Nodes count to trigger memory monitoring
21
+ MEMORY_CHECK_INTERVAL: 1000, // Check memory every N nodes processed
22
+ MEMORY_INCREASE_THRESHOLD: 100 * 1024 * 1024, // 100MB increase triggers GC
23
+ } as const
24
+
25
+ // ─── Token Budgeting ───────────────────────────────────────────────────────
26
+
27
+ export const TOKEN_BUDGETS = {
28
+ DEFAULT_CLAUDE_MD: 12000,
29
+ DEFAULT_CONTEXT_QUERY: 6000,
30
+ MIN_TOKEN_BUDGET: 1000,
31
+ MAX_TOKEN_BUDGET: 50000,
32
+ } as const
33
+
34
+ export const TOKEN_ESTIMATION = {
35
+ CHARS_PER_TOKEN: 3.8, // Average for GPT-4 tokenizer
36
+ MIN_CHARS_PER_TOKEN: 2.0, // For dense code
37
+ MAX_CHARS_PER_TOKEN: 6.0, // For sparse text
38
+ OVERFLOW_ALLOWANCE: 0.1, // 10% buffer
39
+ } as const
40
+
41
+ // ─── Graph Traversal ─────────────────────────────────────────────────────────
42
+
43
+ export const GRAPH_LIMITS = {
44
+ DEFAULT_MAX_HOPS: 4,
45
+ MAX_HOPS_HARD_LIMIT: 10,
46
+ MIN_HOPS: 1,
47
+ BREADTH_FIRST_QUEUE_SIZE: 1000,
48
+ } as const
49
+
50
+ export const RISK_SCORING = {
51
+ CRITICAL_THRESHOLD: 80,
52
+ HIGH_THRESHOLD: 60,
53
+ MEDIUM_THRESHOLD: 40,
54
+ MODULE_BOUNDARY_BOOST: 80, // Risk boost for crossing module boundaries
55
+ } as const
56
+
57
+ export const CONFIDENCE_SCORING = {
58
+ DEFAULT_CONFIDENCE: 1.0,
59
+ MIN_CONFIDENCE: 0.0,
60
+ MAX_CONFIDENCE: 1.0,
61
+ DECIMAL_PLACES: 3,
62
+ } as const
63
+
64
+ // ─── File Processing ───────────────────────────────────────────────────────
65
+
66
+ export const FILE_LIMITS = {
67
+ MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
68
+ MAX_FILE_SIZE_FOR_PROCESSING: 10 * 1024 * 1024, // 10MB
69
+ MAX_FILES_PER_DIRECTORY: 1000,
70
+ } as const
71
+
72
+ export const FILE_PATTERNS = {
73
+ IGNORED_DIRECTORIES: [
74
+ 'node_modules',
75
+ '.git',
76
+ '.vscode',
77
+ '.idea',
78
+ 'dist',
79
+ 'build',
80
+ 'coverage',
81
+ '.next',
82
+ '.nuxt',
83
+ '.cache',
84
+ 'tmp',
85
+ 'temp',
86
+ ],
87
+ SOURCE_EXTENSIONS: [
88
+ 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
89
+ 'py', 'go', 'rs', 'java', 'kt', 'cs', 'swift',
90
+ 'php', 'rb', 'dart', 'ex', 'exs',
91
+ ],
92
+ CONFIG_EXTENSIONS: [
93
+ 'json', 'yaml', 'yml', 'toml', 'xml',
94
+ 'md', 'txt', 'env', 'config',
95
+ ],
96
+ } as const
97
+
98
+ // ─── Benchmark Configuration ─────────────────────────────────────────────────
99
+
100
+ export const BENCHMARK_CONFIG = {
101
+ DEFAULT_TIMEOUT: 10000, // 10 seconds
102
+ COMMAND_TIMEOUT: 15000, // 15 seconds for external commands
103
+ MAX_TEST_CASES: 100,
104
+ SCORING_PRECISION: 2, // Decimal places for scores
105
+ } as const
106
+
107
+ export const BENCHMARK_THRESHOLDS = {
108
+ MIN_ACCURACY: 0, // 0%
109
+ MAX_ACCURACY: 100, // 100%
110
+ TOKEN_EFFICIENCY_MIN: 0.1, // 10% efficiency
111
+ TOKEN_EFFICIENCY_MAX: 10.0, // 1000% efficiency
112
+ } as const
113
+
114
+ // ─── Dead Code Detection ─────────────────────────────────────────────────────
115
+
116
+ export const DEAD_CODE_PATTERNS = {
117
+ ENTRY_POINT_PATTERNS: [
118
+ /^(main|bootstrap|start|init|setup|configure|register|mount)$/i,
119
+ /^(app|server|index|mod|program)$/i,
120
+ /Handler$/i,
121
+ /Middleware$/i,
122
+ /Controller$/i,
123
+ /^use[A-Z]/, // React hooks
124
+ /^handle[A-Z]/, // Event handlers
125
+ /^on[A-Z]/, // Event listeners
126
+ ],
127
+ TEST_PATTERNS: [
128
+ /^(it|describe|test|beforeAll|afterAll|beforeEach|afterEach)$/,
129
+ /\.test\./,
130
+ /\.spec\./,
131
+ /__test__/,
132
+ ],
133
+ DYNAMIC_USAGE_PATTERNS: [
134
+ /^addEventListener$/i,
135
+ /^removeEventListener$/i,
136
+ /^on[A-Z]/,
137
+ /(invoke|dispatch|emit|call|apply)/i,
138
+ /^ngOnInit$/i,
139
+ /^componentDidMount$/i,
140
+ /^componentWillUnmount$/i,
141
+ ],
142
+ } as const
143
+
144
+ export const DEAD_CODE_CONFIDENCE = {
145
+ HIGH_CONFIDENCE_RULES: 3, // Number of rules to pass for high confidence
146
+ MEDIUM_CONFIDENCE_RULES: 2, // Number of rules to pass for medium confidence
147
+ } as const
148
+
149
+ // ─── Error Handling ─────────────────────────────────────────────────────────
150
+
151
+ export const ERROR_CODES = {
152
+ // File system errors
153
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
154
+ FILE_TOO_LARGE: 'FILE_TOO_LARGE',
155
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
156
+ DIRECTORY_NOT_FOUND: 'DIRECTORY_NOT_FOUND',
157
+
158
+ // Module loading errors
159
+ MODULE_NOT_FOUND: 'MODULE_NOT_FOUND',
160
+ MODULE_LOAD_FAILED: 'MODULE_LOAD_FAILED',
161
+
162
+ // Graph errors
163
+ GRAPH_BUILD_FAILED: 'GRAPH_BUILD_FAILED',
164
+ NODE_NOT_FOUND: 'NODE_NOT_FOUND',
165
+ CIRCULAR_DEPENDENCY: 'CIRCULAR_DEPENDENCY',
166
+
167
+ // Token/budget errors
168
+ TOKEN_BUDGET_EXCEEDED: 'TOKEN_BUDGET_EXCEEDED',
169
+ INVALID_TOKEN_COUNT: 'INVALID_TOKEN_COUNT',
170
+
171
+ // General errors
172
+ INVALID_INPUT: 'INVALID_INPUT',
173
+ OPERATION_TIMEOUT: 'OPERATION_TIMEOUT',
174
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
175
+ } as const
176
+
177
+ export const ERROR_MESSAGES = {
178
+ [ERROR_CODES.FILE_NOT_FOUND]: 'Required file not found: {file}. Run \'mikk init\' first.',
179
+ [ERROR_CODES.FILE_TOO_LARGE]: 'File too large for processing: {file} ({size} bytes, limit: {limit} bytes)',
180
+ [ERROR_CODES.PERMISSION_DENIED]: 'Permission denied accessing: {path}',
181
+ [ERROR_CODES.DIRECTORY_NOT_FOUND]: 'Directory not found: {path}',
182
+
183
+ [ERROR_CODES.MODULE_NOT_FOUND]: 'Required module not found: {module}',
184
+ [ERROR_CODES.MODULE_LOAD_FAILED]: 'Failed to load module: {module}. Ensure \'bun run build\' has been executed.',
185
+
186
+ [ERROR_CODES.GRAPH_BUILD_FAILED]: 'Failed to build dependency graph: {reason}',
187
+ [ERROR_CODES.NODE_NOT_FOUND]: 'Node not found in graph: {nodeId}',
188
+ [ERROR_CODES.CIRCULAR_DEPENDENCY]: 'Circular dependency detected: {path}',
189
+
190
+ [ERROR_CODES.TOKEN_BUDGET_EXCEEDED]: 'Token budget exceeded: {used} > {budget}',
191
+ [ERROR_CODES.INVALID_TOKEN_COUNT]: 'Invalid token count: {count}',
192
+
193
+ [ERROR_CODES.INVALID_INPUT]: 'Invalid input: {reason}',
194
+ [ERROR_CODES.OPERATION_TIMEOUT]: 'Operation timed out after {timeout}ms',
195
+ [ERROR_CODES.UNKNOWN_ERROR]: 'Unexpected error: {message}',
196
+ } as const
197
+
198
+ // ─── Performance Tuning ─────────────────────────────────────────────────────
199
+
200
+ export const PERFORMANCE_CONFIG = {
201
+ CACHE_SIZE_LIMIT: 10000, // Maximum cache entries
202
+ CACHE_TTL: 30 * 60 * 1000, // 30 minutes
203
+ BATCH_SIZE: 100, // Operations per batch
204
+ CONCURRENT_LIMIT: 10, // Maximum concurrent operations
205
+ } as const
206
+
207
+ // ─── Logging & Debugging ────────────────────────────────────────────────────
208
+
209
+ export const LOG_LEVELS = {
210
+ ERROR: 0,
211
+ WARN: 1,
212
+ INFO: 2,
213
+ DEBUG: 3,
214
+ TRACE: 4,
215
+ } as const
216
+
217
+ export const DEBUG_CONFIG = {
218
+ ENABLE_MEMORY_LOGGING: false,
219
+ ENABLE_PERFORMANCE_LOGGING: false,
220
+ ENABLE_GRAPH_LOGGING: false,
221
+ LOG_SAMPLE_RATE: 0.1, // Log 10% of operations
222
+ } as const
223
+
224
+ // ─── Validation Rules ───────────────────────────────────────────────────────
225
+
226
+ export const VALIDATION_RULES = {
227
+ MIN_PROJECT_NAME_LENGTH: 1,
228
+ MAX_PROJECT_NAME_LENGTH: 100,
229
+ MIN_MODULE_NAME_LENGTH: 1,
230
+ MAX_MODULE_NAME_LENGTH: 50,
231
+ MIN_FUNCTION_NAME_LENGTH: 1,
232
+ MAX_FUNCTION_NAME_LENGTH: 100,
233
+ MAX_DESCRIPTION_LENGTH: 1000,
234
+ } as const
235
+
236
+ // ─── API Configuration ───────────────────────────────────────────────────────
237
+
238
+ export const API_CONFIG = {
239
+ DEFAULT_PAGE_SIZE: 20,
240
+ MAX_PAGE_SIZE: 100,
241
+ MIN_PAGE_SIZE: 1,
242
+ RATE_LIMIT_WINDOW: 60 * 1000, // 1 minute
243
+ RATE_LIMIT_MAX_REQUESTS: 1000,
244
+ } as const
245
+
246
+ // ─── Utility Functions ─────────────────────────────────────────────────────
247
+
248
+ /**
249
+ * Format bytes to human readable string
250
+ */
251
+ export function formatBytes(bytes: number): string {
252
+ const units = ['B', 'KB', 'MB', 'GB']
253
+ let size = bytes
254
+ let unitIndex = 0
255
+
256
+ while (size >= 1024 && unitIndex < units.length - 1) {
257
+ size /= 1024
258
+ unitIndex++
259
+ }
260
+
261
+ return `${size.toFixed(1)}${units[unitIndex]}`
262
+ }
263
+
264
+ /**
265
+ * Format milliseconds to human readable string
266
+ */
267
+ export function formatDuration(ms: number): string {
268
+ if (ms < 1000) return `${ms}ms`
269
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
270
+ return `${(ms / 60000).toFixed(1)}m`
271
+ }
272
+
273
+ /**
274
+ * Check if a value is within a range
275
+ */
276
+ export function inRange(value: number, min: number, max: number): boolean {
277
+ return value >= min && value <= max
278
+ }
279
+
280
+ /**
281
+ * Clamp a value to a range
282
+ */
283
+ export function clamp(value: number, min: number, max: number): number {
284
+ return Math.max(min, Math.min(max, value))
285
+ }
@@ -76,6 +76,13 @@ export class ContractGenerator {
76
76
  mode: 'never',
77
77
  requireConfirmation: true,
78
78
  },
79
+ policies: {
80
+ maxRiskScore: 70,
81
+ maxImpactNodes: 10,
82
+ protectedModules: ['auth', 'security', 'billing'],
83
+ enforceStrictBoundaries: true,
84
+ requireReasoningForCritical: true,
85
+ },
79
86
  }
80
87
  }
81
88
 
@@ -1,8 +1,8 @@
1
1
  export {
2
2
  MikkContractSchema, MikkLockSchema,
3
- MikkModuleSchema, MikkDecisionSchema, MikkOverwriteSchema,
3
+ MikkModuleSchema, MikkDecisionSchema, MikkOverwriteSchema, MikkPolicySchema,
4
4
  MikkLockFunctionSchema, MikkLockModuleSchema, MikkLockFileSchema,
5
- type MikkContract, type MikkLock, type MikkModule, type MikkDecision,
5
+ type MikkContract, type MikkLock, type MikkModule, type MikkDecision, type MikkPolicy,
6
6
  type MikkLockFunction, type MikkLockModule, type MikkLockFile,
7
7
  } from './schema.js'
8
8
  export { LockCompiler } from './lock-compiler.js'
@@ -11,4 +11,3 @@ export { ContractReader } from './contract-reader.js'
11
11
  export { LockReader } from './lock-reader.js'
12
12
  export { ContractGenerator } from './contract-generator.js'
13
13
  export { AdrManager } from './adr-manager.js'
14
-
@@ -4,6 +4,7 @@ import type { MikkContract, MikkLock } from './schema.js'
4
4
  import type { DependencyGraph } from '../graph/types.js'
5
5
  import type { ParsedFile } from '../parser/types.js'
6
6
  import type { ContextFile } from '../utils/fs.js'
7
+ import * as nodePath from 'node:path'
7
8
  import { hashContent } from '../hash/file-hasher.js'
8
9
  import { computeModuleHash, computeRootHash } from '../hash/tree-hasher.js'
9
10
  import { minimatch } from '../utils/minimatch.js'
@@ -42,6 +43,17 @@ const GETTER_PREFIXES = ['get', 'fetch', 'load', 'find', 'query', 'retrieve', 'r
42
43
  const SETTER_PREFIXES = ['set', 'update', 'save', 'write', 'put', 'patch', 'create', 'delete', 'remove']
43
44
  const CHECKER_PREFIXES = ['is', 'has', 'can', 'should', 'check', 'validate']
44
45
 
46
+ function getModuleMatchPath(filePath: string, projectRootPath: string | null): string {
47
+ const normalized = filePath.replace(/\\/g, '/')
48
+ if (!projectRootPath) return normalized
49
+ const relative = nodePath.relative(projectRootPath, filePath)
50
+ if (relative === '') return normalized
51
+ if (!relative.startsWith('..')) {
52
+ return relative.replace(/\\/g, '/')
53
+ }
54
+ return normalized
55
+ }
56
+
45
57
  /** Infer a short purpose string from function metadata when JSDoc is missing */
46
58
  function inferPurpose(
47
59
  name: string,
@@ -111,6 +123,7 @@ function capitalise(s: string): string {
111
123
  * and compiles the complete mikk.lock.json.
112
124
  */
113
125
  export class LockCompiler {
126
+ private projectRootPath: string | null = null
114
127
  /** Main entry -- compile full lock from graph + contract + parsed files */
115
128
  compile(
116
129
  graph: DependencyGraph,
@@ -119,6 +132,7 @@ export class LockCompiler {
119
132
  contextFiles?: ContextFile[],
120
133
  projectRoot?: string
121
134
  ): MikkLock {
135
+ this.projectRootPath = projectRoot ? nodePath.resolve(projectRoot) : null
122
136
  const functions = this.compileFunctions(graph, contract)
123
137
  const classes = this.compileClasses(graph, contract)
124
138
  const generics = this.compileGenerics(graph, contract)
@@ -132,7 +146,7 @@ export class LockCompiler {
132
146
  }
133
147
 
134
148
  const lockData: MikkLock = {
135
- version: '1.7.0',
149
+ version: '2.0.0',
136
150
  generatedAt: new Date().toISOString(),
137
151
  generatorVersion: VERSION,
138
152
  projectRoot: projectRoot ?? contract.project.name,
@@ -181,33 +195,35 @@ export class LockCompiler {
181
195
  if (node.type !== 'function') continue
182
196
 
183
197
  const moduleId = this.findModule(node.file, contract.declared.modules)
198
+ const displayName = node.name ?? ''
199
+ const metadata = node.metadata ?? {}
184
200
  const inEdges = graph.inEdges.get(id) || []
185
201
  const outEdges = graph.outEdges.get(id) || []
186
202
 
187
203
  result[id] = {
188
204
  id,
189
- name: node.label,
205
+ name: displayName,
190
206
  file: node.file,
191
- startLine: node.metadata.startLine ?? 0,
192
- endLine: node.metadata.endLine ?? 0,
193
- hash: node.metadata.hash ?? '',
194
- calls: outEdges.filter(e => e.type === 'calls').map(e => e.target),
195
- calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.source),
207
+ startLine: metadata.startLine ?? 0,
208
+ endLine: metadata.endLine ?? 0,
209
+ hash: metadata.hash ?? '',
210
+ calls: outEdges.filter(e => e.type === 'calls').map(e => e.to),
211
+ calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.from),
196
212
  moduleId: moduleId || 'unknown',
197
- ...(node.metadata.params && node.metadata.params.length > 0
198
- ? { params: node.metadata.params }
213
+ ...(metadata.params && metadata.params.length > 0
214
+ ? { params: metadata.params }
199
215
  : {}),
200
- ...(node.metadata.returnType ? { returnType: node.metadata.returnType } : {}),
201
- ...(node.metadata.isAsync ? { isAsync: true } : {}),
202
- ...(node.metadata.isExported ? { isExported: true } : {}),
203
- purpose: node.metadata.purpose || inferPurpose(
204
- node.label,
205
- node.metadata.params,
206
- node.metadata.returnType,
207
- node.metadata.isAsync,
216
+ ...(metadata.returnType ? { returnType: metadata.returnType } : {}),
217
+ ...(metadata.isAsync ? { isAsync: true } : {}),
218
+ ...(metadata.isExported ? { isExported: true } : {}),
219
+ purpose: metadata.purpose || inferPurpose(
220
+ displayName,
221
+ metadata.params,
222
+ metadata.returnType,
223
+ metadata.isAsync,
208
224
  ),
209
- edgeCasesHandled: node.metadata.edgeCasesHandled,
210
- errorHandling: node.metadata.errorHandling,
225
+ edgeCasesHandled: metadata.edgeCasesHandled,
226
+ errorHandling: metadata.errorHandling,
211
227
  }
212
228
  }
213
229
 
@@ -222,17 +238,19 @@ export class LockCompiler {
222
238
  for (const [id, node] of graph.nodes) {
223
239
  if (node.type !== 'class') continue
224
240
  const moduleId = this.findModule(node.file, contract.declared.modules)
241
+ const className = node.name ?? ''
242
+ const metadata = node.metadata ?? {}
225
243
  result[id] = {
226
244
  id,
227
- name: node.label,
245
+ name: className,
228
246
  file: node.file,
229
- startLine: node.metadata.startLine ?? 0,
230
- endLine: node.metadata.endLine ?? 0,
247
+ startLine: metadata.startLine ?? 0,
248
+ endLine: metadata.endLine ?? 0,
231
249
  moduleId: moduleId || 'unknown',
232
- isExported: node.metadata.isExported ?? false,
233
- purpose: node.metadata.purpose || inferPurpose(node.label),
234
- edgeCasesHandled: node.metadata.edgeCasesHandled,
235
- errorHandling: node.metadata.errorHandling,
250
+ isExported: metadata.isExported ?? false,
251
+ purpose: metadata.purpose || inferPurpose(className),
252
+ edgeCasesHandled: metadata.edgeCasesHandled,
253
+ errorHandling: metadata.errorHandling,
236
254
  }
237
255
  }
238
256
  return result
@@ -247,18 +265,20 @@ export class LockCompiler {
247
265
  if (node.type !== 'generic') continue
248
266
  // Only include exported generics non-exported types/interfaces are
249
267
  // internal implementation details that add noise without value.
250
- if (!node.metadata.isExported) continue
268
+ if (!(node.metadata?.isExported)) continue
251
269
  const moduleId = this.findModule(node.file, contract.declared.modules)
270
+ const genericName = node.name ?? ''
271
+ const metadata = node.metadata ?? {}
252
272
  raw[id] = {
253
273
  id,
254
- name: node.label,
255
- type: node.metadata.hash ?? 'generic', // we stored type name in hash
274
+ name: genericName,
275
+ type: metadata.hash ?? 'generic', // we stored type name in hash
256
276
  file: node.file,
257
- startLine: node.metadata.startLine ?? 0,
258
- endLine: node.metadata.endLine ?? 0,
277
+ startLine: metadata.startLine ?? 0,
278
+ endLine: metadata.endLine ?? 0,
259
279
  moduleId: moduleId || 'unknown',
260
- isExported: node.metadata.isExported ?? false,
261
- purpose: node.metadata.purpose || inferPurpose(node.label),
280
+ isExported: metadata.isExported ?? false,
281
+ purpose: metadata.purpose || inferPurpose(genericName),
262
282
  }
263
283
  }
264
284
 
@@ -379,9 +399,20 @@ export class LockCompiler {
379
399
 
380
400
  /** Check if a file path matches any of the module's path patterns */
381
401
  private fileMatchesModule(filePath: string, patterns: string[]): boolean {
382
- const normalized = filePath.replace(/\\/g, '/')
402
+ const relativePath = getModuleMatchPath(filePath, this.projectRootPath)
403
+ const normalizedRelative = relativePath.replace(/\\/g, '/').toLowerCase()
404
+ const normalizedAbsolute = filePath.replace(/\\/g, '/').toLowerCase()
405
+ const normalizedProjectRoot = this.projectRootPath
406
+ ? this.projectRootPath.replace(/\\/g, '/').toLowerCase()
407
+ : null
408
+
383
409
  for (const pattern of patterns) {
384
- if (minimatch(normalized, pattern)) {
410
+ const normalizedPattern = pattern.replace(/\\/g, '/').toLowerCase()
411
+ const relativePattern = normalizedProjectRoot && normalizedPattern.startsWith(`${normalizedProjectRoot}/`)
412
+ ? normalizedPattern.slice(normalizedProjectRoot.length + 1)
413
+ : normalizedPattern
414
+ if (minimatch(normalizedRelative, relativePattern) ||
415
+ minimatch(normalizedAbsolute, normalizedPattern)) {
385
416
  return true
386
417
  }
387
418
  }
@@ -77,6 +77,10 @@ function compactifyLock(lock: MikkLock): any {
77
77
  // P4: no hash, P6: no moduleId
78
78
  }
79
79
  // P7: integer calls/calledBy referencing fnIndex positions
80
+ const { name: parsedName } = parseEntityKey(fn.id, 'fn:')
81
+ if (fn.name && fn.name !== parsedName) {
82
+ c.name = fn.name
83
+ }
80
84
  if (fn.calls.length > 0) c.calls = fn.calls.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
81
85
  if (fn.calledBy.length > 0) c.calledBy = fn.calledBy.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
82
86
  if (fn.params && fn.params.length > 0) c.params = fn.params
@@ -137,7 +141,9 @@ function compactifyLock(lock: MikkLock): any {
137
141
  lastModified: file.lastModified,
138
142
  }
139
143
  if (file.moduleId && file.moduleId !== 'unknown') c.moduleId = file.moduleId
140
- if (file.imports && file.imports.length > 0) c.imports = file.imports
144
+ if (file.imports && file.imports.length > 0) {
145
+ c.imports = file.imports.map(normalizeImportEntry)
146
+ }
141
147
  out.files[key] = c
142
148
  }
143
149
 
@@ -180,7 +186,10 @@ function hydrateLock(raw: any): any {
180
186
  // P6: build file->moduleId map before function loop
181
187
  const fileModuleMap: Record<string, string> = {}
182
188
  for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
183
- fileModuleMap[key] = c.moduleId || 'unknown'
189
+ const moduleId = c.moduleId || 'unknown'
190
+ const normalizedKey = normalizeFilePath(key)
191
+ fileModuleMap[key] = moduleId
192
+ fileModuleMap[normalizedKey] = moduleId
184
193
  }
185
194
 
186
195
  // Hydrate functions
@@ -188,12 +197,14 @@ function hydrateLock(raw: any): any {
188
197
  for (const [key, c] of Object.entries(raw.functions || {}) as [string, any][]) {
189
198
  // P7: key is integer index -> look up full ID via fnIndex
190
199
  const fullId = hasFnIndex ? (fnIndex[parseInt(key)] || key) : key
191
- const { name, file } = parseEntityKey(fullId, 'fn:')
200
+ const { name: parsedName, file } = parseEntityKey(fullId, 'fn:')
201
+ const name = c.name || parsedName
192
202
  const lines = c.lines || [c.startLine || 0, c.endLine || 0]
193
203
  // P7: integer calls/calledBy -> resolve to full string IDs (backward compat: strings pass through)
194
204
  const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
195
205
  const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
196
206
 
207
+ const normalizedFile = normalizeFilePath(file)
197
208
  out.functions[fullId] = {
198
209
  id: fullId,
199
210
  name,
@@ -203,7 +214,7 @@ function hydrateLock(raw: any): any {
203
214
  hash: c.hash || '', // P4: empty string when not stored
204
215
  calls,
205
216
  calledBy,
206
- moduleId: fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
217
+ moduleId: fileModuleMap[normalizedFile] || fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
207
218
  ...(c.params ? { params: c.params } : {}),
208
219
  ...(c.returnType ? { returnType: c.returnType } : {}),
209
220
  ...(c.isAsync ? { isAsync: true } : {}),
@@ -276,7 +287,7 @@ function hydrateLock(raw: any): any {
276
287
  hash: c.hash || '',
277
288
  moduleId: c.moduleId || 'unknown',
278
289
  lastModified: c.lastModified || '',
279
- ...(c.imports && c.imports.length > 0 ? { imports: c.imports } : {}),
290
+ ...(c.imports && c.imports.length > 0 ? { imports: c.imports.map(normalizeImportEntry) } : {}),
280
291
  }
281
292
  }
282
293
 
@@ -315,3 +326,17 @@ function parseEntityKeyFull(key: string): { prefix: string; file: string; name:
315
326
  name: rest.slice(lastColon + 1),
316
327
  }
317
328
  }
329
+
330
+ function normalizeImportEntry(entry: any): { source: string; resolvedPath?: string; names?: string[] } {
331
+ if (!entry) return { source: '' }
332
+ if (typeof entry === 'string') return { source: entry }
333
+ return {
334
+ source: entry.source,
335
+ resolvedPath: entry.resolvedPath || undefined,
336
+ names: entry.names?.length ? entry.names : undefined,
337
+ }
338
+ }
339
+
340
+ function normalizeFilePath(p: string): string {
341
+ return (p || '').replace(/\\/g, '/').toLowerCase()
342
+ }
@@ -26,6 +26,20 @@ export const MikkOverwriteSchema = z.object({
26
26
  lastOverwrittenAt: z.string().optional(),
27
27
  }).default({ mode: 'never', requireConfirmation: true })
28
28
 
29
+ export const MikkPolicySchema = z.object({
30
+ maxRiskScore: z.number().default(70),
31
+ maxImpactNodes: z.number().default(10),
32
+ protectedModules: z.array(z.string()).default(['auth', 'security', 'billing']),
33
+ enforceStrictBoundaries: z.boolean().default(true),
34
+ requireReasoningForCritical: z.boolean().default(true),
35
+ }).default({
36
+ maxRiskScore: 70,
37
+ maxImpactNodes: 10,
38
+ protectedModules: ['auth', 'security', 'billing'],
39
+ enforceStrictBoundaries: true,
40
+ requireReasoningForCritical: true,
41
+ })
42
+
29
43
  export const MikkContractSchema = z.object({
30
44
  version: z.string(),
31
45
  project: z.object({
@@ -41,11 +55,13 @@ export const MikkContractSchema = z.object({
41
55
  decisions: z.array(MikkDecisionSchema).default([]),
42
56
  }),
43
57
  overwrite: MikkOverwriteSchema,
58
+ policies: MikkPolicySchema,
44
59
  })
45
60
 
46
61
  export type MikkContract = z.infer<typeof MikkContractSchema>
47
62
  export type MikkModule = z.infer<typeof MikkModuleSchema>
48
63
  export type MikkDecision = z.infer<typeof MikkDecisionSchema>
64
+ export type MikkPolicy = z.infer<typeof MikkPolicySchema>
49
65
 
50
66
  // ─── mikk.lock.json schema ──────────────────────────────
51
67
 
@@ -74,6 +90,8 @@ export const MikkLockFunctionSchema = z.object({
74
90
  type: z.enum(['try-catch', 'throw']),
75
91
  detail: z.string(),
76
92
  })).optional(),
93
+ confidence: z.number().optional(),
94
+ riskScore: z.number().optional(),
77
95
  })
78
96
 
79
97
  export const MikkLockModuleSchema = z.object({
@@ -86,6 +104,7 @@ export const MikkLockModuleSchema = z.object({
86
104
  export const MikkLockImportSchema = z.object({
87
105
  source: z.string(),
88
106
  resolvedPath: z.string().optional(),
107
+ names: z.array(z.string()).optional(),
89
108
  })
90
109
 
91
110
  export const MikkLockFileSchema = z.object({
@@ -111,6 +130,8 @@ export const MikkLockClassSchema = z.object({
111
130
  type: z.enum(['try-catch', 'throw']),
112
131
  detail: z.string(),
113
132
  })).optional(),
133
+ confidence: z.number().optional(),
134
+ riskScore: z.number().optional(),
114
135
  })
115
136
 
116
137
  export const MikkLockGenericSchema = z.object({