@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.
- package/package.json +6 -4
- 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 +66 -35
- package/src/contract/lock-reader.ts +30 -5
- package/src/contract/schema.ts +21 -0
- package/src/error-handler.ts +432 -0
- package/src/graph/cluster-detector.ts +52 -22
- package/src/graph/confidence-engine.ts +85 -0
- package/src/graph/graph-builder.ts +298 -255
- package/src/graph/impact-analyzer.ts +132 -119
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +186 -0
- package/src/graph/query-engine.ts +76 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -65
- package/src/index.ts +2 -0
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +18 -8
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +86 -36
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +708 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +19 -10
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +229 -589
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +16 -4
- 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": "
|
|
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
|
}
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
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({
|
|
@@ -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({
|