@getmikk/core 1.7.1 → 1.8.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.
@@ -3,7 +3,7 @@ import { MikkLockSchema, type MikkLock } from './schema.js'
3
3
  import { LockNotFoundError } from '../utils/errors.js'
4
4
 
5
5
  /**
6
- * LockReader reads and validates mikk.lock.json from disk.
6
+ * LockReader -- reads and validates mikk.lock.json from disk.
7
7
  * Uses compact format on disk: default values are omitted to save space.
8
8
  * Hydrates omitted fields before validation; compactifies before writing.
9
9
  */
@@ -17,7 +17,7 @@ export class LockReader {
17
17
  throw new LockNotFoundError()
18
18
  }
19
19
 
20
- const json = JSON.parse(content)
20
+ const json = JSON.parse(content.replace(/^\uFEFF/, ''))
21
21
  const hydrated = hydrateLock(json)
22
22
  const result = MikkLockSchema.safeParse(hydrated)
23
23
 
@@ -38,11 +38,11 @@ export class LockReader {
38
38
  }
39
39
 
40
40
  // ---------------------------------------------------------------------------
41
- // Compact format omit-defaults serialization
41
+ // Compact format -- omit-defaults serialization
42
42
  // ---------------------------------------------------------------------------
43
43
  // Rules:
44
44
  // 1. Never write a field whose value equals its default ([], "", undefined, "unknown")
45
- // 2. id/name/file/path are derivable from the record keyomit them
45
+ // 2. id/name/file/path are derivable from the record key omit them
46
46
  // 3. Line ranges become tuples: [startLine, endLine]
47
47
  // 4. errorHandling becomes tuples: [line, type, detail]
48
48
  // 5. detailedLines becomes tuples: [startLine, endLine, blockType]
