@getmikk/core 1.3.2 → 1.5.1
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 +1 -1
- package/src/contract/contract-generator.ts +87 -8
- package/src/contract/lock-compiler.ts +174 -8
- package/src/contract/lock-reader.ts +269 -3
- package/src/contract/schema.ts +31 -0
- package/src/graph/cluster-detector.ts +286 -18
- package/src/graph/graph-builder.ts +2 -0
- package/src/graph/types.ts +2 -0
- package/src/index.ts +2 -1
- package/src/parser/boundary-checker.ts +74 -2
- package/src/parser/types.ts +11 -0
- package/src/parser/typescript/ts-extractor.ts +146 -8
- package/src/parser/typescript/ts-parser.ts +32 -5
- package/src/utils/fs.ts +586 -4
- package/tests/fs.test.ts +186 -0
- package/tests/helpers.ts +6 -0
|
@@ -4,6 +4,8 @@ import { LockNotFoundError } from '../utils/errors.js'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* LockReader — reads and validates mikk.lock.json from disk.
|
|
7
|
+
* Uses compact format on disk: default values are omitted to save space.
|
|
8
|
+
* Hydrates omitted fields before validation; compactifies before writing.
|
|
7
9
|
*/
|
|
8
10
|
export class LockReader {
|
|
9
11
|
/** Read and validate mikk.lock.json */
|
|
@@ -16,7 +18,8 @@ export class LockReader {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const json = JSON.parse(content)
|
|
19
|
-
const
|
|
21
|
+
const hydrated = hydrateLock(json)
|
|
22
|
+
const result = MikkLockSchema.safeParse(hydrated)
|
|
20
23
|
|
|
21
24
|
if (!result.success) {
|
|
22
25
|
const errors = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n')
|
|
@@ -26,9 +29,272 @@ export class LockReader {
|
|
|
26
29
|
return result.data
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
/** Write lock file to disk */
|
|
32
|
+
/** Write lock file to disk in compact format */
|
|
30
33
|
async write(lock: MikkLock, lockPath: string): Promise<void> {
|
|
31
|
-
const
|
|
34
|
+
const compact = compactifyLock(lock)
|
|
35
|
+
const json = JSON.stringify(compact, null, 2)
|
|
32
36
|
await fs.writeFile(lockPath, json, 'utf-8')
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Compact format — omit-defaults serialization
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Rules:
|
|
44
|
+
// 1. Never write a field whose value equals its default ([], "", undefined, "unknown")
|
|
45
|
+
// 2. id/name/file/path are derivable from the record key — omit them
|
|
46
|
+
// 3. Line ranges become tuples: [startLine, endLine]
|
|
47
|
+
// 4. errorHandling becomes tuples: [line, type, detail]
|
|
48
|
+
// 5. detailedLines becomes tuples: [startLine, endLine, blockType]
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Strip defaults and redundant fields for compact on-disk storage */
|
|
52
|
+
function compactifyLock(lock: MikkLock): any {
|
|
53
|
+
const out: any = {
|
|
54
|
+
version: lock.version,
|
|
55
|
+
generatedAt: lock.generatedAt,
|
|
56
|
+
generatorVersion: lock.generatorVersion,
|
|
57
|
+
projectRoot: lock.projectRoot,
|
|
58
|
+
syncState: lock.syncState,
|
|
59
|
+
graph: lock.graph,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Functions — biggest savings
|
|
63
|
+
out.functions = {}
|
|
64
|
+
for (const [key, fn] of Object.entries(lock.functions)) {
|
|
65
|
+
const c: any = {
|
|
66
|
+
lines: [fn.startLine, fn.endLine],
|
|
67
|
+
hash: fn.hash,
|
|
68
|
+
}
|
|
69
|
+
// Only write non-default fields
|
|
70
|
+
if (fn.moduleId && fn.moduleId !== 'unknown') c.moduleId = fn.moduleId
|
|
71
|
+
if (fn.calls.length > 0) c.calls = fn.calls
|
|
72
|
+
if (fn.calledBy.length > 0) c.calledBy = fn.calledBy
|
|
73
|
+
if (fn.params && fn.params.length > 0) c.params = fn.params
|
|
74
|
+
if (fn.returnType) c.returnType = fn.returnType
|
|
75
|
+
if (fn.isAsync) c.isAsync = true
|
|
76
|
+
if (fn.isExported) c.isExported = true
|
|
77
|
+
if (fn.purpose) c.purpose = fn.purpose
|
|
78
|
+
if (fn.edgeCasesHandled && fn.edgeCasesHandled.length > 0) c.edgeCases = fn.edgeCasesHandled
|
|
79
|
+
if (fn.errorHandling && fn.errorHandling.length > 0) {
|
|
80
|
+
c.errors = fn.errorHandling.map(e => [e.line, e.type, e.detail])
|
|
81
|
+
}
|
|
82
|
+
if (fn.detailedLines && fn.detailedLines.length > 0) {
|
|
83
|
+
c.details = fn.detailedLines.map(d => [d.startLine, d.endLine, d.blockType])
|
|
84
|
+
}
|
|
85
|
+
out.functions[key] = c
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Classes
|
|
89
|
+
if (lock.classes && Object.keys(lock.classes).length > 0) {
|
|
90
|
+
out.classes = {}
|
|
91
|
+
for (const [key, cls] of Object.entries(lock.classes)) {
|
|
92
|
+
const c: any = {
|
|
93
|
+
lines: [cls.startLine, cls.endLine],
|
|
94
|
+
isExported: cls.isExported,
|
|
95
|
+
}
|
|
96
|
+
if (cls.moduleId && cls.moduleId !== 'unknown') c.moduleId = cls.moduleId
|
|
97
|
+
if (cls.purpose) c.purpose = cls.purpose
|
|
98
|
+
if (cls.edgeCasesHandled && cls.edgeCasesHandled.length > 0) c.edgeCases = cls.edgeCasesHandled
|
|
99
|
+
if (cls.errorHandling && cls.errorHandling.length > 0) {
|
|
100
|
+
c.errors = cls.errorHandling.map(e => [e.line, e.type, e.detail])
|
|
101
|
+
}
|
|
102
|
+
out.classes[key] = c
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Generics
|
|
107
|
+
if (lock.generics && Object.keys(lock.generics).length > 0) {
|
|
108
|
+
out.generics = {}
|
|
109
|
+
for (const [key, gen] of Object.entries(lock.generics)) {
|
|
110
|
+
const c: any = {
|
|
111
|
+
lines: [gen.startLine, gen.endLine],
|
|
112
|
+
}
|
|
113
|
+
if (gen.type && gen.type !== 'generic') c.type = gen.type
|
|
114
|
+
if (gen.moduleId && gen.moduleId !== 'unknown') c.moduleId = gen.moduleId
|
|
115
|
+
if (gen.isExported) c.isExported = true
|
|
116
|
+
if (gen.purpose) c.purpose = gen.purpose
|
|
117
|
+
if (gen.alsoIn && gen.alsoIn.length > 0) c.alsoIn = gen.alsoIn
|
|
118
|
+
out.generics[key] = c
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Modules — keep as-is (already small)
|
|
123
|
+
out.modules = lock.modules
|
|
124
|
+
|
|
125
|
+
// Files — strip redundant path (it's the key)
|
|
126
|
+
out.files = {}
|
|
127
|
+
for (const [key, file] of Object.entries(lock.files)) {
|
|
128
|
+
const c: any = {
|
|
129
|
+
hash: file.hash,
|
|
130
|
+
lastModified: file.lastModified,
|
|
131
|
+
}
|
|
132
|
+
if (file.moduleId && file.moduleId !== 'unknown') c.moduleId = file.moduleId
|
|
133
|
+
if (file.imports && file.imports.length > 0) c.imports = file.imports
|
|
134
|
+
out.files[key] = c
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Context files — keep as-is (content is the bulk, no savings)
|
|
138
|
+
if (lock.contextFiles && lock.contextFiles.length > 0) {
|
|
139
|
+
out.contextFiles = lock.contextFiles
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Routes — keep as-is (already compact)
|
|
143
|
+
if (lock.routes && lock.routes.length > 0) {
|
|
144
|
+
out.routes = lock.routes
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Restore omitted defaults and redundant fields from compact format */
|
|
151
|
+
function hydrateLock(raw: any): any {
|
|
152
|
+
if (!raw || typeof raw !== 'object') return raw
|
|
153
|
+
|
|
154
|
+
// If it already has the old format (functions have id/name/file), pass through
|
|
155
|
+
const firstFn = Object.values(raw.functions || {})[0] as any
|
|
156
|
+
if (firstFn && typeof firstFn === 'object' && 'id' in firstFn && 'name' in firstFn && 'file' in firstFn) {
|
|
157
|
+
return raw // Already in full format — no hydration needed
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const out: any = {
|
|
161
|
+
version: raw.version,
|
|
162
|
+
generatedAt: raw.generatedAt,
|
|
163
|
+
generatorVersion: raw.generatorVersion,
|
|
164
|
+
projectRoot: raw.projectRoot,
|
|
165
|
+
syncState: raw.syncState,
|
|
166
|
+
graph: raw.graph,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Hydrate functions
|
|
170
|
+
out.functions = {}
|
|
171
|
+
for (const [key, c] of Object.entries(raw.functions || {}) as [string, any][]) {
|
|
172
|
+
// Parse key: "fn:filepath:functionName"
|
|
173
|
+
const { name, file } = parseEntityKey(key, 'fn:')
|
|
174
|
+
const lines = c.lines || [c.startLine || 0, c.endLine || 0]
|
|
175
|
+
|
|
176
|
+
out.functions[key] = {
|
|
177
|
+
id: key,
|
|
178
|
+
name,
|
|
179
|
+
file,
|
|
180
|
+
startLine: lines[0],
|
|
181
|
+
endLine: lines[1],
|
|
182
|
+
hash: c.hash || '',
|
|
183
|
+
calls: c.calls || [],
|
|
184
|
+
calledBy: c.calledBy || [],
|
|
185
|
+
moduleId: c.moduleId || 'unknown',
|
|
186
|
+
...(c.params ? { params: c.params } : {}),
|
|
187
|
+
...(c.returnType ? { returnType: c.returnType } : {}),
|
|
188
|
+
...(c.isAsync ? { isAsync: true } : {}),
|
|
189
|
+
...(c.isExported ? { isExported: true } : {}),
|
|
190
|
+
...(c.purpose ? { purpose: c.purpose } : {}),
|
|
191
|
+
...(c.edgeCases && c.edgeCases.length > 0 ? { edgeCasesHandled: c.edgeCases } : {}),
|
|
192
|
+
...(c.errors && c.errors.length > 0 ? {
|
|
193
|
+
errorHandling: c.errors.map((e: any) => ({
|
|
194
|
+
line: e[0], type: e[1], detail: e[2]
|
|
195
|
+
}))
|
|
196
|
+
} : {}),
|
|
197
|
+
...(c.details && c.details.length > 0 ? {
|
|
198
|
+
detailedLines: c.details.map((d: any) => ({
|
|
199
|
+
startLine: d[0], endLine: d[1], blockType: d[2]
|
|
200
|
+
}))
|
|
201
|
+
} : {}),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Hydrate classes
|
|
206
|
+
if (raw.classes) {
|
|
207
|
+
out.classes = {}
|
|
208
|
+
for (const [key, c] of Object.entries(raw.classes) as [string, any][]) {
|
|
209
|
+
const { name, file } = parseEntityKey(key, 'class:')
|
|
210
|
+
const lines = c.lines || [c.startLine || 0, c.endLine || 0]
|
|
211
|
+
|
|
212
|
+
out.classes[key] = {
|
|
213
|
+
id: key,
|
|
214
|
+
name,
|
|
215
|
+
file,
|
|
216
|
+
startLine: lines[0],
|
|
217
|
+
endLine: lines[1],
|
|
218
|
+
moduleId: c.moduleId || 'unknown',
|
|
219
|
+
isExported: c.isExported ?? false,
|
|
220
|
+
...(c.purpose ? { purpose: c.purpose } : {}),
|
|
221
|
+
...(c.edgeCases && c.edgeCases.length > 0 ? { edgeCasesHandled: c.edgeCases } : {}),
|
|
222
|
+
...(c.errors && c.errors.length > 0 ? {
|
|
223
|
+
errorHandling: c.errors.map((e: any) => ({
|
|
224
|
+
line: e[0], type: e[1], detail: e[2]
|
|
225
|
+
}))
|
|
226
|
+
} : {}),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Hydrate generics
|
|
232
|
+
if (raw.generics) {
|
|
233
|
+
out.generics = {}
|
|
234
|
+
for (const [key, c] of Object.entries(raw.generics) as [string, any][]) {
|
|
235
|
+
const { name, file, prefix } = parseEntityKeyFull(key)
|
|
236
|
+
const lines = c.lines || [c.startLine || 0, c.endLine || 0]
|
|
237
|
+
const inferredType = prefix === 'intf' ? 'interface' : prefix === 'type' ? 'type' : prefix === 'const' ? 'const' : c.type || 'generic'
|
|
238
|
+
|
|
239
|
+
out.generics[key] = {
|
|
240
|
+
id: key,
|
|
241
|
+
name,
|
|
242
|
+
type: c.type || inferredType,
|
|
243
|
+
file,
|
|
244
|
+
startLine: lines[0],
|
|
245
|
+
endLine: lines[1],
|
|
246
|
+
moduleId: c.moduleId || 'unknown',
|
|
247
|
+
isExported: c.isExported ?? false,
|
|
248
|
+
...(c.purpose ? { purpose: c.purpose } : {}),
|
|
249
|
+
...(c.alsoIn && c.alsoIn.length > 0 ? { alsoIn: c.alsoIn } : {}),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Hydrate files
|
|
255
|
+
out.files = {}
|
|
256
|
+
for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
|
|
257
|
+
out.files[key] = {
|
|
258
|
+
path: key,
|
|
259
|
+
hash: c.hash || '',
|
|
260
|
+
moduleId: c.moduleId || 'unknown',
|
|
261
|
+
lastModified: c.lastModified || '',
|
|
262
|
+
...(c.imports && c.imports.length > 0 ? { imports: c.imports } : {}),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Modules — already in full format
|
|
267
|
+
out.modules = raw.modules
|
|
268
|
+
|
|
269
|
+
// Pass through
|
|
270
|
+
if (raw.contextFiles) out.contextFiles = raw.contextFiles
|
|
271
|
+
if (raw.routes) out.routes = raw.routes
|
|
272
|
+
|
|
273
|
+
return out
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Parse entity key like "fn:path/to/file.ts:FunctionName" */
|
|
277
|
+
function parseEntityKey(key: string, prefix: string): { name: string; file: string } {
|
|
278
|
+
const withoutPrefix = key.startsWith(prefix) ? key.slice(prefix.length) : key
|
|
279
|
+
const lastColon = withoutPrefix.lastIndexOf(':')
|
|
280
|
+
if (lastColon === -1) return { name: withoutPrefix, file: '' }
|
|
281
|
+
return {
|
|
282
|
+
file: withoutPrefix.slice(0, lastColon),
|
|
283
|
+
name: withoutPrefix.slice(lastColon + 1),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Parse any entity key, returning prefix too */
|
|
288
|
+
function parseEntityKeyFull(key: string): { prefix: string; file: string; name: string } {
|
|
289
|
+
const firstColon = key.indexOf(':')
|
|
290
|
+
if (firstColon === -1) return { prefix: '', file: '', name: key }
|
|
291
|
+
const prefix = key.slice(0, firstColon)
|
|
292
|
+
const rest = key.slice(firstColon + 1)
|
|
293
|
+
const lastColon = rest.lastIndexOf(':')
|
|
294
|
+
if (lastColon === -1) return { prefix, file: rest, name: '' }
|
|
295
|
+
return {
|
|
296
|
+
prefix,
|
|
297
|
+
file: rest.slice(0, lastColon),
|
|
298
|
+
name: rest.slice(lastColon + 1),
|
|
299
|
+
}
|
|
300
|
+
}
|
package/src/contract/schema.ts
CHANGED
|
@@ -59,6 +59,14 @@ export const MikkLockFunctionSchema = z.object({
|
|
|
59
59
|
calls: z.array(z.string()),
|
|
60
60
|
calledBy: z.array(z.string()),
|
|
61
61
|
moduleId: z.string(),
|
|
62
|
+
params: z.array(z.object({
|
|
63
|
+
name: z.string(),
|
|
64
|
+
type: z.string(),
|
|
65
|
+
optional: z.boolean().optional(),
|
|
66
|
+
})).optional(),
|
|
67
|
+
returnType: z.string().optional(),
|
|
68
|
+
isAsync: z.boolean().optional(),
|
|
69
|
+
isExported: z.boolean().optional(),
|
|
62
70
|
purpose: z.string().optional(),
|
|
63
71
|
edgeCasesHandled: z.array(z.string()).optional(),
|
|
64
72
|
errorHandling: z.array(z.object({
|
|
@@ -85,6 +93,7 @@ export const MikkLockFileSchema = z.object({
|
|
|
85
93
|
hash: z.string(),
|
|
86
94
|
moduleId: z.string(),
|
|
87
95
|
lastModified: z.string(),
|
|
96
|
+
imports: z.array(z.string()).optional(),
|
|
88
97
|
})
|
|
89
98
|
|
|
90
99
|
export const MikkLockClassSchema = z.object({
|
|
@@ -114,6 +123,24 @@ export const MikkLockGenericSchema = z.object({
|
|
|
114
123
|
moduleId: z.string(),
|
|
115
124
|
isExported: z.boolean(),
|
|
116
125
|
purpose: z.string().optional(),
|
|
126
|
+
/** Other files that contain an identical generic (same name + type). Dedup. */
|
|
127
|
+
alsoIn: z.array(z.string()).optional(),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
export const MikkLockContextFileSchema = z.object({
|
|
131
|
+
path: z.string(),
|
|
132
|
+
content: z.string(),
|
|
133
|
+
type: z.enum(['schema', 'model', 'types', 'routes', 'config', 'api-spec', 'migration', 'docker']),
|
|
134
|
+
size: z.number(),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const MikkLockRouteSchema = z.object({
|
|
138
|
+
method: z.string(),
|
|
139
|
+
path: z.string(),
|
|
140
|
+
handler: z.string(),
|
|
141
|
+
middlewares: z.array(z.string()),
|
|
142
|
+
file: z.string(),
|
|
143
|
+
line: z.number(),
|
|
117
144
|
})
|
|
118
145
|
|
|
119
146
|
export const MikkLockSchema = z.object({
|
|
@@ -132,6 +159,8 @@ export const MikkLockSchema = z.object({
|
|
|
132
159
|
classes: z.record(MikkLockClassSchema).optional(),
|
|
133
160
|
generics: z.record(MikkLockGenericSchema).optional(),
|
|
134
161
|
files: z.record(MikkLockFileSchema),
|
|
162
|
+
contextFiles: z.array(MikkLockContextFileSchema).optional(),
|
|
163
|
+
routes: z.array(MikkLockRouteSchema).optional(),
|
|
135
164
|
graph: z.object({
|
|
136
165
|
nodes: z.number(),
|
|
137
166
|
edges: z.number(),
|
|
@@ -145,3 +174,5 @@ export type MikkLockModule = z.infer<typeof MikkLockModuleSchema>
|
|
|
145
174
|
export type MikkLockFile = z.infer<typeof MikkLockFileSchema>
|
|
146
175
|
export type MikkLockClass = z.infer<typeof MikkLockClassSchema>
|
|
147
176
|
export type MikkLockGeneric = z.infer<typeof MikkLockGenericSchema>
|
|
177
|
+
export type MikkLockContextFile = z.infer<typeof MikkLockContextFileSchema>
|
|
178
|
+
export type MikkLockRoute = z.infer<typeof MikkLockRouteSchema>
|