@getmikk/ai-context 1.2.0 → 1.3.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.
@@ -1,265 +1,265 @@
1
- import type { MikkContract, MikkLock, MikkLockFunction } from '@getmikk/core'
2
-
3
- /** Default token budget for claude.md — prevents bloating the context window */
4
- const DEFAULT_TOKEN_BUDGET = 6000
5
-
6
- /** Rough token estimation: ~4 chars per token */
7
- function estimateTokens(text: string): number {
8
- return Math.ceil(text.length / 4)
9
- }
10
-
11
- /**
12
- * ClaudeMdGenerator — generates an always-accurate `claude.md` and `AGENTS.md`
13
- * from the lock file and contract. Every function name, file path, and module
14
- * relationship is sourced from the AST-derived lock file — never hand-authored.
15
- *
16
- * Tiered system per spec:
17
- * Tier 1: Summary (~500 tokens) — always included
18
- * Tier 2: Module details (~300 tokens/module) — included if budget allows
19
- * Tier 3: Recent changes (~50 tokens/change) — last section added
20
- */
21
- export class ClaudeMdGenerator {
22
- constructor(
23
- private contract: MikkContract,
24
- private lock: MikkLock,
25
- private tokenBudget: number = DEFAULT_TOKEN_BUDGET
26
- ) { }
27
-
28
- /** Generate the full claude.md content */
29
- generate(): string {
30
- const sections: string[] = []
31
- let usedTokens = 0
32
-
33
- // ── Tier 1: Summary (always included) ──────────────────────
34
- const summary = this.generateSummary()
35
- sections.push(summary)
36
- usedTokens += estimateTokens(summary)
37
-
38
- // ── Tier 2: Module details (if budget allows) ──────────────
39
- const modules = this.getModulesSortedByDependencyOrder()
40
- for (const module of modules) {
41
- const moduleSection = this.generateModuleSection(module.id)
42
- const tokens = estimateTokens(moduleSection)
43
- if (usedTokens + tokens > this.tokenBudget) {
44
- sections.push('\n> Full details available in `mikk.lock.json`\n')
45
- break
46
- }
47
- sections.push(moduleSection)
48
- usedTokens += tokens
49
- }
50
-
51
- // ── Tier 3: Constraints & decisions ────────────────────────
52
- const constraintsSection = this.generateConstraintsSection()
53
- const constraintTokens = estimateTokens(constraintsSection)
54
- if (usedTokens + constraintTokens <= this.tokenBudget) {
55
- sections.push(constraintsSection)
56
- usedTokens += constraintTokens
57
- }
58
-
59
- const decisionsSection = this.generateDecisionsSection()
60
- const decisionTokens = estimateTokens(decisionsSection)
61
- if (usedTokens + decisionTokens <= this.tokenBudget) {
62
- sections.push(decisionsSection)
63
- usedTokens += decisionTokens
64
- }
65
-
66
- return sections.join('\n')
67
- }
68
-
69
- // ── Tier 1: Summary ───────────────────────────────────────────
70
-
71
- private generateSummary(): string {
72
- const lines: string[] = []
73
- const moduleCount = this.contract.declared.modules.length
74
- const functionCount = Object.keys(this.lock.functions).length
75
- const fileCount = Object.keys(this.lock.files).length
76
-
77
- lines.push(`# ${this.contract.project.name} — Architecture Overview`)
78
- lines.push('')
79
-
80
- if (this.contract.project.description) {
81
- lines.push('## What this project does')
82
- lines.push(this.contract.project.description)
83
- lines.push('')
84
- }
85
-
86
- lines.push('## Modules')
87
- for (const module of this.contract.declared.modules) {
88
- const fnCount = Object.values(this.lock.functions)
89
- .filter(f => f.moduleId === module.id).length
90
- const desc = module.intent || module.description || ''
91
- const descStr = desc ? ` — ${desc}` : ''
92
- lines.push(`- **${module.name}** (\`${module.id}\`): ${fnCount} functions${descStr}`)
93
- }
94
- lines.push('')
95
-
96
- lines.push(`## Stats`)
97
- lines.push(`- ${fileCount} files, ${functionCount} functions, ${moduleCount} modules`)
98
- lines.push(`- Language: ${this.contract.project.language}`)
99
- lines.push('')
100
-
101
- // Critical constraints summary
102
- if (this.contract.declared.constraints.length > 0) {
103
- lines.push('## Critical Constraints')
104
- for (const c of this.contract.declared.constraints) {
105
- lines.push(`- ${c}`)
106
- }
107
- lines.push('')
108
- }
109
-
110
- return lines.join('\n')
111
- }
112
-
113
- // ── Tier 2: Module Details ────────────────────────────────────
114
-
115
- private generateModuleSection(moduleId: string): string {
116
- const module = this.contract.declared.modules.find(m => m.id === moduleId)
117
- if (!module) return ''
118
-
119
- const lines: string[] = []
120
- const moduleFunctions = Object.values(this.lock.functions)
121
- .filter(f => f.moduleId === moduleId)
122
-
123
- lines.push(`## ${module.name} module`)
124
-
125
- // Location
126
- if (module.paths.length > 0) {
127
- lines.push(`**Location:** ${module.paths.join(', ')}`)
128
- }
129
-
130
- // Intent
131
- if (module.intent) {
132
- lines.push(`**Purpose:** ${module.intent}`)
133
- } else if (module.description) {
134
- lines.push(`**Purpose:** ${module.description}`)
135
- }
136
-
137
- lines.push('')
138
-
139
- // Entry points: functions with no calledBy (likely public API surface)
140
- const entryPoints = moduleFunctions
141
- .filter(fn => fn.calledBy.length === 0)
142
- .sort((a, b) => b.calls.length - a.calls.length)
143
- .slice(0, 5)
144
-
145
- if (entryPoints.length > 0) {
146
- lines.push('**Entry points:**')
147
- for (const fn of entryPoints) {
148
- const sig = this.formatSignature(fn)
149
- const purpose = fn.purpose ? ` — ${fn.purpose}` : ''
150
- lines.push(` - \`${sig}\`${purpose}`)
151
- }
152
- lines.push('')
153
- }
154
-
155
- // Key functions: top 5 by calledBy count (most depended upon)
156
- const keyFunctions = [...moduleFunctions]
157
- .sort((a, b) => b.calledBy.length - a.calledBy.length)
158
- .filter(fn => fn.calledBy.length > 0)
159
- .slice(0, 5)
160
-
161
- if (keyFunctions.length > 0) {
162
- lines.push('**Key internal functions:**')
163
- for (const fn of keyFunctions) {
164
- const callerCount = fn.calledBy.length
165
- const purpose = fn.purpose ? ` — ${fn.purpose}` : ''
166
- lines.push(` - \`${fn.name}\` (called by ${callerCount})${purpose}`)
167
- }
168
- lines.push('')
169
- }
170
-
171
- // Dependencies: other modules this module imports from
172
- const depModuleIds = new Set<string>()
173
- for (const fn of moduleFunctions) {
174
- for (const callId of fn.calls) {
175
- const target = this.lock.functions[callId]
176
- if (target && target.moduleId !== moduleId) {
177
- depModuleIds.add(target.moduleId)
178
- }
179
- }
180
- }
181
-
182
- if (depModuleIds.size > 0) {
183
- const depNames = [...depModuleIds].map(id => {
184
- const mod = this.contract.declared.modules.find(m => m.id === id)
185
- return mod?.name || id
186
- })
187
- lines.push(`**Depends on:** ${depNames.join(', ')}`)
188
- lines.push('')
189
- }
190
-
191
- // Module-specific constraints
192
- const moduleConstraints = this.contract.declared.constraints.filter(c =>
193
- c.toLowerCase().includes(moduleId.toLowerCase()) ||
194
- c.toLowerCase().includes(module.name.toLowerCase())
195
- )
196
- if (moduleConstraints.length > 0) {
197
- lines.push('**Constraints:**')
198
- for (const c of moduleConstraints) {
199
- lines.push(` - ${c}`)
200
- }
201
- lines.push('')
202
- }
203
-
204
- return lines.join('\n')
205
- }
206
-
207
- // ── Tier 3: Constraints & Decisions ───────────────────────────
208
-
209
- private generateConstraintsSection(): string {
210
- if (this.contract.declared.constraints.length === 0) return ''
211
- const lines: string[] = []
212
- lines.push('## Cross-Cutting Constraints')
213
- for (const c of this.contract.declared.constraints) {
214
- lines.push(`- ${c}`)
215
- }
216
- lines.push('')
217
- return lines.join('\n')
218
- }
219
-
220
- private generateDecisionsSection(): string {
221
- if (this.contract.declared.decisions.length === 0) return ''
222
- const lines: string[] = []
223
- lines.push('## Architectural Decisions')
224
- for (const d of this.contract.declared.decisions) {
225
- lines.push(`- **${d.title}:** ${d.reason}`)
226
- }
227
- lines.push('')
228
- return lines.join('\n')
229
- }
230
-
231
- // ── Helpers ───────────────────────────────────────────────────
232
-
233
- /** Format a function into a readable signature */
234
- private formatSignature(fn: MikkLockFunction): string {
235
- return `${fn.name}() [${fn.file}:${fn.startLine}]`
236
- }
237
-
238
- /** Sort modules by inter-module dependency order (depended-on modules first) */
239
- private getModulesSortedByDependencyOrder(): typeof this.contract.declared.modules {
240
- const modules = [...this.contract.declared.modules]
241
- const dependencyCount = new Map<string, number>()
242
-
243
- for (const mod of modules) {
244
- dependencyCount.set(mod.id, 0)
245
- }
246
-
247
- // Count how many other modules depend on each module
248
- for (const fn of Object.values(this.lock.functions)) {
249
- for (const callId of fn.calls) {
250
- const target = this.lock.functions[callId]
251
- if (target && target.moduleId !== fn.moduleId) {
252
- dependencyCount.set(
253
- target.moduleId,
254
- (dependencyCount.get(target.moduleId) || 0) + 1
255
- )
256
- }
257
- }
258
- }
259
-
260
- // Sort: most depended-on first
261
- return modules.sort((a, b) =>
262
- (dependencyCount.get(b.id) || 0) - (dependencyCount.get(a.id) || 0)
263
- )
264
- }
265
- }
1
+ import type { MikkContract, MikkLock, MikkLockFunction } from '@getmikk/core'
2
+
3
+ /** Default token budget for claude.md — prevents bloating the context window */
4
+ const DEFAULT_TOKEN_BUDGET = 6000
5
+
6
+ /** Rough token estimation: ~4 chars per token */
7
+ function estimateTokens(text: string): number {
8
+ return Math.ceil(text.length / 4)
9
+ }
10
+
11
+ /**
12
+ * ClaudeMdGenerator — generates an always-accurate `claude.md` and `AGENTS.md`
13
+ * from the lock file and contract. Every function name, file path, and module
14
+ * relationship is sourced from the AST-derived lock file — never hand-authored.
15
+ *
16
+ * Tiered system per spec:
17
+ * Tier 1: Summary (~500 tokens) — always included
18
+ * Tier 2: Module details (~300 tokens/module) — included if budget allows
19
+ * Tier 3: Recent changes (~50 tokens/change) — last section added
20
+ */
21
+ export class ClaudeMdGenerator {
22
+ constructor(
23
+ private contract: MikkContract,
24
+ private lock: MikkLock,
25
+ private tokenBudget: number = DEFAULT_TOKEN_BUDGET
26
+ ) { }
27
+
28
+ /** Generate the full claude.md content */
29
+ generate(): string {
30
+ const sections: string[] = []
31
+ let usedTokens = 0
32
+
33
+ // ── Tier 1: Summary (always included) ──────────────────────
34
+ const summary = this.generateSummary()
35
+ sections.push(summary)
36
+ usedTokens += estimateTokens(summary)
37
+
38
+ // ── Tier 2: Module details (if budget allows) ──────────────
39
+ const modules = this.getModulesSortedByDependencyOrder()
40
+ for (const module of modules) {
41
+ const moduleSection = this.generateModuleSection(module.id)
42
+ const tokens = estimateTokens(moduleSection)
43
+ if (usedTokens + tokens > this.tokenBudget) {
44
+ sections.push('\n> Full details available in `mikk.lock.json`\n')
45
+ break
46
+ }
47
+ sections.push(moduleSection)
48
+ usedTokens += tokens
49
+ }
50
+
51
+ // ── Tier 3: Constraints & decisions ────────────────────────
52
+ const constraintsSection = this.generateConstraintsSection()
53
+ const constraintTokens = estimateTokens(constraintsSection)
54
+ if (usedTokens + constraintTokens <= this.tokenBudget) {
55
+ sections.push(constraintsSection)
56
+ usedTokens += constraintTokens
57
+ }
58
+
59
+ const decisionsSection = this.generateDecisionsSection()
60
+ const decisionTokens = estimateTokens(decisionsSection)
61
+ if (usedTokens + decisionTokens <= this.tokenBudget) {
62
+ sections.push(decisionsSection)
63
+ usedTokens += decisionTokens
64
+ }
65
+
66
+ return sections.join('\n')
67
+ }
68
+
69
+ // ── Tier 1: Summary ───────────────────────────────────────────
70
+
71
+ private generateSummary(): string {
72
+ const lines: string[] = []
73
+ const moduleCount = this.contract.declared.modules.length
74
+ const functionCount = Object.keys(this.lock.functions).length
75
+ const fileCount = Object.keys(this.lock.files).length
76
+
77
+ lines.push(`# ${this.contract.project.name} — Architecture Overview`)
78
+ lines.push('')
79
+
80
+ if (this.contract.project.description) {
81
+ lines.push('## What this project does')
82
+ lines.push(this.contract.project.description)
83
+ lines.push('')
84
+ }
85
+
86
+ lines.push('## Modules')
87
+ for (const module of this.contract.declared.modules) {
88
+ const fnCount = Object.values(this.lock.functions)
89
+ .filter(f => f.moduleId === module.id).length
90
+ const desc = module.intent || module.description || ''
91
+ const descStr = desc ? ` — ${desc}` : ''
92
+ lines.push(`- **${module.name}** (\`${module.id}\`): ${fnCount} functions${descStr}`)
93
+ }
94
+ lines.push('')
95
+
96
+ lines.push(`## Stats`)
97
+ lines.push(`- ${fileCount} files, ${functionCount} functions, ${moduleCount} modules`)
98
+ lines.push(`- Language: ${this.contract.project.language}`)
99
+ lines.push('')
100
+
101
+ // Critical constraints summary
102
+ if (this.contract.declared.constraints.length > 0) {
103
+ lines.push('## Critical Constraints')
104
+ for (const c of this.contract.declared.constraints) {
105
+ lines.push(`- ${c}`)
106
+ }
107
+ lines.push('')
108
+ }
109
+
110
+ return lines.join('\n')
111
+ }
112
+
113
+ // ── Tier 2: Module Details ────────────────────────────────────
114
+
115
+ private generateModuleSection(moduleId: string): string {
116
+ const module = this.contract.declared.modules.find(m => m.id === moduleId)
117
+ if (!module) return ''
118
+
119
+ const lines: string[] = []
120
+ const moduleFunctions = Object.values(this.lock.functions)
121
+ .filter(f => f.moduleId === moduleId)
122
+
123
+ lines.push(`## ${module.name} module`)
124
+
125
+ // Location
126
+ if (module.paths.length > 0) {
127
+ lines.push(`**Location:** ${module.paths.join(', ')}`)
128
+ }
129
+
130
+ // Intent
131
+ if (module.intent) {
132
+ lines.push(`**Purpose:** ${module.intent}`)
133
+ } else if (module.description) {
134
+ lines.push(`**Purpose:** ${module.description}`)
135
+ }
136
+
137
+ lines.push('')
138
+
139
+ // Entry points: functions with no calledBy (likely public API surface)
140
+ const entryPoints = moduleFunctions
141
+ .filter(fn => fn.calledBy.length === 0)
142
+ .sort((a, b) => b.calls.length - a.calls.length)
143
+ .slice(0, 5)
144
+
145
+ if (entryPoints.length > 0) {
146
+ lines.push('**Entry points:**')
147
+ for (const fn of entryPoints) {
148
+ const sig = this.formatSignature(fn)
149
+ const purpose = fn.purpose ? ` — ${fn.purpose}` : ''
150
+ lines.push(` - \`${sig}\`${purpose}`)
151
+ }
152
+ lines.push('')
153
+ }
154
+
155
+ // Key functions: top 5 by calledBy count (most depended upon)
156
+ const keyFunctions = [...moduleFunctions]
157
+ .sort((a, b) => b.calledBy.length - a.calledBy.length)
158
+ .filter(fn => fn.calledBy.length > 0)
159
+ .slice(0, 5)
160
+
161
+ if (keyFunctions.length > 0) {
162
+ lines.push('**Key internal functions:**')
163
+ for (const fn of keyFunctions) {
164
+ const callerCount = fn.calledBy.length
165
+ const purpose = fn.purpose ? ` — ${fn.purpose}` : ''
166
+ lines.push(` - \`${fn.name}\` (called by ${callerCount})${purpose}`)
167
+ }
168
+ lines.push('')
169
+ }
170
+
171
+ // Dependencies: other modules this module imports from
172
+ const depModuleIds = new Set<string>()
173
+ for (const fn of moduleFunctions) {
174
+ for (const callId of fn.calls) {
175
+ const target = this.lock.functions[callId]
176
+ if (target && target.moduleId !== moduleId) {
177
+ depModuleIds.add(target.moduleId)
178
+ }
179
+ }
180
+ }
181
+
182
+ if (depModuleIds.size > 0) {
183
+ const depNames = [...depModuleIds].map(id => {
184
+ const mod = this.contract.declared.modules.find(m => m.id === id)
185
+ return mod?.name || id
186
+ })
187
+ lines.push(`**Depends on:** ${depNames.join(', ')}`)
188
+ lines.push('')
189
+ }
190
+
191
+ // Module-specific constraints
192
+ const moduleConstraints = this.contract.declared.constraints.filter(c =>
193
+ c.toLowerCase().includes(moduleId.toLowerCase()) ||
194
+ c.toLowerCase().includes(module.name.toLowerCase())
195
+ )
196
+ if (moduleConstraints.length > 0) {
197
+ lines.push('**Constraints:**')
198
+ for (const c of moduleConstraints) {
199
+ lines.push(` - ${c}`)
200
+ }
201
+ lines.push('')
202
+ }
203
+
204
+ return lines.join('\n')
205
+ }
206
+
207
+ // ── Tier 3: Constraints & Decisions ───────────────────────────
208
+
209
+ private generateConstraintsSection(): string {
210
+ if (this.contract.declared.constraints.length === 0) return ''
211
+ const lines: string[] = []
212
+ lines.push('## Cross-Cutting Constraints')
213
+ for (const c of this.contract.declared.constraints) {
214
+ lines.push(`- ${c}`)
215
+ }
216
+ lines.push('')
217
+ return lines.join('\n')
218
+ }
219
+
220
+ private generateDecisionsSection(): string {
221
+ if (this.contract.declared.decisions.length === 0) return ''
222
+ const lines: string[] = []
223
+ lines.push('## Architectural Decisions')
224
+ for (const d of this.contract.declared.decisions) {
225
+ lines.push(`- **${d.title}:** ${d.reason}`)
226
+ }
227
+ lines.push('')
228
+ return lines.join('\n')
229
+ }
230
+
231
+ // ── Helpers ───────────────────────────────────────────────────
232
+
233
+ /** Format a function into a readable signature */
234
+ private formatSignature(fn: MikkLockFunction): string {
235
+ return `${fn.name}() [${fn.file}:${fn.startLine}]`
236
+ }
237
+
238
+ /** Sort modules by inter-module dependency order (depended-on modules first) */
239
+ private getModulesSortedByDependencyOrder(): typeof this.contract.declared.modules {
240
+ const modules = [...this.contract.declared.modules]
241
+ const dependencyCount = new Map<string, number>()
242
+
243
+ for (const mod of modules) {
244
+ dependencyCount.set(mod.id, 0)
245
+ }
246
+
247
+ // Count how many other modules depend on each module
248
+ for (const fn of Object.values(this.lock.functions)) {
249
+ for (const callId of fn.calls) {
250
+ const target = this.lock.functions[callId]
251
+ if (target && target.moduleId !== fn.moduleId) {
252
+ dependencyCount.set(
253
+ target.moduleId,
254
+ (dependencyCount.get(target.moduleId) || 0) + 1
255
+ )
256
+ }
257
+ }
258
+ }
259
+
260
+ // Sort: most depended-on first
261
+ return modules.sort((a, b) =>
262
+ (dependencyCount.get(b.id) || 0) - (dependencyCount.get(a.id) || 0)
263
+ )
264
+ }
265
+ }