@@ -65,7 +65,7 @@ function compactifyLock(lock: MikkLock): any {
65
65
  fnKeys.forEach((k, i) => fnIndexMap.set(k, i))
66
66
  out.fnIndex = fnKeys
67
67
 
68
- // Functions biggest savings
68
+ // Functions -- biggest savings
69
69
  out.functions = {}
70
70
  for (let idx = 0; idx < fnKeys.length; idx++) {
71
71
  const fn = lock.functions[fnKeys[idx]]
@@ -123,10 +123,10 @@ function compactifyLock(lock: MikkLock): any {
123
123
  }
124
124
  }
125
125
 
126
- // Modules keep as-is (already small)
126
+ // Modules -- keep as-is (already small)
127
127
  out.modules = lock.modules
128
128
 
129
- // Files strip redundant path (it's the key)
129
+ // Files -- strip redundant path (it's the key)
130
130
  out.files = {}
131
131
  for (const [key, file] of Object.entries(lock.files)) {
132
132
  const c: any = {
@@ -138,12 +138,12 @@ function compactifyLock(lock: MikkLock): any {
138
138
  out.files[key] = c
139
139
  }
140
140
 
141
- // Context files paths/type only, no content
141
+ // Context files -- paths/type only, no content
142
142
  if (lock.contextFiles && lock.contextFiles.length > 0) {
143
143
  out.contextFiles = lock.contextFiles.map(({ path, type, size }) => ({ path, type, size }))
144
144
  }
145
145
 
146
- // Routes keep as-is (already compact)
146
+ // Routes -- keep as-is (already compact)
147
147
  if (lock.routes && lock.routes.length > 0) {
148
148
  out.routes = lock.routes
149
149
  }
@@ -158,7 +158,7 @@ function hydrateLock(raw: any): any {
158
158
  // If it already has the old format (functions have id/name/file), pass through
159
159
  const firstFn = Object.values(raw.functions || {})[0] as any
160
160
  if (firstFn && typeof firstFn === 'object' && 'id' in firstFn && 'name' in firstFn && 'file' in firstFn) {
161
- return raw // Already in full format no hydration needed
161
+ return raw // Already in full format -- no hydration needed
162
162
  }
163
163
 
164
164
  const out: any = {
@@ -174,7 +174,7 @@ function hydrateLock(raw: any): any {
174
174
  const fnIndex: string[] = raw.fnIndex || []
175
175
  const hasFnIndex = fnIndex.length > 0
176
176
 
177
- // P6: build filemoduleId map before function loop
177
+ // P6: build file->moduleId map before function loop
178
178
  const fileModuleMap: Record<string, string> = {}
179
179
  for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
180
180
  fileModuleMap[key] = c.moduleId || 'unknown'
@@ -183,11 +183,11 @@ function hydrateLock(raw: any): any {
183
183
  // Hydrate functions
184
184
  out.functions = {}
185
185
  for (const [key, c] of Object.entries(raw.functions || {}) as [string, any][]) {
186
- // P7: key is integer index look up full ID via fnIndex
186
+ // P7: key is integer index -> look up full ID via fnIndex
187
187
  const fullId = hasFnIndex ? (fnIndex[parseInt(key)] || key) : key
188
188
  const { name, file } = parseEntityKey(fullId, 'fn:')
189
189
  const lines = c.lines || [c.startLine || 0, c.endLine || 0]
190
- // P7: integer calls/calledBy resolve to full string IDs (backward compat: strings pass through)
190
+ // P7: integer calls/calledBy -> resolve to full string IDs (backward compat: strings pass through)
191
191
  const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
192
192
  const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
193
193
 
@@ -277,7 +277,7 @@ function hydrateLock(raw: any): any {
277
277
  }
278
278
  }
279
279
 
280
- // Modules already in full format
280
+ // Modules -- already in full format
281
281
  out.modules = raw.modules
282
282
 
283
283
  // Pass through
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
 
3
- // ─── mikk.json schema ──────────────────────────────────────
3
+ // ─── mikk.json schema ───────────────────────────────────
4
4
 
5
5
  export const MikkModuleSchema = z.object({
6
6
  id: z.string(),
@@ -33,7 +33,7 @@ export const MikkContractSchema = z.object({
33
33
  description: z.string(),
34
34
  language: z.string(),
35
35
  framework: z.string().optional(),
36
- entryPoints: z.array(z.string()),
36
+ entryPoints: z.array(z.string()).default([]),
37
37
  }),
38
38
  declared: z.object({
39
39
  modules: z.array(MikkModuleSchema),
@@ -47,7 +47,7 @@ export type MikkContract = z.infer<typeof MikkContractSchema>
47
47
  export type MikkModule = z.infer<typeof MikkModuleSchema>
48
48
  export type MikkDecision = z.infer<typeof MikkDecisionSchema>
49
49
 
50
- // ─── mikk.lock.json schema ─────────────────────────────────
50
+ // ─── mikk.lock.json schema ──────────────────────────────
51
51
 
52
52
  export const MikkLockFunctionSchema = z.object({
53
53
  id: z.string(),
package/src/index.ts CHANGED
@@ -5,9 +5,10 @@ export * from './parser/index.js'
5
5
  export * from './graph/index.js'
6
6
  export * from './contract/index.js'
7
7
  export * from './hash/index.js'
8
+ export * from './search/index.js'
8
9
  export * from './utils/errors.js'
9
10
  export * from './utils/logger.js'
10
- export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore } from './utils/fs.js'
11
+ export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore, updateGitIgnore, cleanupGitIgnore } from './utils/fs.js'
11
12
  export type { ContextFile, ContextFileType, ProjectLanguage } from './utils/fs.js'
12
13
  export { minimatch } from './utils/minimatch.js'
13
14
  export { scoreFunctions, findFuzzyMatches, levenshtein, splitCamelCase, extractKeywords } from './utils/fuzzy-match.js'
@@ -6,7 +6,7 @@ import type { ParsedFile, ParsedImport } from './types.js'
6
6
  */
7
7
  export abstract class BaseParser {
8
8
  /** Given raw file content as a string, return ParsedFile */
9
- abstract parse(filePath: string, content: string): ParsedFile
9
+ abstract parse(filePath: string, content: string): Promise<ParsedFile>
10
10
 
11
11
  /** Given a list of parsed files, resolve all import paths to absolute project paths */
12
12
  abstract resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[]
@@ -1,29 +1,11 @@
1
1
  import * as path from 'node:path'
2
- import type { MikkContract, MikkLock, MikkLockFunction } from '@getmikk/core'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Types
6
- // ---------------------------------------------------------------------------
2
+ import type { MikkContract, MikkLock, MikkLockFunction } from '../contract/schema.js'
7
3
 
8
4
  export type ViolationSeverity = 'error' | 'warning'
9
5
 
10
6
  export interface BoundaryViolation {
11
- /** The function making the illegal import */
12
- from: {
13
- functionId: string
14
- functionName: string
15
- file: string
16
- moduleId: string
17
- moduleName: string
18
- }
19
- /** The function being illegally called */
20
- to: {
21
- functionId: string
22
- functionName: string
23
- file: string
24
- moduleId: string
25
- moduleName: string
26
- }
7
+ from: { functionId: string; functionName: string; file: string; moduleId: string; moduleName: string }
8
+ to: { functionId: string; functionName: string; file: string; moduleId: string; moduleName: string }
27
9
  rule: string
28
10
  severity: ViolationSeverity
29
11
  }
@@ -34,251 +16,131 @@ export interface BoundaryCheckResult {
34
16
  summary: string
35
17
  }
36
18
 
37
- // ---------------------------------------------------------------------------
38
- // Rule parsing
39
- // ---------------------------------------------------------------------------
40
-
41
- /**
42
- * Parse constraint strings into structured allow/deny rules.
43
- *
44
- * Supported syntax in mikk.json constraints:
45
- * "module:auth cannot import module:payments"
46
- * "module:cli cannot import module:db"
47
- * "module:core has no imports" → core is completely isolated
48
- * "module:api can only import module:core, module:utils"
49
- */
50
19
  interface ParsedRule {
51
20
  type: 'deny' | 'allow_only' | 'isolated'
52
21
  fromModuleId: string
53
- toModuleIds: string[] // For deny: what's forbidden. For allow_only: what's allowed.
22
+ toModuleIds: string[]
54
23
  raw: string
55
24
  }
56
25
 
57
- function parseConstraint(constraint: string): ParsedRule | null {
58
- const c = constraint.trim().toLowerCase()
59
-
60
- // "module:X cannot import module:Y" or "module:X cannot import module:Y, module:Z"
61
- const denyMatch = c.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
62
- if (denyMatch) {
63
- const toModules = denyMatch[2]
64
- .split(',')
65
- .map(s => s.trim().replace('module:', ''))
66
- .filter(Boolean)
67
- return { type: 'deny', fromModuleId: denyMatch[1], toModuleIds: toModules, raw: constraint }
68
- }
69
-
70
- // "module:X can only import module:A, module:B"
71
- const allowOnlyMatch = c.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
72
- if (allowOnlyMatch) {
73
- const toModules = allowOnlyMatch[2]
74
- .split(',')
75
- .map(s => s.trim().replace('module:', ''))
76
- .filter(Boolean)
77
- return { type: 'allow_only', fromModuleId: allowOnlyMatch[1], toModuleIds: toModules, raw: constraint }
78
- }
79
-
80
- // "module:X has no imports" or "module:X is isolated"
81
- const isolatedMatch = c.match(/^module:(\S+)\s+(has\s+no\s+imports|is\s+isolated)$/)
82
- if (isolatedMatch) {
83
- return { type: 'isolated', fromModuleId: isolatedMatch[1], toModuleIds: [], raw: constraint }
84
- }
26
+ function stripPrefix(s: string): string {
27
+ return s.trim().replace(/^module:/, '')
28
+ }
85
29
 
86
- return null // unrecognized constraint — skip silently
30
+ function parseList(raw: string): string[] {
31
+ return raw.split(/,\s*/).map(stripPrefix).filter(Boolean)
87
32
  }
88
33
 
89
- // ---------------------------------------------------------------------------
90
- // BoundaryChecker
91
- // ---------------------------------------------------------------------------
34
+ function parseConstraint(constraint: string): ParsedRule | null {
35
+ const c = constraint.trim()
36
+ const l = c.toLowerCase()
37
+ const natDeny = l.match(/^(\S+)\s+(?:must\s+not|cannot|should\s+not)\s+(?:import\s+from|import|call\s+into|call)\s+(.+)$/)
38
+ if (natDeny) return { type: 'deny', fromModuleId: stripPrefix(natDeny[1]), toModuleIds: parseList(natDeny[2]), raw: c }
39
+ const natAllow = l.match(/^(\S+)\s+can\s+only\s+(?:import\s+from|import)\s+(.+)$/)
40
+ if (natAllow) return { type: 'allow_only', fromModuleId: stripPrefix(natAllow[1]), toModuleIds: parseList(natAllow[2]), raw: c }
41
+ const natIso = l.match(/^(\S+)\s+(?:is\s+isolated|has\s+no\s+imports)$/)
42
+ if (natIso) return { type: 'isolated', fromModuleId: stripPrefix(natIso[1]), toModuleIds: [], raw: c }
43
+ const legDeny = l.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
44
+ if (legDeny) return { type: 'deny', fromModuleId: legDeny[1], toModuleIds: parseList(legDeny[2]), raw: c }
45
+ const legAllow = l.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
46
+ if (legAllow) return { type: 'allow_only', fromModuleId: legAllow[1], toModuleIds: parseList(legAllow[2]), raw: c }
47
+ const legIso = l.match(/^module:(\S+)\s+(?:has\s+no\s+imports|is\s+isolated)$/)
48
+ if (legIso) return { type: 'isolated', fromModuleId: legIso[1], toModuleIds: [], raw: c }
49
+ console.warn(`[mikk] Constraint skipped: "${c}" - use "auth must not import from payments"`)
50
+ return null
51
+ }
92
52
 
93
- /**
94
- * BoundaryChecker — walks the lock file's call graph and checks every
95
- * cross-module call against the rules declared in mikk.json constraints.
96
- *
97
- * This is the CI-ready enforcement layer.
98
- */
99
53
  export class BoundaryChecker {
100
54
  private rules: ParsedRule[]
101
- private moduleNames: Map<string, string> // id → name
102
-
103
- constructor(
104
- private contract: MikkContract,
105
- private lock: MikkLock
106
- ) {
107
- this.rules = contract.declared.constraints
108
- .map(parseConstraint)
109
- .filter((r): r is ParsedRule => r !== null)
55
+ private moduleNames: Map<string, string>
110
56
 
57
+ constructor(private contract: MikkContract, private lock: MikkLock) {
58
+ this.rules = contract.declared.constraints.map(parseConstraint).filter((r): r is ParsedRule => r !== null)
111
59
  this.moduleNames = new Map(contract.declared.modules.map(m => [m.id, m.name]))
112
60
  }
113
61
 
114
- /** Run boundary check. Returns pass/fail + all violations. */
115
62
  check(): BoundaryCheckResult {
116
63
  const violations: BoundaryViolation[] = []
117
64
 
118
- // Pass 1: Check cross-module function calls
119
65
  for (const fn of Object.values(this.lock.functions)) {
66
+ if (fn.moduleId === 'unknown') continue
120
67
  for (const calleeId of fn.calls) {
121
68
  const callee = this.lock.functions[calleeId]
122
- if (!callee) continue
123
- if (fn.moduleId === callee.moduleId) continue // same module — fine
124
-
125
- // Check this cross-module call against all parsed rules
126
- const violation = this.checkCall(fn, callee)
127
- if (violation) violations.push(violation)
69
+ if (!callee || callee.moduleId === 'unknown' || fn.moduleId === callee.moduleId) continue
70
+ const v = this.checkCall(fn, callee)
71
+ if (v) violations.push(v)
128
72
  }
129
73
  }
130
74
 
131
- // Pass 2: Check cross-module file-level imports
132
75
  for (const file of Object.values(this.lock.files)) {
133
- if (!file.imports || file.imports.length === 0) continue
76
+ if (file.moduleId === 'unknown' || !file.imports?.length) continue
134
77
  for (const importedPath of file.imports) {
135
78
  const importedFile = this.lock.files[importedPath]
136
- if (!importedFile) continue
137
- if (file.moduleId === importedFile.moduleId) continue // same module — fine
138
-
139
- const violation = this.checkFileImport(file, importedFile)
140
- if (violation) violations.push(violation)
79
+ if (!importedFile || importedFile.moduleId === 'unknown' || file.moduleId === importedFile.moduleId) continue
80
+ const v = this.checkFileImport(file, importedFile)
81
+ if (v) violations.push(v)
141
82
  }
142
83
  }
143
84
 
144
- const errorCount = violations.filter(v => v.severity === 'error').length
145
- const warnCount = violations.filter(v => v.severity === 'warning').length
146
-
147
- const summary = violations.length === 0
148
- ? `✓ All module boundaries respected (${Object.keys(this.lock.functions).length} functions, ${Object.keys(this.lock.files).length} files checked)`
149
- : `✗ ${errorCount} boundary error(s), ${warnCount} warning(s) found`
150
-
151
- return {
152
- pass: errorCount === 0,
153
- violations,
154
- summary,
155
- }
85
+ const seen = new Set<string>()
86
+ const unique = violations.filter(v => {
87
+ const key = `${v.from.functionId}|${v.to.functionId}|${v.rule}`
88
+ if (seen.has(key)) return false
89
+ seen.add(key)
90
+ return true
91
+ })
92
+
93
+ const fnCount = Object.keys(this.lock.functions).length
94
+ const fileCount = Object.keys(this.lock.files).length
95
+ const errorCount = unique.filter(v => v.severity === 'error').length
96
+ const warnCount = unique.filter(v => v.severity === 'warning').length
97
+ const summary = unique.length === 0
98
+ ? `All module boundaries respected (${fnCount} functions, ${fileCount} files checked)`
99
+ : `${errorCount} boundary error(s), ${warnCount} warning(s) found`
100
+
101
+ return { pass: errorCount === 0, violations: unique, summary }
156
102
  }
157
103
 
158
- /**
159
- * Check a single cross-module call against parsed rules.
160
- * Returns a violation if the call is forbidden, null if it's allowed.
161
- */
162
- private checkCall(
163
- caller: MikkLockFunction,
164
- callee: MikkLockFunction
165
- ): BoundaryViolation | null {
104
+ private checkCall(caller: MikkLockFunction, callee: MikkLockFunction): BoundaryViolation | null {
166
105
  for (const rule of this.rules) {
167
106
  if (rule.fromModuleId !== caller.moduleId) continue
168
-
169
- let forbidden = false
170
- let ruleDesc = rule.raw
171
-
172
- if (rule.type === 'isolated') {
173
- // Module may not call anything outside itself
174
- forbidden = true
175
- } else if (rule.type === 'deny') {
176
- // Module may not call into these specific modules
177
- forbidden = rule.toModuleIds.includes(callee.moduleId)
178
- } else if (rule.type === 'allow_only') {
179
- // Module may ONLY call into the listed modules (+ itself)
180
- forbidden = !rule.toModuleIds.includes(callee.moduleId)
181
- }
182
-
183
- if (forbidden) {
184
- return {
185
- from: {
186
- functionId: caller.id,
187
- functionName: caller.name,
188
- file: caller.file,
189
- moduleId: caller.moduleId,
190
- moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId,
191
- },
192
- to: {
193
- functionId: callee.id,
194
- functionName: callee.name,
195
- file: callee.file,
196
- moduleId: callee.moduleId,
197
- moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId,
198
- },
199
- rule: ruleDesc,
200
- severity: 'error',
201
- }
107
+ const forbidden = rule.type === 'isolated' ? true
108
+ : rule.type === 'deny' ? rule.toModuleIds.includes(callee.moduleId)
109
+ : !rule.toModuleIds.includes(callee.moduleId)
110
+ if (forbidden) return {
111
+ from: { functionId: caller.id, functionName: caller.name, file: caller.file, moduleId: caller.moduleId, moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId },
112
+ to: { functionId: callee.id, functionName: callee.name, file: callee.file, moduleId: callee.moduleId, moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId },
113
+ rule: rule.raw, severity: 'error'
202
114
  }
203
115
  }
204
116
  return null
205
117
  }
206
118
 
207
- /**
208
- * Check a single cross-module file import against parsed rules.
209
- * Returns a violation if the import is forbidden, null if it's allowed.
210
- */
211
- private checkFileImport(
212
- sourceFile: { path: string; moduleId: string },
213
- targetFile: { path: string; moduleId: string }
214
- ): BoundaryViolation | null {
119
+ private checkFileImport(sourceFile: { path: string; moduleId: string }, targetFile: { path: string; moduleId: string }): BoundaryViolation | null {
215
120
  for (const rule of this.rules) {
216
121
  if (rule.fromModuleId !== sourceFile.moduleId) continue
217
-
218
- let forbidden = false
219
-
220
- if (rule.type === 'isolated') {
221
- forbidden = true
222
- } else if (rule.type === 'deny') {
223
- forbidden = rule.toModuleIds.includes(targetFile.moduleId)
224
- } else if (rule.type === 'allow_only') {
225
- forbidden = !rule.toModuleIds.includes(targetFile.moduleId)
226
- }
227
-
228
- if (forbidden) {
229
- return {
230
- from: {
231
- functionId: `file:${sourceFile.path}`,
232
- functionName: path.basename(sourceFile.path),
233
- file: sourceFile.path,
234
- moduleId: sourceFile.moduleId,
235
- moduleName: this.moduleNames.get(sourceFile.moduleId) ?? sourceFile.moduleId,
236
- },
237
- to: {
238
- functionId: `file:${targetFile.path}`,
239
- functionName: path.basename(targetFile.path),
240
- file: targetFile.path,
241
- moduleId: targetFile.moduleId,
242
- moduleName: this.moduleNames.get(targetFile.moduleId) ?? targetFile.moduleId,
243
- },
244
- rule: rule.raw,
245
- severity: 'error',
246
- }
122
+ const forbidden = rule.type === 'isolated' ? true
123
+ : rule.type === 'deny' ? rule.toModuleIds.includes(targetFile.moduleId)
124
+ : !rule.toModuleIds.includes(targetFile.moduleId)
125
+ if (forbidden) return {
126
+ from: { functionId: `file:${sourceFile.path}`, functionName: path.basename(sourceFile.path), file: sourceFile.path, moduleId: sourceFile.moduleId, moduleName: this.moduleNames.get(sourceFile.moduleId) ?? sourceFile.moduleId },
127
+ to: { functionId: `file:${targetFile.path}`, functionName: path.basename(targetFile.path), file: targetFile.path, moduleId: targetFile.moduleId, moduleName: this.moduleNames.get(targetFile.moduleId) ?? targetFile.moduleId },
128
+ rule: rule.raw, severity: 'error'
247
129
  }
248
130
  }
249
131
  return null
250
132
  }
251
133
 
252
- /** Return all cross-module call pairs (useful for generating allow rules) */
253
134
  allCrossModuleCalls(): { from: string; to: string; count: number }[] {
254
135
  const counts = new Map<string, number>()
255
-
256
- // Count function-level cross-module calls
257
136
  for (const fn of Object.values(this.lock.functions)) {
258
137
  for (const calleeId of fn.calls) {
259
138
  const callee = this.lock.functions[calleeId]
260
- if (!callee || fn.moduleId === callee.moduleId) continue
261
- const key = `${fn.moduleId}→${callee.moduleId}`
139
+ if (!callee || fn.moduleId === callee.moduleId || fn.moduleId === 'unknown' || callee.moduleId === 'unknown') continue
140
+ const key = `${fn.moduleId}>${callee.moduleId}`
262
141
  counts.set(key, (counts.get(key) ?? 0) + 1)
263
142
  }
264
143
  }
265
-
266
- // Count file-level cross-module imports
267
- for (const file of Object.values(this.lock.files)) {
268
- if (!file.imports) continue
269
- for (const importedPath of file.imports) {
270
- const importedFile = this.lock.files[importedPath]
271
- if (!importedFile || file.moduleId === importedFile.moduleId) continue
272
- const key = `${file.moduleId}→${importedFile.moduleId}`
273
- counts.set(key, (counts.get(key) ?? 0) + 1)
274
- }
275
- }
276
-
277
- return [...counts.entries()]
278
- .map(([key, count]) => {
279
- const [from, to] = key.split('→')
280
- return { from, to, count }
281
- })
282
- .sort((a, b) => b.count - a.count)
144
+ return [...counts.entries()].map(([key, count]) => { const [from, to] = key.split('>'); return { from, to, count } }).sort((a, b) => b.count - a.count)
283
145
  }
284
146
  }
@@ -4,7 +4,7 @@ import type {
4
4
  ParsedParam, ParsedGeneric, ParsedRoute,
5
5
  } from '../types.js'
6
6
 
7
- // ─── Go builtins / keywords to skip when extracting calls ───────────────────
7
+ // --- Go builtins / keywords to skip when extracting calls -------------------
8
8
  const GO_BUILTINS = new Set([
9
9
  'if', 'else', 'for', 'switch', 'select', 'case', 'default', 'break',
10
10
  'continue', 'goto', 'fallthrough', 'return', 'go', 'defer', 'range',
@@ -16,7 +16,7 @@ const GO_BUILTINS = new Set([
16
16
  'complex64', 'complex128', 'bool', 'byte', 'rune', 'error', 'any',
17
17
  ])
18
18
 
19
- // ─── Route detection patterns (Gin, Echo, Chi, Mux, net/http, Fiber) ────────
19
+ // --- Route detection patterns (Gin, Echo, Chi, Mux, net/http, Fiber) --------
20
20
  type RoutePattern = { re: RegExp; methodGroup: number; pathGroup: number; handlerGroup: number; fixedMethod?: string }
21
21
 
22
22
  const ROUTE_PATTERNS: RoutePattern[] = [
@@ -48,7 +48,7 @@ const ROUTE_PATTERNS: RoutePattern[] = [
48
48
  ]
49
49
 
50
50
  /**
51
- * GoExtractor pure regex + stateful line scanner for .go files.
51
+ * GoExtractor -- pure regex + stateful line scanner for .go files.
52
52
  * Extracts functions, structs (as classes), imports, exports, and HTTP routes
53
53
  * without any external Go AST dependency.
54
54
  */
@@ -62,7 +62,7 @@ export class GoExtractor {
62
62
  this.lines = content.split('\n')
63
63
  }
64
64
 
65
- // ── Public API ──────────────────────────────────────────────────────────
65
+ // --- Public API ---------------------------------------------------------
66
66
 
67
67
  /** Extract all top-level functions (no receiver) */
68
68
  extractFunctions(): ParsedFunction[] {
@@ -160,7 +160,7 @@ export class GoExtractor {
160
160
  return routes
161
161
  }
162
162
 
163
- // ── Internal scanning ───────────────────────────────────────────────────
163
+ // --- Internal scanning ---------------------------------------------------
164
164
 
165
165
  /** Scanned raw function data (before building ParsedFunction) */
166
166
  private scanFunctions(): Array<{
@@ -343,7 +343,7 @@ export class GoExtractor {
343
343
  }
344
344
  }
345
345
 
346
- // ─── Signature parsing ────────────────────────────────────────────────────────
346
+ // --- Signature parsing --------------------------------------------------------
347
347
 
348
348
  interface GoFuncSignature {
349
349
  name: string
@@ -481,7 +481,7 @@ function cleanReturnType(ret: string): string {
481
481
  return ret
482
482
  }
483
483
 
484
- // ─── Import line parsing ──────────────────────────────────────────────────────
484
+ // --- Import line parsing ------------------------------------------------------
485
485
 
486
486
  function parseImportLine(line: string): ParsedImport | null {
487
487
  const trimmed = line.trim()
@@ -509,7 +509,7 @@ function parseImportLine(line: string): ParsedImport | null {
509
509
  return null
510
510
  }
511
511
 
512
- // ─── Body analysis ────────────────────────────────────────────────────────────
512
+ // --- Body analysis ------------------------------------------------------------
513
513
 
514
514
  /**
515
515
  * Statefully track brace depth through content, handling:
@@ -661,7 +661,7 @@ function extractErrorHandling(bodyLines: string[], baseLineNumber: number): { li
661
661
  return errors
662
662
  }
663
663
 
664
- // ─── Comment extraction ───────────────────────────────────────────────────────
664
+ // --- Comment extraction -------------------------------------------------------
665
665
 
666
666
  function extractLeadingComment(lines: string[], funcLine: number): string {
667
667
  // Scan backwards from funcLine for consecutive comment lines
@@ -686,7 +686,7 @@ function extractLeadingComment(lines: string[], funcLine: number): string {
686
686
  return ''
687
687
  }
688
688
 
689
- // ─── Utility helpers ──────────────────────────────────────────────────────────
689
+ // --- Utility helpers ----------------------------------------------------------
690
690
 
691
691
  function isExported(name: string): boolean {
692
692
  return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()
@@ -5,12 +5,12 @@ import { hashContent } from '../../hash/file-hasher.js'
5
5
  import type { ParsedFile } from '../types.js'
6
6
 
7
7
  /**
8
- * GoParser implements BaseParser for .go files.
8
+ * GoParser -- implements BaseParser for .go files.
9
9
  * Uses GoExtractor (regex-based) to pull structured data from Go source
10
10
  * without requiring the Go toolchain.
11
11
  */
12
12
  export class GoParser extends BaseParser {
13
- parse(filePath: string, content: string): ParsedFile {
13
+ async parse(filePath: string, content: string): Promise<ParsedFile> {
14
14
  const extractor = new GoExtractor(filePath, content)
15
15
 
16
16
  return {