@getmikk/core 1.3.1 → 1.5.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.
@@ -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 result = MikkLockSchema.safeParse(json)
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 json = JSON.stringify(lock, null, 2)
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
+ }
@@ -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>