@getmikk/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +431 -0
  2. package/package.json +6 -2
  3. package/src/contract/contract-generator.ts +85 -85
  4. package/src/contract/contract-reader.ts +28 -28
  5. package/src/contract/contract-writer.ts +114 -114
  6. package/src/contract/index.ts +12 -12
  7. package/src/contract/lock-compiler.ts +221 -221
  8. package/src/contract/lock-reader.ts +34 -34
  9. package/src/contract/schema.ts +147 -147
  10. package/src/graph/cluster-detector.ts +312 -312
  11. package/src/graph/graph-builder.ts +211 -211
  12. package/src/graph/impact-analyzer.ts +55 -55
  13. package/src/graph/index.ts +4 -4
  14. package/src/graph/types.ts +59 -59
  15. package/src/hash/file-hasher.ts +30 -30
  16. package/src/hash/hash-store.ts +119 -119
  17. package/src/hash/index.ts +3 -3
  18. package/src/hash/tree-hasher.ts +20 -20
  19. package/src/index.ts +12 -12
  20. package/src/parser/base-parser.ts +16 -16
  21. package/src/parser/boundary-checker.ts +211 -211
  22. package/src/parser/index.ts +46 -46
  23. package/src/parser/types.ts +90 -90
  24. package/src/parser/typescript/ts-extractor.ts +543 -543
  25. package/src/parser/typescript/ts-parser.ts +41 -41
  26. package/src/parser/typescript/ts-resolver.ts +86 -86
  27. package/src/utils/errors.ts +42 -42
  28. package/src/utils/fs.ts +75 -75
  29. package/src/utils/fuzzy-match.ts +186 -186
  30. package/src/utils/logger.ts +36 -36
  31. package/src/utils/minimatch.ts +19 -19
  32. package/tests/contract.test.ts +134 -134
  33. package/tests/fixtures/simple-api/package.json +5 -5
  34. package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
  35. package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
  36. package/tests/fixtures/simple-api/src/index.ts +9 -9
  37. package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
  38. package/tests/fixtures/simple-api/tsconfig.json +8 -8
  39. package/tests/fuzzy-match.test.ts +142 -142
  40. package/tests/graph.test.ts +169 -169
  41. package/tests/hash.test.ts +49 -49
  42. package/tests/helpers.ts +83 -83
  43. package/tests/parser.test.ts +218 -218
  44. package/tsconfig.json +15 -15
