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.
Files changed (72) hide show
  1. package/.contextforge/config.ai.example.json +28 -0
  2. package/.contextforge/config.example.json +29 -0
  3. package/.contextforge/prompts/README.md +50 -0
  4. package/.env.example +73 -0
  5. package/LICENSE +21 -0
  6. package/README.md +223 -0
  7. package/bin/contextforge.js +14 -0
  8. package/bin/postinstall-worker.js +14 -0
  9. package/bin/postinstall.js +26 -0
  10. package/package.json +65 -0
  11. package/prompts/README.md +50 -0
  12. package/prompts/response-schema.md +22 -0
  13. package/prompts/retry-addon.md +1 -0
  14. package/prompts/system.md +10 -0
  15. package/prompts/user-template.md +22 -0
  16. package/src/analyzers/ast-parser.js +139 -0
  17. package/src/analyzers/bugs-lite.js +118 -0
  18. package/src/analyzers/changelog.js +190 -0
  19. package/src/analyzers/code-insights.js +225 -0
  20. package/src/analyzers/dependencies.js +110 -0
  21. package/src/analyzers/detection-quality.js +106 -0
  22. package/src/analyzers/eslint-runner.js +56 -0
  23. package/src/analyzers/folder-tree.js +60 -0
  24. package/src/analyzers/git-insights.js +94 -0
  25. package/src/analyzers/project-meta.js +98 -0
  26. package/src/cli/commands/changes.js +36 -0
  27. package/src/cli/commands/doctor.js +152 -0
  28. package/src/cli/commands/generate.js +7 -0
  29. package/src/cli/commands/init.js +98 -0
  30. package/src/cli/commands/prompt.js +53 -0
  31. package/src/cli/commands/stop.js +10 -0
  32. package/src/cli/commands/summary.js +22 -0
  33. package/src/cli/commands/watch.js +9 -0
  34. package/src/cli/index.js +120 -0
  35. package/src/core/confidence.js +51 -0
  36. package/src/core/config.js +183 -0
  37. package/src/core/cursor-rules.js +293 -0
  38. package/src/core/ensure-setup.js +45 -0
  39. package/src/core/env.js +113 -0
  40. package/src/core/package-meta.js +35 -0
  41. package/src/core/paths.js +39 -0
  42. package/src/core/pipeline.js +256 -0
  43. package/src/core/postinstall.js +168 -0
  44. package/src/core/setup-env.js +86 -0
  45. package/src/core/setup-prompts.js +31 -0
  46. package/src/core/snapshot-hash.js +70 -0
  47. package/src/detectors/architecture.js +117 -0
  48. package/src/detectors/auth-deploy.js +51 -0
  49. package/src/detectors/database.js +127 -0
  50. package/src/detectors/tech-stack.js +180 -0
  51. package/src/generators/artifacts.js +44 -0
  52. package/src/generators/changes-markdown.js +72 -0
  53. package/src/generators/context-json.js +91 -0
  54. package/src/generators/context-markdown.js +571 -0
  55. package/src/generators/mermaid.js +59 -0
  56. package/src/index.js +13 -0
  57. package/src/scanner/change-detector.js +24 -0
  58. package/src/scanner/config-files.js +52 -0
  59. package/src/scanner/file-index.js +55 -0
  60. package/src/scanner/package-deps.js +32 -0
  61. package/src/scanner/repo-scanner.js +143 -0
  62. package/src/services/ai/background.js +87 -0
  63. package/src/services/ai/config.js +48 -0
  64. package/src/services/ai/enrich.js +87 -0
  65. package/src/services/ai/index.js +3 -0
  66. package/src/services/ai/prompt-loader.js +104 -0
  67. package/src/services/ai/prompt.js +42 -0
  68. package/src/services/ai/providers/groq.js +36 -0
  69. package/src/services/ai/providers/openai.js +32 -0
  70. package/src/services/ai/redact.js +33 -0
  71. package/src/services/ai/validate.js +70 -0
  72. package/src/watcher/watcher.js +128 -0
