contextforge-cli-ai-prompt-pirates 0.4.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/.contextforge/config.ai.example.json +28 -0
- package/.contextforge/config.example.json +29 -0
- package/.contextforge/prompts/README.md +50 -0
- package/.env.example +73 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/contextforge.js +14 -0
- package/bin/postinstall-worker.js +14 -0
- package/bin/postinstall.js +26 -0
- package/package.json +65 -0
- package/prompts/README.md +50 -0
- package/prompts/response-schema.md +22 -0
- package/prompts/retry-addon.md +1 -0
- package/prompts/system.md +10 -0
- package/prompts/user-template.md +22 -0
- package/src/analyzers/ast-parser.js +139 -0
- package/src/analyzers/bugs-lite.js +118 -0
- package/src/analyzers/changelog.js +190 -0
- package/src/analyzers/code-insights.js +225 -0
- package/src/analyzers/dependencies.js +110 -0
- package/src/analyzers/detection-quality.js +106 -0
- package/src/analyzers/eslint-runner.js +56 -0
- package/src/analyzers/folder-tree.js +60 -0
- package/src/analyzers/git-insights.js +94 -0
- package/src/analyzers/project-meta.js +98 -0
- package/src/cli/commands/changes.js +36 -0
- package/src/cli/commands/doctor.js +152 -0
- package/src/cli/commands/generate.js +7 -0
- package/src/cli/commands/init.js +98 -0
- package/src/cli/commands/prompt.js +53 -0
- package/src/cli/commands/stop.js +10 -0
- package/src/cli/commands/summary.js +22 -0
- package/src/cli/commands/watch.js +9 -0
- package/src/cli/index.js +120 -0
- package/src/core/confidence.js +51 -0
- package/src/core/config.js +183 -0
- package/src/core/cursor-rules.js +293 -0
- package/src/core/ensure-setup.js +45 -0
- package/src/core/env.js +113 -0
- package/src/core/package-meta.js +35 -0
- package/src/core/paths.js +39 -0
- package/src/core/pipeline.js +256 -0
- package/src/core/postinstall.js +168 -0
- package/src/core/setup-env.js +86 -0
- package/src/core/setup-prompts.js +31 -0
- package/src/core/snapshot-hash.js +70 -0
- package/src/detectors/architecture.js +117 -0
- package/src/detectors/auth-deploy.js +51 -0
- package/src/detectors/database.js +127 -0
- package/src/detectors/tech-stack.js +180 -0
- package/src/generators/artifacts.js +44 -0
- package/src/generators/changes-markdown.js +72 -0
- package/src/generators/context-json.js +91 -0
- package/src/generators/context-markdown.js +571 -0
- package/src/generators/mermaid.js +59 -0
- package/src/index.js +13 -0
- package/src/scanner/change-detector.js +24 -0
- package/src/scanner/config-files.js +52 -0
- package/src/scanner/file-index.js +55 -0
- package/src/scanner/package-deps.js +32 -0
- package/src/scanner/repo-scanner.js +143 -0
- package/src/services/ai/background.js +87 -0
- package/src/services/ai/config.js +48 -0
- package/src/services/ai/enrich.js +87 -0
- package/src/services/ai/index.js +3 -0
- package/src/services/ai/prompt-loader.js +104 -0
- package/src/services/ai/prompt.js +42 -0
- package/src/services/ai/providers/groq.js +36 -0
- package/src/services/ai/providers/openai.js +32 -0
- package/src/services/ai/redact.js +33 -0
- package/src/services/ai/validate.js +70 -0
- package/src/watcher/watcher.js +128 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { getOutputPath, OUTPUT_FILES } from '../core/paths.js';
|
|
3
|
+
import { renderTreeAscii } from '../analyzers/folder-tree.js';
|
|
4
|
+
import { confidenceBadge } from '../core/confidence.js';
|
|
5
|
+
import { buildMermaidDiagrams, wrapMermaid } from './mermaid.js';
|
|
6
|
+
|
|
7
|
+
function joinSection(title, lines) {
|
|
8
|
+
const body = lines.filter((l) => l !== null && l !== undefined && l !== '').join('\n');
|
|
9
|
+
if (!body.trim()) return '';
|
|
10
|
+
return `## ${title}\n\n${body}\n\n`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bullets(items, empty = '_None detected_') {
|
|
14
|
+
if (!items?.length) return empty;
|
|
15
|
+
return items.map((i) => `- ${i}`).join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detailed context.md aligned with requirements §7.9 and §8.
|
|
20
|
+
*/
|
|
21
|
+
export async function renderContextMarkdown(projectRoot, snapshot) {
|
|
22
|
+
const ts = snapshot.techStack;
|
|
23
|
+
const arch = snapshot.architecture;
|
|
24
|
+
const db = snapshot.database;
|
|
25
|
+
const auth = snapshot.authDeploy;
|
|
26
|
+
const git = snapshot.git;
|
|
27
|
+
const bugs = snapshot.bugs;
|
|
28
|
+
const deps = snapshot.dependencies;
|
|
29
|
+
const code = snapshot.codeInsights;
|
|
30
|
+
const conf = snapshot.confidence || {};
|
|
31
|
+
const meta = snapshot.projectMeta;
|
|
32
|
+
const mermaid = buildMermaidDiagrams(snapshot);
|
|
33
|
+
|
|
34
|
+
const treeAscii = renderTreeAscii(snapshot.folderTree, '', true, 0, snapshot.config?.maxTreeDepth || 4);
|
|
35
|
+
|
|
36
|
+
const frontendLine = formatStackLine(ts.frontend, ts.styling);
|
|
37
|
+
const backendLine = formatStackLine(ts.backend, ts.http);
|
|
38
|
+
const databaseLine = formatStackLine([...ts.database, ...ts.orm]);
|
|
39
|
+
const stateLine = ts.state?.length ? bullets(ts.state) : null;
|
|
40
|
+
const authLine = formatStackLine(auth.authentication.length ? auth.authentication : ts.auth);
|
|
41
|
+
const deployLine = formatStackLine(auth.deployment.length ? auth.deployment : ts.deploy);
|
|
42
|
+
|
|
43
|
+
let md = `# Project Context
|
|
44
|
+
|
|
45
|
+
> **ContextForge** — AI-ready repository memory
|
|
46
|
+
> Generated: ${snapshot.generatedAt}
|
|
47
|
+
> Project: **${snapshot.projectName}**
|
|
48
|
+
> Use with ChatGPT, Claude, Cursor, GitHub Copilot
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
md += joinSection('Project Overview', [
|
|
55
|
+
snapshot.description ? `**Description:** ${snapshot.description}` : null,
|
|
56
|
+
`**Root:** \`${snapshot.projectRoot}\``,
|
|
57
|
+
meta?.packageVersion ? `**Version:** ${meta.packageVersion}` : null,
|
|
58
|
+
meta?.engines ? `**Engines:** ${meta.engines}` : null,
|
|
59
|
+
meta?.workspaces ? `**Workspaces:** ${meta.workspaces}` : null,
|
|
60
|
+
`**Files scanned:** ${snapshot.filesScanned}`,
|
|
61
|
+
`**Detection confidence:** ${ts.confidence}`,
|
|
62
|
+
conf.overall ? `**Overall accuracy:** ${confidenceBadge(conf.overall)}` : null,
|
|
63
|
+
code?.projectStructure?.length
|
|
64
|
+
? `**Key directories:** ${code.projectStructure.join(', ')}`
|
|
65
|
+
: snapshot.projectDirs?.length
|
|
66
|
+
? `**Detected dirs:** ${snapshot.projectDirs.join(', ')}`
|
|
67
|
+
: null,
|
|
68
|
+
code?.summary ? `\n${code.summary}` : null,
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
md += renderDetectionReliability(snapshot.detectionQuality, ts);
|
|
72
|
+
md += renderQuickReference(snapshot);
|
|
73
|
+
md += renderProjectStatistics(code, snapshot.ast);
|
|
74
|
+
md += renderTechStackDetailed(ts);
|
|
75
|
+
|
|
76
|
+
if (snapshot.changelog) {
|
|
77
|
+
const ch = snapshot.changelog;
|
|
78
|
+
md += joinSection('What Changed Since Last Run', [
|
|
79
|
+
`**Last run:** ${ch.runAt}`,
|
|
80
|
+
ch.previousRunAt ? `**Previous run:** ${ch.previousRunAt}` : null,
|
|
81
|
+
`**Status:** ${ch.hasChanges ? 'Changes detected' : 'No changes'}`,
|
|
82
|
+
ch.summary,
|
|
83
|
+
ch.addedFiles?.length
|
|
84
|
+
? `**New files (${ch.addedFiles.length}):** ${ch.addedFiles.slice(0, 8).map((f) => `\`${f}\``).join(', ')}${ch.addedFiles.length > 8 ? '...' : ''}`
|
|
85
|
+
: null,
|
|
86
|
+
ch.modifiedFiles?.length
|
|
87
|
+
? `**Modified (${ch.modifiedFiles.length}):** ${ch.modifiedFiles.slice(0, 8).map((f) => `\`${f}\``).join(', ')}${ch.modifiedFiles.length > 8 ? '...' : ''}`
|
|
88
|
+
: null,
|
|
89
|
+
ch.removedFiles?.length
|
|
90
|
+
? `**Removed (${ch.removedFiles.length}):** ${ch.removedFiles.slice(0, 5).map((f) => `\`${f}\``).join(', ')}`
|
|
91
|
+
: null,
|
|
92
|
+
ch.techStackChanged?.length ? bullets(ch.techStackChanged) : null,
|
|
93
|
+
ch.architectureChanged ? `**Architecture:** ${ch.architectureChanged}` : null,
|
|
94
|
+
'_See `.contextforge/CHANGES.md` for full history._',
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (conf.overall) {
|
|
99
|
+
md += joinSection('Accuracy Confidence', [
|
|
100
|
+
`| Area | Confidence |`,
|
|
101
|
+
`|------|------------|`,
|
|
102
|
+
`| Tech stack | ${confidenceBadge(conf.techStack)} |`,
|
|
103
|
+
`| Architecture | ${confidenceBadge(conf.architecture)} |`,
|
|
104
|
+
`| Database | ${confidenceBadge(conf.database)} |`,
|
|
105
|
+
`| API routes | ${confidenceBadge(conf.apiRoutes)} |`,
|
|
106
|
+
`| Git history | ${confidenceBadge(conf.git)} |`,
|
|
107
|
+
`| Issues detected | ${confidenceBadge(conf.bugs)} |`,
|
|
108
|
+
`| **Overall** | **${confidenceBadge(conf.overall)}** |`,
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
md += renderAiEnrichmentSections(snapshot.aiEnrichment);
|
|
113
|
+
|
|
114
|
+
md += joinSection('Frontend', [
|
|
115
|
+
frontendLine || '_No frontend framework detected from dependencies._',
|
|
116
|
+
ts.styling?.length ? `**Styling:** ${ts.styling.join(', ')}` : null,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
md += joinSection('Backend', [
|
|
120
|
+
backendLine || '_No backend framework detected._',
|
|
121
|
+
ts.http?.length ? `**HTTP client:** ${ts.http.join(', ')}` : null,
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
md += joinSection('Database', [
|
|
125
|
+
databaseLine || '_No database/ORM detected._',
|
|
126
|
+
db.provider ? `**Provider:** ${db.provider}` : null,
|
|
127
|
+
db.migrations?.length ? `**Migrations path:** ${db.migrations.slice(0, 3).join(', ')}` : null,
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (stateLine) {
|
|
131
|
+
md += joinSection('State Management', [stateLine]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (ts.testing?.length) {
|
|
135
|
+
md += joinSection('Testing', [bullets(ts.testing)]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
md += joinSection('Architecture', [
|
|
139
|
+
`**Pattern:** ${arch.primary.displayLabel} (${arch.primary.confidence} confidence)`,
|
|
140
|
+
arch.allMatches.length > 1
|
|
141
|
+
? `**Also matches:** ${arch.allMatches.map((m) => m.label).join(', ')}`
|
|
142
|
+
: null,
|
|
143
|
+
arch.folderHierarchy?.length
|
|
144
|
+
? `**Top-level layout:** ${arch.folderHierarchy.slice(0, 12).join(', ')}`
|
|
145
|
+
: null,
|
|
146
|
+
mermaid.architectureFlow ? wrapMermaid(mermaid.architectureFlow) : null,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
md += joinSection('API Flow', [
|
|
150
|
+
arch.apiFlow ? `**Primary flow:** ${arch.apiFlow}` : null,
|
|
151
|
+
arch.dependencyFlow ? `**Dependency flow:** ${arch.dependencyFlow}` : null,
|
|
152
|
+
code?.pipelineFlow ? `**Inferred pipeline:** ${code.pipelineFlow}` : null,
|
|
153
|
+
renderRoutesTable(code?.apiEndpoints, snapshot.ast?.routes),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
if (code?.modules?.middleware?.length) {
|
|
157
|
+
md += joinSection('Middleware', [
|
|
158
|
+
bullets(
|
|
159
|
+
code.modules.middleware.map((m) => `\`${m.file}\`${m.name ? ` — ${m.name}` : ''}`)
|
|
160
|
+
),
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (code?.modules?.components?.length) {
|
|
165
|
+
md += joinSection('UI Components', [
|
|
166
|
+
...code.modules.components.slice(0, 15).map((c) => {
|
|
167
|
+
const names = c.components?.length ? ` (${c.components.join(', ')})` : '';
|
|
168
|
+
return `- \`${c.file}\`${names}`;
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (meta?.scripts?.length) {
|
|
174
|
+
md += joinSection('npm Scripts & Commands', [
|
|
175
|
+
'| Script | Command |',
|
|
176
|
+
'|--------|---------|',
|
|
177
|
+
...meta.scripts.map((s) => `| \`${s.name}\` | \`${s.command.replace(/\|/g, '\\|')}\` |`),
|
|
178
|
+
'',
|
|
179
|
+
'_Run these from the project root._',
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (meta?.entryPoints?.length) {
|
|
184
|
+
md += joinSection('Entry Points', [
|
|
185
|
+
bullets(
|
|
186
|
+
meta.entryPoints.map(
|
|
187
|
+
(e) => `**${e.type}**${e.name ? ` (${e.name})` : ''}: \`${e.path}\``
|
|
188
|
+
)
|
|
189
|
+
),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (meta?.envVarNames?.length) {
|
|
194
|
+
md += joinSection('Environment Variables', [
|
|
195
|
+
'_Variable names from `.env.example` (values are never read)._',
|
|
196
|
+
'',
|
|
197
|
+
bullets(meta.envVarNames.map((k) => `\`${k}\``)),
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (meta?.dependenciesSummary?.production > 0) {
|
|
202
|
+
md += joinSection('Dependencies Overview', [
|
|
203
|
+
`**Production:** ${meta.dependenciesSummary.production} packages`,
|
|
204
|
+
`**Development:** ${meta.dependenciesSummary.development} packages`,
|
|
205
|
+
meta.dependenciesSummary.topProduction?.length
|
|
206
|
+
? `**Key production deps:** ${meta.dependenciesSummary.topProduction.join(', ')}`
|
|
207
|
+
: null,
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
md += joinSection('Folder Structure', [
|
|
212
|
+
treeAscii ? `\`\`\`\n${treeAscii}\n\`\`\`` : '_No folder tree available._',
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
if (db.models?.length) {
|
|
216
|
+
md += joinSection('Database Schemas', [
|
|
217
|
+
db.orm ? `**ORM:** ${db.orm}` : null,
|
|
218
|
+
db.enums?.length ? `**Enums:** ${db.enums.join(', ')}` : null,
|
|
219
|
+
'**Entity diagram:**',
|
|
220
|
+
'```',
|
|
221
|
+
db.textDiagram || '(see models in schema files)',
|
|
222
|
+
'```',
|
|
223
|
+
...db.models.slice(0, 8).map((m) => {
|
|
224
|
+
const rel = m.relations?.length ? ` → relates to: ${m.relations.join(', ')}` : '';
|
|
225
|
+
return `- **${m.name}**: ${(m.fields || []).join(', ')}${rel}`;
|
|
226
|
+
}),
|
|
227
|
+
mermaid.databaseEr ? wrapMermaid(mermaid.databaseEr) : null,
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
md += joinSection('Authentication', [
|
|
232
|
+
authLine || '_No auth library detected._',
|
|
233
|
+
auth.hints?.length ? bullets(auth.hints) : null,
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
md += joinSection('Deployment', [
|
|
237
|
+
deployLine || '_No deployment platform detected._',
|
|
238
|
+
snapshot.configFiles?.filter((p) => /docker|vercel|netlify|nginx/i.test(p)).length
|
|
239
|
+
? `**Config:** ${snapshot.configFiles.filter((p) => /docker|vercel|netlify|nginx/i.test(p)).join(', ')}`
|
|
240
|
+
: null,
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
md += joinSection('Business Logic Summary', [
|
|
244
|
+
code?.summary || '_No dedicated business-logic layer identified._',
|
|
245
|
+
code?.modules?.services?.length
|
|
246
|
+
? `**Services (${code.modules.services.length}):** ${code.modules.services.slice(0, 8).map((s) => `\`${s.file}\``).join(', ')}`
|
|
247
|
+
: null,
|
|
248
|
+
code?.modules?.controllers?.length
|
|
249
|
+
? `**Controllers (${code.modules.controllers.length}):** ${code.modules.controllers.slice(0, 6).map((c) => `\`${c.file}\``).join(', ')}`
|
|
250
|
+
: null,
|
|
251
|
+
code?.businessLogic?.length
|
|
252
|
+
? [
|
|
253
|
+
'**Exported service functions:**',
|
|
254
|
+
bullets(code.businessLogic.slice(0, 12).map((b) => `${b.name} (\`${b.file}\`)`)),
|
|
255
|
+
].join('\n')
|
|
256
|
+
: null,
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
md += joinSection('Dependency Relationships', [
|
|
260
|
+
deps?.summary || null,
|
|
261
|
+
deps?.chains?.length
|
|
262
|
+
? ['**Inferred chains:**', bullets(deps.chains)].join('\n')
|
|
263
|
+
: null,
|
|
264
|
+
deps?.flowHints?.length ? bullets(deps.flowHints) : null,
|
|
265
|
+
deps?.edges?.length
|
|
266
|
+
? [
|
|
267
|
+
'**Sample import graph:**',
|
|
268
|
+
...deps.edges.slice(0, 8).map(
|
|
269
|
+
(e) => `- \`${e.file}\`${e.role ? ` (${e.role})` : ''} → ${e.imports.slice(0, 4).join(', ')}`
|
|
270
|
+
),
|
|
271
|
+
].join('\n')
|
|
272
|
+
: null,
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
if (git.available || git.insights?.length) {
|
|
276
|
+
md += joinSection('Git Intelligence', [
|
|
277
|
+
git.branch ? `**Current branch:** ${git.branch}` : null,
|
|
278
|
+
git.narratives?.length
|
|
279
|
+
? ['**Timeline insights:**', bullets(git.narratives)].join('\n')
|
|
280
|
+
: null,
|
|
281
|
+
git.insights?.length ? bullets(git.insights) : null,
|
|
282
|
+
git.packageChanges ? `- ${git.packageChanges}` : null,
|
|
283
|
+
git.timeline?.length
|
|
284
|
+
? [
|
|
285
|
+
'**Recent timeline:**',
|
|
286
|
+
'',
|
|
287
|
+
'| Date | Event |',
|
|
288
|
+
'|------|-------|',
|
|
289
|
+
...git.timeline.map((t) => `| ${t.date} | ${t.event.replace(/\|/g, '/')} |`),
|
|
290
|
+
].join('\n')
|
|
291
|
+
: null,
|
|
292
|
+
git.commits?.length
|
|
293
|
+
? [
|
|
294
|
+
'**Recent commits:**',
|
|
295
|
+
'',
|
|
296
|
+
'| Commit | Date | Author | Message |',
|
|
297
|
+
'|--------|------|--------|---------|',
|
|
298
|
+
...git.commits.slice(0, 12).map(
|
|
299
|
+
(c) =>
|
|
300
|
+
`| ${c.hash} | ${c.date?.slice(0, 10) || ''} | ${c.author} | ${c.message.replace(/\|/g, '/').slice(0, 55)} |`
|
|
301
|
+
),
|
|
302
|
+
].join('\n')
|
|
303
|
+
: null,
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
md += joinSection('Possible Issues', [
|
|
308
|
+
bugs.summary,
|
|
309
|
+
bugs.issues?.length
|
|
310
|
+
? bugs.issues.map((i) => `- **[${i.severity}]** ${i.message} — \`${i.file}\``).join('\n')
|
|
311
|
+
: '- No issues flagged by static heuristics.',
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
md += joinSection('Suggested Improvements', [
|
|
315
|
+
bullets(generateSuggestions(snapshot)),
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
const standards = [];
|
|
319
|
+
if (snapshot.configFiles?.some((p) => /eslint/i.test(p))) {
|
|
320
|
+
standards.push('ESLint is configured — run linter before commits');
|
|
321
|
+
}
|
|
322
|
+
if (snapshot.configFiles?.some((p) => /prettier/i.test(p))) {
|
|
323
|
+
standards.push('Prettier is configured — match existing formatting');
|
|
324
|
+
}
|
|
325
|
+
if (ts.confidence === 'high') {
|
|
326
|
+
standards.push('Tech stack detected from package.json — prefer existing libraries over new deps');
|
|
327
|
+
}
|
|
328
|
+
if (code?.fileStats?.services) {
|
|
329
|
+
standards.push('Place reusable logic in services/, not routes or controllers');
|
|
330
|
+
}
|
|
331
|
+
if (standards.length) {
|
|
332
|
+
md += joinSection('Coding Standards', [bullets(standards)]);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (code?.patterns?.length) {
|
|
336
|
+
md += joinSection('Code Patterns Detected', [bullets(code.patterns)]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (code?.importantRules?.length) {
|
|
340
|
+
md += joinSection('Important Rules', [bullets(code.importantRules)]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (snapshot.configFiles?.length) {
|
|
344
|
+
md += joinSection('Key Config Files', [
|
|
345
|
+
bullets(snapshot.configFiles.slice(0, 25)),
|
|
346
|
+
'',
|
|
347
|
+
'_Sensitive files (.env, keys) are never read by ContextForge._',
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
md += `---
|
|
352
|
+
|
|
353
|
+
## How to Use This File
|
|
354
|
+
|
|
355
|
+
1. Add to **Cursor rules**: _Read \`.contextforge/context.md\` before coding._
|
|
356
|
+
2. Run \`npx contextforge prompt\` to paste into any AI chat.
|
|
357
|
+
3. Regenerate: \`npx contextforge generate\` or \`npx contextforge watch\`
|
|
358
|
+
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
await fs.writeFile(getOutputPath(projectRoot, OUTPUT_FILES.contextMd), md, 'utf8');
|
|
362
|
+
return md;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderAiEnrichmentSections(ai) {
|
|
366
|
+
if (!ai?.sections) return '';
|
|
367
|
+
|
|
368
|
+
const s = ai.sections;
|
|
369
|
+
const header = `*Enriched by **${ai.provider}** (\`${ai.model}\`) at ${ai.generatedAt}*`;
|
|
370
|
+
|
|
371
|
+
let out = `## AI-Enhanced Analysis\n\n${header}\n\n`;
|
|
372
|
+
|
|
373
|
+
const blocks = [
|
|
374
|
+
['Executive Summary', s.executiveSummary],
|
|
375
|
+
['Tech Stack (AI Narrative)', s.techStackNarrative],
|
|
376
|
+
['Architecture Deep Dive', s.architectureDeepDive],
|
|
377
|
+
['API & Data Flow', s.apiAndDataFlow],
|
|
378
|
+
['Business Logic Analysis', s.businessLogicAnalysis],
|
|
379
|
+
['Database Analysis', s.databaseAnalysis],
|
|
380
|
+
['Authentication Notes', s.authenticationNotes],
|
|
381
|
+
['Deployment Notes', s.deploymentNotes],
|
|
382
|
+
['Git Evolution', s.gitEvolution],
|
|
383
|
+
['AI — Important Rules', Array.isArray(s.importantRules) ? bullets(s.importantRules) : s.importantRules],
|
|
384
|
+
['AI — Coding Conventions', Array.isArray(s.codingConventions) ? bullets(s.codingConventions) : s.codingConventions],
|
|
385
|
+
['AI — Possible Issues', Array.isArray(s.possibleIssues) ? bullets(s.possibleIssues) : s.possibleIssues],
|
|
386
|
+
['AI — Suggested Improvements', Array.isArray(s.suggestedImprovements) ? bullets(s.suggestedImprovements) : s.suggestedImprovements],
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
for (const [title, body] of blocks) {
|
|
390
|
+
if (body && String(body).trim()) {
|
|
391
|
+
out += joinSection(title, [typeof body === 'string' ? body : body]);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function renderDetectionReliability(quality, ts) {
|
|
399
|
+
if (!quality) return '';
|
|
400
|
+
|
|
401
|
+
const lines = [
|
|
402
|
+
`**Detection score:** ${quality.score}/100 (${quality.grade} reliability)`,
|
|
403
|
+
quality.summary,
|
|
404
|
+
ts.packageJsonCount > 1
|
|
405
|
+
? `**Monorepo:** ${ts.packageJsonCount} package.json file(s) merged for stack detection`
|
|
406
|
+
: '_Single package.json used for dependency detection._',
|
|
407
|
+
quality.reliableForAi
|
|
408
|
+
? '✓ **Suitable for AI context** — scan evidence is consistent.'
|
|
409
|
+
: '⚠ **Verify manually** — some sections may be incomplete before trusting AI output.',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
if (quality.strengths?.length) {
|
|
413
|
+
lines.push('', '**What was detected reliably:**', bullets(quality.strengths));
|
|
414
|
+
}
|
|
415
|
+
if (quality.warnings?.length) {
|
|
416
|
+
lines.push('', '**Verify or improve:**', bullets(quality.warnings));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
lines.push(
|
|
420
|
+
'',
|
|
421
|
+
'_Accuracy depends on: `package.json`, folder layout, model/schema files, and route parsing. Re-run `generate` after changing stack._'
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return joinSection('Detection Reliability (read this first)', lines);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderQuickReference(snapshot) {
|
|
428
|
+
const ts = snapshot.techStack;
|
|
429
|
+
const arch = snapshot.architecture;
|
|
430
|
+
const db = snapshot.database;
|
|
431
|
+
const auth = snapshot.authDeploy;
|
|
432
|
+
|
|
433
|
+
const rows = [
|
|
434
|
+
['Frontend', [...(ts.frontend || []), ...(ts.styling || [])].join(', ') || '—'],
|
|
435
|
+
['Backend', (ts.backend || []).join(', ') || '—'],
|
|
436
|
+
['Database / ORM', [...(ts.database || []), ...(ts.orm || [])].join(', ') || '—'],
|
|
437
|
+
['State', (ts.state || []).join(', ') || '—'],
|
|
438
|
+
[
|
|
439
|
+
'Auth',
|
|
440
|
+
[...new Set([...(auth.authentication || []), ...(ts.auth || [])])].join(', ') || '—',
|
|
441
|
+
],
|
|
442
|
+
['Deploy', [...(auth.deployment || []), ...(ts.deploy || [])].join(', ') || '—'],
|
|
443
|
+
['Architecture', arch?.primary?.label || '—'],
|
|
444
|
+
['API style', arch?.apiFlow || snapshot.codeInsights?.pipelineFlow || '—'],
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
return joinSection('At a Glance (AI Quick Reference)', [
|
|
448
|
+
'_Copy this table into any AI chat for instant project context._',
|
|
449
|
+
'',
|
|
450
|
+
'| Area | Detected |',
|
|
451
|
+
'|------|----------|',
|
|
452
|
+
...rows.map(([a, b]) => `| **${a}** | ${b} |`),
|
|
453
|
+
]);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function renderProjectStatistics(code, ast) {
|
|
457
|
+
if (!code?.fileStats && !ast?.routesCount) return '';
|
|
458
|
+
const fs = code?.fileStats || {};
|
|
459
|
+
const rows = [
|
|
460
|
+
`| Metric | Count |`,
|
|
461
|
+
`|--------|-------|`,
|
|
462
|
+
fs.totalSource ? `| Source files analyzed | ${fs.totalSource} |` : null,
|
|
463
|
+
fs.routes ? `| Route modules | ${fs.routes} |` : null,
|
|
464
|
+
fs.services ? `| Service modules | ${fs.services} |` : null,
|
|
465
|
+
fs.components ? `| Component files | ${fs.components} |` : null,
|
|
466
|
+
ast?.filesParsed ? `| AST-parsed files | ${ast.filesParsed} |` : null,
|
|
467
|
+
ast?.routesCount ? `| Routes (AST) | ${ast.routesCount} |` : null,
|
|
468
|
+
code?.apiEndpoints?.length ? `| API endpoints (total) | ${code.apiEndpoints.length} |` : null,
|
|
469
|
+
code?.businessLogic?.length
|
|
470
|
+
? `| Exported service functions | ${code.businessLogic.length} |`
|
|
471
|
+
: null,
|
|
472
|
+
].filter(Boolean);
|
|
473
|
+
if (rows.length <= 2) return '';
|
|
474
|
+
return joinSection('Project Statistics', rows);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderTechStackDetailed(ts) {
|
|
478
|
+
const categories = [
|
|
479
|
+
['Frontend', ts.frontend],
|
|
480
|
+
['Backend', ts.backend],
|
|
481
|
+
['Database', ts.database],
|
|
482
|
+
['ORM', ts.orm],
|
|
483
|
+
['Styling', ts.styling],
|
|
484
|
+
['State', ts.state],
|
|
485
|
+
['Testing', ts.testing],
|
|
486
|
+
['Authentication', ts.auth],
|
|
487
|
+
['HTTP client', ts.http],
|
|
488
|
+
['Deployment', ts.deploy],
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
const lines = [
|
|
492
|
+
'| Category | Technologies |',
|
|
493
|
+
'|----------|--------------|',
|
|
494
|
+
...categories
|
|
495
|
+
.filter(([, items]) => items?.length)
|
|
496
|
+
.map(([cat, items]) => `| ${cat} | ${items.join(', ')} |`),
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
if (lines.length <= 1) return '';
|
|
500
|
+
|
|
501
|
+
const signalBlock =
|
|
502
|
+
ts.signals?.length > 0
|
|
503
|
+
? ['', '**Detection signals:**', bullets(ts.signals.slice(0, 15)), ts.signals.length > 15 ? '_…and more_' : null]
|
|
504
|
+
: [];
|
|
505
|
+
|
|
506
|
+
return joinSection('Tech Stack (Detailed)', [...lines, ...signalBlock.filter((x) => x != null)]);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function renderRoutesTable(endpoints = [], astRoutes = []) {
|
|
510
|
+
const merged = dedupeRoutesForTable(endpoints, astRoutes);
|
|
511
|
+
if (!merged.length) {
|
|
512
|
+
return '_No explicit HTTP routes parsed — may be CLI, library, or serverless._';
|
|
513
|
+
}
|
|
514
|
+
return [
|
|
515
|
+
'**Registered routes / handlers:**',
|
|
516
|
+
'',
|
|
517
|
+
'| Method | Path | File | Source |',
|
|
518
|
+
'|--------|------|------|--------|',
|
|
519
|
+
...merged.slice(0, 30).map(
|
|
520
|
+
(e) =>
|
|
521
|
+
`| ${e.method} | ${e.path} | \`${e.file}\` | ${e.source || 'scan'} |`
|
|
522
|
+
),
|
|
523
|
+
merged.length > 30 ? `\n_+ ${merged.length - 30} more routes not shown_` : null,
|
|
524
|
+
]
|
|
525
|
+
.filter(Boolean)
|
|
526
|
+
.join('\n');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function dedupeRoutesForTable(endpoints, astRoutes) {
|
|
530
|
+
const list = [...(endpoints || [])];
|
|
531
|
+
for (const r of astRoutes || []) {
|
|
532
|
+
list.push({
|
|
533
|
+
method: r.method,
|
|
534
|
+
path: r.path,
|
|
535
|
+
file: r.file,
|
|
536
|
+
source: 'ast',
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const seen = new Set();
|
|
540
|
+
return list.filter((e) => {
|
|
541
|
+
const key = `${e.method}:${e.path}:${e.file}`;
|
|
542
|
+
if (seen.has(key)) return false;
|
|
543
|
+
seen.add(key);
|
|
544
|
+
return true;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatStackLine(primary, extra = []) {
|
|
549
|
+
const all = [...(primary || []), ...(extra || [])];
|
|
550
|
+
const unique = [...new Set(all)];
|
|
551
|
+
return unique.length ? unique.join(' + ') : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function generateSuggestions(snapshot) {
|
|
555
|
+
const tips = [
|
|
556
|
+
'Keep controllers thin; business logic belongs in services',
|
|
557
|
+
'Use consistent validation (Zod/Joi) at API boundaries',
|
|
558
|
+
'Reference this file in AI sessions to avoid re-explaining the stack',
|
|
559
|
+
];
|
|
560
|
+
const ts = snapshot.techStack;
|
|
561
|
+
if (ts.confidence === 'low') {
|
|
562
|
+
tips.push('Add explicit dependencies to package.json for richer detection');
|
|
563
|
+
}
|
|
564
|
+
if (!snapshot.database?.models?.length && (ts.database.length || ts.orm.length)) {
|
|
565
|
+
tips.push('Add schema.prisma or model files for database section accuracy');
|
|
566
|
+
}
|
|
567
|
+
if (snapshot.bugs?.issues?.some((i) => i.severity === 'high')) {
|
|
568
|
+
tips.push('Review high-severity issues in Possible Issues section first');
|
|
569
|
+
}
|
|
570
|
+
return tips;
|
|
571
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Mermaid diagrams for context.md.
|
|
3
|
+
* @param {object} snapshot
|
|
4
|
+
*/
|
|
5
|
+
export function buildMermaidDiagrams(snapshot) {
|
|
6
|
+
const diagrams = {};
|
|
7
|
+
|
|
8
|
+
diagrams.architectureFlow = buildArchitectureMermaid(snapshot);
|
|
9
|
+
diagrams.databaseEr = buildDatabaseMermaid(snapshot);
|
|
10
|
+
|
|
11
|
+
return diagrams;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildArchitectureMermaid(snapshot) {
|
|
15
|
+
const flow = snapshot.architecture?.apiFlow;
|
|
16
|
+
if (!flow) return null;
|
|
17
|
+
|
|
18
|
+
if (flow.includes('→')) {
|
|
19
|
+
const steps = flow.split('→').map((s) => s.trim()).filter(Boolean);
|
|
20
|
+
if (steps.length < 2) return null;
|
|
21
|
+
let chart = 'flowchart LR\n';
|
|
22
|
+
steps.forEach((step, i) => {
|
|
23
|
+
const id = `n${i}`;
|
|
24
|
+
const label = step.replace(/[^\w\s/-]/g, '').slice(0, 40);
|
|
25
|
+
chart += ` ${id}["${label}"]\n`;
|
|
26
|
+
if (i > 0) chart += ` n${i - 1} --> ${id}\n`;
|
|
27
|
+
});
|
|
28
|
+
return chart;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildDatabaseMermaid(snapshot) {
|
|
35
|
+
const models = snapshot.database?.models;
|
|
36
|
+
if (!models?.length) return null;
|
|
37
|
+
|
|
38
|
+
let er = 'erDiagram\n';
|
|
39
|
+
for (const m of models.slice(0, 8)) {
|
|
40
|
+
const name = m.name || 'Entity';
|
|
41
|
+
er += ` ${name} {\n`;
|
|
42
|
+
const fields = (m.fields || []).slice(0, 6);
|
|
43
|
+
for (const f of fields) {
|
|
44
|
+
er += ` string ${f}\n`;
|
|
45
|
+
}
|
|
46
|
+
er += ` }\n`;
|
|
47
|
+
if (m.relations?.length) {
|
|
48
|
+
for (const rel of m.relations.slice(0, 3)) {
|
|
49
|
+
er += ` ${name} ||--o{ ${rel} : has\n`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return er;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function wrapMermaid(code) {
|
|
57
|
+
if (!code) return '';
|
|
58
|
+
return `\`\`\`mermaid\n${code.trim()}\n\`\`\``;
|
|
59
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { runPipeline } from './core/pipeline.js';
|
|
2
|
+
export {
|
|
3
|
+
loadConfig,
|
|
4
|
+
writeDefaultConfig,
|
|
5
|
+
syncConfigFile,
|
|
6
|
+
isInitialized,
|
|
7
|
+
DEFAULT_CONFIG,
|
|
8
|
+
} from './core/config.js';
|
|
9
|
+
export { detectFileChanges } from './scanner/change-detector.js';
|
|
10
|
+
export { detectTechStack } from './detectors/tech-stack.js';
|
|
11
|
+
export { analyzeArchitecture } from './detectors/architecture.js';
|
|
12
|
+
export { scanRepository } from './scanner/repo-scanner.js';
|
|
13
|
+
export { enrichWithAI, isAiAvailable, scheduleBackgroundEnrichment } from './services/ai/index.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { scanRepository } from './repo-scanner.js';
|
|
2
|
+
import { loadFileIndex } from './file-index.js';
|
|
3
|
+
import { computeFileDiff } from '../analyzers/changelog.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Quick check: did any tracked file content change since last run?
|
|
7
|
+
* @param {string} projectRoot
|
|
8
|
+
* @param {object} config
|
|
9
|
+
*/
|
|
10
|
+
export async function detectFileChanges(projectRoot, config) {
|
|
11
|
+
const scanResult = await scanRepository(projectRoot, config);
|
|
12
|
+
const previousIndex = await loadFileIndex(projectRoot);
|
|
13
|
+
const fileDiff = computeFileDiff(previousIndex, scanResult.files);
|
|
14
|
+
return {
|
|
15
|
+
hasChanges: fileDiff.hasFileChanges,
|
|
16
|
+
fileDiff,
|
|
17
|
+
scanResult,
|
|
18
|
+
counts: {
|
|
19
|
+
added: fileDiff.added.length,
|
|
20
|
+
modified: fileDiff.modified.length,
|
|
21
|
+
removed: fileDiff.removed.length,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|