@getmikk/core 1.8.2 → 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 +3 -1
- package/src/constants.ts +285 -0
- package/src/contract/contract-generator.ts +7 -0
- package/src/contract/index.ts +2 -3
- package/src/contract/lock-compiler.ts +74 -42
- package/src/contract/lock-reader.ts +24 -4
- package/src/contract/schema.ts +27 -1
- package/src/error-handler.ts +430 -0
- package/src/graph/cluster-detector.ts +45 -20
- package/src/graph/confidence-engine.ts +60 -0
- package/src/graph/dead-code-detector.ts +27 -5
- package/src/graph/graph-builder.ts +298 -238
- package/src/graph/impact-analyzer.ts +131 -114
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +345 -0
- package/src/graph/query-engine.ts +79 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -64
- package/src/parser/boundary-checker.ts +3 -1
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +28 -9
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +88 -38
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +675 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +27 -15
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +241 -537
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +5 -2
- package/src/utils/minimatch.ts +1 -1
- package/tests/contract.test.ts +2 -2
- package/tests/dead-code.test.ts +7 -7
- package/tests/esm-resolver.test.ts +75 -0
- package/tests/graph.test.ts +20 -20
- package/tests/helpers.ts +11 -6
- package/tests/impact-classified.test.ts +37 -41
- package/tests/parser.test.ts +7 -5
- package/tests/ts-parser.test.ts +27 -52
- package/test-output.txt +0 -373
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,6 +24,8 @@
|
|
|
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
31
|
"zod": "^3.22.0"
|
package/src/constants.ts
ADDED
|
@@ -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
|
|
package/src/contract/index.ts
CHANGED
|
@@ -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: '
|
|
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:
|
|
205
|
+
name: displayName,
|
|
190
206
|
file: node.file,
|
|
191
|
-
startLine:
|
|
192
|
-
endLine:
|
|
193
|
-
hash:
|
|
194
|
-
calls: outEdges.filter(e => e.type === 'calls').map(e => e.
|
|
195
|
-
calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.
|
|
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
|
-
...(
|
|
198
|
-
? { params:
|
|
213
|
+
...(metadata.params && metadata.params.length > 0
|
|
214
|
+
? { params: metadata.params }
|
|
199
215
|
: {}),
|
|
200
|
-
...(
|
|
201
|
-
...(
|
|
202
|
-
...(
|
|
203
|
-
purpose:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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:
|
|
210
|
-
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:
|
|
245
|
+
name: className,
|
|
228
246
|
file: node.file,
|
|
229
|
-
startLine:
|
|
230
|
-
endLine:
|
|
247
|
+
startLine: metadata.startLine ?? 0,
|
|
248
|
+
endLine: metadata.endLine ?? 0,
|
|
231
249
|
moduleId: moduleId || 'unknown',
|
|
232
|
-
isExported:
|
|
233
|
-
purpose:
|
|
234
|
-
edgeCasesHandled:
|
|
235
|
-
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
|
|
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:
|
|
255
|
-
type:
|
|
274
|
+
name: genericName,
|
|
275
|
+
type: metadata.hash ?? 'generic', // we stored type name in hash
|
|
256
276
|
file: node.file,
|
|
257
|
-
startLine:
|
|
258
|
-
endLine:
|
|
277
|
+
startLine: metadata.startLine ?? 0,
|
|
278
|
+
endLine: metadata.endLine ?? 0,
|
|
259
279
|
moduleId: moduleId || 'unknown',
|
|
260
|
-
isExported:
|
|
261
|
-
purpose:
|
|
280
|
+
isExported: metadata.isExported ?? false,
|
|
281
|
+
purpose: metadata.purpose || inferPurpose(genericName),
|
|
262
282
|
}
|
|
263
283
|
}
|
|
264
284
|
|
|
@@ -325,18 +345,19 @@ export class LockCompiler {
|
|
|
325
345
|
for (const file of parsedFiles) {
|
|
326
346
|
const moduleId = this.findModule(file.path, contract.declared.modules)
|
|
327
347
|
|
|
328
|
-
// Collect file-level imports from the
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
|
|
348
|
+
// Collect file-level imports from the parsed file info directly
|
|
349
|
+
// to include both source and resolvedPath for unresolved analysis.
|
|
350
|
+
const imports = file.imports.map(imp => ({
|
|
351
|
+
source: imp.source,
|
|
352
|
+
resolvedPath: imp.resolvedPath || undefined,
|
|
353
|
+
}))
|
|
354
|
+
|
|
334
355
|
result[file.path] = {
|
|
335
356
|
path: file.path,
|
|
336
357
|
hash: file.hash,
|
|
337
358
|
moduleId: moduleId || 'unknown',
|
|
338
359
|
lastModified: new Date(file.parsedAt).toISOString(),
|
|
339
|
-
...(
|
|
360
|
+
...(imports.length > 0 ? { imports } : {}),
|
|
340
361
|
}
|
|
341
362
|
}
|
|
342
363
|
|
|
@@ -378,9 +399,20 @@ export class LockCompiler {
|
|
|
378
399
|
|
|
379
400
|
/** Check if a file path matches any of the module's path patterns */
|
|
380
401
|
private fileMatchesModule(filePath: string, patterns: string[]): boolean {
|
|
381
|
-
const
|
|
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
|
+
|
|
382
409
|
for (const pattern of patterns) {
|
|
383
|
-
|
|
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)) {
|
|
384
416
|
return true
|
|
385
417
|
}
|
|
386
418
|
}
|
|
@@ -137,7 +137,9 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
137
137
|
lastModified: file.lastModified,
|
|
138
138
|
}
|
|
139
139
|
if (file.moduleId && file.moduleId !== 'unknown') c.moduleId = file.moduleId
|
|
140
|
-
if (file.imports && file.imports.length > 0)
|
|
140
|
+
if (file.imports && file.imports.length > 0) {
|
|
141
|
+
c.imports = file.imports.map(normalizeImportEntry)
|
|
142
|
+
}
|
|
141
143
|
out.files[key] = c
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -180,7 +182,10 @@ function hydrateLock(raw: any): any {
|
|
|
180
182
|
// P6: build file->moduleId map before function loop
|
|
181
183
|
const fileModuleMap: Record<string, string> = {}
|
|
182
184
|
for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
|
|
183
|
-
|
|
185
|
+
const moduleId = c.moduleId || 'unknown'
|
|
186
|
+
const normalizedKey = normalizeFilePath(key)
|
|
187
|
+
fileModuleMap[key] = moduleId
|
|
188
|
+
fileModuleMap[normalizedKey] = moduleId
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
// Hydrate functions
|
|
@@ -194,6 +199,7 @@ function hydrateLock(raw: any): any {
|
|
|
194
199
|
const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
195
200
|
const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
196
201
|
|
|
202
|
+
const normalizedFile = normalizeFilePath(file)
|
|
197
203
|
out.functions[fullId] = {
|
|
198
204
|
id: fullId,
|
|
199
205
|
name,
|
|
@@ -203,7 +209,7 @@ function hydrateLock(raw: any): any {
|
|
|
203
209
|
hash: c.hash || '', // P4: empty string when not stored
|
|
204
210
|
calls,
|
|
205
211
|
calledBy,
|
|
206
|
-
moduleId: fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
|
|
212
|
+
moduleId: fileModuleMap[normalizedFile] || fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
|
|
207
213
|
...(c.params ? { params: c.params } : {}),
|
|
208
214
|
...(c.returnType ? { returnType: c.returnType } : {}),
|
|
209
215
|
...(c.isAsync ? { isAsync: true } : {}),
|
|
@@ -276,7 +282,7 @@ function hydrateLock(raw: any): any {
|
|
|
276
282
|
hash: c.hash || '',
|
|
277
283
|
moduleId: c.moduleId || 'unknown',
|
|
278
284
|
lastModified: c.lastModified || '',
|
|
279
|
-
...(c.imports && c.imports.length > 0 ? { imports: c.imports } : {}),
|
|
285
|
+
...(c.imports && c.imports.length > 0 ? { imports: c.imports.map(normalizeImportEntry) } : {}),
|
|
280
286
|
}
|
|
281
287
|
}
|
|
282
288
|
|
|
@@ -315,3 +321,17 @@ function parseEntityKeyFull(key: string): { prefix: string; file: string; name:
|
|
|
315
321
|
name: rest.slice(lastColon + 1),
|
|
316
322
|
}
|
|
317
323
|
}
|
|
324
|
+
|
|
325
|
+
function normalizeImportEntry(entry: any): { source: string; resolvedPath?: string; names?: string[] } {
|
|
326
|
+
if (!entry) return { source: '' }
|
|
327
|
+
if (typeof entry === 'string') return { source: entry }
|
|
328
|
+
return {
|
|
329
|
+
source: entry.source,
|
|
330
|
+
resolvedPath: entry.resolvedPath || undefined,
|
|
331
|
+
names: entry.names?.length ? entry.names : undefined,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function normalizeFilePath(p: string): string {
|
|
336
|
+
return (p || '').replace(/\\/g, '/').toLowerCase()
|
|
337
|
+
}
|
package/src/contract/schema.ts
CHANGED
|
@@ -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({
|
|
@@ -83,12 +101,18 @@ export const MikkLockModuleSchema = z.object({
|
|
|
83
101
|
fragmentPath: z.string(),
|
|
84
102
|
})
|
|
85
103
|
|
|
104
|
+
export const MikkLockImportSchema = z.object({
|
|
105
|
+
source: z.string(),
|
|
106
|
+
resolvedPath: z.string().optional(),
|
|
107
|
+
names: z.array(z.string()).optional(),
|
|
108
|
+
})
|
|
109
|
+
|
|
86
110
|
export const MikkLockFileSchema = z.object({
|
|
87
111
|
path: z.string(),
|
|
88
112
|
hash: z.string(),
|
|
89
113
|
moduleId: z.string(),
|
|
90
114
|
lastModified: z.string(),
|
|
91
|
-
imports: z.array(
|
|
115
|
+
imports: z.array(MikkLockImportSchema).optional(),
|
|
92
116
|
})
|
|
93
117
|
|
|
94
118
|
export const MikkLockClassSchema = z.object({
|
|
@@ -106,6 +130,8 @@ export const MikkLockClassSchema = z.object({
|
|
|
106
130
|
type: z.enum(['try-catch', 'throw']),
|
|
107
131
|
detail: z.string(),
|
|
108
132
|
})).optional(),
|
|
133
|
+
confidence: z.number().optional(),
|
|
134
|
+
riskScore: z.number().optional(),
|
|
109
135
|
})
|
|
110
136
|
|
|
111
137
|
export const MikkLockGenericSchema = z.object({
|