@@ -0,0 +1,139 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+
4
+ let parseFn = null;
5
+
6
+ async function getParser() {
7
+ if (parseFn) return parseFn;
8
+ try {
9
+ const babel = await import('@babel/parser');
10
+ parseFn = babel.parse;
11
+ return parseFn;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * AST-based route and export extraction (accurate vs regex).
19
+ * @param {string} projectRoot
20
+ * @param {string[]} relativePaths
21
+ */
22
+ export async function parseSourceFilesAst(projectRoot, relativePaths) {
23
+ const parse = await getParser();
24
+ if (!parse) {
25
+ return { available: false, routes: [], exports: [], parseErrors: [] };
26
+ }
27
+
28
+ const routes = [];
29
+ const exports = [];
30
+ const parseErrors = [];
31
+
32
+ const files = relativePaths
33
+ .filter((p) => /\.(js|jsx|ts|tsx|mjs|cjs)$/.test(p))
34
+ .filter((p) => !p.includes('node_modules') && !p.includes('.test.'))
35
+ .slice(0, 100);
36
+
37
+ for (const rel of files) {
38
+ const full = path.join(projectRoot, rel);
39
+ let content;
40
+ try {
41
+ content = await fs.readFile(full, 'utf8');
42
+ } catch {
43
+ continue;
44
+ }
45
+
46
+ const plugins = ['typescript', 'jsx'];
47
+ if (rel.endsWith('.js') || rel.endsWith('.mjs') || rel.endsWith('.cjs')) {
48
+ plugins.length = 0;
49
+ plugins.push('jsx');
50
+ }
51
+
52
+ let ast;
53
+ try {
54
+ ast = parse(content, {
55
+ sourceType: 'module',
56
+ plugins,
57
+ errorRecovery: true,
58
+ });
59
+ } catch (e) {
60
+ parseErrors.push({ file: rel, message: e.message?.slice(0, 80) });
61
+ continue;
62
+ }
63
+
64
+ walkAst(ast, rel, routes, exports);
65
+ }
66
+
67
+ return {
68
+ available: true,
69
+ routes: dedupeRoutes(routes),
70
+ exports: exports.slice(0, 50),
71
+ parseErrors: parseErrors.slice(0, 10),
72
+ filesParsed: files.length,
73
+ };
74
+ }
75
+
76
+ function walkAst(node, file, routes, exports, parent = null) {
77
+ if (!node || typeof node !== 'object') return;
78
+
79
+ if (node.type === 'CallExpression' && node.callee) {
80
+ const method = getCalleeMethod(node.callee);
81
+ if (method && node.arguments?.[0]) {
82
+ const routePath = getStringArg(node.arguments[0]);
83
+ if (routePath) {
84
+ routes.push({ method: method.toUpperCase(), path: routePath, file, source: 'ast' });
85
+ }
86
+ }
87
+ }
88
+
89
+ if (
90
+ node.type === 'ExportNamedDeclaration' &&
91
+ node.declaration?.type === 'FunctionDeclaration' &&
92
+ node.declaration.id?.name
93
+ ) {
94
+ exports.push({ name: node.declaration.id.name, file, type: 'function' });
95
+ }
96
+
97
+ if (node.type === 'ExportNamedDeclaration' && node.specifiers) {
98
+ for (const spec of node.specifiers) {
99
+ if (spec.exported?.name) {
100
+ exports.push({ name: spec.exported.name, file, type: 'export' });
101
+ }
102
+ }
103
+ }
104
+
105
+ for (const key of Object.keys(node)) {
106
+ const child = node[key];
107
+ if (Array.isArray(child)) {
108
+ child.forEach((c) => walkAst(c, file, routes, exports, node));
109
+ } else if (child && typeof child === 'object' && child.type) {
110
+ walkAst(child, file, routes, exports, node);
111
+ }
112
+ }
113
+ }
114
+
115
+ function getCalleeMethod(callee) {
116
+ if (callee.type === 'MemberExpression' && callee.property?.name) {
117
+ const m = callee.property.name.toLowerCase();
118
+ if (['get', 'post', 'put', 'patch', 'delete', 'all', 'use'].includes(m)) return m;
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function getStringArg(arg) {
124
+ if (arg.type === 'StringLiteral') return arg.value;
125
+ if (arg.type === 'TemplateLiteral' && arg.quasis?.[0]) {
126
+ return arg.quasis[0].value.cooked || arg.quasis[0].value.raw;
127
+ }
128
+ return null;
129
+ }
130
+
131
+ function dedupeRoutes(routes) {
132
+ const seen = new Set();
133
+ return routes.filter((r) => {
134
+ const k = `${r.method}:${r.path}:${r.file}`;
135
+ if (seen.has(k)) return false;
136
+ seen.add(k);
137
+ return true;
138
+ });
139
+ }
@@ -0,0 +1,118 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+
4
+ const ASYNC_NO_CATCH = /async\s+function|\basync\s*\(/;
5
+ const TRY_CATCH = /\btry\s*\{/;
6
+
7
+ /**
8
+ * @param {string} projectRoot
9
+ * @param {{ relativePaths: string[] }} scanResult
10
+ */
11
+ export async function runBugsLite(projectRoot, scanResult, _config) {
12
+ const issues = [];
13
+
14
+ const sourceFiles = scanResult.relativePaths.filter(
15
+ (p) => /\.(js|jsx|ts|tsx)$/.test(p) && !p.includes('node_modules') && !p.includes('.test.')
16
+ );
17
+
18
+ const contentCache = new Map();
19
+
20
+ for (const rel of sourceFiles.slice(0, 80)) {
21
+ const full = path.join(projectRoot, rel);
22
+ try {
23
+ const content = await fs.readFile(full, 'utf8');
24
+ contentCache.set(rel, content);
25
+
26
+ if (ASYNC_NO_CATCH.test(content) && !TRY_CATCH.test(content) && content.length > 200) {
27
+ if (rel.includes('service') || rel.includes('controller') || rel.includes('api')) {
28
+ issues.push({
29
+ severity: 'medium',
30
+ type: 'missing-error-handling',
31
+ file: rel,
32
+ message: 'Async code without try/catch in service/controller layer',
33
+ });
34
+ }
35
+ }
36
+
37
+ if (/\beval\s*\(/.test(content) && !/\/\\beval|RegExp|\.test\(content\)/.test(content)) {
38
+ const lines = content.split('\n').filter((line) => /\beval\s*\(/.test(line) && !/^\s*\/\//.test(line));
39
+ if (lines.some((line) => !line.includes('RegExp') && !line.includes('/.test'))) {
40
+ issues.push({
41
+ severity: 'high',
42
+ type: 'security',
43
+ file: rel,
44
+ message: 'Use of eval() detected',
45
+ });
46
+ }
47
+ }
48
+
49
+ if (/console\.log\(/.test(content) && !rel.includes('test')) {
50
+ const count = (content.match(/console\.log\(/g) || []).length;
51
+ if (count > 5) {
52
+ issues.push({
53
+ severity: 'low',
54
+ type: 'cleanup',
55
+ file: rel,
56
+ message: `Many console.log statements (${count}) — consider removing for production`,
57
+ });
58
+ }
59
+ }
60
+ } catch {
61
+ /* skip */
62
+ }
63
+ }
64
+
65
+ const validationSnippets = [];
66
+ for (const [rel, content] of contentCache) {
67
+ if (/\.validate\(|Joi\.|zod\.|yup\./i.test(content)) {
68
+ const snippet = content.match(/\.validate\([^)]+\)|Joi\.\w+|z\.object/g);
69
+ if (snippet) validationSnippets.push({ file: rel, patterns: snippet.slice(0, 3) });
70
+ }
71
+ }
72
+
73
+ if (validationSnippets.length >= 3) {
74
+ const files = validationSnippets.map((v) => v.file);
75
+ const unique = new Set(files);
76
+ if (unique.size >= 2) {
77
+ issues.push({
78
+ severity: 'low',
79
+ type: 'duplicate-logic',
80
+ file: files.join(', '),
81
+ message: 'Duplicate validation patterns may exist across multiple controllers',
82
+ });
83
+ }
84
+ }
85
+
86
+ const eslintConfig = scanResult.relativePaths.find(
87
+ (p) => /^\.eslintrc/i.test(path.basename(p)) || p.endsWith('eslint.config.js')
88
+ );
89
+
90
+ if (eslintConfig) {
91
+ issues.push({
92
+ severity: 'info',
93
+ type: 'tooling',
94
+ file: eslintConfig,
95
+ message: 'ESLint config present — run eslint for deeper analysis',
96
+ });
97
+ }
98
+
99
+ const unusedCandidates = scanResult.relativePaths.filter(
100
+ (p) => p.includes('/old/') || p.includes('.backup') || p.includes('.bak')
101
+ );
102
+ for (const rel of unusedCandidates) {
103
+ issues.push({
104
+ severity: 'low',
105
+ type: 'dead-code',
106
+ file: rel,
107
+ message: 'Possibly unused or legacy file path',
108
+ });
109
+ }
110
+
111
+ return {
112
+ issues: issues.slice(0, 25),
113
+ summary:
114
+ issues.length === 0
115
+ ? 'No significant issues detected by heuristic scan.'
116
+ : `Found ${issues.length} potential issue(s) via static heuristics.`,
117
+ };
118
+ }
@@ -0,0 +1,190 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { getOutputPath, OUTPUT_FILES, getCacheDir } from '../core/paths.js';
4
+
5
+ function getChangesHistoryPath(projectRoot) {
6
+ return path.join(getCacheDir(projectRoot), 'changes-history.json');
7
+ }
8
+
9
+ /**
10
+ * Compare file hashes between previous index and current scan.
11
+ */
12
+ export function computeFileDiff(previousIndex, scannedFiles) {
13
+ const prevFiles = previousIndex?.files || {};
14
+ const currentMap = Object.fromEntries(
15
+ scannedFiles.map((f) => [f.relativePath, f])
16
+ );
17
+
18
+ const added = [];
19
+ const removed = [];
20
+ const modified = [];
21
+
22
+ for (const f of scannedFiles) {
23
+ const prev = prevFiles[f.relativePath];
24
+ if (!prev) added.push(f.relativePath);
25
+ else if (prev.hash !== f.hash) modified.push(f.relativePath);
26
+ }
27
+
28
+ for (const rel of Object.keys(prevFiles)) {
29
+ if (!currentMap[rel]) removed.push(rel);
30
+ }
31
+
32
+ return {
33
+ added: added.sort(),
34
+ removed: removed.sort(),
35
+ modified: modified.sort(),
36
+ hasFileChanges: added.length + removed.length + modified.length > 0,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Compare current snapshot with previous context.json + file diff.
42
+ */
43
+ export async function computeChangelog(projectRoot, currentSnapshot, fileDiff = null) {
44
+ const jsonPath = getOutputPath(projectRoot, OUTPUT_FILES.contextJson);
45
+ const now = new Date().toISOString();
46
+
47
+ const changes = {
48
+ runAt: now,
49
+ hasPrevious: false,
50
+ hasChanges: false,
51
+ techStackChanged: [],
52
+ architectureChanged: null,
53
+ filesScannedDelta: 0,
54
+ newBugs: 0,
55
+ bugsResolved: 0,
56
+ addedFiles: fileDiff?.added || [],
57
+ removedFiles: fileDiff?.removed || [],
58
+ modifiedFiles: fileDiff?.modified || [],
59
+ summary: 'First generation — no previous snapshot to compare.',
60
+ details: [],
61
+ };
62
+
63
+ if (fileDiff?.hasFileChanges) {
64
+ changes.hasChanges = true;
65
+ if (fileDiff.added.length) {
66
+ changes.details.push(`${fileDiff.added.length} new file(s)`);
67
+ }
68
+ if (fileDiff.modified.length) {
69
+ changes.details.push(`${fileDiff.modified.length} modified file(s)`);
70
+ const codeFiles = fileDiff.modified.filter((p) =>
71
+ /\.(js|jsx|ts|tsx|mjs|cjs|vue|svelte|py|go|rs|java|php)$/.test(p)
72
+ );
73
+ if (codeFiles.length) {
74
+ changes.details.push(`${codeFiles.length} code file(s) edited`);
75
+ }
76
+ }
77
+ if (fileDiff.removed.length) {
78
+ changes.details.push(`${fileDiff.removed.length} removed file(s)`);
79
+ }
80
+ }
81
+
82
+ if (!(await fs.pathExists(jsonPath))) {
83
+ await appendChangesHistory(projectRoot, changes);
84
+ return changes;
85
+ }
86
+
87
+ let previous;
88
+ try {
89
+ previous = await fs.readJson(jsonPath);
90
+ } catch {
91
+ await appendChangesHistory(projectRoot, changes);
92
+ return changes;
93
+ }
94
+
95
+ changes.hasPrevious = true;
96
+ changes.previousRunAt = previous.generatedAt;
97
+
98
+ const prevTs = previous.techStack || {};
99
+ const curTs = currentSnapshot.techStack || {};
100
+ const categories = ['frontend', 'backend', 'database', 'orm', 'auth', 'state'];
101
+
102
+ for (const cat of categories) {
103
+ const prev = new Set(prevTs[cat] || []);
104
+ const cur = curTs[cat] || [];
105
+ for (const item of cur) {
106
+ if (!prev.has(item)) {
107
+ changes.techStackChanged.push(`+${cat}: ${item}`);
108
+ changes.hasChanges = true;
109
+ }
110
+ }
111
+ for (const item of prev) {
112
+ if (!cur.includes(item)) {
113
+ changes.techStackChanged.push(`-${cat}: ${item}`);
114
+ changes.hasChanges = true;
115
+ }
116
+ }
117
+ }
118
+
119
+ const prevArch = previous.architecture?.primary?.label;
120
+ const curArch = currentSnapshot.architecture?.primary?.label;
121
+ if (prevArch !== curArch) {
122
+ changes.architectureChanged = `${prevArch || 'unknown'} → ${curArch || 'unknown'}`;
123
+ changes.hasChanges = true;
124
+ }
125
+
126
+ changes.filesScannedDelta =
127
+ (currentSnapshot.filesScanned || 0) - (previous.filesScanned || 0);
128
+ if (changes.filesScannedDelta !== 0) changes.hasChanges = true;
129
+
130
+ const prevBugCount = previous.bugs?.issues?.length || 0;
131
+ const curBugCount = currentSnapshot.bugs?.issues?.length || 0;
132
+ changes.newBugs = Math.max(0, curBugCount - prevBugCount);
133
+ changes.bugsResolved = Math.max(0, prevBugCount - curBugCount);
134
+ if (changes.newBugs > 0) changes.hasChanges = true;
135
+
136
+ const parts = [];
137
+ if (changes.hasChanges) {
138
+ if (changes.details.length) parts.push(changes.details.join(', '));
139
+ if (changes.techStackChanged.length) {
140
+ parts.push(`Tech: ${changes.techStackChanged.join('; ')}`);
141
+ }
142
+ if (changes.architectureChanged) {
143
+ parts.push(`Architecture: ${changes.architectureChanged}`);
144
+ }
145
+ if (changes.filesScannedDelta !== 0) {
146
+ parts.push(`Files scanned: ${changes.filesScannedDelta > 0 ? '+' : ''}${changes.filesScannedDelta}`);
147
+ }
148
+ if (changes.newBugs > 0) parts.push(`New issues: +${changes.newBugs}`);
149
+ changes.summary = parts.join('. ');
150
+ } else {
151
+ changes.summary = 'No changes detected since last run.';
152
+ }
153
+
154
+ await appendChangesHistory(projectRoot, changes);
155
+ return changes;
156
+ }
157
+
158
+ async function appendChangesHistory(projectRoot, entry) {
159
+ const historyPath = getChangesHistoryPath(projectRoot);
160
+ let history = { runs: [] };
161
+ if (await fs.pathExists(historyPath)) {
162
+ try {
163
+ history = await fs.readJson(historyPath);
164
+ } catch {
165
+ history = { runs: [] };
166
+ }
167
+ }
168
+
169
+ history.runs.unshift({
170
+ runAt: entry.runAt,
171
+ hasChanges: entry.hasChanges,
172
+ summary: entry.summary,
173
+ added: entry.addedFiles?.length || 0,
174
+ modified: entry.modifiedFiles?.length || 0,
175
+ removed: entry.removedFiles?.length || 0,
176
+ techStackChanged: entry.techStackChanged?.length || 0,
177
+ });
178
+
179
+ history.runs = history.runs.slice(0, 50);
180
+ await fs.writeJson(historyPath, history, { spaces: 2 });
181
+ }
182
+
183
+ export async function loadChangesHistory(projectRoot, limit = 10) {
184
+ const historyPath = getChangesHistoryPath(projectRoot);
185
+ if (!(await fs.pathExists(historyPath))) {
186
+ return { runs: [] };
187
+ }
188
+ const data = await fs.readJson(historyPath);
189
+ return { runs: (data.runs || []).slice(0, limit) };
190
+ }
@@ -0,0 +1,225 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+
4
+ const EXPORT_FN = /export\s+(?:async\s+)?(?:function|const)\s+(\w+)/g;
5
+ const ROUTE_PATTERNS = [
6
+ /router\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
7
+ /app\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
8
+ /\.route\s*\(\s*['"`]([^'"`]+)['"`]/gi,
9
+ ];
10
+ const COMPONENT_RE = /export\s+(?:default\s+)?function\s+(\w+)|export\s+const\s+(\w+)\s*=/g;
11
+
12
+ /**
13
+ * Deep code structure analysis for business logic & API context (§7.5).
14
+ * @param {string} projectRoot
15
+ * @param {{ relativePaths: string[] }} scanResult
16
+ * @param {object} configFiles
17
+ */
18
+ export async function analyzeCodeInsights(projectRoot, scanResult, configFiles, astResult = null) {
19
+ const normalized = scanResult.relativePaths.map((p) => p.replace(/\\/g, '/'));
20
+
21
+ const structure = detectProjectStructure(normalized);
22
+ const modules = {
23
+ routes: [],
24
+ controllers: [],
25
+ services: [],
26
+ middleware: [],
27
+ models: [],
28
+ components: [],
29
+ hooks: [],
30
+ utils: [],
31
+ apiHandlers: [],
32
+ };
33
+
34
+ const businessLogic = [];
35
+ const apiEndpoints = [];
36
+ const patterns = [];
37
+ const importantRules = [];
38
+
39
+ const sourceFiles = normalized.filter(
40
+ (p) => /\.(js|jsx|ts|tsx|mjs|cjs)$/.test(p) && !p.includes('node_modules') && !p.includes('.test.')
41
+ );
42
+
43
+ for (const rel of sourceFiles.slice(0, 120)) {
44
+ const full = path.join(projectRoot, rel);
45
+ let content;
46
+ try {
47
+ content = await fs.readFile(full, 'utf8');
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ categorizeFile(rel, content, modules);
53
+
54
+ for (const re of ROUTE_PATTERNS) {
55
+ re.lastIndex = 0;
56
+ let m;
57
+ while ((m = re.exec(content)) !== null) {
58
+ const method = (m[1] || 'route').toUpperCase();
59
+ const routePath = m[2] || m[1];
60
+ apiEndpoints.push({ method, path: routePath, file: rel });
61
+ }
62
+ }
63
+
64
+ if (/\/api\/|pages\/api\//i.test(rel)) {
65
+ apiEndpoints.push({ method: 'API', path: rel, file: rel });
66
+ }
67
+
68
+ let exp;
69
+ EXPORT_FN.lastIndex = 0;
70
+ while ((exp = EXPORT_FN.exec(content)) !== null) {
71
+ if (rel.includes('services/') || rel.includes('service.')) {
72
+ businessLogic.push({ type: 'service', name: exp[1], file: rel });
73
+ }
74
+ }
75
+
76
+ if (content.includes('useEffect') || content.includes('useState')) {
77
+ patterns.push('React hooks usage');
78
+ }
79
+ if (/\btry\s*\{/.test(content) && /catch\s*\(/.test(content)) {
80
+ if (rel.includes('service') || rel.includes('controller')) {
81
+ patterns.push(`Error handling in ${path.basename(rel)}`);
82
+ }
83
+ }
84
+ if (/\.interceptors\.(request|response)/.test(content)) {
85
+ importantRules.push('Use Axios interceptors for HTTP layer');
86
+ }
87
+ if (/z\.object|zod\.|Joi\.|yup\./.test(content)) {
88
+ patterns.push('Schema validation (Zod/Joi/Yup)');
89
+ }
90
+ }
91
+
92
+ const pkg = configFiles.packageJson;
93
+ if (pkg?.scripts) {
94
+ if (pkg.scripts.test) importantRules.push(`Run tests: \`npm test\` (${pkg.scripts.test})`);
95
+ if (pkg.scripts.lint) importantRules.push(`Lint: \`npm run lint\``);
96
+ if (pkg.scripts.build) importantRules.push(`Build: \`npm run build\``);
97
+ }
98
+
99
+ if (modules.controllers.length && modules.services.length) {
100
+ importantRules.push('Keep controllers thin — delegate business logic to services');
101
+ }
102
+ if (modules.middleware.length) {
103
+ importantRules.push('Middleware layer detected — apply cross-cutting concerns there');
104
+ }
105
+
106
+ if (astResult?.available && astResult.routes?.length) {
107
+ for (const r of astResult.routes) {
108
+ apiEndpoints.push({ method: r.method, path: r.path, file: r.file, source: 'ast' });
109
+ }
110
+ }
111
+
112
+ let summary = buildBusinessSummary(modules, businessLogic, apiEndpoints, structure, normalized);
113
+ if (astResult?.available) {
114
+ summary += ` AST parsed ${astResult.filesParsed} files for accurate route detection.`;
115
+ }
116
+
117
+ const pipelineFlow = inferPipelineFlow(normalized);
118
+
119
+ return {
120
+ pipelineFlow,
121
+ projectStructure: structure,
122
+ modules,
123
+ businessLogic: businessLogic.slice(0, 30),
124
+ apiEndpoints: dedupeEndpoints(apiEndpoints).slice(0, 40),
125
+ patterns: [...new Set(patterns)].slice(0, 15),
126
+ importantRules: [...new Set(importantRules)].slice(0, 12),
127
+ summary,
128
+ fileStats: {
129
+ totalSource: sourceFiles.length,
130
+ routes: modules.routes.length,
131
+ services: modules.services.length,
132
+ components: modules.components.length,
133
+ },
134
+ };
135
+ }
136
+
137
+ function detectProjectStructure(paths) {
138
+ const STANDARD_DIRS = [
139
+ 'src', 'app', 'pages', 'components', 'controllers', 'routes',
140
+ 'services', 'models', 'middleware', 'hooks', 'utils', 'config', 'api',
141
+ ];
142
+ const found = [];
143
+ for (const dir of STANDARD_DIRS) {
144
+ if (paths.some((p) => p === dir || p.startsWith(`${dir}/`))) {
145
+ found.push(dir);
146
+ }
147
+ }
148
+ return found;
149
+ }
150
+
151
+ function categorizeFile(rel, content, modules) {
152
+ const base = path.basename(rel);
153
+ const entry = { file: rel, name: base };
154
+
155
+ if (/routes?\//i.test(rel) || rel.includes('/routes/')) modules.routes.push(entry);
156
+ if (/controllers?\//i.test(rel)) modules.controllers.push(entry);
157
+ if (/services?\//i.test(rel)) modules.services.push(entry);
158
+ if (/middleware/i.test(rel)) modules.middleware.push(entry);
159
+ if (/models?\//i.test(rel)) modules.models.push(entry);
160
+ if (/components?\//i.test(rel) || /\.(tsx|jsx)$/.test(rel)) {
161
+ let m;
162
+ COMPONENT_RE.lastIndex = 0;
163
+ const names = [];
164
+ while ((m = COMPONENT_RE.exec(content)) !== null) {
165
+ names.push(m[1] || m[2]);
166
+ }
167
+ modules.components.push({ ...entry, components: names.slice(0, 5) });
168
+ }
169
+ if (/hooks?\//i.test(rel) || /^use[A-Z]/.test(base)) modules.hooks.push(entry);
170
+ if (/utils?\//i.test(rel)) modules.utils.push(entry);
171
+ }
172
+
173
+ function dedupeEndpoints(endpoints) {
174
+ const seen = new Set();
175
+ return endpoints.filter((e) => {
176
+ const key = `${e.method}:${e.path}:${e.file}`;
177
+ if (seen.has(key)) return false;
178
+ seen.add(key);
179
+ return true;
180
+ });
181
+ }
182
+
183
+ function inferPipelineFlow(paths) {
184
+ if (paths.some((p) => p.includes('src/core/pipeline.js'))) {
185
+ return 'Repository → Scanner → Detectors → Analyzers → Context Generator → context.md';
186
+ }
187
+ if (paths.some((p) => p.includes('watcher'))) {
188
+ return 'File change → Watcher (chokidar) → Pipeline → .contextforge/context.md';
189
+ }
190
+ return null;
191
+ }
192
+
193
+ function buildBusinessSummary(modules, businessLogic, apiEndpoints, structure, paths) {
194
+ const parts = [];
195
+
196
+ if (structure.length) {
197
+ parts.push(`Project uses directories: ${structure.join(', ')}.`);
198
+ }
199
+ if (modules.services.length) {
200
+ parts.push(`${modules.services.length} service module(s) contain core business logic.`);
201
+ }
202
+ if (modules.controllers.length) {
203
+ parts.push(`${modules.controllers.length} controller file(s) handle HTTP/request orchestration.`);
204
+ }
205
+ if (apiEndpoints.length) {
206
+ parts.push(`${apiEndpoints.length} API route(s) or handlers detected.`);
207
+ }
208
+ if (businessLogic.length) {
209
+ const names = businessLogic.slice(0, 8).map((b) => b.name).join(', ');
210
+ parts.push(`Exported service functions include: ${names}.`);
211
+ }
212
+ if (modules.components.length) {
213
+ parts.push(`${modules.components.length} UI component file(s) in the tree.`);
214
+ }
215
+ if (paths.some((p) => p.includes('src/cli'))) {
216
+ parts.push('CLI entry via commander; commands delegate to core pipeline.');
217
+ }
218
+ if (paths.some((p) => p.includes('src/generators'))) {
219
+ parts.push('Context output produced by generators/context-markdown.js.');
220
+ }
221
+
222
+ return parts.length
223
+ ? parts.join(' ')
224
+ : 'Business logic distributed across scanned source files; no dedicated services/ folder detected.';
225
+ }