@getmikk/core 1.5.1 → 1.7.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/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # @getmikk/core
2
2
 
3
- > AST parsing, dependency graph construction, Merkle-tree hashing, contract management, and foundational utilities for the Mikk ecosystem.
3
+ > The foundation of the Mikk ecosystem — TypeScript AST parsing, dependency graph construction, Merkle-tree hashing, contract management, and lock file compilation.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@getmikk/core)](https://www.npmjs.com/package/@getmikk/core)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](../../LICENSE)
7
7
 
8
- `@getmikk/core` is the foundation package that every other Mikk package depends on. It provides the complete pipeline for understanding a TypeScript codebase: parsing source files into structured ASTs, building a full dependency graph, computing Merkle-tree hashes for drift detection, and managing the `mikk.json` contract and `mikk.lock.json` lock file.
8
+ `@getmikk/core` is the foundation every other Mikk package builds on. It owns the complete pipeline for turning raw **TypeScript and Go** source into structured, queryable intelligence: parsing source files into real ASTs (TS Compiler API for TypeScript; regex + stateful scanning for Go; no external toolchain required), building a two-pass dependency graph with O(1) adjacency lookups, computing Merkle-tree SHA-256 hashes at function → file → module → root level, and compiling everything into a `mikk.lock.json` snapshot that every other package reads from.
9
+
10
+ Every AI context query, impact analysis, contract validation, and diagram generation ultimately runs on the graph and lock file produced here.
11
+
12
+ > Part of [Mikk](../../README.md) — the codebase nervous system for AI-assisted development.
9
13
 
10
14
  ---
11
15
 
@@ -22,11 +26,11 @@ bun add @getmikk/core
22
26
  ## Architecture Overview
23
27
 
24
28
  ```
25
- Source Files (.ts/.tsx)
29
+ Source Files (.ts/.tsx/.go)
26
30
 
27
31
 
28
32
  ┌─────────┐
29
- │ Parser │ ← TypeScriptParser + TypeScriptExtractor
33
+ │ Parser │ ← TypeScriptParser / GoParser
30
34
  └────┬────┘
31
35
  │ ParsedFile[]
32
36
 
@@ -96,6 +100,39 @@ const resolver = new TypeScriptResolver()
96
100
  const resolved = resolver.resolve(importDecl, fromFilePath, allProjectFiles)
97
101
  ```
98
102
 
103
+ #### Go Parser
104
+
105
+ Parses `.go` files without requiring the Go toolchain:
106
+
107
+ ```typescript
108
+ import { GoParser } from '@getmikk/core'
109
+
110
+ const parser = new GoParser()
111
+ const parsed = parser.parse('service.go', fileContent)
112
+
113
+ console.log(parsed.functions) // ParsedFunction[] — name, params with types, return type, calls[]
114
+ console.log(parsed.classes) // ParsedClass[] — receiver-based methods grouped by type name
115
+ console.log(parsed.imports) // ParsedImport[] — resolved against go.mod module path
116
+ console.log(parsed.exports) // ParsedExport[] — uppercase identifiers (Go convention)
117
+ console.log(parsed.routes) // ParsedRoute[] — Gin, Echo, Chi, Mux, net/http routes
118
+ ```
119
+
120
+ **Features**:
121
+ - Stateful line/character scanning (handles strings, comments, nested braces correctly)
122
+ - Receiver methods grouped with struct types as classes
123
+ - HTTP route detection (Gin/Echo/Chi/Mux/net.http/Fiber patterns)
124
+ - Error handling detection (`if err != nil` patterns)
125
+ - Function call extraction from bodies
126
+ - Grouped parameter expansion (`first, last string` → both typed as `string`)
127
+ - Import resolution via `go.mod` module path
128
+
129
+ #### Auto-detection by file extension
130
+
131
+ ```typescript
132
+ const parser = getParser('file.ts') // → TypeScriptParser
133
+ const parser = getParser('service.go') // → GoParser
134
+ ```
135
+
99
136
  ---
100
137
 
101
138
  ### 2. Graph — Dependency Graph Construction
@@ -135,7 +172,8 @@ const impact = analyzer.analyze(['src/utils/math.ts::calculateTotal'])
135
172
  console.log(impact.changed) // string[] — directly changed node IDs
136
173
  console.log(impact.impacted) // string[] — transitively affected nodes
137
174
  console.log(impact.depth) // number — max propagation depth
138
- console.log(impact.confidence) // number 0-1 confidence score
175
+ console.log(impact.confidence) // 'high' | 'medium' | 'low'
176
+ console.log(impact.classified) // { critical: [], high: [], medium: [], low: [] }
139
177
  ```
140
178
 
141
179
  #### ClusterDetector
@@ -414,7 +452,7 @@ import type {
414
452
  // Parser
415
453
  ParsedFile, ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric,
416
454
  // Graph
417
- DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster,
455
+ DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster, ClassifiedImpact, RiskLevel,
418
456
  // Contract
419
457
  MikkContract, MikkLock, MikkModule, MikkDecision, MikkLockFunction, MikkLockModule, MikkLockFile,
420
458
  // Boundary
package/out.log ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/core",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,8 @@
27
27
  "zod": "^3.22.0"
28
28
  },
29
29
  "devDependencies": {
30
- "typescript": "^5.7.0",
31
- "@types/node": "^22.0.0"
30
+ "@types/bun": "^1.3.10",
31
+ "@types/node": "^22.0.0",
32
+ "typescript": "^5.7.0"
32
33
  }
33
34
  }
@@ -0,0 +1,75 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import type { MikkContract, MikkDecision } from './schema.js'
3
+ import { MikkContractSchema } from './schema.js'
4
+
5
+ /**
6
+ * AdrManager — CRUD operations on Architectural Decision Records
7
+ * stored in the `declared.decisions` array of mikk.json.
8
+ *
9
+ * Each ADR has: id, title, reason, date.
10
+ * Exposed as an MCP tool so AI assistants can read and update ADRs.
11
+ */
12
+ export class AdrManager {
13
+ constructor(private contractPath: string) { }
14
+
15
+ // ─── Read ──────────────────────────────────────────────────────
16
+
17
+ async list(): Promise<MikkDecision[]> {
18
+ const contract = await this.readContract()
19
+ return contract.declared.decisions ?? []
20
+ }
21
+
22
+ async get(id: string): Promise<MikkDecision | null> {
23
+ const decisions = await this.list()
24
+ return decisions.find(d => d.id === id) ?? null
25
+ }
26
+
27
+ // ─── Write ─────────────────────────────────────────────────────
28
+
29
+ async add(decision: MikkDecision): Promise<void> {
30
+ const contract = await this.readContract()
31
+ if (!contract.declared.decisions) {
32
+ contract.declared.decisions = []
33
+ }
34
+ // Check for duplicate ID
35
+ if (contract.declared.decisions.some(d => d.id === decision.id)) {
36
+ throw new Error(`ADR with id "${decision.id}" already exists. Use update() instead.`)
37
+ }
38
+ contract.declared.decisions.push(decision)
39
+ await this.writeContract(contract)
40
+ }
41
+
42
+ async update(id: string, fields: Partial<Omit<MikkDecision, 'id'>>): Promise<void> {
43
+ const contract = await this.readContract()
44
+ const decisions = contract.declared.decisions ?? []
45
+ const idx = decisions.findIndex(d => d.id === id)
46
+ if (idx === -1) {
47
+ throw new Error(`ADR "${id}" not found. Use add() to create a new decision.`)
48
+ }
49
+ decisions[idx] = { ...decisions[idx], ...fields, id } // preserve id
50
+ contract.declared.decisions = decisions
51
+ await this.writeContract(contract)
52
+ }
53
+
54
+ async remove(id: string): Promise<boolean> {
55
+ const contract = await this.readContract()
56
+ const decisions = contract.declared.decisions ?? []
57
+ const idx = decisions.findIndex(d => d.id === id)
58
+ if (idx === -1) return false
59
+ decisions.splice(idx, 1)
60
+ contract.declared.decisions = decisions
61
+ await this.writeContract(contract)
62
+ return true
63
+ }
64
+
65
+ // ─── Helpers ───────────────────────────────────────────────────
66
+
67
+ private async readContract(): Promise<MikkContract> {
68
+ const raw = await fs.readFile(this.contractPath, 'utf-8')
69
+ return MikkContractSchema.parse(JSON.parse(raw))
70
+ }
71
+
72
+ private async writeContract(contract: MikkContract): Promise<void> {
73
+ await fs.writeFile(this.contractPath, JSON.stringify(contract, null, 2), 'utf-8')
74
+ }
75
+ }
@@ -10,3 +10,5 @@ export { ContractWriter, type UpdateResult } from './contract-writer.js'
10
10
  export { ContractReader } from './contract-reader.js'
11
11
  export { LockReader } from './lock-reader.js'
12
12
  export { ContractGenerator } from './contract-generator.js'
13
+ export { AdrManager } from './adr-manager.js'
14
+
@@ -1,4 +1,4 @@
1
- import * as path from 'node:path'
1
+ import * as path from 'node:path'
2
2
  import { createHash } from 'node:crypto'
3
3
  import type { MikkContract, MikkLock } from './schema.js'
4
4
  import type { DependencyGraph } from '../graph/types.js'
@@ -131,7 +131,7 @@ export class LockCompiler {
131
131
  }
132
132
 
133
133
  const lockData: MikkLock = {
134
- version: '1.0.0',
134
+ version: '1.7.0',
135
135
  generatedAt: new Date().toISOString(),
136
136
  generatorVersion: VERSION,
137
137
  projectRoot: contract.project.name,
@@ -146,7 +146,9 @@ export class LockCompiler {
146
146
  classes: Object.keys(classes).length > 0 ? classes : undefined,
147
147
  generics: Object.keys(generics).length > 0 ? generics : undefined,
148
148
  files,
149
- contextFiles: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
149
+ contextFiles: contextFiles && contextFiles.length > 0
150
+ ? contextFiles.map(({ path, type, size }) => ({ path, type, size }))
151
+ : undefined,
150
152
  routes: routes.length > 0 ? routes : undefined,
151
153
  graph: {
152
154
  nodes: graph.nodes.size,
@@ -205,7 +207,6 @@ export class LockCompiler {
205
207
  ),
206
208
  edgeCasesHandled: node.metadata.edgeCasesHandled,
207
209
  errorHandling: node.metadata.errorHandling,
208
- detailedLines: node.metadata.detailedLines,
209
210
  }
210
211
  }
211
212
 
@@ -333,7 +334,7 @@ export class LockCompiler {
333
334
  path: file.path,
334
335
  hash: file.hash,
335
336
  moduleId: moduleId || 'unknown',
336
- lastModified: new Date().toISOString(),
337
+ lastModified: new Date(file.parsedAt).toISOString(),
337
338
  ...(importedFiles.length > 0 ? { imports: importedFiles } : {}),
338
339
  }
339
340
  }
@@ -32,7 +32,7 @@ export class LockReader {
32
32
  /** Write lock file to disk in compact format */
33
33
  async write(lock: MikkLock, lockPath: string): Promise<void> {
34
34
  const compact = compactifyLock(lock)
35
- const json = JSON.stringify(compact, null, 2)
35
+ const json = JSON.stringify(compact)
36
36
  await fs.writeFile(lockPath, json, 'utf-8')
37
37
  }
38
38
  }
@@ -59,17 +59,23 @@ function compactifyLock(lock: MikkLock): any {
59
59
  graph: lock.graph,
60
60
  }
61
61
 
62
+ // P7: Build fnIndex for integer edge references
63
+ const fnKeys = Object.keys(lock.functions)
64
+ const fnIndexMap = new Map<string, number>()
65
+ fnKeys.forEach((k, i) => fnIndexMap.set(k, i))
66
+ out.fnIndex = fnKeys
67
+
62
68
  // Functions — biggest savings
63
69
  out.functions = {}
64
- for (const [key, fn] of Object.entries(lock.functions)) {
70
+ for (let idx = 0; idx < fnKeys.length; idx++) {
71
+ const fn = lock.functions[fnKeys[idx]]
65
72
  const c: any = {
66
73
  lines: [fn.startLine, fn.endLine],
67
- hash: fn.hash,
74
+ // P4: no hash, P6: no moduleId
68
75
  }
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
76
+ // P7: integer calls/calledBy referencing fnIndex positions
77
+ if (fn.calls.length > 0) c.calls = fn.calls.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
78
+ if (fn.calledBy.length > 0) c.calledBy = fn.calledBy.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
73
79
  if (fn.params && fn.params.length > 0) c.params = fn.params
74
80
  if (fn.returnType) c.returnType = fn.returnType
75
81
  if (fn.isAsync) c.isAsync = true
@@ -79,10 +85,8 @@ function compactifyLock(lock: MikkLock): any {
79
85
  if (fn.errorHandling && fn.errorHandling.length > 0) {
80
86
  c.errors = fn.errorHandling.map(e => [e.line, e.type, e.detail])
81
87
  }
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
88
+ // P2: no c.details (detailedLines removed)
89
+ out.functions[String(idx)] = c
86
90
  }
87
91
 
88
92
  // Classes
@@ -134,9 +138,9 @@ function compactifyLock(lock: MikkLock): any {
134
138
  out.files[key] = c
135
139
  }
136
140
 
137
- // Context files — keep as-is (content is the bulk, no savings)
141
+ // Context files — paths/type only, no content
138
142
  if (lock.contextFiles && lock.contextFiles.length > 0) {
139
- out.contextFiles = lock.contextFiles
143
+ out.contextFiles = lock.contextFiles.map(({ path, type, size }) => ({ path, type, size }))
140
144
  }
141
145
 
142
146
  // Routes — keep as-is (already compact)
@@ -166,23 +170,37 @@ function hydrateLock(raw: any): any {
166
170
  graph: raw.graph,
167
171
  }
168
172
 
173
+ // P7: function index for integer edge resolution
174
+ const fnIndex: string[] = raw.fnIndex || []
175
+ const hasFnIndex = fnIndex.length > 0
176
+
177
+ // P6: build file→moduleId map before function loop
178
+ const fileModuleMap: Record<string, string> = {}
179
+ for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
180
+ fileModuleMap[key] = c.moduleId || 'unknown'
181
+ }
182
+
169
183
  // Hydrate functions
170
184
  out.functions = {}
171
185
  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:')
186
+ // P7: key is integer index → look up full ID via fnIndex
187
+ const fullId = hasFnIndex ? (fnIndex[parseInt(key)] || key) : key
188
+ const { name, file } = parseEntityKey(fullId, 'fn:')
174
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)
191
+ const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
192
+ const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
175
193
 
176
- out.functions[key] = {
177
- id: key,
194
+ out.functions[fullId] = {
195
+ id: fullId,
178
196
  name,
179
197
  file,
180
198
  startLine: lines[0],
181
199
  endLine: lines[1],
182
- hash: c.hash || '',
183
- calls: c.calls || [],
184
- calledBy: c.calledBy || [],
185
- moduleId: c.moduleId || 'unknown',
200
+ hash: c.hash || '', // P4: empty string when not stored
201
+ calls,
202
+ calledBy,
203
+ moduleId: fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
186
204
  ...(c.params ? { params: c.params } : {}),
187
205
  ...(c.returnType ? { returnType: c.returnType } : {}),
188
206
  ...(c.isAsync ? { isAsync: true } : {}),
@@ -194,11 +212,7 @@ function hydrateLock(raw: any): any {
194
212
  line: e[0], type: e[1], detail: e[2]
195
213
  }))
196
214
  } : {}),
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
- } : {}),
215
+ // P2: no detailedLines restoration
202
216
  }
203
217
  }
204
218
 
@@ -74,11 +74,6 @@ export const MikkLockFunctionSchema = z.object({
74
74
  type: z.enum(['try-catch', 'throw']),
75
75
  detail: z.string(),
76
76
  })).optional(),
77
- detailedLines: z.array(z.object({
78
- startLine: z.number(),
79
- endLine: z.number(),
80
- blockType: z.string(),
81
- })).optional()
82
77
  })
83
78
 
84
79
  export const MikkLockModuleSchema = z.object({
@@ -129,9 +124,9 @@ export const MikkLockGenericSchema = z.object({
129
124
 
130
125
  export const MikkLockContextFileSchema = z.object({
131
126
  path: z.string(),
132
- content: z.string(),
127
+ content: z.string().optional(),
133
128
  type: z.enum(['schema', 'model', 'types', 'routes', 'config', 'api-spec', 'migration', 'docker']),
134
- size: z.number(),
129
+ size: z.number().optional(),
135
130
  })
136
131
 
137
132
  export const MikkLockRouteSchema = z.object({
@@ -0,0 +1,194 @@
1
+ import type { DependencyGraph } from './types.js'
2
+ import type { MikkLock } from '../contract/schema.js'
3
+
4
+ // ─── Types ──────────────────────────────────────────────────────────
5
+
6
+ export interface DeadCodeEntry {
7
+ id: string
8
+ name: string
9
+ file: string
10
+ moduleId?: string
11
+ type: 'function' | 'class'
12
+ reason: string
13
+ }
14
+
15
+ export interface DeadCodeResult {
16
+ deadFunctions: DeadCodeEntry[]
17
+ totalFunctions: number
18
+ deadCount: number
19
+ deadPercentage: number
20
+ byModule: Record<string, { dead: number; total: number; items: DeadCodeEntry[] }>
21
+ }
22
+
23
+ // ─── Exemption patterns ────────────────────────────────────────────
24
+
25
+ /** Common entry-point function names that are never "dead" even with 0 callers */
26
+ const ENTRY_POINT_PATTERNS = [
27
+ /^(main|bootstrap|start|init|setup|configure|register|mount)$/i,
28
+ /^(app|server|index|mod|program)$/i,
29
+ /Handler$/i, // Express/Koa/Hono handlers
30
+ /Middleware$/i,
31
+ /Controller$/i,
32
+ /^use[A-Z]/, // React hooks
33
+ /^handle[A-Z]/, // Event handlers
34
+ /^on[A-Z]/, // Event listeners
35
+ ]
36
+
37
+ /** Common test function patterns */
38
+ const TEST_PATTERNS = [
39
+ /^(it|describe|test|beforeAll|afterAll|beforeEach|afterEach)$/,
40
+ /\.test\./,
41
+ /\.spec\./,
42
+ /__test__/,
43
+ ]
44
+
45
+ // ─── Detector ──────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * DeadCodeDetector — walks the dependency graph and finds functions
49
+ * with zero incoming `calls` edges after applying multi-pass exemptions.
50
+ *
51
+ * Exemptions:
52
+ * 1. Exported symbols (may be consumed externally)
53
+ * 2. Entry point patterns (main, handler, middleware, hooks, etc.)
54
+ * 3. Route handlers (detected HTTP routes)
55
+ * 4. Test functions (describe, it, test, etc.)
56
+ * 5. Decorated classes/functions (typically framework-managed)
57
+ * 6. Constructor methods (called implicitly)
58
+ */
59
+ export class DeadCodeDetector {
60
+ private routeHandlers: Set<string>
61
+
62
+ constructor(
63
+ private graph: DependencyGraph,
64
+ private lock: MikkLock,
65
+ ) {
66
+ // Build a set of handler function names from detected routes
67
+ this.routeHandlers = new Set(
68
+ (lock.routes ?? []).map(r => r.handler).filter(Boolean),
69
+ )
70
+ }
71
+
72
+ detect(): DeadCodeResult {
73
+ const dead: DeadCodeEntry[] = []
74
+ let totalFunctions = 0
75
+ const byModule: DeadCodeResult['byModule'] = {}
76
+
77
+ for (const [id, fn] of Object.entries(this.lock.functions)) {
78
+ totalFunctions++
79
+ const moduleId = fn.moduleId ?? 'unknown'
80
+
81
+ // Initialize module bucket
82
+ if (!byModule[moduleId]) {
83
+ byModule[moduleId] = { dead: 0, total: 0, items: [] }
84
+ }
85
+ byModule[moduleId].total++
86
+
87
+ // Check if this function has any incoming call edges
88
+ const inEdges = this.graph.inEdges.get(id) || []
89
+ const hasCallers = inEdges.some(e => e.type === 'calls')
90
+
91
+ if (hasCallers) continue // Not dead
92
+
93
+ // Apply exemptions
94
+ if (this.isExempt(fn, id)) continue
95
+
96
+ const entry: DeadCodeEntry = {
97
+ id,
98
+ name: fn.name,
99
+ file: fn.file,
100
+ moduleId,
101
+ type: 'function',
102
+ reason: this.inferReason(fn, id),
103
+ }
104
+ dead.push(entry)
105
+ byModule[moduleId].dead++
106
+ byModule[moduleId].items.push(entry)
107
+ }
108
+
109
+ // Also check classes (if present in lock)
110
+ if (this.lock.classes) {
111
+ for (const [id, cls] of Object.entries(this.lock.classes)) {
112
+ const moduleId = cls.moduleId ?? 'unknown'
113
+ if (!byModule[moduleId]) {
114
+ byModule[moduleId] = { dead: 0, total: 0, items: [] }
115
+ }
116
+
117
+ const inEdges = this.graph.inEdges.get(id) || []
118
+ const hasCallers = inEdges.some(e => e.type === 'calls' || e.type === 'imports')
119
+
120
+ if (hasCallers) continue
121
+ if (cls.isExported) continue // Exported classes are exempt
122
+
123
+ const entry: DeadCodeEntry = {
124
+ id,
125
+ name: cls.name,
126
+ file: cls.file,
127
+ moduleId,
128
+ type: 'class',
129
+ reason: 'Class has no callers or importers and is not exported',
130
+ }
131
+ dead.push(entry)
132
+ byModule[moduleId].dead++
133
+ byModule[moduleId].items.push(entry)
134
+ }
135
+ }
136
+
137
+ return {
138
+ deadFunctions: dead,
139
+ totalFunctions,
140
+ deadCount: dead.length,
141
+ deadPercentage: totalFunctions > 0
142
+ ? Math.round((dead.length / totalFunctions) * 1000) / 10
143
+ : 0,
144
+ byModule,
145
+ }
146
+ }
147
+
148
+ // ─── Exemption checks ──────────────────────────────────────────
149
+
150
+ private isExempt(fn: MikkLock['functions'][string], id: string): boolean {
151
+ // 1. Exported functions — may be consumed by external packages
152
+ if (fn.isExported) return true
153
+
154
+ // 2. Entry point patterns
155
+ if (ENTRY_POINT_PATTERNS.some(p => p.test(fn.name))) return true
156
+
157
+ // 3. Route handlers
158
+ if (this.routeHandlers.has(fn.name)) return true
159
+
160
+ // 4. Test functions or in test files
161
+ if (TEST_PATTERNS.some(p => p.test(fn.name) || p.test(fn.file))) return true
162
+
163
+ // 5. Constructor methods
164
+ if (fn.name === 'constructor' || fn.name === '__init__') return true
165
+
166
+ // 6. Functions called by exported functions in the same file
167
+ // (transitive liveness — if an exported fn calls this, it's alive)
168
+ if (this.isCalledByExportedInSameFile(fn, id)) return true
169
+
170
+ return false
171
+ }
172
+
173
+ private isCalledByExportedInSameFile(
174
+ fn: MikkLock['functions'][string],
175
+ fnId: string,
176
+ ): boolean {
177
+ // Check calledBy — if any caller is exported and in the same file, exempt
178
+ for (const callerId of fn.calledBy) {
179
+ const caller = this.lock.functions[callerId]
180
+ if (caller && caller.isExported && caller.file === fn.file) {
181
+ return true
182
+ }
183
+ }
184
+ return false
185
+ }
186
+
187
+ private inferReason(fn: MikkLock['functions'][string], id: string): string {
188
+ if (fn.calledBy.length === 0) {
189
+ return 'No callers found anywhere in the codebase'
190
+ }
191
+ // calledBy has entries but they didn't resolve to graph edges
192
+ return `${fn.calledBy.length} references exist but none resolved to active call edges`
193
+ }
194
+ }
@@ -121,6 +121,7 @@ export class GraphBuilder {
121
121
  source: file.path,
122
122
  target: imp.resolvedPath,
123
123
  type: 'imports',
124
+ confidence: 1.0, // Import edges are always deterministic from AST
124
125
  })
125
126
  }
126
127
  }
@@ -145,24 +146,27 @@ export class GraphBuilder {
145
146
  // Try to resolve: first check imported names, then local functions
146
147
  const simpleName = call.includes('.') ? call.split('.').pop()! : call
147
148
 
148
- // Check if it's an imported function
149
+ // Check if it's an imported function (confidence 0.8 — resolved via import names)
149
150
  const importedId = importedNames.get(simpleName) || importedNames.get(call)
150
151
  if (importedId && graph.nodes.has(importedId)) {
151
152
  graph.edges.push({
152
153
  source: fn.id,
153
154
  target: importedId,
154
155
  type: 'calls',
156
+ confidence: 0.8, // Resolved through import names, not direct AST binding
155
157
  })
156
158
  continue
157
159
  }
158
160
 
159
- // Check if it's a local function in the same file
161
+ // Check if it's a local function in the same file (confidence 1.0 for exact, 0.5 for fuzzy)
160
162
  const localId = `fn:${file.path}:${simpleName}`
161
163
  if (graph.nodes.has(localId) && localId !== fn.id) {
164
+ // Direct local match — high confidence
162
165
  graph.edges.push({
163
166
  source: fn.id,
164
167
  target: localId,
165
168
  type: 'calls',
169
+ confidence: simpleName === call ? 1.0 : 0.5, // exact name = 1.0, dot-access strip = 0.5
166
170
  })
167
171
  }
168
172
  }