@getmikk/ai-context 1.7.0 → 1.8.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 +59 -288
- package/package.json +3 -3
- package/src/claude-md-generator.ts +56 -59
- package/tests/claude-md.test.ts +88 -8
package/README.md
CHANGED
|
@@ -1,329 +1,100 @@
|
|
|
1
|
-
# @getmikk/ai-context
|
|
1
|
+
# @getmikk/ai-context
|
|
2
2
|
|
|
3
|
-
> Token-budgeted
|
|
3
|
+
> Token-budgeted AI context builder and claude.md / AGENTS.md generator.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@getmikk/ai-context)
|
|
6
6
|
[](../../LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Two things: a graph-traced context builder that packs the most relevant functions into a token budget for any given task, and a documentation generator that produces always-accurate `claude.md` and `AGENTS.md` files from the lock file.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
> Part of [Mikk](../../README.md) — the codebase nervous system for AI-assisted development.
|
|
10
|
+
> Part of [Mikk](../../README.md) — live architectural context for your AI agent.
|
|
13
11
|
|
|
14
12
|
---
|
|
15
13
|
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install @getmikk/ai-context
|
|
20
|
-
# or
|
|
21
|
-
bun add @getmikk/ai-context
|
|
22
|
-
```
|
|
14
|
+
## Context Builder
|
|
23
15
|
|
|
24
|
-
|
|
16
|
+
Given a task description, BFS-traces the dependency graph from matched seed functions and returns the most relevant context within a configurable token budget.
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Quick Start
|
|
29
|
-
|
|
30
|
-
### Context Queries
|
|
18
|
+
### Usage
|
|
31
19
|
|
|
32
20
|
```typescript
|
|
33
21
|
import { ContextBuilder, getProvider } from '@getmikk/ai-context'
|
|
34
|
-
import { ContractReader, LockReader } from '@getmikk/core'
|
|
35
|
-
|
|
36
|
-
const contract = await new ContractReader().read('./mikk.json')
|
|
37
|
-
const lock = await new LockReader().read('./mikk.lock.json')
|
|
38
22
|
|
|
39
23
|
const builder = new ContextBuilder(contract, lock)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
maxHops:
|
|
24
|
+
|
|
25
|
+
const ctx = builder.build({
|
|
26
|
+
task: 'Add rate limiting to all API routes',
|
|
27
|
+
maxHops: 4,
|
|
28
|
+
tokenBudget: 6000,
|
|
29
|
+
focusModules: ['api-gateway'],
|
|
44
30
|
includeCallGraph: true,
|
|
31
|
+
includeBodies: true,
|
|
32
|
+
projectRoot: '/path/to/project',
|
|
45
33
|
})
|
|
46
34
|
|
|
47
|
-
// Format for
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
//
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Claude.md / AGENTS.md Generation
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { ClaudeMdGenerator } from '@getmikk/ai-context'
|
|
57
|
-
|
|
58
|
-
const generator = new ClaudeMdGenerator(contract, lock, /* tokenBudget */ 4000)
|
|
59
|
-
const markdown = generator.generate()
|
|
35
|
+
// Format for your AI client
|
|
36
|
+
const formatter = getProvider('claude') // XML tags
|
|
37
|
+
// const formatter = getProvider('generic') // plain text
|
|
38
|
+
// const formatter = getProvider('compact') // minimal tokens
|
|
60
39
|
|
|
61
|
-
|
|
62
|
-
await writeFile('./claude.md', markdown)
|
|
63
|
-
await writeFile('./AGENTS.md', markdown)
|
|
40
|
+
const output = formatter.formatContext(ctx)
|
|
64
41
|
```
|
|
65
42
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## How Context Building Works
|
|
69
|
-
|
|
70
|
-
The `ContextBuilder` uses a 6-step algorithm:
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
1. Resolve Seeds
|
|
74
|
-
└─ Parse task → extract keywords → match against lock file functions/modules
|
|
43
|
+
### How context is built
|
|
75
44
|
|
|
76
|
-
|
|
77
|
-
|
|
45
|
+
1. **Seed** — match task keywords against function names, module descriptions, and file paths
|
|
46
|
+
2. **Walk** — BFS from seed nodes, following call graph edges outward up to `maxHops` depth
|
|
47
|
+
3. **Score** — each function scored by: proximity to seed (depth penalty), keyword match bonus, entry-point bonus
|
|
48
|
+
4. **Budget** — greedy knapsack: highest-scoring functions added until token budget is consumed
|
|
49
|
+
5. **Format** — serialized with function bodies, params, return types, call graph, and file locations
|
|
78
50
|
|
|
79
|
-
|
|
80
|
-
└─ Each function gets a relevance score:
|
|
81
|
-
• Proximity score (closer to seed = higher)
|
|
82
|
-
• Keyword match score (task keywords in function name)
|
|
83
|
-
• Entry-point bonus (exported functions score higher)
|
|
84
|
-
|
|
85
|
-
4. Sort by Score
|
|
86
|
-
└─ Descending relevance
|
|
87
|
-
|
|
88
|
-
5. Fill Token Budget
|
|
89
|
-
└─ Greedily add functions until budget is exhausted
|
|
90
|
-
(each function's token cost ≈ line count × 4)
|
|
91
|
-
|
|
92
|
-
6. Group by Module
|
|
93
|
-
└─ Organize selected functions into module-level context
|
|
94
|
-
```
|
|
51
|
+
The result is surgical — agents get exactly the functions relevant to their task, not the entire codebase.
|
|
95
52
|
|
|
96
53
|
---
|
|
97
54
|
|
|
98
|
-
##
|
|
55
|
+
## claude.md / AGENTS.md Generator
|
|
99
56
|
|
|
100
|
-
|
|
57
|
+
Generates `claude.md` and `AGENTS.md` automatically during `mikk init` and `mikk analyze`. Every piece of content is derived from the AST-parsed lock file — never hand-authored, never stale.
|
|
101
58
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
import { ContextBuilder } from '@getmikk/ai-context'
|
|
106
|
-
|
|
107
|
-
const builder = new ContextBuilder(contract, lock)
|
|
108
|
-
const context = builder.build(query)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**`ContextQuery`:**
|
|
112
|
-
|
|
113
|
-
| Field | Type | Default | Description |
|
|
114
|
-
|-------|------|---------|-------------|
|
|
115
|
-
| `task` | `string` | — | Natural-language description of the task |
|
|
116
|
-
| `focusFiles` | `string[]` | `[]` | Specific files to prioritize |
|
|
117
|
-
| `focusModules` | `string[]` | `[]` | Specific modules to prioritize |
|
|
118
|
-
| `maxFunctions` | `number` | `50` | Maximum functions to include |
|
|
119
|
-
| `maxHops` | `number` | `3` | BFS depth limit from seed functions |
|
|
120
|
-
| `tokenBudget` | `number` | `8000` | Maximum estimated tokens |
|
|
121
|
-
| `includeCallGraph` | `boolean` | `true` | Include `calls[]` and `calledBy[]` per function |
|
|
122
|
-
|
|
123
|
-
**`AIContext` (returned):**
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
type AIContext = {
|
|
127
|
-
project: string // Project name from contract
|
|
128
|
-
modules: ContextModule[] // Relevant modules with their functions
|
|
129
|
-
constraints: string[] // Active architectural constraints
|
|
130
|
-
decisions: string[] // Relevant ADRs
|
|
131
|
-
prompt: string // Original task
|
|
132
|
-
meta: {
|
|
133
|
-
seedCount: number // How many seed functions were found
|
|
134
|
-
totalFunctionsConsidered: number
|
|
135
|
-
selectedFunctions: number // Functions that fit in the budget
|
|
136
|
-
estimatedTokens: number // Approximate token count
|
|
137
|
-
keywords: string[] // Extracted keywords from the task
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**`ContextModule`:**
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
type ContextModule = {
|
|
146
|
-
id: string
|
|
147
|
-
name: string
|
|
148
|
-
description?: string
|
|
149
|
-
intent?: string // Module's purpose from mikk.json
|
|
150
|
-
functions: ContextFunction[]
|
|
151
|
-
files: string[]
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
**`ContextFunction`:**
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
type ContextFunction = {
|
|
159
|
-
name: string
|
|
160
|
-
file: string
|
|
161
|
-
startLine: number
|
|
162
|
-
endLine: number
|
|
163
|
-
calls: string[] // Functions this one calls
|
|
164
|
-
calledBy: string[] // Functions that call this one
|
|
165
|
-
purpose?: string // Inferred purpose
|
|
166
|
-
errorHandling?: string // Error patterns detected
|
|
167
|
-
edgeCases?: string // Edge cases noted
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
### ClaudeMdGenerator
|
|
174
|
-
|
|
175
|
-
Generates tiered markdown documentation files for AI agents.
|
|
59
|
+
### Usage
|
|
176
60
|
|
|
177
61
|
```typescript
|
|
178
62
|
import { ClaudeMdGenerator } from '@getmikk/ai-context'
|
|
179
63
|
|
|
180
|
-
const generator = new ClaudeMdGenerator(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
**All data is sourced from the AST-derived lock file** — no hallucinated descriptions. Module intents come from `mikk.json`, function lists and call graphs come from `mikk.lock.json`.
|
|
64
|
+
const generator = new ClaudeMdGenerator(
|
|
65
|
+
contract,
|
|
66
|
+
lock,
|
|
67
|
+
12000, // token budget
|
|
68
|
+
{ // from package.json
|
|
69
|
+
description: 'Multi-channel AI gateway',
|
|
70
|
+
scripts: { build: 'bun run build', test: 'vitest' },
|
|
71
|
+
dependencies: { ... },
|
|
72
|
+
},
|
|
73
|
+
projectRoot
|
|
74
|
+
)
|
|
193
75
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```markdown
|
|
197
|
-
# Project: my-app
|
|
198
|
-
|
|
199
|
-
## Architecture Overview
|
|
200
|
-
- **Modules:** 5
|
|
201
|
-
- **Total Functions:** 87
|
|
202
|
-
- **Total Files:** 23
|
|
203
|
-
|
|
204
|
-
## Modules
|
|
205
|
-
|
|
206
|
-
### auth
|
|
207
|
-
**Intent:** Handle user authentication and session management
|
|
208
|
-
**Public API:** `login`, `logout`, `validateToken`, `refreshSession`
|
|
209
|
-
**Files:** auth/login.ts, auth/session.ts, auth/middleware.ts
|
|
210
|
-
**Key Functions:**
|
|
211
|
-
- `validateToken` (auth/middleware.ts:15-42) → calls: `decodeJWT`, `checkExpiry`
|
|
212
|
-
- `login` (auth/login.ts:8-35) → calls: `validateCredentials`, `createSession`
|
|
213
|
-
|
|
214
|
-
### payments
|
|
215
|
-
...
|
|
216
|
-
|
|
217
|
-
## Constraints
|
|
218
|
-
- auth: no-import from payments
|
|
219
|
-
- payments: must-use stripe-sdk
|
|
220
|
-
|
|
221
|
-
## Decisions
|
|
222
|
-
- ADR-001: Use JWT for stateless auth (2024-01-15)
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
### Providers
|
|
228
|
-
|
|
229
|
-
Providers format the `AIContext` object into a string optimized for a specific AI model.
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
import { getProvider, ClaudeProvider, GenericProvider } from '@getmikk/ai-context'
|
|
233
|
-
|
|
234
|
-
// Factory
|
|
235
|
-
const provider = getProvider('claude') // or 'generic'
|
|
236
|
-
|
|
237
|
-
// Format context for the provider
|
|
238
|
-
const formatted = provider.formatContext(context)
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### ClaudeProvider
|
|
242
|
-
|
|
243
|
-
Formats with structured XML tags optimized for Anthropic Claude:
|
|
244
|
-
|
|
245
|
-
```xml
|
|
246
|
-
<architecture>
|
|
247
|
-
<module name="auth" intent="Handle authentication">
|
|
248
|
-
<function name="validateToken" file="auth/middleware.ts" lines="15-42">
|
|
249
|
-
<calls>decodeJWT, checkExpiry</calls>
|
|
250
|
-
<calledBy>authMiddleware</calledBy>
|
|
251
|
-
</function>
|
|
252
|
-
</module>
|
|
253
|
-
</architecture>
|
|
254
|
-
<constraints>
|
|
255
|
-
auth: no-import from payments
|
|
256
|
-
</constraints>
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
#### GenericProvider
|
|
260
|
-
|
|
261
|
-
Clean plain-text format for any AI model:
|
|
262
|
-
|
|
263
|
-
```
|
|
264
|
-
## Module: auth
|
|
265
|
-
Intent: Handle authentication
|
|
266
|
-
|
|
267
|
-
### validateToken (auth/middleware.ts:15-42)
|
|
268
|
-
Calls: decodeJWT, checkExpiry
|
|
269
|
-
Called by: authMiddleware
|
|
76
|
+
const content = generator.generate()
|
|
77
|
+
// Write to claude.md and AGENTS.md
|
|
270
78
|
```
|
|
271
79
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
## Usage Examples
|
|
275
|
-
|
|
276
|
-
### "What breaks if I change this file?"
|
|
80
|
+
### Tiered output
|
|
277
81
|
|
|
278
|
-
|
|
279
|
-
const context = builder.build({
|
|
280
|
-
task: `Impact analysis for changes to src/auth/login.ts`,
|
|
281
|
-
focusFiles: ['src/auth/login.ts'],
|
|
282
|
-
maxHops: 5,
|
|
283
|
-
tokenBudget: 4000,
|
|
284
|
-
})
|
|
285
|
-
// Returns all functions transitively affected by login.ts
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### "Get context for adding a feature"
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
const context = builder.build({
|
|
292
|
-
task: 'Add two-factor authentication to the login flow',
|
|
293
|
-
focusModules: ['auth'],
|
|
294
|
-
tokenBudget: 12000,
|
|
295
|
-
includeCallGraph: true,
|
|
296
|
-
})
|
|
297
|
-
// Returns auth module functions + related cross-module calls
|
|
298
|
-
```
|
|
82
|
+
Content is generated in priority order until the token budget (default 12,000) is consumed:
|
|
299
83
|
|
|
300
|
-
|
|
84
|
+
| Tier | Content | Always included? |
|
|
85
|
+
|------|---------|-----------------|
|
|
86
|
+
| 1 | Project summary — name, description, module list with function counts, stats, critical constraints | Yes |
|
|
87
|
+
| — | Tech stack — detected frameworks, runtime, build tool | If detectable |
|
|
88
|
+
| — | Build/test/run commands — from package.json scripts | If present |
|
|
89
|
+
| 2 | Per-module detail — exported API, key functions, internal call summary (~300 tokens/module) | Until budget |
|
|
90
|
+
| — | Context files — discovered schemas, configs, data models | If budget allows |
|
|
91
|
+
| — | Import graph — cross-file import relationships per module | If budget allows |
|
|
92
|
+
| — | HTTP routes — detected routes with method, path, handler | If budget allows |
|
|
93
|
+
| 3 | Constraints — all declared constraint rules | If budget allows |
|
|
94
|
+
| — | ADR decisions — architectural decisions with reasons | If budget allows |
|
|
301
95
|
|
|
302
|
-
|
|
303
|
-
const context = builder.build({
|
|
304
|
-
task: 'Refactor the payment processing pipeline',
|
|
305
|
-
focusModules: ['payments'],
|
|
306
|
-
maxFunctions: 30,
|
|
307
|
-
tokenBudget: 6000,
|
|
308
|
-
})
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## Types
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import type {
|
|
317
|
-
AIContext,
|
|
318
|
-
ContextModule,
|
|
319
|
-
ContextFunction,
|
|
320
|
-
ContextQuery,
|
|
321
|
-
ContextProvider,
|
|
322
|
-
} from '@getmikk/ai-context'
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
96
|
+
Modules with zero functions are skipped entirely.
|
|
326
97
|
|
|
327
|
-
|
|
98
|
+
### Token estimation
|
|
328
99
|
|
|
329
|
-
|
|
100
|
+
Uses a ~4 chars/token approximation. The generator stops adding sections the moment adding the next section would exceed the budget, so the output always fits within the specified window.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/ai-context",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"dev": "tsc --watch"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@getmikk/core": "^1.
|
|
25
|
-
"@getmikk/intent-engine": "^1.
|
|
24
|
+
"@getmikk/core": "^1.8.0",
|
|
25
|
+
"@getmikk/intent-engine": "^1.8.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "^5.7.0",
|
|
@@ -46,26 +46,26 @@ export class ClaudeMdGenerator {
|
|
|
46
46
|
const sections: string[] = []
|
|
47
47
|
let usedTokens = 0
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// --- Tier 1: Summary (always included) ----------------------
|
|
50
50
|
const summary = this.generateSummary()
|
|
51
51
|
sections.push(summary)
|
|
52
52
|
usedTokens += estimateTokens(summary)
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// --- Tech stack & conventions (always included if detectable) ---
|
|
55
55
|
const techSection = this.generateTechStackSection()
|
|
56
56
|
if (techSection) {
|
|
57
57
|
sections.push(techSection)
|
|
58
58
|
usedTokens += estimateTokens(techSection)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// --- Build / test / run commands -----------------------------
|
|
62
62
|
const commandsSection = this.generateCommandsSection()
|
|
63
63
|
if (commandsSection) {
|
|
64
64
|
sections.push(commandsSection)
|
|
65
65
|
usedTokens += estimateTokens(commandsSection)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// --- Tier 2: Module details (if budget allows) --------------
|
|
69
69
|
// Skip modules with zero functions — they waste AI tokens
|
|
70
70
|
const modules = this.getModulesSortedByDependencyOrder()
|
|
71
71
|
.filter(m => {
|
|
@@ -78,14 +78,16 @@ export class ClaudeMdGenerator {
|
|
|
78
78
|
const moduleSection = this.generateModuleSection(module.id)
|
|
79
79
|
const tokens = estimateTokens(moduleSection)
|
|
80
80
|
if (usedTokens + tokens > this.tokenBudget) {
|
|
81
|
-
sections.push('\n
|
|
81
|
+
sections.push('\n <!-- Full details truncated due to context budget -->\n')
|
|
82
82
|
break
|
|
83
83
|
}
|
|
84
84
|
sections.push(moduleSection)
|
|
85
85
|
usedTokens += tokens
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
sections.push('</modules>\n')
|
|
89
|
+
|
|
90
|
+
// --- Context files: schemas, data models, config ---------
|
|
89
91
|
const contextSection = this.generateContextFilesSection()
|
|
90
92
|
if (contextSection) {
|
|
91
93
|
const ctxTokens = estimateTokens(contextSection)
|
|
@@ -95,7 +97,7 @@ export class ClaudeMdGenerator {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
//
|
|
100
|
+
// --- File import graph per module ----------------------------
|
|
99
101
|
const importSection = this.generateImportGraphSection()
|
|
100
102
|
if (importSection) {
|
|
101
103
|
const impTokens = estimateTokens(importSection)
|
|
@@ -105,7 +107,7 @@ export class ClaudeMdGenerator {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
//
|
|
110
|
+
// --- HTTP Routes (Express + Next.js) -------------------------
|
|
109
111
|
const routesSection = this.generateRoutesSection()
|
|
110
112
|
if (routesSection) {
|
|
111
113
|
const routeTokens = estimateTokens(routesSection)
|
|
@@ -115,7 +117,7 @@ export class ClaudeMdGenerator {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
//
|
|
120
|
+
// --- Tier 3: Constraints & decisions ------------------------
|
|
119
121
|
const constraintsSection = this.generateConstraintsSection()
|
|
120
122
|
const constraintTokens = estimateTokens(constraintsSection)
|
|
121
123
|
if (usedTokens + constraintTokens <= this.tokenBudget) {
|
|
@@ -141,15 +143,13 @@ export class ClaudeMdGenerator {
|
|
|
141
143
|
const functionCount = Object.keys(this.lock.functions).length
|
|
142
144
|
const fileCount = Object.keys(this.lock.files).length
|
|
143
145
|
|
|
144
|
-
lines.push(
|
|
145
|
-
lines.push(
|
|
146
|
+
lines.push('<repository_context>')
|
|
147
|
+
lines.push(` <name>${this.contract.project.name}</name>`)
|
|
146
148
|
|
|
147
149
|
// Project description: prefer contract, fall back to package.json
|
|
148
150
|
const description = this.contract.project.description || this.meta.description
|
|
149
151
|
if (description) {
|
|
150
|
-
lines.push(
|
|
151
|
-
lines.push(description)
|
|
152
|
-
lines.push('')
|
|
152
|
+
lines.push(` <description>${description}</description>`)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Only list modules that have functions (skip empty ones)
|
|
@@ -159,32 +159,26 @@ export class ClaudeMdGenerator {
|
|
|
159
159
|
return fnCount > 0
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
lines.push(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const cleanDesc = desc.replace(/^\d+ functions\s*—\s*/, '')
|
|
169
|
-
const descStr = cleanDesc ? ` — ${cleanDesc}` : ''
|
|
170
|
-
lines.push(`- **${module.name}** (\`${module.id}\`): ${fnCount} functions${descStr}`)
|
|
171
|
-
}
|
|
172
|
-
lines.push('')
|
|
173
|
-
|
|
174
|
-
lines.push(`## Stats`)
|
|
175
|
-
lines.push(`- ${fileCount} files, ${functionCount} functions, ${nonEmptyModules.length} modules`)
|
|
176
|
-
lines.push(`- Language: ${this.contract.project.language}`)
|
|
177
|
-
lines.push('')
|
|
162
|
+
lines.push(` <stats>`)
|
|
163
|
+
lines.push(` <files>${fileCount}</files>`)
|
|
164
|
+
lines.push(` <functions>${functionCount}</functions>`)
|
|
165
|
+
lines.push(` <modules>${nonEmptyModules.length}</modules>`)
|
|
166
|
+
lines.push(` <language>${this.contract.project.language}</language>`)
|
|
167
|
+
lines.push(` </stats>`)
|
|
178
168
|
|
|
179
169
|
// Critical constraints summary
|
|
180
170
|
if (this.contract.declared.constraints.length > 0) {
|
|
181
|
-
lines.push('
|
|
171
|
+
lines.push(' <critical_constraints>')
|
|
182
172
|
for (const c of this.contract.declared.constraints) {
|
|
183
|
-
lines.push(
|
|
173
|
+
lines.push(` <constraint>${c}</constraint>`)
|
|
184
174
|
}
|
|
185
|
-
lines.push('')
|
|
175
|
+
lines.push(' </critical_constraints>')
|
|
186
176
|
}
|
|
187
177
|
|
|
178
|
+
lines.push('</repository_context>')
|
|
179
|
+
lines.push('')
|
|
180
|
+
lines.push('<modules>')
|
|
181
|
+
|
|
188
182
|
return lines.join('\n')
|
|
189
183
|
}
|
|
190
184
|
|
|
@@ -198,23 +192,22 @@ export class ClaudeMdGenerator {
|
|
|
198
192
|
const moduleFunctions = Object.values(this.lock.functions)
|
|
199
193
|
.filter(f => f.moduleId === moduleId)
|
|
200
194
|
|
|
201
|
-
lines.push(
|
|
195
|
+
lines.push(` <module id="${moduleId}">`)
|
|
196
|
+
lines.push(` <name>${module.name}</name>`)
|
|
202
197
|
|
|
203
198
|
// Location — collapse to common prefix when many paths share a root
|
|
204
199
|
if (module.paths.length > 0) {
|
|
205
200
|
const collapsed = this.collapsePaths(module.paths)
|
|
206
|
-
lines.push(
|
|
201
|
+
lines.push(` <location>${collapsed}</location>`)
|
|
207
202
|
}
|
|
208
203
|
|
|
209
204
|
// Intent
|
|
210
205
|
if (module.intent) {
|
|
211
|
-
lines.push(
|
|
206
|
+
lines.push(` <purpose>${module.intent}</purpose>`)
|
|
212
207
|
} else if (module.description) {
|
|
213
|
-
lines.push(
|
|
208
|
+
lines.push(` <purpose>${module.description}</purpose>`)
|
|
214
209
|
}
|
|
215
210
|
|
|
216
|
-
lines.push('')
|
|
217
|
-
|
|
218
211
|
// Entry points: functions with no calledBy (likely public API surface)
|
|
219
212
|
const entryPoints = moduleFunctions
|
|
220
213
|
.filter(fn => fn.calledBy.length === 0)
|
|
@@ -222,13 +215,13 @@ export class ClaudeMdGenerator {
|
|
|
222
215
|
.slice(0, 5)
|
|
223
216
|
|
|
224
217
|
if (entryPoints.length > 0) {
|
|
225
|
-
lines.push('
|
|
218
|
+
lines.push(' <entry_points>')
|
|
226
219
|
for (const fn of entryPoints) {
|
|
227
220
|
const sig = this.formatSignature(fn)
|
|
228
|
-
const purpose = fn.purpose ?
|
|
229
|
-
lines.push(`
|
|
221
|
+
const purpose = fn.purpose ? `${this.oneLine(fn.purpose)}` : ''
|
|
222
|
+
lines.push(` <function signature="${sig.replace(/"/g, '"')}" purpose="${purpose.replace(/"/g, '"')}" />`)
|
|
230
223
|
}
|
|
231
|
-
lines.push('')
|
|
224
|
+
lines.push(' </entry_points>')
|
|
232
225
|
}
|
|
233
226
|
|
|
234
227
|
// Key functions: top 5 by calledBy count (most depended upon)
|
|
@@ -238,13 +231,13 @@ export class ClaudeMdGenerator {
|
|
|
238
231
|
.slice(0, 5)
|
|
239
232
|
|
|
240
233
|
if (keyFunctions.length > 0) {
|
|
241
|
-
lines.push('
|
|
234
|
+
lines.push(' <key_internal_functions>')
|
|
242
235
|
for (const fn of keyFunctions) {
|
|
243
236
|
const callerCount = fn.calledBy.length
|
|
244
|
-
const purpose = fn.purpose ?
|
|
245
|
-
lines.push(`
|
|
237
|
+
const purpose = fn.purpose ? `${this.oneLine(fn.purpose)}` : ''
|
|
238
|
+
lines.push(` <function name="${fn.name.replace(/"/g, '"')}" callers="${callerCount}" purpose="${purpose.replace(/"/g, '"')}" />`)
|
|
246
239
|
}
|
|
247
|
-
lines.push('')
|
|
240
|
+
lines.push(' </key_internal_functions>')
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
// Dependencies: other modules this module imports from
|
|
@@ -263,8 +256,7 @@ export class ClaudeMdGenerator {
|
|
|
263
256
|
const mod = this.contract.declared.modules.find(m => m.id === id)
|
|
264
257
|
return mod?.name || id
|
|
265
258
|
})
|
|
266
|
-
lines.push(
|
|
267
|
-
lines.push('')
|
|
259
|
+
lines.push(` <depends_on>${depNames.join(', ')}</depends_on>`)
|
|
268
260
|
}
|
|
269
261
|
|
|
270
262
|
// Module-specific constraints
|
|
@@ -273,13 +265,14 @@ export class ClaudeMdGenerator {
|
|
|
273
265
|
c.toLowerCase().includes(module.name.toLowerCase())
|
|
274
266
|
)
|
|
275
267
|
if (moduleConstraints.length > 0) {
|
|
276
|
-
lines.push('
|
|
268
|
+
lines.push(' <module_constraints>')
|
|
277
269
|
for (const c of moduleConstraints) {
|
|
278
|
-
lines.push(`
|
|
270
|
+
lines.push(` <constraint>${c}</constraint>`)
|
|
279
271
|
}
|
|
280
|
-
lines.push('')
|
|
272
|
+
lines.push(' </module_constraints>')
|
|
281
273
|
}
|
|
282
274
|
|
|
275
|
+
lines.push(` </module>`)
|
|
283
276
|
return lines.join('\n')
|
|
284
277
|
}
|
|
285
278
|
|
|
@@ -520,9 +513,11 @@ export class ClaudeMdGenerator {
|
|
|
520
513
|
if (detected.length === 0) return null
|
|
521
514
|
|
|
522
515
|
const lines: string[] = []
|
|
523
|
-
lines.push('
|
|
524
|
-
|
|
525
|
-
|
|
516
|
+
lines.push('<tech_stack>')
|
|
517
|
+
for (const d of detected) {
|
|
518
|
+
lines.push(` <technology>${d}</technology>`)
|
|
519
|
+
}
|
|
520
|
+
lines.push('</tech_stack>')
|
|
526
521
|
return lines.join('\n')
|
|
527
522
|
}
|
|
528
523
|
|
|
@@ -554,12 +549,14 @@ export class ClaudeMdGenerator {
|
|
|
554
549
|
if (useful.length === 0) return null
|
|
555
550
|
|
|
556
551
|
const lines: string[] = []
|
|
557
|
-
lines.push('
|
|
552
|
+
lines.push('<commands>')
|
|
558
553
|
for (const [key, cmd] of useful) {
|
|
559
|
-
lines.push(
|
|
554
|
+
lines.push(` <command>`)
|
|
555
|
+
lines.push(` <run>${pm} ${key}</run>`)
|
|
556
|
+
lines.push(` <executes>${cmd.replace(/"/g, '"')}</executes>`)
|
|
557
|
+
lines.push(` </command>`)
|
|
560
558
|
}
|
|
561
|
-
|
|
562
|
-
lines.push('')
|
|
559
|
+
lines.push('</commands>')
|
|
563
560
|
return lines.join('\n')
|
|
564
561
|
}
|
|
565
562
|
|
package/tests/claude-md.test.ts
CHANGED
|
@@ -64,8 +64,8 @@ describe('ClaudeMdGenerator', () => {
|
|
|
64
64
|
test('generates valid markdown', () => {
|
|
65
65
|
const gen = new ClaudeMdGenerator(mockContract, mockLock)
|
|
66
66
|
const md = gen.generate()
|
|
67
|
-
expect(md).toContain('
|
|
68
|
-
expect(md).toContain('
|
|
67
|
+
expect(md).toContain('<name>TestProject</name>')
|
|
68
|
+
expect(md).toContain('<repository_context>')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
test('includes project description', () => {
|
|
@@ -77,8 +77,8 @@ describe('ClaudeMdGenerator', () => {
|
|
|
77
77
|
test('includes module sections', () => {
|
|
78
78
|
const gen = new ClaudeMdGenerator(mockContract, mockLock)
|
|
79
79
|
const md = gen.generate()
|
|
80
|
-
expect(md).toContain('
|
|
81
|
-
expect(md).toContain('
|
|
80
|
+
expect(md).toContain('<module id="auth">')
|
|
81
|
+
expect(md).toContain('<module id="api">')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
test('includes function names', () => {
|
|
@@ -105,7 +105,7 @@ describe('ClaudeMdGenerator', () => {
|
|
|
105
105
|
const gen = new ClaudeMdGenerator(mockContract, mockLock)
|
|
106
106
|
const md = gen.generate()
|
|
107
107
|
// API depends on Auth (handleLogin calls verifyToken)
|
|
108
|
-
expect(md).toContain('
|
|
108
|
+
expect(md).toContain('<depends_on>Authentication</depends_on>')
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
test('respects token budget', () => {
|
|
@@ -119,8 +119,8 @@ describe('ClaudeMdGenerator', () => {
|
|
|
119
119
|
test('includes stats', () => {
|
|
120
120
|
const gen = new ClaudeMdGenerator(mockContract, mockLock)
|
|
121
121
|
const md = gen.generate()
|
|
122
|
-
expect(md).toContain('3
|
|
123
|
-
expect(md).toContain('2
|
|
122
|
+
expect(md).toContain('<functions>3</functions>')
|
|
123
|
+
expect(md).toContain('<modules>2</modules>')
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
test('shows purpose when available', () => {
|
|
@@ -132,6 +132,86 @@ describe('ClaudeMdGenerator', () => {
|
|
|
132
132
|
test('shows calledBy count for key functions', () => {
|
|
133
133
|
const gen = new ClaudeMdGenerator(mockContract, mockLock)
|
|
134
134
|
const md = gen.generate()
|
|
135
|
-
expect(md).toContain('
|
|
135
|
+
expect(md).toContain('callers="1"')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('Edge Cases and Fault Tolerance', () => {
|
|
139
|
+
test('handles completely empty lock and contract without throwing', () => {
|
|
140
|
+
const emptyContract: MikkContract = {
|
|
141
|
+
version: '1',
|
|
142
|
+
project: { name: 'Empty', language: 'TS', description: '', entryPoints: [] },
|
|
143
|
+
declared: { modules: [], constraints: [], decisions: [] },
|
|
144
|
+
overwrite: { mode: 'never', requireConfirmation: false }
|
|
145
|
+
}
|
|
146
|
+
const emptyLock: MikkLock = {
|
|
147
|
+
version: '1', generatedAt: new Date().toISOString(), generatorVersion: '1', projectRoot: '', syncState: { status: 'clean', lastSyncAt: '', lockHash: '', contractHash: '' },
|
|
148
|
+
files: {}, functions: {}, classes: {}, modules: {}, graph: { nodes: 0, edges: 0, rootHash: '' }
|
|
149
|
+
}
|
|
150
|
+
const gen = new ClaudeMdGenerator(emptyContract, emptyLock)
|
|
151
|
+
const md = gen.generate()
|
|
152
|
+
expect(md).toContain('<name>Empty</name>')
|
|
153
|
+
expect(md).toContain('<modules>0</modules>')
|
|
154
|
+
expect(md).toContain('<functions>0</functions>')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('handles missing optional fields gracefully', () => {
|
|
158
|
+
const partialContract: MikkContract = {
|
|
159
|
+
version: '1',
|
|
160
|
+
project: { name: 'Partial', language: 'TS', description: '', entryPoints: [] },
|
|
161
|
+
declared: { modules: [{ id: 'core', name: 'Core', description: '', paths: [] }], constraints: [], decisions: [] },
|
|
162
|
+
overwrite: { mode: 'never', requireConfirmation: false }
|
|
163
|
+
}
|
|
164
|
+
const partialLock: MikkLock = {
|
|
165
|
+
version: '1', generatedAt: new Date().toISOString(), generatorVersion: '1', projectRoot: '', syncState: { status: 'clean', lastSyncAt: '', lockHash: '', contractHash: '' },
|
|
166
|
+
files: {},
|
|
167
|
+
functions: {
|
|
168
|
+
'f1': { id: 'f1', name: 'func', file: 'a.ts', startLine: 1, endLine: 2, hash: 'h', calls: [], calledBy: [], moduleId: 'core' }
|
|
169
|
+
},
|
|
170
|
+
classes: {},
|
|
171
|
+
modules: {
|
|
172
|
+
'core': { id: 'core', files: [], hash: 'h', fragmentPath: 'p' }
|
|
173
|
+
},
|
|
174
|
+
graph: { nodes: 0, edges: 0, rootHash: '' }
|
|
175
|
+
}
|
|
176
|
+
const gen = new ClaudeMdGenerator(partialContract, partialLock)
|
|
177
|
+
const md = gen.generate()
|
|
178
|
+
expect(md).toContain('<name>Core</name>')
|
|
179
|
+
expect(md).toContain('func')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('handles circular dependencies between functions gracefully', () => {
|
|
183
|
+
const circLock: MikkLock = {
|
|
184
|
+
...mockLock,
|
|
185
|
+
functions: {
|
|
186
|
+
'fn:a': { id: 'fn:a', name: 'A', file: 'a.ts', startLine: 1, endLine: 2, hash: 'h', calls: ['fn:b'], calledBy: ['fn:b'], moduleId: 'auth' },
|
|
187
|
+
'fn:b': { id: 'fn:b', name: 'B', file: 'b.ts', startLine: 1, endLine: 2, hash: 'h', calls: ['fn:a'], calledBy: ['fn:a'], moduleId: 'auth' },
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const gen = new ClaudeMdGenerator(mockContract, circLock)
|
|
191
|
+
const md = gen.generate()
|
|
192
|
+
expect(md).toContain('A')
|
|
193
|
+
expect(md).toContain('B')
|
|
194
|
+
expect(md.length).toBeLessThan(10000)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('truncates extremely strict token budgets without losing core layout', () => {
|
|
198
|
+
const gen = new ClaudeMdGenerator(mockContract, mockLock, 50)
|
|
199
|
+
const md = gen.generate()
|
|
200
|
+
expect(md).toContain('<repository_context>')
|
|
201
|
+
expect(md).not.toContain('Use JWT')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('handles foreign or orphaned modules robustly', () => {
|
|
205
|
+
const orphanedLock: MikkLock = {
|
|
206
|
+
...mockLock,
|
|
207
|
+
functions: {
|
|
208
|
+
'fn:orphan': { id: 'fn:orphan', name: 'OrphanFn', file: 'o.ts', startLine: 1, endLine: 2, hash: 'h', calls: [], calledBy: [], moduleId: 'unknown-module' }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const gen = new ClaudeMdGenerator(mockContract, orphanedLock)
|
|
212
|
+
const md = gen.generate()
|
|
213
|
+
// Unknown module functions are typically skipped entirely. The generator should not crash.
|
|
214
|
+
expect(md).not.toContain('OrphanFn')
|
|
215
|
+
})
|
|
136
216
|
})
|
|
137
217
|
})
|