aibridge-context 1.5.1 → 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/init.js +16 -2
- package/core/stateManager.js +802 -1307
- 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,1494 +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
|
|
60
|
+
configFile: path.join(contextDir, 'config.json'),
|
|
61
|
+
briefingFile: path.join(contextDir, 'briefing.md')
|
|
157
62
|
};
|
|
158
63
|
}
|
|
159
64
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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)
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function detectFramework(dependencies) {
|
|
191
|
-
if (dependencies.next) {
|
|
192
|
-
return 'Next.js';
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (dependencies.react) {
|
|
196
|
-
return 'React';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (dependencies.express) {
|
|
200
|
-
return 'Express';
|
|
201
|
-
}
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────
|
|
66
|
+
// Utilities
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────
|
|
202
68
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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) : ''; }
|
|
206
72
|
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
79
|
}
|
|
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
|
-
|
|
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;
|
|
163
|
+
}
|
|
164
|
+
return visit(root, 0);
|
|
373
165
|
}
|
|
374
166
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await fsp.writeFile(tempFilePath, content, 'utf8');
|
|
379
|
-
await fsp.rename(tempFilePath, filePath);
|
|
380
|
-
}
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────
|
|
168
|
+
// Package.json / metadata
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────
|
|
381
170
|
|
|
382
|
-
function
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
return accumulator.split(`{{${key}}}`).join(safeValue);
|
|
386
|
-
}, template);
|
|
171
|
+
function readPkg(projectRoot) {
|
|
172
|
+
try { return JSON.parse(fs.readFileSync(path.join(resolveRoot(projectRoot), 'package.json'), 'utf8')); }
|
|
173
|
+
catch (_) { return null; }
|
|
387
174
|
}
|
|
388
175
|
|
|
389
|
-
function
|
|
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);
|
|
390
181
|
return {
|
|
391
|
-
|
|
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
|
|
392
192
|
};
|
|
393
193
|
}
|
|
394
194
|
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
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 bootstrapProjectAnalysis(projectRoot) {
|
|
420
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
421
|
-
const metadata = detectProjectMetadata(resolvedRoot);
|
|
422
|
-
const rootPackage = readRootPackageJson(resolvedRoot);
|
|
423
|
-
const techStack = metadata.techStack;
|
|
424
|
-
const analysisInputs = collectAnalysisInputs(resolvedRoot);
|
|
425
|
-
const implementationSignals = detectImplementationSignals(analysisInputs, rootPackage, techStack);
|
|
426
|
-
const architecturePatterns = buildArchitecturePatterns(
|
|
427
|
-
implementationSignals,
|
|
428
|
-
analysisInputs,
|
|
429
|
-
techStack,
|
|
430
|
-
rootPackage
|
|
431
|
-
);
|
|
432
|
-
const implementationDetails = buildImplementationDetails(implementationSignals, techStack);
|
|
433
|
-
const keyFeatures = buildKeyFeatures(implementationSignals, techStack);
|
|
434
|
-
const projectType = determineProjectType(implementationSignals, techStack, rootPackage);
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
projectType,
|
|
438
|
-
techStack,
|
|
439
|
-
architecturePatterns,
|
|
440
|
-
implementationDetails,
|
|
441
|
-
keyFeatures,
|
|
442
|
-
signals: implementationSignals
|
|
443
|
-
};
|
|
195
|
+
function extractRepoUrl(pkg) {
|
|
196
|
+
if (!pkg || !pkg.repository) return '';
|
|
197
|
+
return typeof pkg.repository === 'string' ? pkg.repository : (pkg.repository.url || '');
|
|
444
198
|
}
|
|
445
199
|
|
|
446
|
-
function
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
for (const relativeFile of ANALYSIS_ROOT_FILES) {
|
|
451
|
-
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
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));
|
|
452
204
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const discoveredFiles = scanProjectFiles(resolvedRoot, 4, {
|
|
459
|
-
includeDirectories: ANALYSIS_DIRECTORIES
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
for (const relativeFile of discoveredFiles) {
|
|
463
|
-
selectedFiles.add(relativeFile);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return Array.from(selectedFiles)
|
|
467
|
-
.sort()
|
|
468
|
-
.map((relativeFile) => {
|
|
469
|
-
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
const content = fs.readFileSync(absoluteFile, 'utf8');
|
|
473
|
-
return {
|
|
474
|
-
path: relativeFile,
|
|
475
|
-
content: content.slice(0, 50000)
|
|
476
|
-
};
|
|
477
|
-
} catch (error) {
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
})
|
|
481
|
-
.filter(Boolean);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function detectImplementationSignals(analysisInputs, rootPackage, techStack) {
|
|
485
|
-
const dependencyNames = new Set([
|
|
486
|
-
...Object.keys((rootPackage && rootPackage.dependencies) || {}),
|
|
487
|
-
...Object.keys((rootPackage && rootPackage.devDependencies) || {})
|
|
488
|
-
]);
|
|
489
|
-
const runtimeInputs = analysisInputs.filter((input) => isRuntimeImplementationFile(input.path));
|
|
490
|
-
const automationInputs = analysisInputs.filter((input) => isAutomationImplementationFile(input.path));
|
|
491
|
-
const runtimeContent = runtimeInputs.map((input) => input.content).join('\n');
|
|
492
|
-
const automationContent = automationInputs.map((input) => input.content).join('\n');
|
|
493
|
-
const allContent = analysisInputs.map((input) => input.content).join('\n');
|
|
494
|
-
const hasDirectory = (directoryName) =>
|
|
495
|
-
analysisInputs.some((input) => normalizeProjectPath(input.path).startsWith(`${directoryName}/`));
|
|
496
|
-
const hasRuntimePattern = (pattern) => pattern.test(runtimeContent);
|
|
497
|
-
const hasAutomationPattern = (pattern) => pattern.test(automationContent);
|
|
498
|
-
const hasAnyPattern = (pattern) => pattern.test(allContent);
|
|
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'; }
|
|
499
209
|
|
|
500
210
|
return {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
hasNext: dependencyNames.has('next'),
|
|
512
|
-
hasReact: dependencyNames.has('react'),
|
|
513
|
-
hasFastify: dependencyNames.has('fastify'),
|
|
514
|
-
hasKoa: dependencyNames.has('koa'),
|
|
515
|
-
hasRestRoutes: hasRuntimePattern(/\b(router|app)\.(get|post|put|patch|delete)\s*\(/),
|
|
516
|
-
hasMiddleware: hasRuntimePattern(/\bapp\.use\s*\(/) || hasDirectory('middleware'),
|
|
517
|
-
hasJwt:
|
|
518
|
-
dependencyNames.has('jsonwebtoken') ||
|
|
519
|
-
hasRuntimePattern(/\bjwt\.(sign|verify)\s*\(/) ||
|
|
520
|
-
hasRuntimePattern(/\brequire\(['"]jsonwebtoken['"]\)/),
|
|
521
|
-
hasMongoose:
|
|
522
|
-
dependencyNames.has('mongoose') ||
|
|
523
|
-
hasRuntimePattern(/\bmongoose\.connect\s*\(/) ||
|
|
524
|
-
hasRuntimePattern(/\brequire\(['"]mongoose['"]\)/),
|
|
525
|
-
hasSocketIO:
|
|
526
|
-
dependencyNames.has('socket.io') ||
|
|
527
|
-
hasRuntimePattern(/\bsocket\.io\b/) ||
|
|
528
|
-
hasRuntimePattern(/\brequire\(['"]socket\.io['"]\)/),
|
|
529
|
-
hasAxiosOrFetch:
|
|
530
|
-
dependencyNames.has('axios') ||
|
|
531
|
-
hasRuntimePattern(/\baxios\./) ||
|
|
532
|
-
hasRuntimePattern(/\bfetch\s*\(/),
|
|
533
|
-
hasWatcher:
|
|
534
|
-
dependencyNames.has('chokidar') ||
|
|
535
|
-
hasAutomationPattern(/\bchokidar\.watch\s*\(/) ||
|
|
536
|
-
hasAutomationPattern(/\bfs\.watch\s*\(/),
|
|
537
|
-
hasGitAutomation:
|
|
538
|
-
hasAutomationPattern(/\bgit\s+(add|commit|push)\b/) ||
|
|
539
|
-
hasAutomationPattern(/\b(syncContextToGit|linkGithubRepository)\b/),
|
|
540
|
-
hasAiContextArtifacts:
|
|
541
|
-
hasAutomationPattern(/\bstate\.json\b/) ||
|
|
542
|
-
hasAutomationPattern(/\bbrain\.txt\b/) ||
|
|
543
|
-
hasAutomationPattern(/\bcontext\.md\b/) ||
|
|
544
|
-
hasAutomationPattern(/\bchangelog\.json\b/),
|
|
545
|
-
hasControllers: hasDirectory('controllers'),
|
|
546
|
-
hasServices: hasDirectory('services'),
|
|
547
|
-
hasRoutes: hasDirectory('routes'),
|
|
548
|
-
hasServerDirectory: hasDirectory('server'),
|
|
549
|
-
hasConfigDirectory: hasDirectory('config'),
|
|
550
|
-
hasTypeScriptConfig: analysisInputs.some((input) => input.path === 'tsconfig.json'),
|
|
551
|
-
hasPythonRequirements: analysisInputs.some((input) => input.path === 'requirements.txt'),
|
|
552
|
-
hasNodeRuntime: techStack.language === 'Node.js',
|
|
553
|
-
hasPythonRuntime: techStack.language === 'Python',
|
|
554
|
-
hasApplicationEntries: runtimeInputs.length > 0,
|
|
555
|
-
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)
|
|
556
221
|
};
|
|
557
222
|
}
|
|
558
223
|
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return (
|
|
563
|
-
normalizedPath.startsWith('routes/') ||
|
|
564
|
-
normalizedPath.startsWith('server/') ||
|
|
565
|
-
normalizedPath.startsWith('controllers/') ||
|
|
566
|
-
normalizedPath.startsWith('services/') ||
|
|
567
|
-
normalizedPath.startsWith('middleware/') ||
|
|
568
|
-
normalizedPath.startsWith('config/') ||
|
|
569
|
-
normalizedPath.startsWith('src/') ||
|
|
570
|
-
normalizedPath === 'app.js' ||
|
|
571
|
-
normalizedPath === 'app.ts' ||
|
|
572
|
-
normalizedPath === 'index.js' ||
|
|
573
|
-
normalizedPath === 'index.ts' ||
|
|
574
|
-
normalizedPath === 'main.py'
|
|
575
|
-
);
|
|
224
|
+
function anyFileExt(root, exts) {
|
|
225
|
+
return scanFiles(root, 2).some((f) => exts.includes(path.extname(f).toLowerCase()));
|
|
576
226
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return
|
|
582
|
-
normalizedPath.startsWith('core/') ||
|
|
583
|
-
normalizedPath.startsWith('bin/') ||
|
|
584
|
-
normalizedPath === 'package.json' ||
|
|
585
|
-
normalizedPath === 'index.js' ||
|
|
586
|
-
normalizedPath === 'index.ts'
|
|
587
|
-
);
|
|
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';
|
|
588
232
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
602
|
-
patterns.push('REST API architecture');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (signals.hasMiddleware) {
|
|
606
|
-
patterns.push('Middleware-driven request pipeline');
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (signals.hasControllers && signals.hasServices) {
|
|
610
|
-
patterns.push('Layered controller-service architecture');
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (signals.hasServerDirectory && signals.hasRoutes) {
|
|
614
|
-
patterns.push('Separated server bootstrap and route handling');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (signals.hasConfigDirectory) {
|
|
618
|
-
patterns.push('Centralized configuration layer');
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (signals.hasSocketIO) {
|
|
622
|
-
patterns.push('Real-time event architecture');
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (signals.hasNext) {
|
|
626
|
-
patterns.push('Framework-driven web application structure');
|
|
627
|
-
} else if (signals.hasReact) {
|
|
628
|
-
patterns.push('Component-based frontend architecture');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
signals.hasAiContextArtifacts &&
|
|
633
|
-
signals.hasExpress &&
|
|
634
|
-
analysisInputs.some((input) => /\bstate\.json\b|\bbrain\.txt\b|\bcontext\.md\b/.test(input.content))
|
|
635
|
-
) {
|
|
636
|
-
patterns.push('Structured AI context delivery workflow');
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (rootPackage && Array.isArray(rootPackage.keywords) && rootPackage.keywords.includes('cli')) {
|
|
640
|
-
patterns.push('Package-distributed CLI architecture');
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return uniqueNonEmpty(patterns).slice(0, 6);
|
|
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 '';
|
|
644
245
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (signals.hasAxiosOrFetch) {
|
|
670
|
-
details.push('External API integration for outbound service calls');
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (signals.hasWatcher) {
|
|
674
|
-
details.push('File system monitoring for automatic project state updates');
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (signals.hasAiContextArtifacts) {
|
|
678
|
-
details.push('Structured AI context generation across JSON, Markdown, and instruction files');
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (signals.hasGitAutomation) {
|
|
682
|
-
details.push('Git-backed synchronization workflow for publishing project state');
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (signals.hasCliEntry && techStack.language === 'Node.js') {
|
|
686
|
-
details.push('Node.js CLI entrypoint for developer-facing automation');
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return uniqueNonEmpty(details).slice(0, MAX_IMPLEMENTATION_DETAILS);
|
|
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 '';
|
|
690
268
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (signals.hasAiContextArtifacts) {
|
|
696
|
-
features.push(FEATURE_CATALOG.ai_context_generation);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
if (signals.hasCliEntry) {
|
|
700
|
-
features.push(FEATURE_CATALOG.cli_automation);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (signals.hasWatcher) {
|
|
704
|
-
features.push(FEATURE_CATALOG.change_tracking);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (signals.hasGitAutomation) {
|
|
708
|
-
features.push(FEATURE_CATALOG.public_sync);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (signals.hasExpress && signals.hasAiContextArtifacts) {
|
|
712
|
-
features.push(FEATURE_CATALOG.local_context_server);
|
|
713
|
-
} else if (signals.hasExpress && signals.hasRestRoutes) {
|
|
714
|
-
features.push(FEATURE_CATALOG.rest_api);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (signals.hasJwt) {
|
|
718
|
-
features.push(FEATURE_CATALOG.auth);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (signals.hasMongoose) {
|
|
722
|
-
features.push(FEATURE_CATALOG.persistence);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (signals.hasSocketIO) {
|
|
726
|
-
features.push(FEATURE_CATALOG.realtime);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (signals.hasAxiosOrFetch) {
|
|
730
|
-
features.push(FEATURE_CATALOG.external_api);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (signals.hasMiddleware) {
|
|
734
|
-
features.push(FEATURE_CATALOG.middleware);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (signals.hasConfigDirectory) {
|
|
738
|
-
features.push(FEATURE_CATALOG.config_management);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (features.length === 0 && techStack.language) {
|
|
742
|
-
features.push(`${techStack.language} project structure with detectable application entry points`);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
return uniqueNonEmpty(features).slice(0, MAX_KEY_FEATURES);
|
|
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 '';
|
|
746
273
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
|
|
754
|
-
return 'CLI tool';
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
758
|
-
return 'backend API platform';
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (signals.hasReact) {
|
|
762
|
-
return 'frontend application';
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (signals.hasPythonRuntime) {
|
|
766
|
-
return 'Python application';
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (rootPackage && rootPackage.bin) {
|
|
770
|
-
return 'CLI tool';
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (techStack.language === 'Node.js') {
|
|
774
|
-
return 'Node.js application';
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return 'software project';
|
|
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 '';
|
|
778
278
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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 '';
|
|
784
288
|
}
|
|
785
289
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const mergedCurrentConfig = deepMerge(DEFAULT_CONFIG, currentConfig);
|
|
790
|
-
const nextConfig = deepMerge(mergedCurrentConfig, updates || {});
|
|
791
|
-
|
|
792
|
-
await writeJsonAtomic(configFile, nextConfig);
|
|
793
|
-
return nextConfig;
|
|
794
|
-
}
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────
|
|
291
|
+
// Dependency catalogue
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────
|
|
795
293
|
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
},
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const logger = settings.logger;
|
|
805
|
-
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
806
|
-
const contextPaths = getContextPaths(resolvedRoot);
|
|
807
|
-
const metadata = detectProjectMetadata(resolvedRoot);
|
|
808
|
-
const bootstrap = bootstrapProjectAnalysis(resolvedRoot);
|
|
809
|
-
const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(resolvedRoot));
|
|
810
|
-
const existingChangelog = await readJsonFile(
|
|
811
|
-
contextPaths.changelogFile,
|
|
812
|
-
createDefaultChangelog()
|
|
813
|
-
);
|
|
814
|
-
const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
|
|
815
|
-
const validEvents = normalizedEvents.filter(Boolean);
|
|
816
|
-
const timestamp = determineUpdateTimestamp(validEvents);
|
|
817
|
-
const meaningfulEvents = collapseEventsByFile(validEvents.filter((event) => isMeaningfulEvent(event)));
|
|
818
|
-
const groupedUpdates = groupEventsByIntent(meaningfulEvents, bootstrap);
|
|
819
|
-
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
820
|
-
const historyEntries = dedupeHistoryEntries(groupedUpdates.concat(previousHistoryEntries))
|
|
821
|
-
.slice(0, MAX_CHANGELOG_ENTRIES);
|
|
822
|
-
const promotedFeatures = promoteFeatures(historyEntries);
|
|
823
|
-
const keyFeatures = mergeKeyFeatures(bootstrap.keyFeatures, promotedFeatures);
|
|
824
|
-
const recentUpdates = groupedUpdates.length > 0
|
|
825
|
-
? dedupeRecentUpdates(groupedUpdates.map(toStateUpdate).concat(normalizeStoredUpdates(existingState.recent_updates)))
|
|
826
|
-
.slice(0, MAX_RECENT_UPDATES)
|
|
827
|
-
: normalizeStoredUpdates(existingState.recent_updates).slice(0, MAX_RECENT_UPDATES);
|
|
828
|
-
const nextState = {
|
|
829
|
-
project: metadata.project,
|
|
830
|
-
version: metadata.version,
|
|
831
|
-
last_updated: timestamp,
|
|
832
|
-
ai_summary: '',
|
|
833
|
-
tech_stack: bootstrap.techStack,
|
|
834
|
-
architecture_patterns: bootstrap.architecturePatterns,
|
|
835
|
-
implementation_details: bootstrap.implementationDetails,
|
|
836
|
-
current_stage: determineCurrentStage(keyFeatures, historyEntries, bootstrap.implementationDetails),
|
|
837
|
-
recent_updates: recentUpdates,
|
|
838
|
-
key_features: keyFeatures,
|
|
839
|
-
known_issues: deriveKnownIssues(resolvedRoot, bootstrap),
|
|
840
|
-
next_steps: []
|
|
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 || {}
|
|
841
302
|
};
|
|
842
|
-
|
|
843
|
-
nextState.ai_summary = generateAiSummary(nextState, bootstrap);
|
|
844
|
-
nextState.next_steps = generateNextSteps(nextState, bootstrap, historyEntries);
|
|
845
|
-
|
|
846
|
-
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
847
|
-
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
848
|
-
|
|
849
|
-
if (logger) {
|
|
850
|
-
logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (typeof settings.syncCallback === 'function') {
|
|
854
|
-
await settings.syncCallback();
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return nextState;
|
|
858
303
|
}
|
|
859
304
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return scoreEvent(event.file) >= 2;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function collapseEventsByFile(events) {
|
|
869
|
-
const collapsedEvents = new Map();
|
|
870
|
-
|
|
871
|
-
for (const event of events) {
|
|
872
|
-
const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
|
|
873
|
-
collapsedEvents.set(
|
|
874
|
-
normalizedFile,
|
|
875
|
-
Object.assign({}, event, {
|
|
876
|
-
file: normalizeProjectPath(event.file)
|
|
877
|
-
})
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
return Array.from(collapsedEvents.values());
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function determineUpdateTimestamp(events) {
|
|
885
|
-
if (!Array.isArray(events) || events.length === 0) {
|
|
886
|
-
return new Date().toISOString();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const latestEvent = events[events.length - 1];
|
|
890
|
-
return latestEvent.timestamp || new Date().toISOString();
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function classifyChangeArea(filePath) {
|
|
894
|
-
const lowerPath = normalizeProjectPath(filePath).toLowerCase();
|
|
895
|
-
|
|
896
|
-
if (lowerPath === 'package.json') {
|
|
897
|
-
return 'configuration';
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (lowerPath === 'readme.md') {
|
|
901
|
-
return 'documentation';
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if (lowerPath.startsWith('bin/')) {
|
|
905
|
-
return 'cli';
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
909
|
-
return 'backend';
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (
|
|
913
|
-
lowerPath.startsWith('controllers/') ||
|
|
914
|
-
lowerPath.startsWith('services/') ||
|
|
915
|
-
lowerPath.startsWith('middleware/') ||
|
|
916
|
-
lowerPath.startsWith('config/')
|
|
917
|
-
) {
|
|
918
|
-
return 'application';
|
|
919
|
-
}
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────
|
|
306
|
+
// Env variable scanning
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────
|
|
920
308
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (signals.hasCliEntry) {
|
|
938
|
-
return 'cli_automation';
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return 'config_management';
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (lowerPath.startsWith('bin/')) {
|
|
945
|
-
return 'cli_automation';
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
949
|
-
if (signals.hasAiContextArtifacts) {
|
|
950
|
-
return 'local_context_server';
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
return 'rest_api';
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
|
|
957
|
-
return 'public_sync';
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (lowerPath.includes('watch')) {
|
|
961
|
-
return 'change_tracking';
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (lowerPath.includes('state') || lowerPath.includes('context')) {
|
|
965
|
-
return 'ai_context_generation';
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
if (lowerPath.includes('auth') || lowerPath.includes('jwt')) {
|
|
969
|
-
return 'auth';
|
|
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 (_) {}
|
|
970
324
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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 (_) {}
|
|
974
332
|
}
|
|
975
|
-
|
|
976
|
-
return 'ai_context_generation';
|
|
333
|
+
return Array.from(vars).sort();
|
|
977
334
|
}
|
|
978
335
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
const buckets = new Map();
|
|
985
|
-
|
|
986
|
-
for (const event of events) {
|
|
987
|
-
const area = classifyChangeArea(event.file);
|
|
988
|
-
const featureKey = detectEventFeatureKey(event.file, bootstrap);
|
|
989
|
-
const key = `${area}:${featureKey}`;
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────
|
|
337
|
+
// API route extraction
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────
|
|
990
339
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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 (_) {}
|
|
1001
357
|
}
|
|
1002
|
-
|
|
1003
|
-
return Array.from(buckets.values())
|
|
1004
|
-
.map((group) => interpretIntentGroup(group))
|
|
1005
|
-
.filter(Boolean)
|
|
1006
|
-
.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
358
|
+
return routes;
|
|
1007
359
|
}
|
|
1008
360
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}, group[0].timestamp || new Date().toISOString());
|
|
1013
|
-
const featureKey = group[0].featureKey;
|
|
1014
|
-
const area = group[0].area;
|
|
1015
|
-
const type = determineGroupedUpdateType(group);
|
|
1016
|
-
const subject = describeIntentSubject(featureKey);
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────
|
|
362
|
+
// Issue tracker
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1017
364
|
|
|
365
|
+
function createDefaultIssueTracker() {
|
|
1018
366
|
return {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
feature_name: FEATURE_CATALOG[featureKey] || subject
|
|
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
|
|
1026
373
|
};
|
|
1027
374
|
}
|
|
1028
375
|
|
|
1029
|
-
function
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return 'fix';
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (group.length > 2) {
|
|
1042
|
-
return 'refactor';
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
return 'improvement';
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function describeIntentSubject(featureKey) {
|
|
1049
|
-
const subjectMap = {
|
|
1050
|
-
ai_context_generation: 'AI context generation workflow',
|
|
1051
|
-
cli_automation: 'CLI automation workflow',
|
|
1052
|
-
change_tracking: 'change tracking workflow',
|
|
1053
|
-
public_sync: 'GitHub sync workflow',
|
|
1054
|
-
local_context_server: 'context delivery service',
|
|
1055
|
-
rest_api: 'API architecture',
|
|
1056
|
-
auth: 'authentication workflow',
|
|
1057
|
-
persistence: 'data persistence layer',
|
|
1058
|
-
realtime: 'real-time communication layer',
|
|
1059
|
-
external_api: 'external integration workflow',
|
|
1060
|
-
middleware: 'request processing workflow',
|
|
1061
|
-
config_management: 'project configuration workflow'
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
return subjectMap[featureKey] || 'project workflow';
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function buildIntentTitle(type, subject) {
|
|
1068
|
-
const verbs = {
|
|
1069
|
-
feature: 'Expanded',
|
|
1070
|
-
improvement: 'Improved',
|
|
1071
|
-
refactor: 'Refined',
|
|
1072
|
-
fix: 'Stabilized'
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
|
-
return `${verbs[type] || 'Improved'} ${subject}`;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function describeIntentImpact(type, featureKey) {
|
|
1079
|
-
const impactMap = {
|
|
1080
|
-
ai_context_generation: 'Improves the quality of generated AI-readable project context.',
|
|
1081
|
-
cli_automation: 'Improves how developers control the project workflow from the command line.',
|
|
1082
|
-
change_tracking: 'Improves how meaningful project changes are detected without noise.',
|
|
1083
|
-
public_sync: 'Improves how project state is published for external AI consumption.',
|
|
1084
|
-
local_context_server: 'Improves how current project context is delivered over HTTP endpoints.',
|
|
1085
|
-
rest_api: 'Improves the structure and clarity of the project API surface.',
|
|
1086
|
-
auth: 'Improves authentication reliability and access control.',
|
|
1087
|
-
persistence: 'Improves how project data is persisted and retrieved.',
|
|
1088
|
-
realtime: 'Improves real-time communication behavior.',
|
|
1089
|
-
external_api: 'Improves outbound integration reliability.',
|
|
1090
|
-
middleware: 'Improves request handling and middleware orchestration.',
|
|
1091
|
-
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'
|
|
1092
385
|
};
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (type === 'feature') {
|
|
1099
|
-
return impactMap[featureKey].replace(/^Improves /, 'Adds ');
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
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
|
+
});
|
|
1103
391
|
}
|
|
1104
392
|
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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;
|
|
1109
398
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
.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()))
|
|
1114
402
|
);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function normalizeStoredUpdate(update) {
|
|
1118
|
-
if (!update) {
|
|
1119
|
-
return null;
|
|
1120
|
-
}
|
|
1121
403
|
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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'
|
|
1127
420
|
};
|
|
1128
421
|
}
|
|
1129
422
|
|
|
1130
|
-
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
|
+
});
|
|
1131
429
|
}
|
|
1132
430
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────
|
|
432
|
+
// Code change history ← THE KEY NEW SECTION
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1137
434
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (!entry) {
|
|
1147
|
-
return null;
|
|
1148
|
-
}
|
|
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();
|
|
1149
443
|
|
|
1150
|
-
if (
|
|
444
|
+
if (action === 'delete') {
|
|
1151
445
|
return {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
|
1159
455
|
};
|
|
1160
456
|
}
|
|
1161
457
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (combinedText.includes('api') || combinedText.includes('route')) {
|
|
1181
|
-
return 'rest_api';
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
|
|
1185
|
-
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
|
+
};
|
|
1186
475
|
}
|
|
1187
476
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
477
|
+
// change – produce real diff
|
|
478
|
+
const old_ = oldContent || '';
|
|
479
|
+
const new_ = newContent || '';
|
|
1191
480
|
|
|
1192
|
-
if (
|
|
1193
|
-
return 'cli_automation';
|
|
1194
|
-
}
|
|
481
|
+
if (old_ === new_) return null; // no actual change
|
|
1195
482
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
483
|
+
const { patch, linesAdded, linesRemoved, summary } = diffTexts(old_, new_, relPath);
|
|
484
|
+
const signals = isCode ? extractCodeSignals(patch, relPath) : ['File updated'];
|
|
1199
485
|
|
|
1200
|
-
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
|
+
};
|
|
1201
497
|
}
|
|
1202
498
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
499
|
+
// ─────────────────────────────────────────────────────────────────
|
|
500
|
+
// Bootstrap project analysis
|
|
501
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1207
502
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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);
|
|
1216
511
|
return {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
|
1220
518
|
};
|
|
1221
519
|
}
|
|
1222
520
|
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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);
|
|
1239
546
|
}
|
|
1240
547
|
|
|
1241
|
-
function
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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 + '/'));
|
|
1247
554
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
+
};
|
|
1257
585
|
}
|
|
1258
586
|
|
|
1259
|
-
function
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
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: [] }; }
|
|
1263
659
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (!featureName) {
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
scores.set(featureName, (scores.get(featureName) || 0) + scoreHistoryEntry(entry));
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return Array.from(scores.entries())
|
|
1277
|
-
.sort((left, right) => right[1] - left[1])
|
|
1278
|
-
.map(([featureName]) => featureName)
|
|
1279
|
-
.slice(0, MAX_KEY_FEATURES);
|
|
1280
|
-
}
|
|
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);
|
|
1281
665
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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: []
|
|
1288
714
|
};
|
|
1289
715
|
|
|
1290
|
-
|
|
716
|
+
state.ai_summary = genAiSummary(state, boot);
|
|
717
|
+
state.next_steps = genNextSteps(state, boot);
|
|
718
|
+
return state;
|
|
1291
719
|
}
|
|
1292
720
|
|
|
1293
|
-
function
|
|
1294
|
-
|
|
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';
|
|
1295
725
|
}
|
|
1296
726
|
|
|
1297
|
-
function
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
knownIssues.push('Project structure exposes limited implementation signals, so deeper architecture details may still be missing.');
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (!bootstrap.techStack.framework && bootstrap.techStack.language === 'Node.js') {
|
|
1309
|
-
knownIssues.push('No common Node.js application framework dependency is currently detected.');
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
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}.`;
|
|
1313
735
|
}
|
|
1314
736
|
|
|
1315
|
-
function
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
'vitest.config.js',
|
|
1322
|
-
'jest.config.js',
|
|
1323
|
-
'jest.config.cjs',
|
|
1324
|
-
'jest.config.mjs'
|
|
1325
|
-
];
|
|
1326
|
-
|
|
1327
|
-
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);
|
|
1328
743
|
}
|
|
1329
744
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
745
|
+
// ─────────────────────────────────────────────────────────────────
|
|
746
|
+
// I/O helpers
|
|
747
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1334
748
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return 'Early development';
|
|
749
|
+
async function ensureContextDirectory(projectRoot) {
|
|
750
|
+
const { contextDir } = getContextPaths(projectRoot);
|
|
751
|
+
await fsp.mkdir(contextDir, { recursive: true });
|
|
752
|
+
return contextDir;
|
|
1340
753
|
}
|
|
1341
754
|
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const normalizedType = projectType === 'backend API platform'
|
|
1347
|
-
? 'Backend API platform'
|
|
1348
|
-
: capitalize(projectType);
|
|
755
|
+
async function readJsonFile(filePath, fallback) {
|
|
756
|
+
try { return JSON.parse(await fsp.readFile(filePath, 'utf8')); }
|
|
757
|
+
catch (_) { return fallback; }
|
|
758
|
+
}
|
|
1349
759
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
760
|
+
async function writeJsonAtomic(filePath, value) {
|
|
761
|
+
await writeTextAtomic(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
762
|
+
}
|
|
1353
763
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
)
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
+
}
|
|
1360
770
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return `${normalizedType} with RESTful request handling and structured server-side workflow orchestration.`;
|
|
1366
|
-
}
|
|
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
|
+
}
|
|
1367
775
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
776
|
+
// ─────────────────────────────────────────────────────────────────
|
|
777
|
+
// Runtime config
|
|
778
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1371
779
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
}
|
|
780
|
+
async function loadRuntimeConfig(projectRoot) {
|
|
781
|
+
const { configFile } = getContextPaths(projectRoot);
|
|
782
|
+
return deepMerge(DEFAULT_CONFIG, await readJsonFile(configFile, {}));
|
|
783
|
+
}
|
|
1375
784
|
|
|
1376
|
-
|
|
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;
|
|
1377
791
|
}
|
|
1378
792
|
|
|
1379
|
-
|
|
1380
|
-
|
|
793
|
+
// ─────────────────────────────────────────────────────────────────
|
|
794
|
+
// MAIN STATE UPDATER
|
|
795
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1381
796
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
+
}
|
|
1385
828
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
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
|
+
};
|
|
1389
907
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
908
|
+
nextState.ai_summary = genAiSummary(nextState, boot);
|
|
909
|
+
nextState.next_steps = genNextSteps(nextState, boot);
|
|
1393
910
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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);
|
|
1397
916
|
|
|
1398
|
-
|
|
1399
|
-
|
|
917
|
+
if (log) log.debug(`Updated AI context – ${newCodeChanges.length} code change(s) recorded`);
|
|
918
|
+
if (typeof settings.syncCallback === 'function') await settings.syncCallback();
|
|
1400
919
|
|
|
1401
|
-
|
|
1402
|
-
return Array.from(new Set((values || []).filter(Boolean)));
|
|
920
|
+
return nextState;
|
|
1403
921
|
}
|
|
1404
922
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1411
|
-
}
|
|
923
|
+
// ─────────────────────────────────────────────────────────────────
|
|
924
|
+
// Debounced updater
|
|
925
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1412
926
|
|
|
1413
927
|
function createDebouncedStateUpdater(projectRoot, options) {
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
syncCallback: null
|
|
1419
|
-
},
|
|
1420
|
-
options
|
|
1421
|
-
);
|
|
1422
|
-
|
|
1423
|
-
let timer = null;
|
|
1424
|
-
let pendingEvents = [];
|
|
1425
|
-
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();
|
|
1426
932
|
|
|
1427
933
|
async function flush() {
|
|
1428
|
-
if (
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
pendingEvents = [];
|
|
1434
|
-
|
|
1435
|
-
activeFlush = activeFlush.then(() =>
|
|
1436
|
-
updateProjectState(projectRoot, events, {
|
|
1437
|
-
logger: settings.logger,
|
|
1438
|
-
syncCallback: settings.syncCallback
|
|
1439
|
-
})
|
|
1440
|
-
);
|
|
1441
|
-
|
|
1442
|
-
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;
|
|
1443
939
|
}
|
|
1444
940
|
|
|
1445
941
|
return {
|
|
1446
942
|
enqueue(event) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
if (timer) {
|
|
1450
|
-
clearTimeout(timer);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
943
|
+
pending.push(event);
|
|
944
|
+
if (timer) clearTimeout(timer);
|
|
1453
945
|
timer = setTimeout(() => {
|
|
1454
946
|
timer = null;
|
|
1455
|
-
flush().catch((
|
|
1456
|
-
|
|
1457
|
-
settings.logger.error(`Failed to flush AI context updates: ${error.message}`);
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
}, settings.debounceMs);
|
|
947
|
+
flush().catch((err) => { if (s.logger) s.logger.error(`Flush failed: ${err.message}`); });
|
|
948
|
+
}, s.debounceMs);
|
|
1461
949
|
},
|
|
1462
950
|
async flushNow() {
|
|
1463
|
-
if (timer) {
|
|
1464
|
-
clearTimeout(timer);
|
|
1465
|
-
timer = null;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
951
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
1468
952
|
await flush();
|
|
1469
953
|
}
|
|
1470
954
|
};
|
|
1471
955
|
}
|
|
1472
956
|
|
|
957
|
+
// ─────────────────────────────────────────────────────────────────
|
|
958
|
+
// Exports
|
|
959
|
+
// ─────────────────────────────────────────────────────────────────
|
|
960
|
+
|
|
1473
961
|
module.exports = {
|
|
1474
962
|
CONTEXT_DIR_NAME,
|
|
1475
963
|
DEFAULT_CONFIG,
|
|
964
|
+
applyErrorEvent,
|
|
965
|
+
applyResolveEvent,
|
|
1476
966
|
bootstrapProjectAnalysis,
|
|
967
|
+
buildCodeFileCatalogue,
|
|
968
|
+
buildDependencyCatalogue,
|
|
969
|
+
buildFileTree,
|
|
1477
970
|
createDebouncedStateUpdater,
|
|
1478
971
|
createDefaultChangelog,
|
|
972
|
+
createDefaultIssueTracker,
|
|
1479
973
|
createDefaultState,
|
|
1480
974
|
detectProjectMetadata,
|
|
1481
975
|
ensureContextDirectory,
|
|
976
|
+
extractApiRoutes,
|
|
1482
977
|
getContextPaths,
|
|
1483
|
-
groupEventsByIntent,
|
|
1484
978
|
loadRuntimeConfig,
|
|
1485
|
-
promoteFeatures,
|
|
1486
979
|
readJsonFile,
|
|
1487
980
|
renderTemplate,
|
|
981
|
+
scanEnvVars,
|
|
982
|
+
scanFiles,
|
|
1488
983
|
scoreEvent,
|
|
1489
984
|
shouldIgnoreProjectFile,
|
|
1490
985
|
updateRuntimeConfig,
|
|
1491
986
|
updateProjectState,
|
|
1492
987
|
writeJsonAtomic,
|
|
1493
988
|
writeTextAtomic
|
|
1494
|
-
};
|
|
989
|
+
};
|