aibridge-context 1.5.2 → 2.0.0

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