@@ -1,221 +1,221 @@
1
- import * as path from 'node:path'
2
- import { createHash } from 'node:crypto'
3
- import type { MikkContract, MikkLock } from './schema.js'
4
- import type { DependencyGraph } from '../graph/types.js'
5
- import type { ParsedFile } from '../parser/types.js'
6
- import { hashContent } from '../hash/file-hasher.js'
7
- import { computeModuleHash, computeRootHash } from '../hash/tree-hasher.js'
8
- import { minimatch } from '../utils/minimatch.js'
9
-
10
- const VERSION = 'mikk-cli@1.0.0'
11
-
12
- /**
13
- * LockCompiler — takes a DependencyGraph and a MikkContract
14
- * and compiles the complete mikk.lock.json.
15
- */
16
- export class LockCompiler {
17
- /** Main entry — compile full lock from graph + contract + parsed files */
18
- compile(
19
- graph: DependencyGraph,
20
- contract: MikkContract,
21
- parsedFiles: ParsedFile[]
22
- ): MikkLock {
23
- const functions = this.compileFunctions(graph, contract)
24
- const classes = this.compileClasses(graph, contract)
25
- const generics = this.compileGenerics(graph, contract)
26
- const modules = this.compileModules(contract, parsedFiles)
27
- const files = this.compileFiles(parsedFiles, contract)
28
-
29
- const moduleHashes: Record<string, string> = {}
30
- for (const [id, mod] of Object.entries(modules)) {
31
- moduleHashes[id] = mod.hash
32
- }
33
-
34
- const lockData: MikkLock = {
35
- version: '1.0.0',
36
- generatedAt: new Date().toISOString(),
37
- generatorVersion: VERSION,
38
- projectRoot: contract.project.name,
39
- syncState: {
40
- status: 'clean',
41
- lastSyncAt: new Date().toISOString(),
42
- lockHash: '',
43
- contractHash: hashContent(JSON.stringify(contract)),
44
- },
45
- modules,
46
- functions,
47
- classes: Object.keys(classes).length > 0 ? classes : undefined,
48
- generics: Object.keys(generics).length > 0 ? generics : undefined,
49
- files,
50
- graph: {
51
- nodes: graph.nodes.size,
52
- edges: graph.edges.length,
53
- rootHash: computeRootHash(moduleHashes),
54
- },
55
- }
56
-
57
- // Compute overall lock hash from the compiled data
58
- lockData.syncState.lockHash = hashContent(JSON.stringify({
59
- functions: lockData.functions,
60
- classes: lockData.classes,
61
- generics: lockData.generics,
62
- modules: lockData.modules,
63
- files: lockData.files,
64
- }))
65
-
66
- return lockData
67
- }
68
-
69
- /** Compile function entries, assigning each to its module */
70
- private compileFunctions(
71
- graph: DependencyGraph,
72
- contract: MikkContract
73
- ): Record<string, MikkLock['functions'][string]> {
74
- const result: Record<string, MikkLock['functions'][string]> = {}
75
-
76
- for (const [id, node] of graph.nodes) {
77
- if (node.type !== 'function') continue
78
-
79
- const moduleId = this.findModule(node.file, contract.declared.modules)
80
- const inEdges = graph.inEdges.get(id) || []
81
- const outEdges = graph.outEdges.get(id) || []
82
-
83
- result[id] = {
84
- id,
85
- name: node.label,
86
- file: node.file,
87
- startLine: node.metadata.startLine ?? 0,
88
- endLine: node.metadata.endLine ?? 0,
89
- hash: node.metadata.hash ?? '',
90
- calls: outEdges.filter(e => e.type === 'calls').map(e => e.target),
91
- calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.source),
92
- moduleId: moduleId || 'unknown',
93
- purpose: node.metadata.purpose,
94
- edgeCasesHandled: node.metadata.edgeCasesHandled,
95
- errorHandling: node.metadata.errorHandling,
96
- detailedLines: node.metadata.detailedLines,
97
- }
98
- }
99
-
100
- return result
101
- }
102
-
103
- private compileClasses(
104
- graph: DependencyGraph,
105
- contract: MikkContract
106
- ): Record<string, any> {
107
- const result: Record<string, any> = {}
108
- for (const [id, node] of graph.nodes) {
109
- if (node.type !== 'class') continue
110
- const moduleId = this.findModule(node.file, contract.declared.modules)
111
- result[id] = {
112
- id,
113
- name: node.label,
114
- file: node.file,
115
- startLine: node.metadata.startLine ?? 0,
116
- endLine: node.metadata.endLine ?? 0,
117
- moduleId: moduleId || 'unknown',
118
- isExported: node.metadata.isExported ?? false,
119
- purpose: node.metadata.purpose,
120
- edgeCasesHandled: node.metadata.edgeCasesHandled,
121
- errorHandling: node.metadata.errorHandling,
122
- }
123
- }
124
- return result
125
- }
126
-
127
- private compileGenerics(
128
- graph: DependencyGraph,
129
- contract: MikkContract
130
- ): Record<string, any> {
131
- const result: Record<string, any> = {}
132
- for (const [id, node] of graph.nodes) {
133
- if (node.type !== 'generic') continue
134
- const moduleId = this.findModule(node.file, contract.declared.modules)
135
- result[id] = {
136
- id,
137
- name: node.label,
138
- type: node.metadata.hash ?? 'generic', // we stored type name in hash
139
- file: node.file,
140
- startLine: node.metadata.startLine ?? 0,
141
- endLine: node.metadata.endLine ?? 0,
142
- moduleId: moduleId || 'unknown',
143
- isExported: node.metadata.isExported ?? false,
144
- purpose: node.metadata.purpose,
145
- }
146
- }
147
- return result
148
- }
149
-
150
- /** Compile module entries from contract definitions */
151
- private compileModules(
152
- contract: MikkContract,
153
- parsedFiles: ParsedFile[]
154
- ): Record<string, MikkLock['modules'][string]> {
155
- const result: Record<string, MikkLock['modules'][string]> = {}
156
-
157
- for (const module of contract.declared.modules) {
158
- const moduleFiles = parsedFiles
159
- .filter(f => this.fileMatchesModule(f.path, module.paths))
160
- .map(f => f.path)
161
-
162
- const fileHashes = moduleFiles.map(f => {
163
- const parsed = parsedFiles.find(pf => pf.path === f)
164
- return parsed?.hash ?? ''
165
- })
166
-
167
- result[module.id] = {
168
- id: module.id,
169
- files: moduleFiles,
170
- hash: computeModuleHash(fileHashes),
171
- fragmentPath: `.mikk/fragments/${module.id}.lock`,
172
- }
173
- }
174
-
175
- return result
176
- }
177
-
178
- /** Compile file entries */
179
- private compileFiles(
180
- parsedFiles: ParsedFile[],
181
- contract: MikkContract
182
- ): Record<string, MikkLock['files'][string]> {
183
- const result: Record<string, MikkLock['files'][string]> = {}
184
-
185
- for (const file of parsedFiles) {
186
- const moduleId = this.findModule(file.path, contract.declared.modules)
187
- result[file.path] = {
188
- path: file.path,
189
- hash: file.hash,
190
- moduleId: moduleId || 'unknown',
191
- lastModified: new Date().toISOString(),
192
- }
193
- }
194
-
195
- return result
196
- }
197
-
198
- /** Find which module a file belongs to based on path patterns */
199
- private findModule(
200
- filePath: string,
201
- modules: MikkContract['declared']['modules']
202
- ): string | null {
203
- for (const module of modules) {
204
- if (this.fileMatchesModule(filePath, module.paths)) {
205
- return module.id
206
- }
207
- }
208
- return null
209
- }
210
-
211
- /** Check if a file path matches any of the module's path patterns */
212
- private fileMatchesModule(filePath: string, patterns: string[]): boolean {
213
- const normalized = filePath.replace(/\\/g, '/')
214
- for (const pattern of patterns) {
215
- if (minimatch(normalized, pattern)) {
216
- return true
217
- }
218
- }
219
- return false
220
- }
221
- }
1
+ import * as path from 'node:path'
2
+ import { createHash } from 'node:crypto'
3
+ import type { MikkContract, MikkLock } from './schema.js'
4
+ import type { DependencyGraph } from '../graph/types.js'
5
+ import type { ParsedFile } from '../parser/types.js'
6
+ import { hashContent } from '../hash/file-hasher.js'
7
+ import { computeModuleHash, computeRootHash } from '../hash/tree-hasher.js'
8
+ import { minimatch } from '../utils/minimatch.js'
9
+
10
+ const VERSION = '@getmikk/cli@1.2.1'
11
+
12
+ /**
13
+ * LockCompiler — takes a DependencyGraph and a MikkContract
14
+ * and compiles the complete mikk.lock.json.
15
+ */
16
+ export class LockCompiler {
17
+ /** Main entry — compile full lock from graph + contract + parsed files */
18
+ compile(
19
+ graph: DependencyGraph,
20
+ contract: MikkContract,
21
+ parsedFiles: ParsedFile[]
22
+ ): MikkLock {
23
+ const functions = this.compileFunctions(graph, contract)
24
+ const classes = this.compileClasses(graph, contract)
25
+ const generics = this.compileGenerics(graph, contract)
26
+ const modules = this.compileModules(contract, parsedFiles)
27
+ const files = this.compileFiles(parsedFiles, contract)
28
+
29
+ const moduleHashes: Record<string, string> = {}
30
+ for (const [id, mod] of Object.entries(modules)) {
31
+ moduleHashes[id] = mod.hash
32
+ }
33
+
34
+ const lockData: MikkLock = {
35
+ version: '1.0.0',
36
+ generatedAt: new Date().toISOString(),
37
+ generatorVersion: VERSION,
38
+ projectRoot: contract.project.name,
39
+ syncState: {
40
+ status: 'clean',
41
+ lastSyncAt: new Date().toISOString(),
42
+ lockHash: '',
43
+ contractHash: hashContent(JSON.stringify(contract)),
44
+ },
45
+ modules,
46
+ functions,
47
+ classes: Object.keys(classes).length > 0 ? classes : undefined,
48
+ generics: Object.keys(generics).length > 0 ? generics : undefined,
49
+ files,
50
+ graph: {
51
+ nodes: graph.nodes.size,
52
+ edges: graph.edges.length,
53
+ rootHash: computeRootHash(moduleHashes),
54
+ },
55
+ }
56
+
57
+ // Compute overall lock hash from the compiled data
58
+ lockData.syncState.lockHash = hashContent(JSON.stringify({
59
+ functions: lockData.functions,
60
+ classes: lockData.classes,
61
+ generics: lockData.generics,
62
+ modules: lockData.modules,
63
+ files: lockData.files,
64
+ }))
65
+
66
+ return lockData
67
+ }
68
+
69
+ /** Compile function entries, assigning each to its module */
70
+ private compileFunctions(
71
+ graph: DependencyGraph,
72
+ contract: MikkContract
73
+ ): Record<string, MikkLock['functions'][string]> {
74
+ const result: Record<string, MikkLock['functions'][string]> = {}
75
+
76
+ for (const [id, node] of graph.nodes) {
77
+ if (node.type !== 'function') continue
78
+
79
+ const moduleId = this.findModule(node.file, contract.declared.modules)
80
+ const inEdges = graph.inEdges.get(id) || []
81
+ const outEdges = graph.outEdges.get(id) || []
82
+
83
+ result[id] = {
84
+ id,
85
+ name: node.label,
86
+ file: node.file,
87
+ startLine: node.metadata.startLine ?? 0,
88
+ endLine: node.metadata.endLine ?? 0,
89
+ hash: node.metadata.hash ?? '',
90
+ calls: outEdges.filter(e => e.type === 'calls').map(e => e.target),
91
+ calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.source),
92
+ moduleId: moduleId || 'unknown',
93
+ purpose: node.metadata.purpose,
94
+ edgeCasesHandled: node.metadata.edgeCasesHandled,
95
+ errorHandling: node.metadata.errorHandling,
96
+ detailedLines: node.metadata.detailedLines,
97
+ }
98
+ }
99
+
100
+ return result
101
+ }
102
+
103
+ private compileClasses(
104
+ graph: DependencyGraph,
105
+ contract: MikkContract
106
+ ): Record<string, any> {
107
+ const result: Record<string, any> = {}
108
+ for (const [id, node] of graph.nodes) {
109
+ if (node.type !== 'class') continue
110
+ const moduleId = this.findModule(node.file, contract.declared.modules)
111
+ result[id] = {
112
+ id,
113
+ name: node.label,
114
+ file: node.file,
115
+ startLine: node.metadata.startLine ?? 0,
116
+ endLine: node.metadata.endLine ?? 0,
117
+ moduleId: moduleId || 'unknown',
118
+ isExported: node.metadata.isExported ?? false,
119
+ purpose: node.metadata.purpose,
120
+ edgeCasesHandled: node.metadata.edgeCasesHandled,
121
+ errorHandling: node.metadata.errorHandling,
122
+ }
123
+ }
124
+ return result
125
+ }
126
+
127
+ private compileGenerics(
128
+ graph: DependencyGraph,
129
+ contract: MikkContract
130
+ ): Record<string, any> {
131
+ const result: Record<string, any> = {}
132
+ for (const [id, node] of graph.nodes) {
133
+ if (node.type !== 'generic') continue
134
+ const moduleId = this.findModule(node.file, contract.declared.modules)
135
+ result[id] = {
136
+ id,
137
+ name: node.label,
138
+ type: node.metadata.hash ?? 'generic', // we stored type name in hash
139
+ file: node.file,
140
+ startLine: node.metadata.startLine ?? 0,
141
+ endLine: node.metadata.endLine ?? 0,
142
+ moduleId: moduleId || 'unknown',
143
+ isExported: node.metadata.isExported ?? false,
144
+ purpose: node.metadata.purpose,
145
+ }
146
+ }
147
+ return result
148
+ }
149
+
150
+ /** Compile module entries from contract definitions */
151
+ private compileModules(
152
+ contract: MikkContract,
153
+ parsedFiles: ParsedFile[]
154
+ ): Record<string, MikkLock['modules'][string]> {
155
+ const result: Record<string, MikkLock['modules'][string]> = {}
156
+
157
+ for (const module of contract.declared.modules) {
158
+ const moduleFiles = parsedFiles
159
+ .filter(f => this.fileMatchesModule(f.path, module.paths))
160
+ .map(f => f.path)
161
+
162
+ const fileHashes = moduleFiles.map(f => {
163
+ const parsed = parsedFiles.find(pf => pf.path === f)
164
+ return parsed?.hash ?? ''
165
+ })
166
+
167
+ result[module.id] = {
168
+ id: module.id,
169
+ files: moduleFiles,
170
+ hash: computeModuleHash(fileHashes),
171
+ fragmentPath: `.mikk/fragments/${module.id}.lock`,
172
+ }
173
+ }
174
+
175
+ return result
176
+ }
177
+
178
+ /** Compile file entries */
179
+ private compileFiles(
180
+ parsedFiles: ParsedFile[],
181
+ contract: MikkContract
182
+ ): Record<string, MikkLock['files'][string]> {
183
+ const result: Record<string, MikkLock['files'][string]> = {}
184
+
185
+ for (const file of parsedFiles) {
186
+ const moduleId = this.findModule(file.path, contract.declared.modules)
187
+ result[file.path] = {
188
+ path: file.path,
189
+ hash: file.hash,
190
+ moduleId: moduleId || 'unknown',
191
+ lastModified: new Date().toISOString(),
192
+ }
193
+ }
194
+
195
+ return result
196
+ }
197
+
198
+ /** Find which module a file belongs to based on path patterns */
199
+ private findModule(
200
+ filePath: string,
201
+ modules: MikkContract['declared']['modules']
202
+ ): string | null {
203
+ for (const module of modules) {
204
+ if (this.fileMatchesModule(filePath, module.paths)) {
205
+ return module.id
206
+ }
207
+ }
208
+ return null
209
+ }
210
+
211
+ /** Check if a file path matches any of the module's path patterns */
212
+ private fileMatchesModule(filePath: string, patterns: string[]): boolean {
213
+ const normalized = filePath.replace(/\\/g, '/')
214
+ for (const pattern of patterns) {
215
+ if (minimatch(normalized, pattern)) {
216
+ return true
217
+ }
218
+ }
219
+ return false
220
+ }
221
+ }
@@ -1,34 +1,34 @@
1
- import * as fs from 'node:fs/promises'
2
- import { MikkLockSchema, type MikkLock } from './schema.js'
3
- import { LockNotFoundError } from '../utils/errors.js'
4
-
5
- /**
6
- * LockReader — reads and validates mikk.lock.json from disk.
7
- */
8
- export class LockReader {
9
- /** Read and validate mikk.lock.json */
10
- async read(lockPath: string): Promise<MikkLock> {
11
- let content: string
12
- try {
13
- content = await fs.readFile(lockPath, 'utf-8')
14
- } catch {
15
- throw new LockNotFoundError()
16
- }
17
-
18
- const json = JSON.parse(content)
19
- const result = MikkLockSchema.safeParse(json)
20
-
21
- if (!result.success) {
22
- const errors = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n')
23
- throw new Error(`Invalid mikk.lock.json:\n${errors}`)
24
- }
25
-
26
- return result.data
27
- }
28
-
29
- /** Write lock file to disk */
30
- async write(lock: MikkLock, lockPath: string): Promise<void> {
31
- const json = JSON.stringify(lock, null, 2)
32
- await fs.writeFile(lockPath, json, 'utf-8')
33
- }
34
- }
1
+ import * as fs from 'node:fs/promises'
2
+ import { MikkLockSchema, type MikkLock } from './schema.js'
3
+ import { LockNotFoundError } from '../utils/errors.js'
4
+
5
+ /**
6
+ * LockReader — reads and validates mikk.lock.json from disk.
7
+ */
8
+ export class LockReader {
9
+ /** Read and validate mikk.lock.json */
10
+ async read(lockPath: string): Promise<MikkLock> {
11
+ let content: string
12
+ try {
13
+ content = await fs.readFile(lockPath, 'utf-8')
14
+ } catch {
15
+ throw new LockNotFoundError()
16
+ }
17
+
18
+ const json = JSON.parse(content)
19
+ const result = MikkLockSchema.safeParse(json)
20
+
21
+ if (!result.success) {
22
+ const errors = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n')
23
+ throw new Error(`Invalid mikk.lock.json:\n${errors}`)
24
+ }
25
+
26
+ return result.data
27
+ }
28
+
29
+ /** Write lock file to disk */
30
+ async write(lock: MikkLock, lockPath: string): Promise<void> {
31
+ const json = JSON.stringify(lock, null, 2)
32
+ await fs.writeFile(lockPath, json, 'utf-8')
33
+ }
34
+ }