@getmikk/ai-context 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.
- package/README.md +327 -0
- package/package.json +31 -27
- package/src/claude-md-generator.ts +265 -265
- package/src/context-builder.ts +398 -398
- package/src/index.ts +4 -4
- package/src/providers.ts +154 -154
- package/src/types.ts +69 -69
- package/tests/claude-md.test.ts +137 -137
- package/tests/smoke.test.ts +5 -5
- package/tsconfig.json +15 -15
|
@@ -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
|
+
}
|