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.
@@ -1,1494 +1,989 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const fsp = require('fs/promises');
3
+ const fs = require('fs');
4
+ const fsp = require('fs/promises');
5
5
  const path = require('path');
6
6
 
7
- const CONTEXT_DIR_NAME = '.ai-context';
8
- const MAX_RECENT_UPDATES = 5;
9
- const MAX_CHANGELOG_ENTRIES = 50;
10
- const MAX_KEY_FEATURES = 6;
11
- const MAX_IMPLEMENTATION_DETAILS = 8;
12
- const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/', 'src/', 'routes/', 'controllers/', 'services/'];
13
- const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
14
- const IGNORED_DIRECTORY_NAMES = new Set([
15
- 'node_modules',
16
- '.git',
17
- '.ai-context',
18
- 'dist',
19
- 'build',
20
- 'coverage',
21
- '.tmp',
22
- 'logs'
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 ANALYSIS_ROOT_FILES = [
25
- 'package.json',
26
- 'app.js',
27
- 'app.ts',
28
- 'index.js',
29
- 'index.ts',
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
- function resolveProjectRoot(projectRoot) {
73
- return path.resolve(projectRoot || process.cwd());
74
- }
45
+ // ─────────────────────────────────────────────────────────────────
46
+ // Path helpers
47
+ // ─────────────────────────────────────────────────────────────────
75
48
 
76
- function getContextPaths(projectRoot) {
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: path.join(contextDir, 'state.json'),
83
- brainFile: path.join(contextDir, 'brain.txt'),
84
- contextFile: path.join(contextDir, 'context.md'),
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: path.join(contextDir, 'config.json')
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
- 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)
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
- if (dependencies.fastify) {
204
- return 'Fastify';
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
- if (dependencies.koa) {
208
- return 'Koa';
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 buildStackLabel(techStack) {
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 hasAnyFileExtension(projectRoot, extensions) {
220
- return scanProjectFiles(projectRoot, 2).some((filePath) =>
221
- extensions.includes(path.extname(filePath).toLowerCase())
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
- function detectPackageManager(projectRoot) {
226
- const resolvedRoot = resolveProjectRoot(projectRoot);
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 normalizedPath = normalizeProjectPath(filePath).toLowerCase();
294
-
295
- if (!normalizedPath) {
296
- return false;
297
- }
298
-
299
- const segments = normalizedPath.split('/').filter(Boolean);
300
- const baseName = segments[segments.length - 1] || '';
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 normalizedPath = normalizeProjectPath(filePath);
329
- const lowerPath = normalizedPath.toLowerCase();
107
+ const p = normPath(filePath).toLowerCase();
108
+ if (shouldIgnoreProjectFile(p)) return -5;
330
109
  let score = 0;
331
-
332
- if (shouldIgnoreProjectFile(normalizedPath)) {
333
- return -5;
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
- async function ensureContextDirectory(projectRoot) {
356
- const { contextDir } = getContextPaths(projectRoot);
357
- await fsp.mkdir(contextDir, { recursive: true });
358
- return contextDir;
359
- }
117
+ // ─────────────────────────────────────────────────────────────────
118
+ // File scanning
119
+ // ─────────────────────────────────────────────────────────────────
360
120
 
361
- async function readJsonFile(filePath, fallbackValue) {
362
- try {
363
- const raw = await fsp.readFile(filePath, 'utf8');
364
- return JSON.parse(raw);
365
- } catch (error) {
366
- return fallbackValue;
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
- async function writeJsonAtomic(filePath, value) {
371
- const content = `${JSON.stringify(value, null, 2)}\n`;
372
- await writeTextAtomic(filePath, content);
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
- async function writeTextAtomic(filePath, content) {
376
- const tempFilePath = `${filePath}.tmp`;
377
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
378
- await fsp.writeFile(tempFilePath, content, 'utf8');
379
- await fsp.rename(tempFilePath, filePath);
380
- }
167
+ // ─────────────────────────────────────────────────────────────────
168
+ // Package.json / metadata
169
+ // ─────────────────────────────────────────────────────────────────
381
170
 
382
- function renderTemplate(template, variables) {
383
- return Object.entries(variables).reduce((accumulator, [key, value]) => {
384
- const safeValue = value == null ? '' : String(value);
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 createDefaultChangelog() {
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
- entries: []
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 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 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 collectAnalysisInputs(projectRoot) {
447
- const resolvedRoot = resolveProjectRoot(projectRoot);
448
- const selectedFiles = new Set();
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
- if (fs.existsSync(absoluteFile) && isInsideProjectRoot(resolvedRoot, absoluteFile)) {
454
- selectedFiles.add(relativeFile);
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
- hasPackageJson: Boolean(rootPackage),
502
- hasCliEntry:
503
- Boolean(rootPackage && rootPackage.bin && Object.keys(rootPackage.bin).length > 0) ||
504
- hasAutomationPattern(/^#!\/usr\/bin\/env node/m) ||
505
- hasAutomationPattern(/\bprocess\.argv\b/),
506
- hasExpress:
507
- dependencyNames.has('express') ||
508
- hasRuntimePattern(/\brequire\(['"]express['"]\)/) ||
509
- hasRuntimePattern(/\bfrom ['"]express['"]/) ||
510
- hasRuntimePattern(/\bexpress\(\)/),
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 isRuntimeImplementationFile(relativePath) {
560
- const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
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
- function isAutomationImplementationFile(relativePath) {
579
- const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
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
- function buildArchitecturePatterns(signals, analysisInputs, techStack, rootPackage) {
591
- const patterns = [];
592
-
593
- if (signals.hasCliEntry) {
594
- patterns.push('Command-line automation workflow');
595
- }
596
-
597
- if (signals.hasWatcher) {
598
- patterns.push('Event-driven file watching pipeline');
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
- function buildImplementationDetails(signals, techStack) {
647
- const details = [];
648
-
649
- if (signals.hasJwt) {
650
- details.push('JWT-based authentication system');
651
- }
652
-
653
- if (signals.hasMongoose) {
654
- details.push('MongoDB integration through Mongoose ORM');
655
- }
656
-
657
- if (signals.hasExpress && signals.hasRestRoutes) {
658
- details.push('REST API architecture using Express routing');
659
- }
660
-
661
- if (signals.hasMiddleware) {
662
- details.push('Express middleware pipeline for request handling');
663
- }
664
-
665
- if (signals.hasSocketIO) {
666
- details.push('Socket.IO-based real-time communication');
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
- function buildKeyFeatures(signals, techStack) {
693
- const features = [];
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
- function determineProjectType(signals, techStack, rootPackage) {
749
- if (signals.hasNext) {
750
- return 'Next.js application';
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
- async function loadRuntimeConfig(projectRoot) {
781
- const { configFile } = getContextPaths(projectRoot);
782
- const userConfig = await readJsonFile(configFile, {});
783
- return deepMerge(DEFAULT_CONFIG, userConfig);
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
- async function updateRuntimeConfig(projectRoot, updates) {
787
- const { configFile } = getContextPaths(projectRoot);
788
- const currentConfig = await readJsonFile(configFile, {});
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
- async function updateProjectState(projectRoot, changeEvent, options) {
797
- const settings = Object.assign(
798
- {
799
- logger: null,
800
- syncCallback: null
801
- },
802
- options
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
- function isMeaningfulEvent(event) {
861
- if (!event || !event.file) {
862
- return false;
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
- if (lowerPath.startsWith('core/') || lowerPath.startsWith('src/')) {
922
- return 'logic';
923
- }
924
-
925
- return 'project';
926
- }
927
-
928
- function detectEventFeatureKey(filePath, bootstrap) {
929
- const lowerPath = normalizeProjectPath(filePath).toLowerCase();
930
- const signals = bootstrap.signals;
931
-
932
- if (lowerPath === 'package.json') {
933
- if (signals.hasGitAutomation) {
934
- return 'public_sync';
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
- if (lowerPath.includes('service') || lowerPath.includes('controller')) {
973
- return signals.hasAxiosOrFetch ? 'external_api' : 'rest_api';
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
- function groupEventsByIntent(events, bootstrap) {
980
- if (!Array.isArray(events) || events.length === 0) {
981
- return [];
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
- if (!buckets.has(key)) {
992
- buckets.set(key, []);
993
- }
994
-
995
- buckets.get(key).push(
996
- Object.assign({}, event, {
997
- area,
998
- featureKey
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
- function interpretIntentGroup(group) {
1010
- const latestTimestamp = group.reduce((latest, event) => {
1011
- return new Date(event.timestamp) > new Date(latest) ? event.timestamp : latest;
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
- timestamp: latestTimestamp,
1020
- scope: area,
1021
- title: buildIntentTitle(type, subject),
1022
- type,
1023
- impact: describeIntentImpact(type, featureKey),
1024
- feature_key: featureKey,
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 determineGroupedUpdateType(group) {
1030
- const hasAdd = group.some((event) => event.action === 'add');
1031
- const hasDelete = group.some((event) => event.action === 'delete');
1032
-
1033
- if (hasAdd) {
1034
- return 'feature';
1035
- }
1036
-
1037
- if (hasDelete) {
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
- if (type === 'fix') {
1095
- return impactMap[featureKey].replace(/^Improves /, 'Resolves issues in ');
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 normalizeStoredUpdates(updates) {
1106
- if (!Array.isArray(updates)) {
1107
- return [];
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
- return dedupeRecentUpdates(
1111
- updates
1112
- .map((update) => normalizeStoredUpdate(update))
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 (update.title && update.type && update.impact) {
1123
- return {
1124
- title: update.title,
1125
- type: normalizeUpdateType(update.type),
1126
- impact: update.impact
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 null;
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
- function normalizeStoredHistoryEntries(entries) {
1134
- if (!Array.isArray(entries)) {
1135
- return [];
1136
- }
431
+ // ─────────────────────────────────────────────────────────────────
432
+ // Code change history ← THE KEY NEW SECTION
433
+ // ─────────────────────────────────────────────────────────────────
1137
434
 
1138
- return dedupeHistoryEntries(
1139
- entries
1140
- .map((entry) => normalizeStoredHistoryEntry(entry))
1141
- .filter(Boolean)
1142
- );
1143
- }
1144
-
1145
- function normalizeStoredHistoryEntry(entry) {
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 (entry.title && entry.type && entry.impact) {
444
+ if (action === 'delete') {
1151
445
  return {
1152
- timestamp: entry.timestamp || new Date(0).toISOString(),
1153
- scope: entry.scope || 'project',
1154
- title: entry.title,
1155
- type: normalizeUpdateType(entry.type),
1156
- impact: entry.impact,
1157
- feature_key: entry.feature_key || inferFeatureKeyFromText(entry.title, entry.impact),
1158
- feature_name: entry.feature_name || FEATURE_CATALOG[inferFeatureKeyFromText(entry.title, entry.impact)] || 'Project capability'
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
- return null;
1163
- }
1164
-
1165
- function inferFeatureKeyFromText(title, impact) {
1166
- const combinedText = `${title || ''} ${impact || ''}`.toLowerCase();
1167
-
1168
- if (combinedText.includes('jwt') || combinedText.includes('auth')) {
1169
- return 'auth';
1170
- }
1171
-
1172
- if (combinedText.includes('mongo') || combinedText.includes('mongoose') || combinedText.includes('persist')) {
1173
- return 'persistence';
1174
- }
1175
-
1176
- if (combinedText.includes('real-time') || combinedText.includes('socket')) {
1177
- return 'realtime';
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
- if (combinedText.includes('watch') || combinedText.includes('change')) {
1189
- return 'change_tracking';
1190
- }
477
+ // change produce real diff
478
+ const old_ = oldContent || '';
479
+ const new_ = newContent || '';
1191
480
 
1192
- if (combinedText.includes('command line') || combinedText.includes('cli')) {
1193
- return 'cli_automation';
1194
- }
481
+ if (old_ === new_) return null; // no actual change
1195
482
 
1196
- if (combinedText.includes('http') || combinedText.includes('context delivery')) {
1197
- return 'local_context_server';
1198
- }
483
+ const { patch, linesAdded, linesRemoved, summary } = diffTexts(old_, new_, relPath);
484
+ const signals = isCode ? extractCodeSignals(patch, relPath) : ['File updated'];
1199
485
 
1200
- return 'ai_context_generation';
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
- function normalizeUpdateType(type) {
1204
- if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
1205
- return type;
1206
- }
499
+ // ─────────────────────────────────────────────────────────────────
500
+ // Bootstrap project analysis
501
+ // ─────────────────────────────────────────────────────────────────
1207
502
 
1208
- if (type === 'removal') {
1209
- return 'fix';
1210
- }
1211
-
1212
- return 'improvement';
1213
- }
1214
-
1215
- function toStateUpdate(update) {
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
- title: update.title,
1218
- type: normalizeUpdateType(update.type),
1219
- impact: update.impact
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 dedupeRecentUpdates(updates) {
1224
- const seen = new Set();
1225
- const result = [];
1226
-
1227
- for (const update of updates.filter(Boolean)) {
1228
- const key = `${update.title}::${update.type}::${update.impact}`;
1229
-
1230
- if (seen.has(key)) {
1231
- continue;
1232
- }
1233
-
1234
- seen.add(key);
1235
- result.push(update);
1236
- }
1237
-
1238
- return result;
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 dedupeHistoryEntries(entries) {
1242
- const seen = new Set();
1243
- const result = [];
1244
-
1245
- for (const entry of entries.filter(Boolean)) {
1246
- const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
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
- if (seen.has(key)) {
1249
- continue;
1250
- }
1251
-
1252
- seen.add(key);
1253
- result.push(entry);
1254
- }
1255
-
1256
- return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
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 promoteFeatures(historyEntries) {
1260
- if (!Array.isArray(historyEntries) || historyEntries.length === 0) {
1261
- return [];
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
- const scores = new Map();
1265
-
1266
- for (const entry of historyEntries) {
1267
- const featureName = entry.feature_name || FEATURE_CATALOG[entry.feature_key];
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
- function scoreHistoryEntry(entry) {
1283
- const weights = {
1284
- feature: 4,
1285
- refactor: 3,
1286
- improvement: 2,
1287
- fix: 2
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
- return weights[normalizeUpdateType(entry.type)] || 1;
716
+ state.ai_summary = genAiSummary(state, boot);
717
+ state.next_steps = genNextSteps(state, boot);
718
+ return state;
1291
719
  }
1292
720
 
1293
- function mergeKeyFeatures(primaryFeatures, promotedFeatures) {
1294
- return uniqueNonEmpty([].concat(primaryFeatures || [], promotedFeatures || [])).slice(0, MAX_KEY_FEATURES);
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 deriveKnownIssues(projectRoot, bootstrap) {
1298
- const knownIssues = [];
1299
-
1300
- if (!hasTestIndicators(projectRoot)) {
1301
- knownIssues.push('No automated test suite is detected yet.');
1302
- }
1303
-
1304
- if (bootstrap.implementationDetails.length === 0) {
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 hasTestIndicators(projectRoot) {
1316
- const resolvedRoot = resolveProjectRoot(projectRoot);
1317
- const testPaths = [
1318
- 'test',
1319
- 'tests',
1320
- '__tests__',
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
- function determineCurrentStage(keyFeatures, historyEntries, implementationDetails) {
1331
- if (keyFeatures.length >= 4 && implementationDetails.length >= 3) {
1332
- return 'Production-ready';
1333
- }
745
+ // ─────────────────────────────────────────────────────────────────
746
+ // I/O helpers
747
+ // ─────────────────────────────────────────────────────────────────
1334
748
 
1335
- if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
1336
- return 'Functional prototype';
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 generateAiSummary(state, bootstrap) {
1343
- const features = state.key_features || [];
1344
- const details = bootstrap.implementationDetails || [];
1345
- const projectType = bootstrap.projectType || 'software project';
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
- if (details.some((detail) => detail.includes('JWT')) && details.some((detail) => detail.includes('MongoDB'))) {
1351
- return `${normalizedType} with JWT authentication and MongoDB-backed application logic.`;
1352
- }
760
+ async function writeJsonAtomic(filePath, value) {
761
+ await writeTextAtomic(filePath, JSON.stringify(value, null, 2) + '\n');
762
+ }
1353
763
 
1354
- if (
1355
- features.includes(FEATURE_CATALOG.ai_context_generation) &&
1356
- features.includes(FEATURE_CATALOG.public_sync)
1357
- ) {
1358
- return `${normalizedType} that generates AI-readable project context, tracks meaningful changes, and can publish public project state through GitHub.`;
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
- if (
1362
- features.includes(FEATURE_CATALOG.rest_api) &&
1363
- details.some((detail) => detail.includes('Express'))
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
- if (features.length >= 2) {
1369
- return `${normalizedType} focused on ${features.slice(0, 2).join(' and ').toLowerCase()}.`;
1370
- }
776
+ // ─────────────────────────────────────────────────────────────────
777
+ // Runtime config
778
+ // ─────────────────────────────────────────────────────────────────
1371
779
 
1372
- if (details.length > 0) {
1373
- return `${normalizedType} built around ${details[0].toLowerCase()}.`;
1374
- }
780
+ async function loadRuntimeConfig(projectRoot) {
781
+ const { configFile } = getContextPaths(projectRoot);
782
+ return deepMerge(DEFAULT_CONFIG, await readJsonFile(configFile, {}));
783
+ }
1375
784
 
1376
- return `${normalizedType} with detectable project structure and implementation patterns.`;
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
- function generateNextSteps(state, bootstrap, historyEntries) {
1380
- const nextSteps = [];
793
+ // ─────────────────────────────────────────────────────────────────
794
+ // MAIN STATE UPDATER
795
+ // ─────────────────────────────────────────────────────────────────
1381
796
 
1382
- if (state.known_issues.includes('No automated test suite is detected yet.')) {
1383
- nextSteps.push('Add automated tests that cover the main application flow and critical integration points.');
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
- if (bootstrap.implementationDetails.length < 2) {
1387
- nextSteps.push('Strengthen the project structure so major implementation patterns are easier to detect automatically.');
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
- if (historyEntries.length === 0) {
1391
- nextSteps.push('Capture the next meaningful project update so recent evolution is reflected alongside the bootstrap analysis.');
1392
- }
908
+ nextState.ai_summary = genAiSummary(nextState, boot);
909
+ nextState.next_steps = genNextSteps(nextState, boot);
1393
910
 
1394
- if (state.current_stage === 'Early development') {
1395
- nextSteps.push('Ship the next core capability to move the project from initial structure into a functional prototype.');
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
- return Array.from(new Set(nextSteps)).slice(0, 4);
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
- function uniqueNonEmpty(values) {
1402
- return Array.from(new Set((values || []).filter(Boolean)));
920
+ return nextState;
1403
921
  }
1404
922
 
1405
- function capitalize(value) {
1406
- if (!value) {
1407
- return '';
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 settings = Object.assign(
1415
- {
1416
- debounceMs: DEFAULT_CONFIG.debounceMs,
1417
- logger: null,
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 (pendingEvents.length === 0) {
1429
- return;
1430
- }
1431
-
1432
- const events = pendingEvents.slice();
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
- pendingEvents.push(event);
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((error) => {
1456
- if (settings.logger) {
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
+ };