aibridge-context 1.5.2 → 2.0.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/bin/cli.js +469 -170
- package/core/briefingGenerator.js +368 -0
- package/core/codeDiff.js +304 -0
- package/core/fileSnapshot.js +178 -0
- package/core/stateManager.js +803 -1369
- package/core/watcher.js +78 -49
- package/index.js +23 -9
- package/package.json +7 -3
- package/server/routes.js +30 -34
package/core/stateManager.js
CHANGED
|
@@ -1,1555 +1,989 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs
|
|
4
|
-
const fsp
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsp = require('fs/promises');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
const { diffTexts, extractCodeSignals } = require('./codeDiff');
|
|
8
|
+
const { buildCodeFileCatalogue, getSnapshot, readCurrentContent } = require('./fileSnapshot');
|
|
9
|
+
const { generateBriefing } = require('./briefingGenerator');
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────
|
|
12
|
+
// Constants
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const CONTEXT_DIR_NAME = '.ai-context';
|
|
16
|
+
const MAX_RECENT_UPDATES = 15;
|
|
17
|
+
const MAX_CHANGELOG_ENTRIES = 200;
|
|
18
|
+
const MAX_KEY_FEATURES = 12;
|
|
19
|
+
const MAX_IMPL_DETAILS = 15;
|
|
20
|
+
const MAX_TREE_DEPTH = 6;
|
|
21
|
+
const MAX_RESOLVED_ISSUES = 50;
|
|
22
|
+
const MAX_ACTIVE_ERRORS = 30;
|
|
23
|
+
const MAX_CODE_CHANGE_HISTORY = 100;
|
|
24
|
+
|
|
25
|
+
const IMPORTANT_DIRS = new Set([
|
|
26
|
+
'core','server','bin','src','routes','controllers',
|
|
27
|
+
'services','middleware','lib','utils','api','helpers'
|
|
23
28
|
]);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'main.py',
|
|
31
|
-
'requirements.txt',
|
|
32
|
-
'tsconfig.json'
|
|
33
|
-
];
|
|
34
|
-
const ANALYSIS_DIRECTORIES = [
|
|
35
|
-
'routes',
|
|
36
|
-
'server',
|
|
37
|
-
'controllers',
|
|
38
|
-
'services',
|
|
39
|
-
'middleware',
|
|
40
|
-
'config',
|
|
41
|
-
'src',
|
|
42
|
-
'core',
|
|
43
|
-
'bin'
|
|
44
|
-
];
|
|
45
|
-
const FEATURE_CATALOG = {
|
|
46
|
-
ai_context_generation: 'AI-readable project context generation',
|
|
47
|
-
cli_automation: 'Command-line workflow for project setup and automation',
|
|
48
|
-
change_tracking: 'Automatic change tracking with noise filtering',
|
|
49
|
-
public_sync: 'Optional GitHub sync for publishing project context',
|
|
50
|
-
local_context_server: 'HTTP endpoints for accessing current project context',
|
|
51
|
-
rest_api: 'REST-style API surface',
|
|
52
|
-
auth: 'Authenticated workflows secured with JWT',
|
|
53
|
-
persistence: 'MongoDB-backed data persistence',
|
|
54
|
-
realtime: 'Real-time communication channel',
|
|
55
|
-
external_api: 'External service integration',
|
|
56
|
-
middleware: 'Middleware-driven request processing',
|
|
57
|
-
config_management: 'Centralized project configuration management'
|
|
58
|
-
};
|
|
29
|
+
const CODE_EXTENSIONS = new Set(['.js','.ts','.mjs','.cjs','.jsx','.tsx','.py','.go','.rs','.java','.rb','.php','.cs','.swift']);
|
|
30
|
+
const IGNORED_DIRS = new Set([
|
|
31
|
+
'node_modules','.git','.ai-context','dist','build',
|
|
32
|
+
'coverage','.tmp','logs','.cache','out','.next','.nuxt'
|
|
33
|
+
]);
|
|
34
|
+
|
|
59
35
|
const DEFAULT_CONFIG = {
|
|
60
36
|
port: 3333,
|
|
61
37
|
debounceMs: 600,
|
|
62
38
|
gitSync: {
|
|
63
|
-
enabled: false,
|
|
64
|
-
push: true,
|
|
39
|
+
enabled: false, push: true,
|
|
65
40
|
commitMessage: 'auto: update AI context',
|
|
66
|
-
remote: 'origin',
|
|
67
|
-
branch: 'main',
|
|
68
|
-
repoUrl: ''
|
|
41
|
+
remote: 'origin', branch: 'main', repoUrl: ''
|
|
69
42
|
}
|
|
70
43
|
};
|
|
71
44
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────
|
|
46
|
+
// Path helpers
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────
|
|
75
48
|
|
|
76
|
-
function
|
|
77
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
78
|
-
const contextDir = path.join(resolvedRoot, CONTEXT_DIR_NAME);
|
|
49
|
+
function resolveRoot(projectRoot) { return path.resolve(projectRoot || process.cwd()); }
|
|
79
50
|
|
|
51
|
+
function getContextPaths(projectRoot) {
|
|
52
|
+
const root = resolveRoot(projectRoot);
|
|
53
|
+
const contextDir = path.join(root, CONTEXT_DIR_NAME);
|
|
80
54
|
return {
|
|
81
55
|
contextDir,
|
|
82
|
-
stateFile:
|
|
83
|
-
brainFile:
|
|
84
|
-
contextFile:
|
|
56
|
+
stateFile: path.join(contextDir, 'state.json'),
|
|
57
|
+
brainFile: path.join(contextDir, 'brain.txt'),
|
|
58
|
+
contextFile: path.join(contextDir, 'context.md'),
|
|
85
59
|
changelogFile: path.join(contextDir, 'changelog.json'),
|
|
86
|
-
configFile:
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function deepMerge(baseValue, overrideValue) {
|
|
91
|
-
if (Array.isArray(baseValue) || Array.isArray(overrideValue)) {
|
|
92
|
-
return overrideValue === undefined ? baseValue : overrideValue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (isObject(baseValue) && isObject(overrideValue)) {
|
|
96
|
-
const merged = Object.assign({}, baseValue);
|
|
97
|
-
|
|
98
|
-
for (const [key, value] of Object.entries(overrideValue)) {
|
|
99
|
-
merged[key] = deepMerge(baseValue[key], value);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return merged;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return overrideValue === undefined ? baseValue : overrideValue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isObject(value) {
|
|
109
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeProjectPath(filePath) {
|
|
113
|
-
return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isInsideProjectRoot(projectRoot, targetPath) {
|
|
117
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
118
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
119
|
-
const relativePath = path.relative(resolvedRoot, resolvedTarget);
|
|
120
|
-
|
|
121
|
-
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function getRootPackageJsonPath(projectRoot) {
|
|
125
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
126
|
-
return path.join(resolvedRoot, 'package.json');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function readRootPackageJson(projectRoot) {
|
|
130
|
-
const packageJsonPath = getRootPackageJsonPath(projectRoot);
|
|
131
|
-
|
|
132
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function detectProjectMetadata(projectRoot) {
|
|
144
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
145
|
-
const packageJson = readRootPackageJson(resolvedRoot);
|
|
146
|
-
const techStack = detectTechStack(resolvedRoot, packageJson);
|
|
147
|
-
const packageManager = detectPackageManager(resolvedRoot);
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
project: packageJson && packageJson.name ? packageJson.name : path.basename(resolvedRoot),
|
|
151
|
-
version: packageJson && packageJson.version ? packageJson.version : '0.1.0',
|
|
152
|
-
techStack: Object.assign({}, techStack, {
|
|
153
|
-
package_manager: packageManager
|
|
154
|
-
}),
|
|
155
|
-
stackLabel: buildStackLabel(techStack),
|
|
156
|
-
packageManager
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function detectTechStack(projectRoot, packageJson) {
|
|
161
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
162
|
-
const rootPackage = packageJson || readRootPackageJson(resolvedRoot);
|
|
163
|
-
const dependencies = Object.assign(
|
|
164
|
-
{},
|
|
165
|
-
(rootPackage && rootPackage.dependencies) || {},
|
|
166
|
-
(rootPackage && rootPackage.devDependencies) || {}
|
|
167
|
-
);
|
|
168
|
-
const hasPythonMarker = ['pyproject.toml', 'requirements.txt', 'setup.py'].some((marker) =>
|
|
169
|
-
fs.existsSync(path.join(resolvedRoot, marker))
|
|
170
|
-
);
|
|
171
|
-
let language = '';
|
|
172
|
-
let runtime = '';
|
|
173
|
-
|
|
174
|
-
if (rootPackage || hasAnyFileExtension(resolvedRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
|
|
175
|
-
language = 'Node.js';
|
|
176
|
-
runtime = 'Node.js';
|
|
177
|
-
} else if (hasPythonMarker || hasAnyFileExtension(resolvedRoot, ['.py'])) {
|
|
178
|
-
language = 'Python';
|
|
179
|
-
runtime = 'Python';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
language,
|
|
184
|
-
framework: detectFramework(dependencies),
|
|
185
|
-
runtime,
|
|
186
|
-
package_manager: detectPackageManager(resolvedRoot)
|
|
60
|
+
configFile: path.join(contextDir, 'config.json'),
|
|
61
|
+
briefingFile: path.join(contextDir, 'briefing.md')
|
|
187
62
|
};
|
|
188
63
|
}
|
|
189
64
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────
|
|
66
|
+
// Utilities
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────
|
|
194
68
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
69
|
+
function isObj(v) { return Boolean(v) && typeof v === 'object' && !Array.isArray(v); }
|
|
70
|
+
function uniq(arr) { return Array.from(new Set((arr || []).filter(Boolean))); }
|
|
71
|
+
function cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
|
|
198
72
|
|
|
199
|
-
|
|
200
|
-
|
|
73
|
+
function deepMerge(base, override) {
|
|
74
|
+
if (Array.isArray(base) || Array.isArray(override)) return override === undefined ? base : override;
|
|
75
|
+
if (isObj(base) && isObj(override)) {
|
|
76
|
+
const out = Object.assign({}, base);
|
|
77
|
+
for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
|
|
78
|
+
return out;
|
|
201
79
|
}
|
|
202
|
-
|
|
203
|
-
if (dependencies.fastify) {
|
|
204
|
-
return 'Fastify';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (dependencies.koa) {
|
|
208
|
-
return 'Koa';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return '';
|
|
80
|
+
return override === undefined ? base : override;
|
|
212
81
|
}
|
|
213
82
|
|
|
214
|
-
function
|
|
215
|
-
const parts = [techStack.language, techStack.framework].filter(Boolean);
|
|
216
|
-
return parts.length > 0 ? parts.join(' + ') : 'Project';
|
|
217
|
-
}
|
|
83
|
+
function normPath(p) { return String(p || '').split(path.sep).join('/').replace(/^\.\/+/, ''); }
|
|
218
84
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
85
|
+
function insideRoot(root, target) {
|
|
86
|
+
const rel = path.relative(path.resolve(root), path.resolve(target));
|
|
87
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
223
88
|
}
|
|
224
89
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (fs.existsSync(path.join(resolvedRoot, 'pnpm-lock.yaml'))) {
|
|
229
|
-
return 'pnpm';
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (fs.existsSync(path.join(resolvedRoot, 'yarn.lock'))) {
|
|
233
|
-
return 'yarn';
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return 'npm';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function scanProjectFiles(projectRoot, maxDepth, options) {
|
|
240
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
241
|
-
const settings = Object.assign({ includeDirectories: null }, options);
|
|
242
|
-
const includeDirectories = settings.includeDirectories
|
|
243
|
-
? new Set(settings.includeDirectories.map((entry) => normalizeProjectPath(entry).toLowerCase()))
|
|
244
|
-
: null;
|
|
245
|
-
const results = [];
|
|
246
|
-
|
|
247
|
-
function visit(currentDir, depth) {
|
|
248
|
-
if (depth > maxDepth || !isInsideProjectRoot(resolvedRoot, currentDir)) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
let entries = [];
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
256
|
-
} catch (error) {
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for (const entry of entries) {
|
|
261
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
262
|
-
|
|
263
|
-
if (!isInsideProjectRoot(resolvedRoot, fullPath)) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const relativePath = normalizeProjectPath(path.relative(resolvedRoot, fullPath));
|
|
268
|
-
|
|
269
|
-
if (entry.isDirectory()) {
|
|
270
|
-
if (shouldIgnoreProjectFile(relativePath)) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (includeDirectories && depth === 0 && !includeDirectories.has(relativePath.toLowerCase())) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
visit(fullPath, depth + 1);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!shouldIgnoreProjectFile(relativePath)) {
|
|
283
|
-
results.push(relativePath);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
visit(resolvedRoot, 0);
|
|
289
|
-
return results;
|
|
290
|
-
}
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────
|
|
91
|
+
// Ignore / score
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────
|
|
291
93
|
|
|
292
94
|
function shouldIgnoreProjectFile(filePath) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (segments.some((segment) => IGNORED_DIRECTORY_NAMES.has(segment))) {
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (baseName.startsWith('.start')) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
baseName.endsWith('.log') ||
|
|
312
|
-
baseName.endsWith('.tmp') ||
|
|
313
|
-
baseName.endsWith('.lock') ||
|
|
314
|
-
baseName === 'package-lock.json' ||
|
|
315
|
-
baseName === 'yarn.lock' ||
|
|
316
|
-
baseName === 'pnpm-lock.yaml' ||
|
|
317
|
-
/^tmp[._-]/i.test(baseName) ||
|
|
318
|
-
/^temp[._-]/i.test(baseName) ||
|
|
319
|
-
/^debug[._-]/i.test(baseName)
|
|
320
|
-
) {
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
|
|
95
|
+
const p = normPath(filePath).toLowerCase();
|
|
96
|
+
if (!p) return false;
|
|
97
|
+
const segs = p.split('/').filter(Boolean);
|
|
98
|
+
const base = segs[segs.length - 1] || '';
|
|
99
|
+
if (segs.some((s) => IGNORED_DIRS.has(s))) return true;
|
|
100
|
+
if (base.startsWith('.')) return true;
|
|
101
|
+
if (/\.(log|tmp|lock)$/.test(base)) return true;
|
|
102
|
+
if (base === 'package-lock.json' || base === 'yarn.lock' || base === 'pnpm-lock.yaml') return true;
|
|
324
103
|
return false;
|
|
325
104
|
}
|
|
326
105
|
|
|
327
106
|
function scoreEvent(filePath) {
|
|
328
|
-
const
|
|
329
|
-
|
|
107
|
+
const p = normPath(filePath).toLowerCase();
|
|
108
|
+
if (shouldIgnoreProjectFile(p)) return -5;
|
|
330
109
|
let score = 0;
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
|
|
337
|
-
score += 3;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (IMPORTANT_EXTENSIONS.has(path.extname(lowerPath))) {
|
|
341
|
-
score += 2;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (lowerPath === 'package.json') {
|
|
345
|
-
score += 2;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (lowerPath === 'readme.md') {
|
|
349
|
-
score += 1;
|
|
350
|
-
}
|
|
351
|
-
|
|
110
|
+
const firstSeg = p.split('/')[0];
|
|
111
|
+
if (IMPORTANT_DIRS.has(firstSeg)) score += 3;
|
|
112
|
+
if (CODE_EXTENSIONS.has(path.extname(p))) score += 2;
|
|
113
|
+
if (p === 'package.json' || p === 'readme.md') score += 2;
|
|
352
114
|
return score;
|
|
353
115
|
}
|
|
354
116
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return contextDir;
|
|
359
|
-
}
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────
|
|
118
|
+
// File scanning
|
|
119
|
+
// ─────────────────────────────────────────────────────────────────
|
|
360
120
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
121
|
+
function scanFiles(projectRoot, maxDepth) {
|
|
122
|
+
const root = resolveRoot(projectRoot);
|
|
123
|
+
const results = [];
|
|
124
|
+
function visit(dir, depth) {
|
|
125
|
+
if (depth > maxDepth) return;
|
|
126
|
+
let entries = [];
|
|
127
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
const full = path.join(dir, e.name);
|
|
130
|
+
if (!insideRoot(root, full)) continue;
|
|
131
|
+
const rel = normPath(path.relative(root, full));
|
|
132
|
+
if (shouldIgnoreProjectFile(rel)) continue;
|
|
133
|
+
if (e.isDirectory()) { visit(full, depth + 1); continue; }
|
|
134
|
+
results.push(rel);
|
|
135
|
+
}
|
|
367
136
|
}
|
|
137
|
+
visit(root, 0);
|
|
138
|
+
return results;
|
|
368
139
|
}
|
|
369
140
|
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function createDefaultState(projectRoot) {
|
|
396
|
-
const metadata = detectProjectMetadata(projectRoot);
|
|
397
|
-
const bootstrap = bootstrapProjectAnalysis(projectRoot);
|
|
398
|
-
const state = {
|
|
399
|
-
project: metadata.project,
|
|
400
|
-
version: metadata.version,
|
|
401
|
-
last_updated: new Date(0).toISOString(),
|
|
402
|
-
ai_summary: '',
|
|
403
|
-
tech_stack: bootstrap.techStack,
|
|
404
|
-
architecture_patterns: bootstrap.architecturePatterns,
|
|
405
|
-
implementation_details: bootstrap.implementationDetails,
|
|
406
|
-
current_stage: determineCurrentStage(bootstrap.keyFeatures, [], bootstrap.implementationDetails),
|
|
407
|
-
recent_updates: [],
|
|
408
|
-
key_features: bootstrap.keyFeatures,
|
|
409
|
-
known_issues: deriveKnownIssues(projectRoot, bootstrap),
|
|
410
|
-
next_steps: []
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
state.ai_summary = generateAiSummary(state, bootstrap);
|
|
414
|
-
state.next_steps = generateNextSteps(state, bootstrap, []);
|
|
415
|
-
|
|
416
|
-
return state;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function mergePreferredArray(preferredValue, fallbackValue) {
|
|
420
|
-
if (Array.isArray(preferredValue) && preferredValue.length > 0) {
|
|
421
|
-
return preferredValue;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (Array.isArray(fallbackValue) && fallbackValue.length > 0) {
|
|
425
|
-
return fallbackValue;
|
|
141
|
+
function buildFileTree(projectRoot) {
|
|
142
|
+
const root = resolveRoot(projectRoot);
|
|
143
|
+
function visit(dir, depth) {
|
|
144
|
+
const node = {};
|
|
145
|
+
if (depth > MAX_TREE_DEPTH) return node;
|
|
146
|
+
let entries = [];
|
|
147
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return node; }
|
|
148
|
+
const dirs = entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
149
|
+
const files = entries.filter((e) => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
150
|
+
for (const d of dirs) {
|
|
151
|
+
const full = path.join(dir, d.name);
|
|
152
|
+
const rel = normPath(path.relative(root, full));
|
|
153
|
+
if (!insideRoot(root, full) || shouldIgnoreProjectFile(rel)) continue;
|
|
154
|
+
node[d.name] = visit(full, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
for (const f of files) {
|
|
157
|
+
const full = path.join(dir, f.name);
|
|
158
|
+
const rel = normPath(path.relative(root, full));
|
|
159
|
+
if (!insideRoot(root, full) || shouldIgnoreProjectFile(rel)) continue;
|
|
160
|
+
node[f.name] = null;
|
|
161
|
+
}
|
|
162
|
+
return node;
|
|
426
163
|
}
|
|
427
|
-
|
|
428
|
-
return [];
|
|
164
|
+
return visit(root, 0);
|
|
429
165
|
}
|
|
430
166
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────
|
|
168
|
+
// Package.json / metadata
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────
|
|
434
170
|
|
|
435
|
-
|
|
171
|
+
function readPkg(projectRoot) {
|
|
172
|
+
try { return JSON.parse(fs.readFileSync(path.join(resolveRoot(projectRoot), 'package.json'), 'utf8')); }
|
|
173
|
+
catch (_) { return null; }
|
|
436
174
|
}
|
|
437
175
|
|
|
438
|
-
function
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
version: metadata.version,
|
|
444
|
-
last_updated: baseState.last_updated || new Date(0).toISOString(),
|
|
445
|
-
ai_summary: '',
|
|
446
|
-
tech_stack: mergePreferredObject(bootstrap.techStack, baseState.tech_stack),
|
|
447
|
-
architecture_patterns: mergePreferredArray(
|
|
448
|
-
bootstrap.architecturePatterns,
|
|
449
|
-
baseState.architecture_patterns
|
|
450
|
-
),
|
|
451
|
-
implementation_details: mergePreferredArray(
|
|
452
|
-
bootstrap.implementationDetails,
|
|
453
|
-
baseState.implementation_details
|
|
454
|
-
),
|
|
455
|
-
current_stage: '',
|
|
456
|
-
recent_updates: Array.isArray(baseState.recent_updates) ? baseState.recent_updates : [],
|
|
457
|
-
key_features: mergePreferredArray(bootstrap.keyFeatures, baseState.key_features),
|
|
458
|
-
known_issues: [],
|
|
459
|
-
next_steps: []
|
|
460
|
-
},
|
|
461
|
-
overrides || {}
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
return nextState;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function bootstrapProjectAnalysis(projectRoot) {
|
|
468
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
469
|
-
const metadata = detectProjectMetadata(resolvedRoot);
|
|
470
|
-
const rootPackage = readRootPackageJson(resolvedRoot);
|
|
471
|
-
const techStack = metadata.techStack;
|
|
472
|
-
const analysisInputs = collectAnalysisInputs(resolvedRoot);
|
|
473
|
-
const implementationSignals = detectImplementationSignals(analysisInputs, rootPackage, techStack);
|
|
474
|
-
const architecturePatterns = buildArchitecturePatterns(
|
|
475
|
-
implementationSignals,
|
|
476
|
-
analysisInputs,
|
|
477
|
-
techStack,
|
|
478
|
-
rootPackage
|
|
479
|
-
);
|
|
480
|
-
const implementationDetails = buildImplementationDetails(implementationSignals, techStack);
|
|
481
|
-
const keyFeatures = buildKeyFeatures(implementationSignals, techStack);
|
|
482
|
-
const projectType = determineProjectType(implementationSignals, techStack, rootPackage);
|
|
483
|
-
|
|
176
|
+
function detectProjectMetadata(projectRoot) {
|
|
177
|
+
const root = resolveRoot(projectRoot);
|
|
178
|
+
const pkg = readPkg(root);
|
|
179
|
+
const ts = detectTechStack(root, pkg);
|
|
180
|
+
const pm = detectPackageManager(root);
|
|
484
181
|
return {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
182
|
+
project: (pkg && pkg.name) || path.basename(root),
|
|
183
|
+
version: (pkg && pkg.version) || '0.1.0',
|
|
184
|
+
description: (pkg && pkg.description) || '',
|
|
185
|
+
license: (pkg && pkg.license) || '',
|
|
186
|
+
author: (pkg && pkg.author) || '',
|
|
187
|
+
homepage: (pkg && pkg.homepage) || '',
|
|
188
|
+
repository: extractRepoUrl(pkg),
|
|
189
|
+
techStack: Object.assign({}, ts, { package_manager: pm }),
|
|
190
|
+
stackLabel: [ts.language, ts.framework].filter(Boolean).join(' + ') || 'Project',
|
|
191
|
+
packageManager: pm
|
|
491
192
|
};
|
|
492
193
|
}
|
|
493
194
|
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
for (const relativeFile of ANALYSIS_ROOT_FILES) {
|
|
499
|
-
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
500
|
-
|
|
501
|
-
if (fs.existsSync(absoluteFile) && isInsideProjectRoot(resolvedRoot, absoluteFile)) {
|
|
502
|
-
selectedFiles.add(relativeFile);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const discoveredFiles = scanProjectFiles(resolvedRoot, 4, {
|
|
507
|
-
includeDirectories: ANALYSIS_DIRECTORIES
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
for (const relativeFile of discoveredFiles) {
|
|
511
|
-
selectedFiles.add(relativeFile);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return Array.from(selectedFiles)
|
|
515
|
-
.sort()
|
|
516
|
-
.map((relativeFile) => {
|
|
517
|
-
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
const content = fs.readFileSync(absoluteFile, 'utf8');
|
|
521
|
-
return {
|
|
522
|
-
path: relativeFile,
|
|
523
|
-
content: content.slice(0, 50000)
|
|
524
|
-
};
|
|
525
|
-
} catch (error) {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
})
|
|
529
|
-
.filter(Boolean);
|
|
195
|
+
function extractRepoUrl(pkg) {
|
|
196
|
+
if (!pkg || !pkg.repository) return '';
|
|
197
|
+
return typeof pkg.repository === 'string' ? pkg.repository : (pkg.repository.url || '');
|
|
530
198
|
}
|
|
531
199
|
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const allContent = analysisInputs.map((input) => input.content).join('\n');
|
|
542
|
-
const hasDirectory = (directoryName) =>
|
|
543
|
-
analysisInputs.some((input) => normalizeProjectPath(input.path).startsWith(`${directoryName}/`));
|
|
544
|
-
const hasRuntimePattern = (pattern) => pattern.test(runtimeContent);
|
|
545
|
-
const hasAutomationPattern = (pattern) => pattern.test(automationContent);
|
|
546
|
-
const hasAnyPattern = (pattern) => pattern.test(allContent);
|
|
200
|
+
function detectTechStack(root, pkg) {
|
|
201
|
+
const deps = Object.assign({}, (pkg && pkg.dependencies) || {}, (pkg && pkg.devDependencies) || {});
|
|
202
|
+
const has = (k) => Boolean(deps[k]);
|
|
203
|
+
const fileExists = (f) => fs.existsSync(path.join(root, f));
|
|
204
|
+
|
|
205
|
+
const hasPy = ['pyproject.toml','requirements.txt','setup.py'].some(fileExists);
|
|
206
|
+
let language = '', runtime = '';
|
|
207
|
+
if (pkg || anyFileExt(root, ['.js','.ts','.mjs'])) { language = 'Node.js'; runtime = 'Node.js'; }
|
|
208
|
+
else if (hasPy || anyFileExt(root, ['.py'])) { language = 'Python'; runtime = 'Python'; }
|
|
547
209
|
|
|
548
210
|
return {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
hasNext: dependencyNames.has('next'),
|
|
560
|
-
hasReact: dependencyNames.has('react'),
|
|
561
|
-
hasFastify: dependencyNames.has('fastify'),
|
|
562
|
-
hasKoa: dependencyNames.has('koa'),
|
|
563
|
-
hasRestRoutes: hasRuntimePattern(/\b(router|app)\.(get|post|put|patch|delete)\s*\(/),
|
|
564
|
-
hasMiddleware: hasRuntimePattern(/\bapp\.use\s*\(/) || hasDirectory('middleware'),
|
|
565
|
-
hasJwt:
|
|
566
|
-
dependencyNames.has('jsonwebtoken') ||
|
|
567
|
-
hasRuntimePattern(/\bjwt\.(sign|verify)\s*\(/) ||
|
|
568
|
-
hasRuntimePattern(/\brequire\(['"]jsonwebtoken['"]\)/),
|
|
569
|
-
hasMongoose:
|
|
570
|
-
dependencyNames.has('mongoose') ||
|
|
571
|
-
hasRuntimePattern(/\bmongoose\.connect\s*\(/) ||
|
|
572
|
-
hasRuntimePattern(/\brequire\(['"]mongoose['"]\)/),
|
|
573
|
-
hasSocketIO:
|
|
574
|
-
dependencyNames.has('socket.io') ||
|
|
575
|
-
hasRuntimePattern(/\bsocket\.io\b/) ||
|
|
576
|
-
hasRuntimePattern(/\brequire\(['"]socket\.io['"]\)/),
|
|
577
|
-
hasAxiosOrFetch:
|
|
578
|
-
dependencyNames.has('axios') ||
|
|
579
|
-
hasRuntimePattern(/\baxios\./) ||
|
|
580
|
-
hasRuntimePattern(/\bfetch\s*\(/),
|
|
581
|
-
hasWatcher:
|
|
582
|
-
dependencyNames.has('chokidar') ||
|
|
583
|
-
hasAutomationPattern(/\bchokidar\.watch\s*\(/) ||
|
|
584
|
-
hasAutomationPattern(/\bfs\.watch\s*\(/),
|
|
585
|
-
hasGitAutomation:
|
|
586
|
-
hasAutomationPattern(/\bgit\s+(add|commit|push)\b/) ||
|
|
587
|
-
hasAutomationPattern(/\b(syncContextToGit|linkGithubRepository)\b/),
|
|
588
|
-
hasAiContextArtifacts:
|
|
589
|
-
hasAutomationPattern(/\bstate\.json\b/) ||
|
|
590
|
-
hasAutomationPattern(/\bbrain\.txt\b/) ||
|
|
591
|
-
hasAutomationPattern(/\bcontext\.md\b/) ||
|
|
592
|
-
hasAutomationPattern(/\bchangelog\.json\b/),
|
|
593
|
-
hasControllers: hasDirectory('controllers'),
|
|
594
|
-
hasServices: hasDirectory('services'),
|
|
595
|
-
hasRoutes: hasDirectory('routes'),
|
|
596
|
-
hasServerDirectory: hasDirectory('server'),
|
|
597
|
-
hasConfigDirectory: hasDirectory('config'),
|
|
598
|
-
hasTypeScriptConfig: analysisInputs.some((input) => input.path === 'tsconfig.json'),
|
|
599
|
-
hasPythonRequirements: analysisInputs.some((input) => input.path === 'requirements.txt'),
|
|
600
|
-
hasNodeRuntime: techStack.language === 'Node.js',
|
|
601
|
-
hasPythonRuntime: techStack.language === 'Python',
|
|
602
|
-
hasApplicationEntries: runtimeInputs.length > 0,
|
|
603
|
-
hasAnyContent: hasAnyPattern(/\S/)
|
|
211
|
+
language, runtime,
|
|
212
|
+
framework: detectFramework(deps),
|
|
213
|
+
package_manager: detectPackageManager(root),
|
|
214
|
+
typescript: Boolean(has('typescript') || fileExists('tsconfig.json')),
|
|
215
|
+
node_version: (pkg && pkg.engines && pkg.engines.node) || '',
|
|
216
|
+
databases: detectDatabases(deps),
|
|
217
|
+
test_framework: detectTestFw(deps, root),
|
|
218
|
+
bundler: detectBundler(deps),
|
|
219
|
+
linter: detectLinter(deps, root),
|
|
220
|
+
cloud_platform: detectCloud(root)
|
|
604
221
|
};
|
|
605
222
|
}
|
|
606
223
|
|
|
607
|
-
function
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return (
|
|
611
|
-
normalizedPath.startsWith('routes/') ||
|
|
612
|
-
normalizedPath.startsWith('server/') ||
|
|
613
|
-
normalizedPath.startsWith('controllers/') ||
|
|
614
|
-
normalizedPath.startsWith('services/') ||
|
|
615
|
-
normalizedPath.startsWith('middleware/') ||
|
|
616
|
-
normalizedPath.startsWith('config/') ||
|
|
617
|
-
normalizedPath.startsWith('src/') ||
|
|
618
|
-
normalizedPath === 'app.js' ||
|
|
619
|
-
normalizedPath === 'app.ts' ||
|
|
620
|
-
normalizedPath === 'index.js' ||
|
|
621
|
-
normalizedPath === 'index.ts' ||
|
|
622
|
-
normalizedPath === 'main.py'
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function isAutomationImplementationFile(relativePath) {
|
|
627
|
-
const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
|
|
628
|
-
|
|
629
|
-
return (
|
|
630
|
-
normalizedPath.startsWith('core/') ||
|
|
631
|
-
normalizedPath.startsWith('bin/') ||
|
|
632
|
-
normalizedPath === 'package.json' ||
|
|
633
|
-
normalizedPath === 'index.js' ||
|
|
634
|
-
normalizedPath === 'index.ts'
|
|
635
|
-
);
|
|
224
|
+
function anyFileExt(root, exts) {
|
|
225
|
+
return scanFiles(root, 2).some((f) => exts.includes(path.extname(f).toLowerCase()));
|
|
636
226
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
patterns.push('Command-line automation workflow');
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (signals.hasWatcher) {
|
|
646
|
-
patterns.push('Event-driven file watching pipeline');
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
650
|
-
patterns.push('REST API architecture');
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (signals.hasMiddleware) {
|
|
654
|
-
patterns.push('Middleware-driven request pipeline');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (signals.hasControllers && signals.hasServices) {
|
|
658
|
-
patterns.push('Layered controller-service architecture');
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (signals.hasServerDirectory && signals.hasRoutes) {
|
|
662
|
-
patterns.push('Separated server bootstrap and route handling');
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (signals.hasConfigDirectory) {
|
|
666
|
-
patterns.push('Centralized configuration layer');
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (signals.hasSocketIO) {
|
|
670
|
-
patterns.push('Real-time event architecture');
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (signals.hasNext) {
|
|
674
|
-
patterns.push('Framework-driven web application structure');
|
|
675
|
-
} else if (signals.hasReact) {
|
|
676
|
-
patterns.push('Component-based frontend architecture');
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (
|
|
680
|
-
signals.hasAiContextArtifacts &&
|
|
681
|
-
signals.hasExpress &&
|
|
682
|
-
analysisInputs.some((input) => /\bstate\.json\b|\bbrain\.txt\b|\bcontext\.md\b/.test(input.content))
|
|
683
|
-
) {
|
|
684
|
-
patterns.push('Structured AI context delivery workflow');
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (rootPackage && Array.isArray(rootPackage.keywords) && rootPackage.keywords.includes('cli')) {
|
|
688
|
-
patterns.push('Package-distributed CLI architecture');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return uniqueNonEmpty(patterns).slice(0, 6);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function buildImplementationDetails(signals, techStack) {
|
|
695
|
-
const details = [];
|
|
696
|
-
|
|
697
|
-
if (signals.hasJwt) {
|
|
698
|
-
details.push('JWT-based authentication system');
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (signals.hasMongoose) {
|
|
702
|
-
details.push('MongoDB integration through Mongoose ORM');
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
706
|
-
details.push('REST API architecture using Express routing');
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (signals.hasMiddleware) {
|
|
710
|
-
details.push('Express middleware pipeline for request handling');
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (signals.hasSocketIO) {
|
|
714
|
-
details.push('Socket.IO-based real-time communication');
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (signals.hasAxiosOrFetch) {
|
|
718
|
-
details.push('External API integration for outbound service calls');
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (signals.hasWatcher) {
|
|
722
|
-
details.push('File system monitoring for automatic project state updates');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (signals.hasAiContextArtifacts) {
|
|
726
|
-
details.push('Structured AI context generation across JSON, Markdown, and instruction files');
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (signals.hasGitAutomation) {
|
|
730
|
-
details.push('Git-backed synchronization workflow for publishing project state');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (signals.hasCliEntry && techStack.language === 'Node.js') {
|
|
734
|
-
details.push('Node.js CLI entrypoint for developer-facing automation');
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
return uniqueNonEmpty(details).slice(0, MAX_IMPLEMENTATION_DETAILS);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function buildKeyFeatures(signals, techStack) {
|
|
741
|
-
const features = [];
|
|
742
|
-
|
|
743
|
-
if (signals.hasAiContextArtifacts) {
|
|
744
|
-
features.push(FEATURE_CATALOG.ai_context_generation);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (signals.hasCliEntry) {
|
|
748
|
-
features.push(FEATURE_CATALOG.cli_automation);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (signals.hasWatcher) {
|
|
752
|
-
features.push(FEATURE_CATALOG.change_tracking);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (signals.hasGitAutomation) {
|
|
756
|
-
features.push(FEATURE_CATALOG.public_sync);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (signals.hasExpress && signals.hasAiContextArtifacts) {
|
|
760
|
-
features.push(FEATURE_CATALOG.local_context_server);
|
|
761
|
-
} else if (signals.hasExpress && signals.hasRestRoutes) {
|
|
762
|
-
features.push(FEATURE_CATALOG.rest_api);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (signals.hasJwt) {
|
|
766
|
-
features.push(FEATURE_CATALOG.auth);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (signals.hasMongoose) {
|
|
770
|
-
features.push(FEATURE_CATALOG.persistence);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (signals.hasSocketIO) {
|
|
774
|
-
features.push(FEATURE_CATALOG.realtime);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (signals.hasAxiosOrFetch) {
|
|
778
|
-
features.push(FEATURE_CATALOG.external_api);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (signals.hasMiddleware) {
|
|
782
|
-
features.push(FEATURE_CATALOG.middleware);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (signals.hasConfigDirectory) {
|
|
786
|
-
features.push(FEATURE_CATALOG.config_management);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (features.length === 0 && techStack.language) {
|
|
790
|
-
features.push(`${techStack.language} project structure with detectable application entry points`);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
return uniqueNonEmpty(features).slice(0, MAX_KEY_FEATURES);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function determineProjectType(signals, techStack, rootPackage) {
|
|
797
|
-
if (signals.hasNext) {
|
|
798
|
-
return 'Next.js application';
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
|
|
802
|
-
return 'CLI tool';
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
806
|
-
return 'backend API platform';
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (signals.hasReact) {
|
|
810
|
-
return 'frontend application';
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (signals.hasPythonRuntime) {
|
|
814
|
-
return 'Python application';
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (rootPackage && rootPackage.bin) {
|
|
818
|
-
return 'CLI tool';
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (techStack.language === 'Node.js') {
|
|
822
|
-
return 'Node.js application';
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return 'software project';
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
async function loadRuntimeConfig(projectRoot) {
|
|
829
|
-
const { configFile } = getContextPaths(projectRoot);
|
|
830
|
-
const userConfig = await readJsonFile(configFile, {});
|
|
831
|
-
return deepMerge(DEFAULT_CONFIG, userConfig);
|
|
227
|
+
function detectPackageManager(root) {
|
|
228
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
229
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn';
|
|
230
|
+
if (fs.existsSync(path.join(root, 'bun.lockb'))) return 'bun';
|
|
231
|
+
return 'npm';
|
|
832
232
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return
|
|
233
|
+
function detectFramework(d) {
|
|
234
|
+
if (d.next) return 'Next.js';
|
|
235
|
+
if (d.nuxt) return 'Nuxt';
|
|
236
|
+
if (d.react) return 'React';
|
|
237
|
+
if (d.vue) return 'Vue';
|
|
238
|
+
if (d.svelte) return 'Svelte';
|
|
239
|
+
if (d.express) return 'Express';
|
|
240
|
+
if (d.fastify) return 'Fastify';
|
|
241
|
+
if (d.koa) return 'Koa';
|
|
242
|
+
if (d['@nestjs/core'])return 'NestJS';
|
|
243
|
+
if (d.hono) return 'Hono';
|
|
244
|
+
return '';
|
|
842
245
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const meaningfulEvents = collapseEventsByFile(validEvents.filter((event) => isMeaningfulEvent(event)));
|
|
866
|
-
const groupedUpdates = groupEventsByIntent(meaningfulEvents, bootstrap);
|
|
867
|
-
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
868
|
-
const historyEntries = dedupeHistoryEntries(groupedUpdates.concat(previousHistoryEntries))
|
|
869
|
-
.slice(0, MAX_CHANGELOG_ENTRIES);
|
|
870
|
-
const promotedFeatures = promoteFeatures(historyEntries);
|
|
871
|
-
const keyFeatures = mergeKeyFeatures(bootstrap.keyFeatures, promotedFeatures);
|
|
872
|
-
const recentUpdates = groupedUpdates.length > 0
|
|
873
|
-
? dedupeRecentUpdates(groupedUpdates.map(toStateUpdate).concat(normalizeStoredUpdates(existingState.recent_updates)))
|
|
874
|
-
.slice(0, MAX_RECENT_UPDATES)
|
|
875
|
-
: normalizeStoredUpdates(existingState.recent_updates).slice(0, MAX_RECENT_UPDATES);
|
|
876
|
-
const mergedArchitecturePatterns = mergePreferredArray(
|
|
877
|
-
bootstrap.architecturePatterns,
|
|
878
|
-
existingState.architecture_patterns
|
|
879
|
-
);
|
|
880
|
-
const mergedImplementationDetails = mergePreferredArray(
|
|
881
|
-
bootstrap.implementationDetails,
|
|
882
|
-
existingState.implementation_details
|
|
883
|
-
);
|
|
884
|
-
const nextState = composeStateFromAnalysis(existingState, metadata, bootstrap, {
|
|
885
|
-
last_updated: timestamp,
|
|
886
|
-
tech_stack: mergePreferredObject(bootstrap.techStack, existingState.tech_stack),
|
|
887
|
-
architecture_patterns: mergedArchitecturePatterns,
|
|
888
|
-
implementation_details: mergedImplementationDetails,
|
|
889
|
-
current_stage: determineCurrentStage(
|
|
890
|
-
keyFeatures,
|
|
891
|
-
historyEntries,
|
|
892
|
-
mergedImplementationDetails
|
|
893
|
-
),
|
|
894
|
-
recent_updates: recentUpdates,
|
|
895
|
-
key_features: keyFeatures,
|
|
896
|
-
known_issues: deriveKnownIssues(resolvedRoot, Object.assign({}, bootstrap, {
|
|
897
|
-
architecturePatterns: mergedArchitecturePatterns,
|
|
898
|
-
implementationDetails: mergedImplementationDetails,
|
|
899
|
-
keyFeatures
|
|
900
|
-
})),
|
|
901
|
-
next_steps: []
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
nextState.ai_summary = generateAiSummary(nextState, bootstrap);
|
|
905
|
-
nextState.next_steps = generateNextSteps(nextState, bootstrap, historyEntries);
|
|
906
|
-
|
|
907
|
-
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
908
|
-
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
909
|
-
|
|
910
|
-
if (logger) {
|
|
911
|
-
logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
if (typeof settings.syncCallback === 'function') {
|
|
915
|
-
await settings.syncCallback();
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
return nextState;
|
|
246
|
+
function detectDatabases(d) {
|
|
247
|
+
const r = [];
|
|
248
|
+
if (d.mongoose || d.mongodb) r.push('MongoDB');
|
|
249
|
+
if (d.pg || d['pg-pool']) r.push('PostgreSQL');
|
|
250
|
+
if (d.mysql || d.mysql2) r.push('MySQL');
|
|
251
|
+
if (d.sqlite3 || d['better-sqlite3']) r.push('SQLite');
|
|
252
|
+
if (d.redis || d.ioredis) r.push('Redis');
|
|
253
|
+
if (d['@prisma/client']) r.push('Prisma');
|
|
254
|
+
if (d.sequelize) r.push('Sequelize');
|
|
255
|
+
if (d.typeorm) r.push('TypeORM');
|
|
256
|
+
if (d.knex) r.push('Knex');
|
|
257
|
+
return r;
|
|
258
|
+
}
|
|
259
|
+
function detectTestFw(d, root) {
|
|
260
|
+
if (d.jest || d['@jest/core']) return 'Jest';
|
|
261
|
+
if (d.vitest) return 'Vitest';
|
|
262
|
+
if (d.mocha) return 'Mocha';
|
|
263
|
+
if (d.jasmine) return 'Jasmine';
|
|
264
|
+
if (d.ava) return 'AVA';
|
|
265
|
+
if (fs.existsSync(path.join(root, 'jest.config.js'))) return 'Jest';
|
|
266
|
+
if (fs.existsSync(path.join(root, 'vitest.config.js'))) return 'Vitest';
|
|
267
|
+
return '';
|
|
919
268
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
return scoreEvent(event.file) >= 2;
|
|
269
|
+
function detectBundler(d) {
|
|
270
|
+
if (d.webpack) return 'Webpack'; if (d.vite) return 'Vite';
|
|
271
|
+
if (d.esbuild) return 'esbuild'; if (d.rollup) return 'Rollup';
|
|
272
|
+
if (d.parcel) return 'Parcel'; return '';
|
|
927
273
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
for (const event of events) {
|
|
933
|
-
const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
|
|
934
|
-
collapsedEvents.set(
|
|
935
|
-
normalizedFile,
|
|
936
|
-
Object.assign({}, event, {
|
|
937
|
-
file: normalizeProjectPath(event.file)
|
|
938
|
-
})
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
return Array.from(collapsedEvents.values());
|
|
274
|
+
function detectLinter(d, root) {
|
|
275
|
+
if (d.eslint || fs.existsSync(path.join(root, '.eslintrc.js')) || fs.existsSync(path.join(root, '.eslintrc.json'))) return 'ESLint';
|
|
276
|
+
if (d.biome || fs.existsSync(path.join(root, 'biome.json'))) return 'Biome';
|
|
277
|
+
return '';
|
|
943
278
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
279
|
+
function detectCloud(root) {
|
|
280
|
+
const checks = [
|
|
281
|
+
['vercel.json','Vercel'],['netlify.toml','Netlify'],['railway.json','Railway'],
|
|
282
|
+
['fly.toml','Fly.io'],['render.yaml','Render'],['Dockerfile','Docker'],
|
|
283
|
+
['docker-compose.yml','Docker Compose'],['serverless.yml','Serverless Framework']
|
|
284
|
+
];
|
|
285
|
+
for (const [f, name] of checks) if (fs.existsSync(path.join(root, f))) return name;
|
|
286
|
+
if (fs.existsSync(path.join(root, '.github/workflows'))) return 'GitHub Actions';
|
|
287
|
+
return '';
|
|
952
288
|
}
|
|
953
289
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (lowerPath === 'package.json') {
|
|
958
|
-
return 'configuration';
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (lowerPath === 'readme.md') {
|
|
962
|
-
return 'documentation';
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
if (lowerPath.startsWith('bin/')) {
|
|
966
|
-
return 'cli';
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
970
|
-
return 'backend';
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (
|
|
974
|
-
lowerPath.startsWith('controllers/') ||
|
|
975
|
-
lowerPath.startsWith('services/') ||
|
|
976
|
-
lowerPath.startsWith('middleware/') ||
|
|
977
|
-
lowerPath.startsWith('config/')
|
|
978
|
-
) {
|
|
979
|
-
return 'application';
|
|
980
|
-
}
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────
|
|
291
|
+
// Dependency catalogue
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────
|
|
981
293
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
294
|
+
function buildDependencyCatalogue(projectRoot) {
|
|
295
|
+
const pkg = readPkg(resolveRoot(projectRoot));
|
|
296
|
+
if (!pkg) return { production: {}, development: {}, peer: {}, scripts: {} };
|
|
297
|
+
return {
|
|
298
|
+
production: pkg.dependencies || {},
|
|
299
|
+
development: pkg.devDependencies || {},
|
|
300
|
+
peer: pkg.peerDependencies || {},
|
|
301
|
+
scripts: pkg.scripts || {}
|
|
302
|
+
};
|
|
987
303
|
}
|
|
988
304
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (lowerPath === 'package.json') {
|
|
994
|
-
if (signals.hasGitAutomation) {
|
|
995
|
-
return 'public_sync';
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (signals.hasCliEntry) {
|
|
999
|
-
return 'cli_automation';
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
return 'config_management';
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (lowerPath.startsWith('bin/')) {
|
|
1006
|
-
return 'cli_automation';
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
1010
|
-
if (signals.hasAiContextArtifacts) {
|
|
1011
|
-
return 'local_context_server';
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
return 'rest_api';
|
|
1015
|
-
}
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────
|
|
306
|
+
// Env variable scanning
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1016
308
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
309
|
+
function scanEnvVars(projectRoot) {
|
|
310
|
+
const root = resolveRoot(projectRoot);
|
|
311
|
+
const vars = new Set();
|
|
312
|
+
const files = ['.env','.env.example','.env.sample','.env.local'];
|
|
313
|
+
for (const f of files) {
|
|
314
|
+
const fp = path.join(root, f);
|
|
315
|
+
if (!fs.existsSync(fp)) continue;
|
|
316
|
+
try {
|
|
317
|
+
for (const line of fs.readFileSync(fp, 'utf8').split('\n')) {
|
|
318
|
+
const t = line.trim();
|
|
319
|
+
if (!t || t.startsWith('#')) continue;
|
|
320
|
+
const m = t.match(/^([A-Z0-9_]+)\s*=/);
|
|
321
|
+
if (m) vars.add(m[1]);
|
|
322
|
+
}
|
|
323
|
+
} catch (_) {}
|
|
1031
324
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
325
|
+
// scan source for process.env refs
|
|
326
|
+
for (const rel of scanFiles(root, 4).slice(0, 60)) {
|
|
327
|
+
if (!['.js','.ts','.mjs','.cjs'].includes(path.extname(rel))) continue;
|
|
328
|
+
try {
|
|
329
|
+
const src = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
330
|
+
for (const m of src.matchAll(/process\.env\.([A-Z0-9_]+)/g)) vars.add(m[1]);
|
|
331
|
+
} catch (_) {}
|
|
1035
332
|
}
|
|
1036
|
-
|
|
1037
|
-
return 'ai_context_generation';
|
|
333
|
+
return Array.from(vars).sort();
|
|
1038
334
|
}
|
|
1039
335
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────
|
|
337
|
+
// API route extraction
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1044
339
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
)
|
|
340
|
+
function extractApiRoutes(projectRoot) {
|
|
341
|
+
const root = resolveRoot(projectRoot);
|
|
342
|
+
const routes = [];
|
|
343
|
+
const seen = new Set();
|
|
344
|
+
for (const rel of scanFiles(root, 4)) {
|
|
345
|
+
if (!['.js','.ts'].includes(path.extname(rel))) continue;
|
|
346
|
+
try {
|
|
347
|
+
const src = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
348
|
+
const pat = /\b(?:router|app)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gm;
|
|
349
|
+
let m;
|
|
350
|
+
while ((m = pat.exec(src)) !== null) {
|
|
351
|
+
const key = `${m[1].toUpperCase()}:${m[2]}`;
|
|
352
|
+
if (seen.has(key)) continue;
|
|
353
|
+
seen.add(key);
|
|
354
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
355
|
+
}
|
|
356
|
+
} catch (_) {}
|
|
1062
357
|
}
|
|
1063
|
-
|
|
1064
|
-
return Array.from(buckets.values())
|
|
1065
|
-
.map((group) => interpretIntentGroup(group))
|
|
1066
|
-
.filter(Boolean)
|
|
1067
|
-
.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
358
|
+
return routes;
|
|
1068
359
|
}
|
|
1069
360
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}, group[0].timestamp || new Date().toISOString());
|
|
1074
|
-
const featureKey = group[0].featureKey;
|
|
1075
|
-
const area = group[0].area;
|
|
1076
|
-
const type = determineGroupedUpdateType(group);
|
|
1077
|
-
const subject = describeIntentSubject(featureKey);
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────
|
|
362
|
+
// Issue tracker
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1078
364
|
|
|
365
|
+
function createDefaultIssueTracker() {
|
|
1079
366
|
return {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
feature_name: FEATURE_CATALOG[featureKey] || subject
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function determineGroupedUpdateType(group) {
|
|
1091
|
-
const hasAdd = group.some((event) => event.action === 'add');
|
|
1092
|
-
const hasDelete = group.some((event) => event.action === 'delete');
|
|
1093
|
-
|
|
1094
|
-
if (hasAdd) {
|
|
1095
|
-
return 'feature';
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (hasDelete) {
|
|
1099
|
-
return 'fix';
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (group.length > 2) {
|
|
1103
|
-
return 'refactor';
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return 'improvement';
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function describeIntentSubject(featureKey) {
|
|
1110
|
-
const subjectMap = {
|
|
1111
|
-
ai_context_generation: 'AI context generation workflow',
|
|
1112
|
-
cli_automation: 'CLI automation workflow',
|
|
1113
|
-
change_tracking: 'change tracking workflow',
|
|
1114
|
-
public_sync: 'GitHub sync workflow',
|
|
1115
|
-
local_context_server: 'context delivery service',
|
|
1116
|
-
rest_api: 'API architecture',
|
|
1117
|
-
auth: 'authentication workflow',
|
|
1118
|
-
persistence: 'data persistence layer',
|
|
1119
|
-
realtime: 'real-time communication layer',
|
|
1120
|
-
external_api: 'external integration workflow',
|
|
1121
|
-
middleware: 'request processing workflow',
|
|
1122
|
-
config_management: 'project configuration workflow'
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
return subjectMap[featureKey] || 'project workflow';
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
function buildIntentTitle(type, subject) {
|
|
1129
|
-
const verbs = {
|
|
1130
|
-
feature: 'Expanded',
|
|
1131
|
-
improvement: 'Improved',
|
|
1132
|
-
refactor: 'Refined',
|
|
1133
|
-
fix: 'Stabilized'
|
|
367
|
+
active_errors: [],
|
|
368
|
+
resolved_issues: [],
|
|
369
|
+
last_error_at: null,
|
|
370
|
+
last_resolved_at: null,
|
|
371
|
+
total_errors_seen: 0,
|
|
372
|
+
total_resolved: 0
|
|
1134
373
|
};
|
|
1135
|
-
|
|
1136
|
-
return `${verbs[type] || 'Improved'} ${subject}`;
|
|
1137
374
|
}
|
|
1138
375
|
|
|
1139
|
-
function
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
persistence: 'Improves how project data is persisted and retrieved.',
|
|
1149
|
-
realtime: 'Improves real-time communication behavior.',
|
|
1150
|
-
external_api: 'Improves outbound integration reliability.',
|
|
1151
|
-
middleware: 'Improves request handling and middleware orchestration.',
|
|
1152
|
-
config_management: 'Improves project configuration and packaging reliability.'
|
|
376
|
+
function applyErrorEvent(tracker, event) {
|
|
377
|
+
const t = Object.assign({}, createDefaultIssueTracker(), tracker);
|
|
378
|
+
const entry = {
|
|
379
|
+
id: `err_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
380
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
381
|
+
message: event.message || 'Unknown error',
|
|
382
|
+
file: event.file || '',
|
|
383
|
+
stack: event.stack || '',
|
|
384
|
+
status: 'active'
|
|
1153
385
|
};
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (type === 'feature') {
|
|
1160
|
-
return impactMap[featureKey].replace(/^Improves /, 'Adds ');
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return impactMap[featureKey] || 'Improves the overall project workflow.';
|
|
386
|
+
return Object.assign({}, t, {
|
|
387
|
+
active_errors: [entry, ...t.active_errors].slice(0, MAX_ACTIVE_ERRORS),
|
|
388
|
+
last_error_at: entry.timestamp,
|
|
389
|
+
total_errors_seen: t.total_errors_seen + 1
|
|
390
|
+
});
|
|
1164
391
|
}
|
|
1165
392
|
|
|
1166
|
-
function
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
393
|
+
function applyResolveEvent(tracker, event) {
|
|
394
|
+
const t = Object.assign({}, createDefaultIssueTracker(), tracker);
|
|
395
|
+
const now = event.timestamp || new Date().toISOString();
|
|
396
|
+
let actives = [...t.active_errors];
|
|
397
|
+
let resolved = null;
|
|
1170
398
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
.filter(Boolean)
|
|
399
|
+
const idx = actives.findIndex((e) =>
|
|
400
|
+
(event.errorId && e.id === event.errorId) ||
|
|
401
|
+
(event.message && e.message && e.message.toLowerCase().includes(event.message.toLowerCase()))
|
|
1175
402
|
);
|
|
1176
|
-
}
|
|
1177
403
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
404
|
+
if (idx !== -1) {
|
|
405
|
+
resolved = Object.assign({}, actives[idx], {
|
|
406
|
+
status: 'resolved',
|
|
407
|
+
resolved_at: now,
|
|
408
|
+
resolution: event.resolution || 'Marked resolved'
|
|
409
|
+
});
|
|
410
|
+
actives.splice(idx, 1);
|
|
411
|
+
} else {
|
|
412
|
+
resolved = {
|
|
413
|
+
id: `res_${Date.now()}`,
|
|
414
|
+
timestamp: now,
|
|
415
|
+
resolved_at: now,
|
|
416
|
+
message: event.message || 'Issue resolved',
|
|
417
|
+
resolution: event.resolution || 'Marked resolved',
|
|
418
|
+
file: event.file || '',
|
|
419
|
+
status: 'resolved'
|
|
1188
420
|
};
|
|
1189
421
|
}
|
|
1190
422
|
|
|
1191
|
-
return
|
|
423
|
+
return Object.assign({}, t, {
|
|
424
|
+
active_errors: actives,
|
|
425
|
+
resolved_issues: [resolved, ...t.resolved_issues].slice(0, MAX_RESOLVED_ISSUES),
|
|
426
|
+
last_resolved_at: now,
|
|
427
|
+
total_resolved: t.total_resolved + 1
|
|
428
|
+
});
|
|
1192
429
|
}
|
|
1193
430
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
return dedupeHistoryEntries(
|
|
1200
|
-
entries
|
|
1201
|
-
.map((entry) => normalizeStoredHistoryEntry(entry))
|
|
1202
|
-
.filter(Boolean)
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────
|
|
432
|
+
// Code change history ← THE KEY NEW SECTION
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1205
434
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Given a file path and its old/new content snapshots,
|
|
437
|
+
* build a rich code change entry for the changelog.
|
|
438
|
+
*/
|
|
439
|
+
function buildCodeChangeEntry(relPath, oldContent, newContent, action, timestamp) {
|
|
440
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
441
|
+
const isCode = CODE_EXTENSIONS.has(ext);
|
|
442
|
+
const ts = timestamp || new Date().toISOString();
|
|
1210
443
|
|
|
1211
|
-
if (
|
|
444
|
+
if (action === 'delete') {
|
|
1212
445
|
return {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
446
|
+
id: `chg_${Date.now()}_${Math.random().toString(36).slice(2,6)}`,
|
|
447
|
+
timestamp: ts,
|
|
448
|
+
action: 'delete',
|
|
449
|
+
file: relPath,
|
|
450
|
+
summary: `Deleted ${relPath}`,
|
|
451
|
+
signals: ['File removed from project'],
|
|
452
|
+
patch: null,
|
|
453
|
+
lines_added: 0,
|
|
454
|
+
lines_removed: 0
|
|
1220
455
|
};
|
|
1221
456
|
}
|
|
1222
457
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (combinedText.includes('api') || combinedText.includes('route')) {
|
|
1242
|
-
return 'rest_api';
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
|
|
1246
|
-
return 'public_sync';
|
|
458
|
+
if (action === 'add' && !oldContent) {
|
|
459
|
+
const lines = (newContent || '').split('\n').length;
|
|
460
|
+
const signals = isCode ? extractCodeSignals(
|
|
461
|
+
(newContent || '').split('\n').map((l) => `+${l}`).join('\n'),
|
|
462
|
+
relPath
|
|
463
|
+
) : ['New file added'];
|
|
464
|
+
return {
|
|
465
|
+
id: `chg_${Date.now()}_${Math.random().toString(36).slice(2,6)}`,
|
|
466
|
+
timestamp: ts,
|
|
467
|
+
action: 'add',
|
|
468
|
+
file: relPath,
|
|
469
|
+
summary: `Added ${relPath} (${lines} lines)`,
|
|
470
|
+
signals,
|
|
471
|
+
patch: null,
|
|
472
|
+
lines_added: lines,
|
|
473
|
+
lines_removed: 0
|
|
474
|
+
};
|
|
1247
475
|
}
|
|
1248
476
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
477
|
+
// change – produce real diff
|
|
478
|
+
const old_ = oldContent || '';
|
|
479
|
+
const new_ = newContent || '';
|
|
1252
480
|
|
|
1253
|
-
if (
|
|
1254
|
-
return 'cli_automation';
|
|
1255
|
-
}
|
|
481
|
+
if (old_ === new_) return null; // no actual change
|
|
1256
482
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
}
|
|
483
|
+
const { patch, linesAdded, linesRemoved, summary } = diffTexts(old_, new_, relPath);
|
|
484
|
+
const signals = isCode ? extractCodeSignals(patch, relPath) : ['File updated'];
|
|
1260
485
|
|
|
1261
|
-
return
|
|
486
|
+
return {
|
|
487
|
+
id: `chg_${Date.now()}_${Math.random().toString(36).slice(2,6)}`,
|
|
488
|
+
timestamp: ts,
|
|
489
|
+
action: 'change',
|
|
490
|
+
file: relPath,
|
|
491
|
+
summary,
|
|
492
|
+
signals,
|
|
493
|
+
patch: patch.length > 8000 ? patch.slice(0, 8000) + '\n[patch truncated]' : patch,
|
|
494
|
+
lines_added: linesAdded,
|
|
495
|
+
lines_removed: linesRemoved
|
|
496
|
+
};
|
|
1262
497
|
}
|
|
1263
498
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (type === 'removal') {
|
|
1270
|
-
return 'fix';
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
return 'improvement';
|
|
1274
|
-
}
|
|
499
|
+
// ─────────────────────────────────────────────────────────────────
|
|
500
|
+
// Bootstrap project analysis
|
|
501
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1275
502
|
|
|
1276
|
-
function
|
|
503
|
+
function bootstrapProjectAnalysis(projectRoot) {
|
|
504
|
+
const root = resolveRoot(projectRoot);
|
|
505
|
+
const meta = detectProjectMetadata(root);
|
|
506
|
+
const pkg = readPkg(root);
|
|
507
|
+
const ts = meta.techStack;
|
|
508
|
+
const files = scanFiles(root, 4);
|
|
509
|
+
const inputs = collectAnalysisInputs(root, files);
|
|
510
|
+
const signals = detectSignals(inputs, pkg, ts);
|
|
1277
511
|
return {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
512
|
+
projectType: inferProjectType(signals, ts, pkg),
|
|
513
|
+
techStack: ts,
|
|
514
|
+
architecturePatterns: buildArchPatterns(signals, ts, pkg),
|
|
515
|
+
implementationDetails: buildImplDetails(signals, ts),
|
|
516
|
+
keyFeatures: buildKeyFeatures(signals, ts),
|
|
517
|
+
signals
|
|
1281
518
|
};
|
|
1282
519
|
}
|
|
1283
520
|
|
|
1284
|
-
function
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
521
|
+
function collectAnalysisInputs(root, files) {
|
|
522
|
+
const important = new Set([
|
|
523
|
+
'package.json','app.js','app.ts','index.js','index.ts','main.py'
|
|
524
|
+
]);
|
|
525
|
+
const importantDirs = new Set(['routes','server','controllers','services','middleware','config','src','core','bin','lib','api','utils']);
|
|
526
|
+
// Exclude aibridge's own infrastructure files — they contain detection
|
|
527
|
+
// strings (e.g. /new PrismaClient/) that would create false positives
|
|
528
|
+
// when the tool analyses itself.
|
|
529
|
+
const SELF_FILES = new Set([
|
|
530
|
+
'core/stateManager.js','core/codeDiff.js','core/fileSnapshot.js',
|
|
531
|
+
'core/watcher.js','core/gitSync.js','core/init.js',
|
|
532
|
+
'bin/cli.js','utils/logger.js'
|
|
533
|
+
]);
|
|
534
|
+
const selected = files.filter((f) => {
|
|
535
|
+
if (SELF_FILES.has(f)) return false;
|
|
536
|
+
if (important.has(f)) return true;
|
|
537
|
+
const seg = f.split('/')[0];
|
|
538
|
+
return importantDirs.has(seg);
|
|
539
|
+
});
|
|
540
|
+
return selected.slice(0, 80).map((rel) => {
|
|
541
|
+
try {
|
|
542
|
+
const content = fs.readFileSync(path.join(root, rel), 'utf8').slice(0, 50000);
|
|
543
|
+
return { path: rel, content };
|
|
544
|
+
} catch (_) { return null; }
|
|
545
|
+
}).filter(Boolean);
|
|
1300
546
|
}
|
|
1301
547
|
|
|
1302
|
-
function
|
|
1303
|
-
const
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (seen.has(key)) {
|
|
1310
|
-
continue;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
seen.add(key);
|
|
1314
|
-
result.push(entry);
|
|
1315
|
-
}
|
|
548
|
+
function detectSignals(inputs, pkg, ts) {
|
|
549
|
+
const deps = Object.assign({}, (pkg && pkg.dependencies) || {}, (pkg && pkg.devDependencies) || {});
|
|
550
|
+
const all = inputs.map((i) => i.content).join('\n');
|
|
551
|
+
const has = (p) => p.test(all);
|
|
552
|
+
const dep = (k) => Boolean(deps[k]);
|
|
553
|
+
const hasDir = (d) => inputs.some((i) => i.path.startsWith(d + '/'));
|
|
1316
554
|
|
|
1317
|
-
return
|
|
555
|
+
return {
|
|
556
|
+
hasPackageJson: Boolean(pkg),
|
|
557
|
+
hasCliEntry: Boolean(pkg && pkg.bin && Object.keys(pkg.bin).length) || has(/\bprocess\.argv\b/),
|
|
558
|
+
hasExpress: dep('express') || has(/require\(['"]express['"]\)/) || has(/\bexpress\(\)/),
|
|
559
|
+
hasNext: dep('next'),
|
|
560
|
+
hasReact: dep('react'),
|
|
561
|
+
hasFastify: dep('fastify'),
|
|
562
|
+
hasNest: dep('@nestjs/core'),
|
|
563
|
+
hasRestRoutes: has(/\b(router|app)\.(get|post|put|patch|delete)\s*\(/),
|
|
564
|
+
hasMiddleware: has(/\bapp\.use\s*\(/) || hasDir('middleware'),
|
|
565
|
+
hasJwt: dep('jsonwebtoken') || has(/\bjwt\.(sign|verify)/),
|
|
566
|
+
hasMongoose: dep('mongoose') || has(/mongoose\.connect/),
|
|
567
|
+
hasPrisma: dep('@prisma/client') || has(/new PrismaClient/),
|
|
568
|
+
hasSocketIO: dep('socket.io') || has(/require\(['"]socket\.io['"]\)/),
|
|
569
|
+
hasAxiosOrFetch: dep('axios') || has(/\baxios\./) || has(/\bfetch\s*\(/),
|
|
570
|
+
hasWatcher: dep('chokidar') || has(/chokidar\.watch/),
|
|
571
|
+
hasGitAutomation: has(/\bgit\s+(add|commit|push)\b/) || has(/syncContextToGit/),
|
|
572
|
+
hasAiArtifacts: has(/state\.json/) && has(/brain\.txt/),
|
|
573
|
+
hasControllers: hasDir('controllers'),
|
|
574
|
+
hasServices: hasDir('services'),
|
|
575
|
+
hasRoutes: hasDir('routes'),
|
|
576
|
+
hasServerDir: hasDir('server'),
|
|
577
|
+
hasConfigDir: hasDir('config'),
|
|
578
|
+
hasTests: hasDir('test') || hasDir('tests') || hasDir('__tests__') || dep('jest') || dep('vitest') || dep('mocha'),
|
|
579
|
+
hasRedis: dep('redis') || dep('ioredis'),
|
|
580
|
+
hasQueue: dep('bull') || dep('bullmq'),
|
|
581
|
+
hasEmail: dep('nodemailer') || dep('@sendgrid/mail') || dep('resend'),
|
|
582
|
+
hasGraphQL: dep('graphql') || dep('@apollo/server'),
|
|
583
|
+
hasTypeScript: ts.typescript
|
|
584
|
+
};
|
|
1318
585
|
}
|
|
1319
586
|
|
|
1320
|
-
function
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
587
|
+
function inferProjectType(s, ts, pkg) {
|
|
588
|
+
if (s.hasNext) return 'Next.js application';
|
|
589
|
+
if (s.hasCliEntry && s.hasAiArtifacts) return 'CLI tool';
|
|
590
|
+
if (s.hasExpress && s.hasRestRoutes) return 'backend API platform';
|
|
591
|
+
if (s.hasReact) return 'frontend application';
|
|
592
|
+
if (ts.language === 'Python') return 'Python application';
|
|
593
|
+
if (pkg && pkg.bin) return 'CLI tool';
|
|
594
|
+
return 'Node.js application';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function buildArchPatterns(s, ts, pkg) {
|
|
598
|
+
const p = [];
|
|
599
|
+
if (s.hasCliEntry) p.push('Command-line automation workflow');
|
|
600
|
+
if (s.hasWatcher) p.push('Event-driven file watching pipeline');
|
|
601
|
+
if (s.hasExpress && s.hasRestRoutes) p.push('REST API architecture');
|
|
602
|
+
if (s.hasMiddleware) p.push('Middleware-driven request pipeline');
|
|
603
|
+
if (s.hasControllers && s.hasServices) p.push('Layered controller-service architecture');
|
|
604
|
+
if (s.hasServerDir && s.hasRoutes) p.push('Separated server bootstrap and route handling');
|
|
605
|
+
if (s.hasSocketIO) p.push('Real-time event architecture');
|
|
606
|
+
if (s.hasNest) p.push('NestJS modular architecture');
|
|
607
|
+
if (s.hasGraphQL) p.push('GraphQL API layer');
|
|
608
|
+
if (s.hasPrisma || s.hasMongoose) p.push('Database-backed persistence layer');
|
|
609
|
+
if (s.hasRedis) p.push('Redis-backed caching or session layer');
|
|
610
|
+
if (s.hasAiArtifacts && s.hasExpress) p.push('Structured AI context delivery workflow');
|
|
611
|
+
if (pkg && Array.isArray(pkg.keywords) && pkg.keywords.includes('cli')) p.push('Package-distributed CLI architecture');
|
|
612
|
+
return uniq(p).slice(0, 8);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function buildImplDetails(s, ts) {
|
|
616
|
+
const d = [];
|
|
617
|
+
if (s.hasJwt) d.push('JWT-based authentication');
|
|
618
|
+
if (s.hasMongoose) d.push('MongoDB via Mongoose ORM');
|
|
619
|
+
if (s.hasPrisma) d.push('Type-safe database access via Prisma');
|
|
620
|
+
if (s.hasExpress && s.hasRestRoutes) d.push('REST API with Express routing');
|
|
621
|
+
if (s.hasMiddleware) d.push('Express middleware pipeline');
|
|
622
|
+
if (s.hasSocketIO) d.push('Socket.IO real-time communication');
|
|
623
|
+
if (s.hasAxiosOrFetch) d.push('External API integration');
|
|
624
|
+
if (s.hasWatcher) d.push('Debounced file-system event watcher');
|
|
625
|
+
if (s.hasAiArtifacts) d.push('Structured AI context generation (state.json, brain.txt, context.md, changelog.json)');
|
|
626
|
+
if (s.hasGitAutomation) d.push('Git-backed context sync workflow');
|
|
627
|
+
if (s.hasCliEntry) d.push('Node.js CLI entrypoint');
|
|
628
|
+
if (s.hasGraphQL) d.push('GraphQL schema and resolvers');
|
|
629
|
+
if (s.hasRedis) d.push('Redis caching layer');
|
|
630
|
+
if (s.hasQueue) d.push('Background job queue');
|
|
631
|
+
if (s.hasEmail) d.push('Transactional email delivery');
|
|
632
|
+
if (s.hasTests) d.push('Automated test suite');
|
|
633
|
+
return uniq(d).slice(0, MAX_IMPL_DETAILS);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function buildKeyFeatures(s, ts) {
|
|
637
|
+
const f = [];
|
|
638
|
+
if (s.hasAiArtifacts) f.push('AI-readable project context generation');
|
|
639
|
+
if (s.hasCliEntry) f.push('CLI automation workflow');
|
|
640
|
+
if (s.hasWatcher) f.push('Automatic change tracking');
|
|
641
|
+
if (s.hasGitAutomation) f.push('Optional GitHub sync for public AI access');
|
|
642
|
+
if (s.hasExpress && s.hasAiArtifacts) f.push('Local HTTP context server');
|
|
643
|
+
else if (s.hasExpress) f.push('REST API');
|
|
644
|
+
if (s.hasJwt) f.push('Authentication');
|
|
645
|
+
if (s.hasMongoose || s.hasPrisma) f.push('Data persistence');
|
|
646
|
+
if (s.hasSocketIO) f.push('Real-time communication');
|
|
647
|
+
if (s.hasAxiosOrFetch) f.push('External API integration');
|
|
648
|
+
if (s.hasTests) f.push('Automated testing');
|
|
649
|
+
if (s.hasGraphQL) f.push('GraphQL API');
|
|
650
|
+
if (s.hasEmail) f.push('Email notifications');
|
|
651
|
+
return uniq(f).slice(0, MAX_KEY_FEATURES);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ─────────────────────────────────────────────────────────────────
|
|
655
|
+
// Default state / changelog
|
|
656
|
+
// ─────────────────────────────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
function createDefaultChangelog() { return { entries: [] }; }
|
|
1333
659
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
.map(([featureName]) => featureName)
|
|
1340
|
-
.slice(0, MAX_KEY_FEATURES);
|
|
1341
|
-
}
|
|
660
|
+
function createDefaultState(projectRoot) {
|
|
661
|
+
const root = resolveRoot(projectRoot);
|
|
662
|
+
const meta = detectProjectMetadata(root);
|
|
663
|
+
const boot = bootstrapProjectAnalysis(root);
|
|
664
|
+
const fileList = scanFiles(root, MAX_TREE_DEPTH);
|
|
1342
665
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
666
|
+
const state = {
|
|
667
|
+
project: meta.project,
|
|
668
|
+
version: meta.version,
|
|
669
|
+
description: meta.description,
|
|
670
|
+
author: meta.author,
|
|
671
|
+
license: meta.license,
|
|
672
|
+
homepage: meta.homepage,
|
|
673
|
+
repository: meta.repository,
|
|
674
|
+
|
|
675
|
+
last_updated: new Date().toISOString(),
|
|
676
|
+
ai_summary: '',
|
|
677
|
+
tech_stack: boot.techStack,
|
|
678
|
+
architecture_patterns: boot.architecturePatterns,
|
|
679
|
+
implementation_details: boot.implementationDetails,
|
|
680
|
+
current_stage: stageFromFeatures(boot.keyFeatures, boot.implementationDetails),
|
|
681
|
+
recent_updates: [],
|
|
682
|
+
key_features: boot.keyFeatures,
|
|
683
|
+
known_issues: [],
|
|
684
|
+
next_steps: [],
|
|
685
|
+
|
|
686
|
+
// File structure
|
|
687
|
+
file_tree: buildFileTree(root),
|
|
688
|
+
file_list: fileList,
|
|
689
|
+
|
|
690
|
+
// Code catalogue: every file's exports/imports/functions
|
|
691
|
+
code_files: buildCodeFileCatalogue(root, fileList),
|
|
692
|
+
|
|
693
|
+
// Full code change history with real diffs
|
|
694
|
+
code_changes: [],
|
|
695
|
+
|
|
696
|
+
// API surface
|
|
697
|
+
api_routes: extractApiRoutes(root),
|
|
698
|
+
|
|
699
|
+
// Dependencies
|
|
700
|
+
dependencies: buildDependencyCatalogue(root),
|
|
701
|
+
|
|
702
|
+
// Environment
|
|
703
|
+
env_variables: scanEnvVars(root),
|
|
704
|
+
|
|
705
|
+
// Error / issue tracking
|
|
706
|
+
issue_tracker: createDefaultIssueTracker(),
|
|
707
|
+
|
|
708
|
+
// Working context (manually set or updated via CLI)
|
|
709
|
+
current_focus: '',
|
|
710
|
+
working_branch: '',
|
|
711
|
+
open_questions: [],
|
|
712
|
+
decisions_made: [],
|
|
713
|
+
session_notes: []
|
|
1349
714
|
};
|
|
1350
715
|
|
|
1351
|
-
|
|
716
|
+
state.ai_summary = genAiSummary(state, boot);
|
|
717
|
+
state.next_steps = genNextSteps(state, boot);
|
|
718
|
+
return state;
|
|
1352
719
|
}
|
|
1353
720
|
|
|
1354
|
-
function
|
|
1355
|
-
|
|
721
|
+
function stageFromFeatures(features, details) {
|
|
722
|
+
if (features.length >= 4 && details.length >= 3) return 'Production-ready';
|
|
723
|
+
if (features.length >= 2) return 'Functional prototype';
|
|
724
|
+
return 'Early development';
|
|
1356
725
|
}
|
|
1357
726
|
|
|
1358
|
-
function
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
knownIssues.push('Project structure exposes limited implementation signals, so deeper architecture details may still be missing.');
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
if (!bootstrap.techStack.framework && bootstrap.techStack.language === 'Node.js') {
|
|
1370
|
-
knownIssues.push('No common Node.js application framework dependency is currently detected.');
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
return knownIssues;
|
|
727
|
+
function genAiSummary(state, boot) {
|
|
728
|
+
const type = cap(boot.projectType || 'Project');
|
|
729
|
+
const feats = (state.key_features || []).slice(0, 3).join(', ').toLowerCase();
|
|
730
|
+
const db = state.tech_stack.databases && state.tech_stack.databases.length
|
|
731
|
+
? ` backed by ${state.tech_stack.databases.join(', ')}`
|
|
732
|
+
: '';
|
|
733
|
+
if (feats) return `${type}${db} with: ${feats}.`;
|
|
734
|
+
return `${type}${db}.`;
|
|
1374
735
|
}
|
|
1375
736
|
|
|
1376
|
-
function
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
'vitest.config.js',
|
|
1383
|
-
'jest.config.js',
|
|
1384
|
-
'jest.config.cjs',
|
|
1385
|
-
'jest.config.mjs'
|
|
1386
|
-
];
|
|
1387
|
-
|
|
1388
|
-
return testPaths.some((relativePath) => fs.existsSync(path.join(resolvedRoot, relativePath)));
|
|
737
|
+
function genNextSteps(state, boot) {
|
|
738
|
+
const steps = [];
|
|
739
|
+
if (!state.tech_stack.test_framework && !boot.signals.hasTests) steps.push('Add automated tests');
|
|
740
|
+
if (boot.implementationDetails.length < 2) steps.push('Expand project structure so more patterns are detectable');
|
|
741
|
+
if (state.current_stage === 'Early development') steps.push('Ship next core capability to reach functional prototype stage');
|
|
742
|
+
return steps.slice(0, 4);
|
|
1389
743
|
}
|
|
1390
744
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
|
|
1397
|
-
return 'Functional prototype';
|
|
1398
|
-
}
|
|
745
|
+
// ─────────────────────────────────────────────────────────────────
|
|
746
|
+
// I/O helpers
|
|
747
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1399
748
|
|
|
1400
|
-
|
|
749
|
+
async function ensureContextDirectory(projectRoot) {
|
|
750
|
+
const { contextDir } = getContextPaths(projectRoot);
|
|
751
|
+
await fsp.mkdir(contextDir, { recursive: true });
|
|
752
|
+
return contextDir;
|
|
1401
753
|
}
|
|
1402
754
|
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const normalizedType = projectType === 'backend API platform'
|
|
1408
|
-
? 'Backend API platform'
|
|
1409
|
-
: capitalize(projectType);
|
|
755
|
+
async function readJsonFile(filePath, fallback) {
|
|
756
|
+
try { return JSON.parse(await fsp.readFile(filePath, 'utf8')); }
|
|
757
|
+
catch (_) { return fallback; }
|
|
758
|
+
}
|
|
1410
759
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
760
|
+
async function writeJsonAtomic(filePath, value) {
|
|
761
|
+
await writeTextAtomic(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
762
|
+
}
|
|
1414
763
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
)
|
|
1419
|
-
|
|
1420
|
-
|
|
764
|
+
async function writeTextAtomic(filePath, content) {
|
|
765
|
+
const tmp = `${filePath}.tmp`;
|
|
766
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
767
|
+
await fsp.writeFile(tmp, content, 'utf8');
|
|
768
|
+
await fsp.rename(tmp, filePath);
|
|
769
|
+
}
|
|
1421
770
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
return `${normalizedType} with RESTful request handling and structured server-side workflow orchestration.`;
|
|
1427
|
-
}
|
|
771
|
+
function renderTemplate(template, vars) {
|
|
772
|
+
return Object.entries(vars).reduce((acc, [k, v]) =>
|
|
773
|
+
acc.split(`{{${k}}}`).join(v == null ? '' : String(v)), template);
|
|
774
|
+
}
|
|
1428
775
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
776
|
+
// ─────────────────────────────────────────────────────────────────
|
|
777
|
+
// Runtime config
|
|
778
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1432
779
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
}
|
|
780
|
+
async function loadRuntimeConfig(projectRoot) {
|
|
781
|
+
const { configFile } = getContextPaths(projectRoot);
|
|
782
|
+
return deepMerge(DEFAULT_CONFIG, await readJsonFile(configFile, {}));
|
|
783
|
+
}
|
|
1436
784
|
|
|
1437
|
-
|
|
785
|
+
async function updateRuntimeConfig(projectRoot, updates) {
|
|
786
|
+
const { configFile } = getContextPaths(projectRoot);
|
|
787
|
+
const cur = await readJsonFile(configFile, {});
|
|
788
|
+
const next = deepMerge(deepMerge(DEFAULT_CONFIG, cur), updates || {});
|
|
789
|
+
await writeJsonAtomic(configFile, next);
|
|
790
|
+
return next;
|
|
1438
791
|
}
|
|
1439
792
|
|
|
1440
|
-
|
|
1441
|
-
|
|
793
|
+
// ─────────────────────────────────────────────────────────────────
|
|
794
|
+
// MAIN STATE UPDATER
|
|
795
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1442
796
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
797
|
+
async function updateProjectState(projectRoot, changeEvent, options) {
|
|
798
|
+
const settings = Object.assign({ logger: null, syncCallback: null }, options);
|
|
799
|
+
const log = settings.logger;
|
|
800
|
+
const root = resolveRoot(projectRoot);
|
|
801
|
+
const paths = getContextPaths(root);
|
|
802
|
+
|
|
803
|
+
const existing = await readJsonFile(paths.stateFile, createDefaultState(root));
|
|
804
|
+
const existingLog = await readJsonFile(paths.changelogFile, createDefaultChangelog());
|
|
805
|
+
|
|
806
|
+
const events = (Array.isArray(changeEvent) ? changeEvent : [changeEvent]).filter(Boolean);
|
|
807
|
+
const ts = events.length ? (events[events.length - 1].timestamp || new Date().toISOString()) : new Date().toISOString();
|
|
808
|
+
|
|
809
|
+
// ── 1. Handle error/resolve events ──────────────────────────────
|
|
810
|
+
let issueTracker = existing.issue_tracker || createDefaultIssueTracker();
|
|
811
|
+
for (const ev of events) {
|
|
812
|
+
if (ev.type === 'error') issueTracker = applyErrorEvent(issueTracker, ev);
|
|
813
|
+
if (ev.type === 'resolve') issueTracker = applyResolveEvent(issueTracker, ev);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── 2. Build real code-change entries with diffs ────────────────
|
|
817
|
+
const newCodeChanges = [];
|
|
818
|
+
for (const ev of events) {
|
|
819
|
+
if (!ev.file || ev.type === 'error' || ev.type === 'resolve') continue;
|
|
820
|
+
if (scoreEvent(ev.file) < 2) continue;
|
|
821
|
+
|
|
822
|
+
const absPath = path.join(root, ev.file);
|
|
823
|
+
const oldContent = ev.oldContent !== undefined ? ev.oldContent : getSnapshot(absPath);
|
|
824
|
+
let newContent = ev.newContent;
|
|
825
|
+
if (newContent === undefined) {
|
|
826
|
+
newContent = ev.action === 'delete' ? '' : readCurrentContent(absPath);
|
|
827
|
+
}
|
|
1446
828
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
}
|
|
829
|
+
const entry = buildCodeChangeEntry(ev.file, oldContent, newContent, ev.action, ev.timestamp || ts);
|
|
830
|
+
if (entry) newCodeChanges.push(entry);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ── 3. Re-scan project for fresh context ────────────────────────
|
|
834
|
+
const meta = detectProjectMetadata(root);
|
|
835
|
+
const boot = bootstrapProjectAnalysis(root);
|
|
836
|
+
const fileList = scanFiles(root, MAX_TREE_DEPTH);
|
|
837
|
+
|
|
838
|
+
// ── 4. Merge code_changes (newest first, capped) ────────────────
|
|
839
|
+
const prevCodeChanges = Array.isArray(existing.code_changes) ? existing.code_changes : [];
|
|
840
|
+
const allCodeChanges = [...newCodeChanges, ...prevCodeChanges].slice(0, MAX_CODE_CHANGE_HISTORY);
|
|
841
|
+
|
|
842
|
+
// ── 5. Build changelog entries (one per change, rich) ───────────
|
|
843
|
+
const newChangelogEntries = newCodeChanges.map((c) => ({
|
|
844
|
+
id: c.id,
|
|
845
|
+
timestamp: c.timestamp,
|
|
846
|
+
file: c.file,
|
|
847
|
+
action: c.action,
|
|
848
|
+
summary: c.summary,
|
|
849
|
+
signals: c.signals,
|
|
850
|
+
lines_added: c.lines_added,
|
|
851
|
+
lines_removed: c.lines_removed
|
|
852
|
+
// patch intentionally excluded from changelog to keep it lean
|
|
853
|
+
}));
|
|
854
|
+
const prevEntries = Array.isArray(existingLog.entries) ? existingLog.entries : [];
|
|
855
|
+
const allEntries = [...newChangelogEntries, ...prevEntries].slice(0, MAX_CHANGELOG_ENTRIES);
|
|
856
|
+
|
|
857
|
+
// ── 6. recent_updates: human-readable, last N changes ───────────
|
|
858
|
+
const recentUpdates = allCodeChanges.slice(0, MAX_RECENT_UPDATES).map((c) => ({
|
|
859
|
+
timestamp: c.timestamp,
|
|
860
|
+
file: c.file,
|
|
861
|
+
action: c.action,
|
|
862
|
+
summary: c.summary,
|
|
863
|
+
signals: c.signals,
|
|
864
|
+
lines_added: c.lines_added,
|
|
865
|
+
lines_removed: c.lines_removed
|
|
866
|
+
}));
|
|
867
|
+
|
|
868
|
+
// ── 7. Compose next state ───────────────────────────────────────
|
|
869
|
+
const nextState = {
|
|
870
|
+
project: meta.project,
|
|
871
|
+
version: meta.version,
|
|
872
|
+
description: meta.description || existing.description || '',
|
|
873
|
+
author: meta.author || existing.author || '',
|
|
874
|
+
license: meta.license || existing.license || '',
|
|
875
|
+
homepage: meta.homepage || existing.homepage || '',
|
|
876
|
+
repository: meta.repository || existing.repository || '',
|
|
877
|
+
|
|
878
|
+
last_updated: ts,
|
|
879
|
+
tech_stack: deepMerge(boot.techStack, existing.tech_stack || {}),
|
|
880
|
+
architecture_patterns: boot.architecturePatterns.length ? boot.architecturePatterns : (existing.architecture_patterns || []),
|
|
881
|
+
implementation_details: boot.implementationDetails.length ? boot.implementationDetails : (existing.implementation_details || []),
|
|
882
|
+
key_features: boot.keyFeatures.length ? boot.keyFeatures : (existing.key_features || []),
|
|
883
|
+
current_stage: stageFromFeatures(boot.keyFeatures, boot.implementationDetails),
|
|
884
|
+
recent_updates: recentUpdates,
|
|
885
|
+
known_issues: existing.known_issues || [],
|
|
886
|
+
next_steps: [],
|
|
887
|
+
|
|
888
|
+
// Always refreshed
|
|
889
|
+
file_tree: buildFileTree(root),
|
|
890
|
+
file_list: fileList,
|
|
891
|
+
code_files: buildCodeFileCatalogue(root, fileList),
|
|
892
|
+
code_changes: allCodeChanges,
|
|
893
|
+
api_routes: extractApiRoutes(root),
|
|
894
|
+
dependencies: buildDependencyCatalogue(root),
|
|
895
|
+
env_variables: scanEnvVars(root),
|
|
896
|
+
|
|
897
|
+
// Issue tracking
|
|
898
|
+
issue_tracker: issueTracker,
|
|
899
|
+
|
|
900
|
+
// Preserve working context
|
|
901
|
+
current_focus: existing.current_focus || '',
|
|
902
|
+
working_branch: existing.working_branch || '',
|
|
903
|
+
open_questions: existing.open_questions || [],
|
|
904
|
+
decisions_made: existing.decisions_made || [],
|
|
905
|
+
session_notes: existing.session_notes || []
|
|
906
|
+
};
|
|
1450
907
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
908
|
+
nextState.ai_summary = genAiSummary(nextState, boot);
|
|
909
|
+
nextState.next_steps = genNextSteps(nextState, boot);
|
|
1454
910
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
911
|
+
// ── 8. Write state, changelog, and auto-regenerate briefing.md ──
|
|
912
|
+
const briefing = generateBriefing(nextState, root);
|
|
913
|
+
await writeJsonAtomic(paths.stateFile, nextState);
|
|
914
|
+
await writeJsonAtomic(paths.changelogFile, { entries: allEntries });
|
|
915
|
+
await writeTextAtomic(paths.briefingFile, briefing);
|
|
1458
916
|
|
|
1459
|
-
|
|
1460
|
-
|
|
917
|
+
if (log) log.debug(`Updated AI context – ${newCodeChanges.length} code change(s) recorded`);
|
|
918
|
+
if (typeof settings.syncCallback === 'function') await settings.syncCallback();
|
|
1461
919
|
|
|
1462
|
-
|
|
1463
|
-
return Array.from(new Set((values || []).filter(Boolean)));
|
|
920
|
+
return nextState;
|
|
1464
921
|
}
|
|
1465
922
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1472
|
-
}
|
|
923
|
+
// ─────────────────────────────────────────────────────────────────
|
|
924
|
+
// Debounced updater
|
|
925
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1473
926
|
|
|
1474
927
|
function createDebouncedStateUpdater(projectRoot, options) {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
syncCallback: null
|
|
1480
|
-
},
|
|
1481
|
-
options
|
|
1482
|
-
);
|
|
1483
|
-
|
|
1484
|
-
let timer = null;
|
|
1485
|
-
let pendingEvents = [];
|
|
1486
|
-
let activeFlush = Promise.resolve();
|
|
928
|
+
const s = Object.assign({ debounceMs: DEFAULT_CONFIG.debounceMs, logger: null, syncCallback: null }, options);
|
|
929
|
+
let timer = null;
|
|
930
|
+
let pending = [];
|
|
931
|
+
let active = Promise.resolve();
|
|
1487
932
|
|
|
1488
933
|
async function flush() {
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
pendingEvents = [];
|
|
1495
|
-
|
|
1496
|
-
activeFlush = activeFlush.then(() =>
|
|
1497
|
-
updateProjectState(projectRoot, events, {
|
|
1498
|
-
logger: settings.logger,
|
|
1499
|
-
syncCallback: settings.syncCallback
|
|
1500
|
-
})
|
|
1501
|
-
);
|
|
1502
|
-
|
|
1503
|
-
await activeFlush;
|
|
934
|
+
if (!pending.length) return;
|
|
935
|
+
const evs = pending.slice();
|
|
936
|
+
pending = [];
|
|
937
|
+
active = active.then(() => updateProjectState(projectRoot, evs, { logger: s.logger, syncCallback: s.syncCallback }));
|
|
938
|
+
await active;
|
|
1504
939
|
}
|
|
1505
940
|
|
|
1506
941
|
return {
|
|
1507
942
|
enqueue(event) {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (timer) {
|
|
1511
|
-
clearTimeout(timer);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
943
|
+
pending.push(event);
|
|
944
|
+
if (timer) clearTimeout(timer);
|
|
1514
945
|
timer = setTimeout(() => {
|
|
1515
946
|
timer = null;
|
|
1516
|
-
flush().catch((
|
|
1517
|
-
|
|
1518
|
-
settings.logger.error(`Failed to flush AI context updates: ${error.message}`);
|
|
1519
|
-
}
|
|
1520
|
-
});
|
|
1521
|
-
}, settings.debounceMs);
|
|
947
|
+
flush().catch((err) => { if (s.logger) s.logger.error(`Flush failed: ${err.message}`); });
|
|
948
|
+
}, s.debounceMs);
|
|
1522
949
|
},
|
|
1523
950
|
async flushNow() {
|
|
1524
|
-
if (timer) {
|
|
1525
|
-
clearTimeout(timer);
|
|
1526
|
-
timer = null;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
951
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
1529
952
|
await flush();
|
|
1530
953
|
}
|
|
1531
954
|
};
|
|
1532
955
|
}
|
|
1533
956
|
|
|
957
|
+
// ─────────────────────────────────────────────────────────────────
|
|
958
|
+
// Exports
|
|
959
|
+
// ─────────────────────────────────────────────────────────────────
|
|
960
|
+
|
|
1534
961
|
module.exports = {
|
|
1535
962
|
CONTEXT_DIR_NAME,
|
|
1536
963
|
DEFAULT_CONFIG,
|
|
964
|
+
applyErrorEvent,
|
|
965
|
+
applyResolveEvent,
|
|
1537
966
|
bootstrapProjectAnalysis,
|
|
967
|
+
buildCodeFileCatalogue,
|
|
968
|
+
buildDependencyCatalogue,
|
|
969
|
+
buildFileTree,
|
|
1538
970
|
createDebouncedStateUpdater,
|
|
1539
971
|
createDefaultChangelog,
|
|
972
|
+
createDefaultIssueTracker,
|
|
1540
973
|
createDefaultState,
|
|
1541
974
|
detectProjectMetadata,
|
|
1542
975
|
ensureContextDirectory,
|
|
976
|
+
extractApiRoutes,
|
|
1543
977
|
getContextPaths,
|
|
1544
|
-
groupEventsByIntent,
|
|
1545
978
|
loadRuntimeConfig,
|
|
1546
|
-
promoteFeatures,
|
|
1547
979
|
readJsonFile,
|
|
1548
980
|
renderTemplate,
|
|
981
|
+
scanEnvVars,
|
|
982
|
+
scanFiles,
|
|
1549
983
|
scoreEvent,
|
|
1550
984
|
shouldIgnoreProjectFile,
|
|
1551
985
|
updateRuntimeConfig,
|
|
1552
986
|
updateProjectState,
|
|
1553
987
|
writeJsonAtomic,
|
|
1554
988
|
writeTextAtomic
|
|
1555
|
-
};
|
|
989
|
+
};
|