aibridge-context 1.4.1 → 1.5.1

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.
@@ -8,108 +8,54 @@ const CONTEXT_DIR_NAME = '.ai-context';
8
8
  const MAX_RECENT_UPDATES = 5;
9
9
  const MAX_CHANGELOG_ENTRIES = 50;
10
10
  const MAX_KEY_FEATURES = 6;
11
- const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
11
+ const MAX_IMPLEMENTATION_DETAILS = 8;
12
+ const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/', 'src/', 'routes/', 'controllers/', 'services/'];
12
13
  const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
13
- const LOW_VALUE_FEATURE_KEYS = new Set([
14
- 'documentation',
15
- 'package_configuration',
16
- 'context_templates',
17
- 'project_workflow'
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'
18
23
  ]);
19
-
20
- async function safeWriteJSON(filePath, data) {
21
- const fs = require("fs");
22
- const path = require("path");
23
-
24
- const tmpPath = filePath + ".tmp";
25
-
26
- try {
27
- // ensure directory exists
28
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
-
30
- // write temp file
31
- await fs.promises.writeFile(tmpPath, data, "utf-8");
32
-
33
- // rename only if tmp exists
34
- if (fs.existsSync(tmpPath)) {
35
- await fs.promises.rename(tmpPath, filePath);
36
- } else {
37
- await fs.promises.writeFile(filePath, data, "utf-8");
38
- }
39
- } catch (err) {
40
- console.error("[aibridge] Write fallback triggered:", err.message);
41
-
42
- try {
43
- await fs.promises.writeFile(filePath, data, "utf-8");
44
- } catch (e) {
45
- console.error("[aibridge] Failed to write state file:", e.message);
46
- }
47
- }
48
- }
49
-
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
+ ];
50
45
  const FEATURE_CATALOG = {
51
- cli_workflow: {
52
- name: 'CLI workflow for initializing, updating, and linking AI context',
53
- subject: 'CLI workflow',
54
- projectType: 'CLI tool'
55
- },
56
- github_sync: {
57
- name: 'Public GitHub sync for AI-readable project context',
58
- subject: 'GitHub sync system',
59
- projectType: 'CLI tool'
60
- },
61
- project_intelligence: {
62
- name: 'Project intelligence engine that turns development activity into AI-readable state',
63
- subject: 'project intelligence engine',
64
- projectType: 'project intelligence engine'
65
- },
66
- change_tracking: {
67
- name: 'Meaningful change tracking that filters noise from project activity',
68
- subject: 'change tracking engine',
69
- projectType: 'change tracking engine'
70
- },
71
- local_context_server: {
72
- name: 'Local server for AI-readable project context endpoints',
73
- subject: 'AI context delivery service',
74
- projectType: 'context delivery service'
75
- },
76
- context_delivery_system: {
77
- name: 'Unified context delivery system connecting project intelligence and serving layers',
78
- subject: 'AI context delivery system',
79
- projectType: 'AI context system'
80
- },
81
- cli_orchestration: {
82
- name: 'Command workflow that connects project intelligence with developer actions',
83
- subject: 'CLI workflow',
84
- projectType: 'CLI tool'
85
- },
86
- project_setup: {
87
- name: 'Guided setup flow for safe AI context initialization',
88
- subject: 'project setup flow',
89
- projectType: 'CLI tool'
90
- },
91
- documentation: {
92
- name: 'Developer guidance for adopting the AI context workflow',
93
- subject: 'developer guidance',
94
- projectType: 'project'
95
- },
96
- package_configuration: {
97
- name: 'Package configuration for distributing the AI context CLI',
98
- subject: 'package configuration',
99
- projectType: 'package'
100
- },
101
- context_templates: {
102
- name: 'Generated templates for bootstrapping AI-readable project context',
103
- subject: 'generated AI context templates',
104
- projectType: 'template set'
105
- },
106
- project_workflow: {
107
- name: 'Core project workflow for maintaining AI-readable project state',
108
- subject: 'project workflow',
109
- projectType: 'project'
110
- }
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'
111
58
  };
112
-
113
59
  const DEFAULT_CONFIG = {
114
60
  port: 3333,
115
61
  debounceMs: 600,
@@ -123,8 +69,13 @@ const DEFAULT_CONFIG = {
123
69
  }
124
70
  };
125
71
 
72
+ function resolveProjectRoot(projectRoot) {
73
+ return path.resolve(projectRoot || process.cwd());
74
+ }
75
+
126
76
  function getContextPaths(projectRoot) {
127
- const contextDir = path.join(projectRoot, CONTEXT_DIR_NAME);
77
+ const resolvedRoot = resolveProjectRoot(projectRoot);
78
+ const contextDir = path.join(resolvedRoot, CONTEXT_DIR_NAME);
128
79
 
129
80
  return {
130
81
  contextDir,
@@ -158,67 +109,72 @@ function isObject(value) {
158
109
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
159
110
  }
160
111
 
161
- function detectProjectMetadata(projectRoot) {
162
- const packageJsonPath = path.join(projectRoot, 'package.json');
163
- const packageManager = detectPackageManager(projectRoot);
164
- const techStack = detectTechStack(projectRoot);
165
- const metadata = {
166
- project: path.basename(projectRoot),
167
- version: '0.1.0',
168
- techStack: Object.assign({}, techStack, {
169
- package_manager: packageManager
170
- }),
171
- stackLabel: buildStackLabel(techStack),
172
- packageManager
173
- };
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);
174
131
 
175
132
  if (!fs.existsSync(packageJsonPath)) {
176
- return metadata;
133
+ return null;
177
134
  }
178
135
 
179
136
  try {
180
- const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
181
- const parsedPackage = JSON.parse(rawPackage);
182
-
183
- metadata.project = parsedPackage.name || metadata.project;
184
- metadata.version = parsedPackage.version || metadata.version;
137
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
185
138
  } catch (error) {
186
- metadata.stackLabel = buildStackLabel(metadata.techStack);
139
+ return null;
187
140
  }
188
-
189
- return metadata;
190
141
  }
191
142
 
192
- function detectTechStack(projectRoot) {
193
- const packageJsonPath = path.join(projectRoot, 'package.json');
194
- const pythonMarkers = ['pyproject.toml', 'requirements.txt', 'setup.py'];
195
- const hasPackageJson = fs.existsSync(packageJsonPath);
196
- const hasPythonMarker = pythonMarkers.some((marker) =>
197
- fs.existsSync(path.join(projectRoot, marker))
198
- );
199
- let dependencies = {};
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);
200
148
 
201
- if (hasPackageJson) {
202
- try {
203
- const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
204
- const parsedPackage = JSON.parse(rawPackage);
205
- dependencies = Object.assign(
206
- {},
207
- parsedPackage.dependencies || {},
208
- parsedPackage.devDependencies || {}
209
- );
210
- } catch (error) {
211
- dependencies = {};
212
- }
213
- }
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
+ }
214
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
+ );
215
171
  let language = '';
216
172
  let runtime = '';
217
173
 
218
- if (hasPackageJson || hasAnyFileExtension(projectRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
174
+ if (rootPackage || hasAnyFileExtension(resolvedRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
219
175
  language = 'Node.js';
220
176
  runtime = 'Node.js';
221
- } else if (hasPythonMarker || hasAnyFileExtension(projectRoot, ['.py'])) {
177
+ } else if (hasPythonMarker || hasAnyFileExtension(resolvedRoot, ['.py'])) {
222
178
  language = 'Python';
223
179
  runtime = 'Python';
224
180
  }
@@ -227,7 +183,7 @@ function detectTechStack(projectRoot) {
227
183
  language,
228
184
  framework: detectFramework(dependencies),
229
185
  runtime,
230
- package_manager: detectPackageManager(projectRoot)
186
+ package_manager: detectPackageManager(resolvedRoot)
231
187
  };
232
188
  }
233
189
 
@@ -244,6 +200,14 @@ function detectFramework(dependencies) {
244
200
  return 'Express';
245
201
  }
246
202
 
203
+ if (dependencies.fastify) {
204
+ return 'Fastify';
205
+ }
206
+
207
+ if (dependencies.koa) {
208
+ return 'Koa';
209
+ }
210
+
247
211
  return '';
248
212
  }
249
213
 
@@ -258,11 +222,30 @@ function hasAnyFileExtension(projectRoot, extensions) {
258
222
  );
259
223
  }
260
224
 
261
- function scanProjectFiles(projectRoot, maxDepth) {
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;
262
245
  const results = [];
263
246
 
264
247
  function visit(currentDir, depth) {
265
- if (depth > maxDepth) {
248
+ if (depth > maxDepth || !isInsideProjectRoot(resolvedRoot, currentDir)) {
266
249
  return;
267
250
  }
268
251
 
@@ -276,13 +259,22 @@ function scanProjectFiles(projectRoot, maxDepth) {
276
259
 
277
260
  for (const entry of entries) {
278
261
  const fullPath = path.join(currentDir, entry.name);
279
- const relativePath = normalizeProjectPath(path.relative(projectRoot, fullPath));
262
+
263
+ if (!isInsideProjectRoot(resolvedRoot, fullPath)) {
264
+ continue;
265
+ }
266
+
267
+ const relativePath = normalizeProjectPath(path.relative(resolvedRoot, fullPath));
280
268
 
281
269
  if (entry.isDirectory()) {
282
270
  if (shouldIgnoreProjectFile(relativePath)) {
283
271
  continue;
284
272
  }
285
273
 
274
+ if (includeDirectories && depth === 0 && !includeDirectories.has(relativePath.toLowerCase())) {
275
+ continue;
276
+ }
277
+
286
278
  visit(fullPath, depth + 1);
287
279
  continue;
288
280
  }
@@ -293,20 +285,71 @@ function scanProjectFiles(projectRoot, maxDepth) {
293
285
  }
294
286
  }
295
287
 
296
- visit(projectRoot, 0);
288
+ visit(resolvedRoot, 0);
297
289
  return results;
298
290
  }
299
291
 
300
- function detectPackageManager(projectRoot) {
301
- if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
302
- return 'pnpm';
292
+ function shouldIgnoreProjectFile(filePath) {
293
+ const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
294
+
295
+ if (!normalizedPath) {
296
+ return false;
303
297
  }
304
298
 
305
- if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
306
- return 'yarn';
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;
307
304
  }
308
305
 
309
- return 'npm';
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
+
324
+ return false;
325
+ }
326
+
327
+ function scoreEvent(filePath) {
328
+ const normalizedPath = normalizeProjectPath(filePath);
329
+ const lowerPath = normalizedPath.toLowerCase();
330
+ 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
+
352
+ return score;
310
353
  }
311
354
 
312
355
  async function ensureContextDirectory(projectRoot) {
@@ -330,204 +373,488 @@ async function writeJsonAtomic(filePath, value) {
330
373
  }
331
374
 
332
375
  async function writeTextAtomic(filePath, content) {
333
- await safeWriteJSON(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
+ };
334
393
  }
335
394
 
336
395
  function createDefaultState(projectRoot) {
337
396
  const metadata = detectProjectMetadata(projectRoot);
397
+ const bootstrap = bootstrapProjectAnalysis(projectRoot);
338
398
  const state = {
339
399
  project: metadata.project,
340
400
  version: metadata.version,
341
401
  last_updated: new Date(0).toISOString(),
342
402
  ai_summary: '',
343
- tech_stack: metadata.techStack,
344
- current_stage: 'Early development',
403
+ tech_stack: bootstrap.techStack,
404
+ architecture_patterns: bootstrap.architecturePatterns,
405
+ implementation_details: bootstrap.implementationDetails,
406
+ current_stage: determineCurrentStage(bootstrap.keyFeatures, [], bootstrap.implementationDetails),
345
407
  recent_updates: [],
346
- key_features: [],
347
- known_issues: deriveKnownIssues(projectRoot, metadata.techStack, []),
408
+ key_features: bootstrap.keyFeatures,
409
+ known_issues: deriveKnownIssues(projectRoot, bootstrap),
348
410
  next_steps: []
349
411
  };
350
412
 
351
- state.ai_summary = generateAiSummary(state, []);
352
- state.next_steps = generateNextSteps(state, []);
413
+ state.ai_summary = generateAiSummary(state, bootstrap);
414
+ state.next_steps = generateNextSteps(state, bootstrap, []);
353
415
 
354
416
  return state;
355
417
  }
356
418
 
357
- function createDefaultChangelog() {
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
+
358
436
  return {
359
- entries: []
437
+ projectType,
438
+ techStack,
439
+ architecturePatterns,
440
+ implementationDetails,
441
+ keyFeatures,
442
+ signals: implementationSignals
360
443
  };
361
444
  }
362
445
 
363
- function renderTemplate(template, variables) {
364
- return Object.entries(variables).reduce((accumulator, [key, value]) => {
365
- const safeValue = value == null ? '' : String(value);
366
- return accumulator.split(`{{${key}}}`).join(safeValue);
367
- }, template);
368
- }
446
+ function collectAnalysisInputs(projectRoot) {
447
+ const resolvedRoot = resolveProjectRoot(projectRoot);
448
+ const selectedFiles = new Set();
369
449
 
370
- async function loadRuntimeConfig(projectRoot) {
371
- const { configFile } = getContextPaths(projectRoot);
372
- const userConfig = await readJsonFile(configFile, {});
373
- return deepMerge(DEFAULT_CONFIG, userConfig);
450
+ for (const relativeFile of ANALYSIS_ROOT_FILES) {
451
+ const absoluteFile = path.join(resolvedRoot, relativeFile);
452
+
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);
374
482
  }
375
483
 
376
- async function updateRuntimeConfig(projectRoot, updates) {
377
- const { configFile } = getContextPaths(projectRoot);
378
- const currentConfig = await readJsonFile(configFile, {});
379
- const mergedCurrentConfig = deepMerge(DEFAULT_CONFIG, currentConfig);
380
- const nextConfig = deepMerge(mergedCurrentConfig, updates || {});
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);
381
499
 
382
- await writeJsonAtomic(configFile, nextConfig);
383
- return nextConfig;
500
+ 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/)
556
+ };
384
557
  }
385
558
 
386
- async function updateProjectState(projectRoot, changeEvent, options) {
387
- const settings = Object.assign(
388
- {
389
- logger: null,
390
- syncCallback: null
391
- },
392
- options
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'
393
575
  );
394
- const logger = settings.logger;
395
- const contextPaths = getContextPaths(projectRoot);
396
- const metadata = detectProjectMetadata(projectRoot);
397
- const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
398
- const existingChangelog = await readJsonFile(
399
- contextPaths.changelogFile,
400
- createDefaultChangelog()
401
- );
402
- const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
403
- const validEvents = normalizedEvents.filter(Boolean);
404
- const timestamp = determineUpdateTimestamp(validEvents);
405
- const meaningfulEvents = collapseEventsByFile(
406
- validEvents.filter((event) => isMeaningfulEvent(event))
576
+ }
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'
407
587
  );
408
- const groupedUpdates = groupEventsByIntent(meaningfulEvents);
409
- const capabilityHistory = buildProjectCapabilityHistory(projectRoot);
410
- const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
411
- const historyEntries = dedupeHistoryEntries(
412
- groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
413
- ).slice(0, MAX_CHANGELOG_ENTRIES);
414
- const recentUpdates = historyEntries
415
- .filter((entry) => entry.source !== 'project_snapshot')
416
- .slice(0, MAX_RECENT_UPDATES)
417
- .map(toStateUpdate);
418
- const keyFeatures = promoteFeatures(historyEntries);
419
- const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack, keyFeatures);
420
- const nextState = {
421
- project: metadata.project,
422
- version: metadata.version,
423
- last_updated: timestamp,
424
- ai_summary: '',
425
- tech_stack: metadata.techStack,
426
- current_stage: determineCurrentStage(keyFeatures, historyEntries),
427
- recent_updates: recentUpdates,
428
- key_features: keyFeatures,
429
- known_issues: knownIssues,
430
- next_steps: []
431
- };
588
+ }
432
589
 
433
- nextState.ai_summary = generateAiSummary(nextState, historyEntries);
434
- nextState.next_steps = generateNextSteps(nextState, historyEntries);
590
+ function buildArchitecturePatterns(signals, analysisInputs, techStack, rootPackage) {
591
+ const patterns = [];
435
592
 
436
- await writeJsonAtomic(contextPaths.stateFile, nextState);
437
- await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
593
+ if (signals.hasCliEntry) {
594
+ patterns.push('Command-line automation workflow');
595
+ }
438
596
 
439
- if (logger) {
440
- logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
597
+ if (signals.hasWatcher) {
598
+ patterns.push('Event-driven file watching pipeline');
441
599
  }
442
600
 
443
- if (typeof settings.syncCallback === 'function') {
444
- await settings.syncCallback();
601
+ if (signals.hasExpress && signals.hasRestRoutes) {
602
+ patterns.push('REST API architecture');
445
603
  }
446
604
 
447
- return nextState;
448
- }
605
+ if (signals.hasMiddleware) {
606
+ patterns.push('Middleware-driven request pipeline');
607
+ }
449
608
 
450
- function determineUpdateTimestamp(events) {
451
- if (!Array.isArray(events) || events.length === 0) {
452
- return new Date().toISOString();
609
+ if (signals.hasControllers && signals.hasServices) {
610
+ patterns.push('Layered controller-service architecture');
453
611
  }
454
612
 
455
- const latestEvent = events[events.length - 1];
456
- return latestEvent.timestamp || new Date().toISOString();
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);
457
644
  }
458
645
 
459
- function normalizeProjectPath(filePath) {
460
- return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
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);
461
690
  }
462
691
 
463
- function shouldIgnoreProjectFile(filePath) {
464
- const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
692
+ function buildKeyFeatures(signals, techStack) {
693
+ const features = [];
465
694
 
466
- if (!normalizedPath) {
467
- return false;
695
+ if (signals.hasAiContextArtifacts) {
696
+ features.push(FEATURE_CATALOG.ai_context_generation);
468
697
  }
469
698
 
470
- const segments = normalizedPath.split('/');
471
- const baseName = segments[segments.length - 1];
699
+ if (signals.hasCliEntry) {
700
+ features.push(FEATURE_CATALOG.cli_automation);
701
+ }
472
702
 
473
- if (
474
- segments.includes('node_modules') ||
475
- segments.includes('.git') ||
476
- segments.includes('.ai-context') ||
477
- segments.includes('dist') ||
478
- segments.includes('build')
479
- ) {
480
- return true;
703
+ if (signals.hasWatcher) {
704
+ features.push(FEATURE_CATALOG.change_tracking);
481
705
  }
482
706
 
483
- if (baseName.startsWith('.start')) {
484
- return true;
707
+ if (signals.hasGitAutomation) {
708
+ features.push(FEATURE_CATALOG.public_sync);
485
709
  }
486
710
 
487
- if (
488
- baseName.endsWith('.log') ||
489
- baseName.endsWith('.tmp') ||
490
- baseName.endsWith('.lock') ||
491
- baseName === 'package-lock.json' ||
492
- baseName === 'yarn.lock' ||
493
- baseName === 'pnpm-lock.yaml' ||
494
- /^tmp[._-]/i.test(baseName) ||
495
- /^temp[._-]/i.test(baseName) ||
496
- /^debug[._-]/i.test(baseName)
497
- ) {
498
- return true;
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);
499
715
  }
500
716
 
501
- return false;
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);
502
746
  }
503
747
 
504
- function scoreEvent(filePath) {
505
- const normalizedPath = normalizeProjectPath(filePath);
506
- const lowerPath = normalizedPath.toLowerCase();
507
- const baseName = path.basename(normalizedPath).toLowerCase();
508
- let score = 0;
748
+ function determineProjectType(signals, techStack, rootPackage) {
749
+ if (signals.hasNext) {
750
+ return 'Next.js application';
751
+ }
509
752
 
510
- if (shouldIgnoreProjectFile(normalizedPath)) {
511
- return -5;
753
+ if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
754
+ return 'CLI tool';
512
755
  }
513
756
 
514
- if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
515
- score += 3;
516
- }
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';
778
+ }
779
+
780
+ async function loadRuntimeConfig(projectRoot) {
781
+ const { configFile } = getContextPaths(projectRoot);
782
+ const userConfig = await readJsonFile(configFile, {});
783
+ return deepMerge(DEFAULT_CONFIG, userConfig);
784
+ }
785
+
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
+ }
795
+
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: []
841
+ };
842
+
843
+ nextState.ai_summary = generateAiSummary(nextState, bootstrap);
844
+ nextState.next_steps = generateNextSteps(nextState, bootstrap, historyEntries);
517
845
 
518
- if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
519
- score += 2;
520
- }
846
+ await writeJsonAtomic(contextPaths.stateFile, nextState);
847
+ await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
521
848
 
522
- if (lowerPath === 'package.json') {
523
- score += 2;
849
+ if (logger) {
850
+ logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
524
851
  }
525
852
 
526
- if (lowerPath === 'readme.md') {
527
- score += 1;
853
+ if (typeof settings.syncCallback === 'function') {
854
+ await settings.syncCallback();
528
855
  }
529
856
 
530
- return score;
857
+ return nextState;
531
858
  }
532
859
 
533
860
  function isMeaningfulEvent(event) {
@@ -554,365 +881,225 @@ function collapseEventsByFile(events) {
554
881
  return Array.from(collapsedEvents.values());
555
882
  }
556
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
+
557
893
  function classifyChangeArea(filePath) {
558
- const lowerPath = filePath.toLowerCase();
894
+ const lowerPath = normalizeProjectPath(filePath).toLowerCase();
559
895
 
560
896
  if (lowerPath === 'package.json') {
561
- return 'dependencies';
897
+ return 'configuration';
562
898
  }
563
899
 
564
900
  if (lowerPath === 'readme.md') {
565
901
  return 'documentation';
566
902
  }
567
903
 
568
- if (lowerPath.startsWith('core/')) {
569
- return 'logic';
904
+ if (lowerPath.startsWith('bin/')) {
905
+ return 'cli';
570
906
  }
571
907
 
572
- if (lowerPath.startsWith('server/')) {
908
+ if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
573
909
  return 'backend';
574
910
  }
575
911
 
576
- if (lowerPath.startsWith('bin/')) {
577
- return 'cli';
912
+ if (
913
+ lowerPath.startsWith('controllers/') ||
914
+ lowerPath.startsWith('services/') ||
915
+ lowerPath.startsWith('middleware/') ||
916
+ lowerPath.startsWith('config/')
917
+ ) {
918
+ return 'application';
578
919
  }
579
920
 
580
- if (lowerPath.startsWith('templates/')) {
581
- return 'templates';
921
+ if (lowerPath.startsWith('core/') || lowerPath.startsWith('src/')) {
922
+ return 'logic';
582
923
  }
583
924
 
584
925
  return 'project';
585
926
  }
586
927
 
587
- function describeRootDirectory(filePath) {
588
- const normalizedPath = normalizeProjectPath(filePath);
589
- const segments = normalizedPath.split('/');
590
-
591
- if (segments.length === 1) {
592
- return segments[0] || 'project';
593
- }
594
-
595
- return segments[0] || 'project';
596
- }
597
-
598
- function detectIntentTheme(filePath, area) {
599
- const lowerPath = filePath.toLowerCase();
928
+ function detectEventFeatureKey(filePath, bootstrap) {
929
+ const lowerPath = normalizeProjectPath(filePath).toLowerCase();
930
+ const signals = bootstrap.signals;
600
931
 
601
932
  if (lowerPath === 'package.json') {
602
- return 'package_configuration';
603
- }
933
+ if (signals.hasGitAutomation) {
934
+ return 'public_sync';
935
+ }
604
936
 
605
- if (lowerPath === 'readme.md') {
606
- return 'documentation';
937
+ if (signals.hasCliEntry) {
938
+ return 'cli_automation';
939
+ }
940
+
941
+ return 'config_management';
607
942
  }
608
943
 
609
944
  if (lowerPath.startsWith('bin/')) {
610
- return 'cli_workflow';
945
+ return 'cli_automation';
611
946
  }
612
947
 
613
- if (lowerPath.startsWith('server/')) {
614
- return 'local_context_server';
615
- }
948
+ if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
949
+ if (signals.hasAiContextArtifacts) {
950
+ return 'local_context_server';
951
+ }
616
952
 
617
- if (lowerPath.startsWith('templates/')) {
618
- return 'context_templates';
953
+ return 'rest_api';
619
954
  }
620
955
 
621
956
  if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
622
- return 'github_sync';
957
+ return 'public_sync';
623
958
  }
624
959
 
625
- if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
960
+ if (lowerPath.includes('watch')) {
626
961
  return 'change_tracking';
627
962
  }
628
963
 
629
964
  if (lowerPath.includes('state') || lowerPath.includes('context')) {
630
- return 'project_intelligence';
965
+ return 'ai_context_generation';
631
966
  }
632
967
 
633
- if (lowerPath.includes('init')) {
634
- return 'project_setup';
968
+ if (lowerPath.includes('auth') || lowerPath.includes('jwt')) {
969
+ return 'auth';
635
970
  }
636
971
 
637
- if (area === 'logic') {
638
- return 'project_intelligence';
972
+ if (lowerPath.includes('service') || lowerPath.includes('controller')) {
973
+ return signals.hasAxiosOrFetch ? 'external_api' : 'rest_api';
639
974
  }
640
975
 
641
- return 'project_workflow';
642
- }
643
-
644
- function createEventDescriptor(event) {
645
- const normalizedPath = normalizeProjectPath(event.file);
646
-
647
- return {
648
- timestamp: event.timestamp || new Date().toISOString(),
649
- action: event.action || 'change',
650
- file: normalizedPath,
651
- area: classifyChangeArea(normalizedPath),
652
- rootDirectory: describeRootDirectory(normalizedPath),
653
- theme: detectIntentTheme(normalizedPath, classifyChangeArea(normalizedPath))
654
- };
976
+ return 'ai_context_generation';
655
977
  }
656
978
 
657
- function groupEventsByIntent(events) {
979
+ function groupEventsByIntent(events, bootstrap) {
658
980
  if (!Array.isArray(events) || events.length === 0) {
659
981
  return [];
660
982
  }
661
983
 
662
- const groupedByArea = new Map();
984
+ const buckets = new Map();
663
985
 
664
986
  for (const event of events) {
665
- const descriptor = createEventDescriptor(event);
666
- const groupKey = `${descriptor.area}:${descriptor.rootDirectory}`;
667
-
668
- if (!groupedByArea.has(groupKey)) {
669
- groupedByArea.set(groupKey, {
670
- area: descriptor.area,
671
- rootDirectory: descriptor.rootDirectory,
672
- events: []
673
- });
987
+ const area = classifyChangeArea(event.file);
988
+ const featureKey = detectEventFeatureKey(event.file, bootstrap);
989
+ const key = `${area}:${featureKey}`;
990
+
991
+ if (!buckets.has(key)) {
992
+ buckets.set(key, []);
674
993
  }
675
994
 
676
- groupedByArea.get(groupKey).events.push(descriptor);
995
+ buckets.get(key).push(
996
+ Object.assign({}, event, {
997
+ area,
998
+ featureKey
999
+ })
1000
+ );
677
1001
  }
678
1002
 
679
- const mergedGroups = mergeCrossAreaIntentGroups(Array.from(groupedByArea.values()));
680
-
681
- return mergedGroups
1003
+ return Array.from(buckets.values())
682
1004
  .map((group) => interpretIntentGroup(group))
683
1005
  .filter(Boolean)
684
1006
  .sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
685
1007
  }
686
1008
 
687
- function buildProjectCapabilityHistory(projectRoot) {
688
- const snapshotTimestamp = new Date(0).toISOString();
689
- const projectFiles = scanProjectFiles(projectRoot, 2).filter((filePath) => scoreEvent(filePath) >= 2);
690
-
691
- if (projectFiles.length === 0) {
692
- return [];
693
- }
694
-
695
- const capabilityBuckets = new Map();
696
-
697
- for (const filePath of projectFiles) {
698
- const area = classifyChangeArea(filePath);
699
- const featureKey = detectIntentTheme(filePath, area);
700
-
701
- if (!capabilityBuckets.has(featureKey)) {
702
- capabilityBuckets.set(featureKey, []);
703
- }
704
-
705
- capabilityBuckets.get(featureKey).push(filePath);
706
- }
707
-
708
- return Array.from(capabilityBuckets.entries())
709
- .map(([featureKey, files]) => createCapabilitySnapshotEntry(featureKey, files.length, snapshotTimestamp))
710
- .filter(Boolean);
711
- }
712
-
713
- function mergeCrossAreaIntentGroups(groups) {
714
- if (groups.length < 2) {
715
- return groups;
716
- }
717
-
718
- const logicGroup = groups.find((group) => group.area === 'logic');
719
- const backendGroup = groups.find((group) => group.area === 'backend');
720
- const cliGroup = groups.find((group) => group.area === 'cli');
721
-
722
- if (logicGroup && backendGroup && groups.length <= 3) {
723
- return mergeSelectedGroups(groups, [logicGroup, backendGroup], 'system');
724
- }
725
-
726
- if (logicGroup && cliGroup && groups.length <= 3) {
727
- return mergeSelectedGroups(groups, [logicGroup, cliGroup], 'cli_system');
728
- }
729
-
730
- return groups;
731
- }
732
-
733
- function mergeSelectedGroups(groups, groupsToMerge, mergedArea) {
734
- const mergeSet = new Set(groupsToMerge);
735
- const remainingGroups = groups.filter((group) => !mergeSet.has(group));
736
- const mergedGroup = {
737
- area: mergedArea,
738
- rootDirectory: mergedArea,
739
- events: groupsToMerge.flatMap((group) => group.events)
740
- };
741
-
742
- remainingGroups.push(mergedGroup);
743
- return remainingGroups;
744
- }
745
-
746
1009
  function interpretIntentGroup(group) {
747
- if (!group || !Array.isArray(group.events) || group.events.length === 0) {
748
- return null;
749
- }
750
-
751
- const latestTimestamp = group.events.reduce((latest, event) => {
1010
+ const latestTimestamp = group.reduce((latest, event) => {
752
1011
  return new Date(event.timestamp) > new Date(latest) ? event.timestamp : latest;
753
- }, group.events[0].timestamp);
754
- const featureKey = determineFeatureKey(group);
755
- const featureMeta = getFeatureMeta(featureKey);
1012
+ }, group[0].timestamp || new Date().toISOString());
1013
+ const featureKey = group[0].featureKey;
1014
+ const area = group[0].area;
756
1015
  const type = determineGroupedUpdateType(group);
757
- const subject = describeIntentSubject(group, featureMeta.subject);
1016
+ const subject = describeIntentSubject(featureKey);
758
1017
 
759
1018
  return {
760
1019
  timestamp: latestTimestamp,
761
- scope: describeIntentScope(group),
1020
+ scope: area,
762
1021
  title: buildIntentTitle(type, subject),
763
1022
  type,
764
- impact: describeIntentImpact(type, featureKey, subject),
1023
+ impact: describeIntentImpact(type, featureKey),
765
1024
  feature_key: featureKey,
766
- feature_name: featureMeta.name,
767
- source: 'event'
768
- };
769
- }
770
-
771
- function determineFeatureKey(group) {
772
- const areas = new Set(group.events.map((event) => event.area));
773
-
774
- if (group.area === 'system' || (areas.has('logic') && areas.has('backend'))) {
775
- return 'context_delivery_system';
776
- }
777
-
778
- if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
779
- return 'cli_orchestration';
780
- }
781
-
782
- const themeCounts = new Map();
783
-
784
- for (const event of group.events) {
785
- themeCounts.set(event.theme, (themeCounts.get(event.theme) || 0) + 1);
786
- }
787
-
788
- return Array.from(themeCounts.entries()).sort((left, right) => {
789
- if (right[1] !== left[1]) {
790
- return right[1] - left[1];
791
- }
792
-
793
- return getFeaturePriority(right[0]) - getFeaturePriority(left[0]);
794
- })[0][0];
795
- }
796
-
797
- function getFeatureMeta(featureKey) {
798
- return FEATURE_CATALOG[featureKey] || FEATURE_CATALOG.project_workflow;
799
- }
800
-
801
- function getFeaturePriority(featureKey) {
802
- const priorities = {
803
- project_intelligence: 7,
804
- github_sync: 6,
805
- local_context_server: 5,
806
- change_tracking: 4,
807
- cli_workflow: 3,
808
- project_setup: 2,
809
- project_workflow: 1
1025
+ feature_name: FEATURE_CATALOG[featureKey] || subject
810
1026
  };
811
-
812
- return priorities[featureKey] || 0;
813
1027
  }
814
1028
 
815
1029
  function determineGroupedUpdateType(group) {
816
- const actions = new Set(group.events.map((event) => event.action));
817
- const fileCount = group.events.length;
1030
+ const hasAdd = group.some((event) => event.action === 'add');
1031
+ const hasDelete = group.some((event) => event.action === 'delete');
818
1032
 
819
- if (actions.has('add')) {
1033
+ if (hasAdd) {
820
1034
  return 'feature';
821
1035
  }
822
1036
 
823
- if (hasFixSignals(group.events)) {
1037
+ if (hasDelete) {
824
1038
  return 'fix';
825
1039
  }
826
1040
 
827
- if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
1041
+ if (group.length > 2) {
828
1042
  return 'refactor';
829
1043
  }
830
1044
 
831
1045
  return 'improvement';
832
1046
  }
833
1047
 
834
- function hasFixSignals(events) {
835
- return events.some((event) =>
836
- /(fix|bug|error|guard|validate|sanitize|safe|stabilize)/i.test(event.file)
837
- );
838
- }
839
-
840
- function describeIntentSubject(group, fallbackSubject) {
841
- const areas = new Set(group.events.map((event) => event.area));
842
-
843
- if (group.area === 'system' || (areas.has('logic') && areas.has('backend'))) {
844
- return 'AI context delivery system';
845
- }
846
-
847
- if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
848
- return 'CLI workflow';
849
- }
850
-
851
- if (group.area === 'backend' && group.events.length > 1) {
852
- return 'AI context delivery service';
853
- }
854
-
855
- return fallbackSubject || 'project workflow';
856
- }
857
-
858
- function describeIntentScope(group) {
859
- if (group.area === 'system') {
860
- return 'system';
861
- }
862
-
863
- if (group.area === 'cli_system') {
864
- return 'CLI';
865
- }
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
+ };
866
1063
 
867
- return group.area;
1064
+ return subjectMap[featureKey] || 'project workflow';
868
1065
  }
869
1066
 
870
1067
  function buildIntentTitle(type, subject) {
871
1068
  const verbs = {
872
1069
  feature: 'Expanded',
873
1070
  improvement: 'Improved',
874
- refactor: 'Refactored',
1071
+ refactor: 'Refined',
875
1072
  fix: 'Stabilized'
876
1073
  };
877
1074
 
878
1075
  return `${verbs[type] || 'Improved'} ${subject}`;
879
1076
  }
880
1077
 
881
- function describeIntentImpact(type, featureKey, subject) {
882
- const impactByFeature = {
883
- cli_workflow: 'Improves how developers initialize and manage AI context from the command line.',
884
- github_sync: 'Improves reliability of publishing AI-readable project context to GitHub.',
885
- project_intelligence: 'Improves how project progress is summarized for AI systems.',
886
- change_tracking: 'Improves how meaningful project evolution is detected without noise.',
887
- local_context_server: 'Improves how AI tools consume project context through local endpoints.',
888
- context_delivery_system: 'Improves reliability and structure of the end-to-end AI context delivery system.',
889
- cli_orchestration: 'Improves how CLI actions drive the project intelligence workflow.',
890
- project_setup: 'Improves first-run setup and configuration clarity for teams adopting AI context.',
891
- documentation: 'Improves onboarding and usage clarity for developers and AI collaborators.',
892
- package_configuration: 'Improves package installation and distribution behavior.',
893
- context_templates: 'Improves the default AI context generated for new projects.',
894
- project_workflow: 'Improves the overall project workflow for maintaining AI-readable context.'
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.'
895
1092
  };
896
1093
 
897
- if (type === 'feature') {
898
- return impactByFeature[featureKey]
899
- .replace(/^Improves /, 'Adds ')
900
- .replace(/^Improves how /, 'Adds ')
901
- .replace(/^Improves reliability of /, 'Adds ')
902
- .replace(/^Improves first-run setup and configuration clarity for teams adopting /, 'Adds ')
903
- .replace(/^Improves the default AI context generated for /, 'Adds ')
904
- .replace(/^Improves the overall project workflow for maintaining /, 'Adds ');
905
- }
906
-
907
1094
  if (type === 'fix') {
908
- return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
1095
+ return impactMap[featureKey].replace(/^Improves /, 'Resolves issues in ');
909
1096
  }
910
1097
 
911
- return impactByFeature[featureKey] || 'Improves the overall project workflow for maintaining AI-readable context.';
912
- }
1098
+ if (type === 'feature') {
1099
+ return impactMap[featureKey].replace(/^Improves /, 'Adds ');
1100
+ }
913
1101
 
914
- function interpretChange(event) {
915
- return groupEventsByIntent([event])[0] || null;
1102
+ return impactMap[featureKey] || 'Improves the overall project workflow.';
916
1103
  }
917
1104
 
918
1105
  function normalizeStoredUpdates(updates) {
@@ -920,7 +1107,11 @@ function normalizeStoredUpdates(updates) {
920
1107
  return [];
921
1108
  }
922
1109
 
923
- return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
1110
+ return dedupeRecentUpdates(
1111
+ updates
1112
+ .map((update) => normalizeStoredUpdate(update))
1113
+ .filter(Boolean)
1114
+ );
924
1115
  }
925
1116
 
926
1117
  function normalizeStoredUpdate(update) {
@@ -936,10 +1127,6 @@ function normalizeStoredUpdate(update) {
936
1127
  };
937
1128
  }
938
1129
 
939
- if (update.file && update.action && isMeaningfulEvent(update)) {
940
- return toStateUpdate(interpretChange(update));
941
- }
942
-
943
1130
  return null;
944
1131
  }
945
1132
 
@@ -961,118 +1148,71 @@ function normalizeStoredHistoryEntry(entry) {
961
1148
  }
962
1149
 
963
1150
  if (entry.title && entry.type && entry.impact) {
964
- const inferredFeature = inferFeatureFromEntry(entry);
965
- const featureKey = entry.feature_key || inferredFeature.featureKey;
966
- const normalizedType = normalizeUpdateType(entry.type);
967
- const subject = describeCanonicalSubject(featureKey);
968
-
969
1151
  return {
970
1152
  timestamp: entry.timestamp || new Date(0).toISOString(),
971
- scope: entry.scope || inferredFeature.scope,
972
- title: buildIntentTitle(normalizedType, subject),
973
- type: normalizedType,
974
- impact: describeIntentImpact(normalizedType, featureKey, subject),
975
- feature_key: featureKey,
976
- feature_name: entry.feature_name || getFeatureMeta(featureKey).name,
977
- source: entry.source || 'history'
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'
978
1159
  };
979
1160
  }
980
1161
 
981
- if (entry.file && entry.action && isMeaningfulEvent(entry)) {
982
- return interpretChange(entry);
983
- }
984
-
985
1162
  return null;
986
1163
  }
987
1164
 
988
- function inferFeatureFromEntry(entry) {
989
- const combinedText = `${entry.title || ''} ${entry.impact || ''}`.toLowerCase();
1165
+ function inferFeatureKeyFromText(title, impact) {
1166
+ const combinedText = `${title || ''} ${impact || ''}`.toLowerCase();
990
1167
 
991
- if (combinedText.includes('github') || combinedText.includes('sync')) {
992
- return {
993
- featureKey: 'github_sync',
994
- featureName: getFeatureMeta('github_sync').name,
995
- scope: 'logic'
996
- };
1168
+ if (combinedText.includes('jwt') || combinedText.includes('auth')) {
1169
+ return 'auth';
997
1170
  }
998
1171
 
999
- if (combinedText.includes('state') || combinedText.includes('intelligence')) {
1000
- return {
1001
- featureKey: 'project_intelligence',
1002
- featureName: getFeatureMeta('project_intelligence').name,
1003
- scope: 'logic'
1004
- };
1172
+ if (combinedText.includes('mongo') || combinedText.includes('mongoose') || combinedText.includes('persist')) {
1173
+ return 'persistence';
1005
1174
  }
1006
1175
 
1007
- if (combinedText.includes('watch') || combinedText.includes('change tracking')) {
1008
- return {
1009
- featureKey: 'change_tracking',
1010
- featureName: getFeatureMeta('change_tracking').name,
1011
- scope: 'logic'
1012
- };
1176
+ if (combinedText.includes('real-time') || combinedText.includes('socket')) {
1177
+ return 'realtime';
1013
1178
  }
1014
1179
 
1015
- if (
1016
- combinedText.includes('server') ||
1017
- combinedText.includes('backend') ||
1018
- combinedText.includes('endpoint') ||
1019
- combinedText.includes('delivery')
1020
- ) {
1021
- return {
1022
- featureKey: 'local_context_server',
1023
- featureName: getFeatureMeta('local_context_server').name,
1024
- scope: 'backend'
1025
- };
1180
+ if (combinedText.includes('api') || combinedText.includes('route')) {
1181
+ return 'rest_api';
1026
1182
  }
1027
1183
 
1028
- if (combinedText.includes('cli') || combinedText.includes('command line')) {
1029
- return {
1030
- featureKey: 'cli_workflow',
1031
- featureName: getFeatureMeta('cli_workflow').name,
1032
- scope: 'cli'
1033
- };
1184
+ if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
1185
+ return 'public_sync';
1034
1186
  }
1035
1187
 
1036
- if (combinedText.includes('documentation') || combinedText.includes('onboarding')) {
1037
- return {
1038
- featureKey: 'documentation',
1039
- featureName: getFeatureMeta('documentation').name,
1040
- scope: 'documentation'
1041
- };
1188
+ if (combinedText.includes('watch') || combinedText.includes('change')) {
1189
+ return 'change_tracking';
1042
1190
  }
1043
1191
 
1044
- if (combinedText.includes('package') || combinedText.includes('dependency')) {
1045
- return {
1046
- featureKey: 'package_configuration',
1047
- featureName: getFeatureMeta('package_configuration').name,
1048
- scope: 'dependencies'
1049
- };
1192
+ if (combinedText.includes('command line') || combinedText.includes('cli')) {
1193
+ return 'cli_automation';
1050
1194
  }
1051
1195
 
1052
- return {
1053
- featureKey: 'project_workflow',
1054
- featureName: getFeatureMeta('project_workflow').name,
1055
- scope: 'project'
1056
- };
1196
+ if (combinedText.includes('http') || combinedText.includes('context delivery')) {
1197
+ return 'local_context_server';
1198
+ }
1199
+
1200
+ return 'ai_context_generation';
1057
1201
  }
1058
1202
 
1059
1203
  function normalizeUpdateType(type) {
1060
- if (type === 'removal') {
1061
- return 'refactor';
1062
- }
1063
-
1064
1204
  if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
1065
1205
  return type;
1066
1206
  }
1067
1207
 
1208
+ if (type === 'removal') {
1209
+ return 'fix';
1210
+ }
1211
+
1068
1212
  return 'improvement';
1069
1213
  }
1070
1214
 
1071
1215
  function toStateUpdate(update) {
1072
- if (!update) {
1073
- return null;
1074
- }
1075
-
1076
1216
  return {
1077
1217
  title: update.title,
1078
1218
  type: normalizeUpdateType(update.type),
@@ -1081,17 +1221,17 @@ function toStateUpdate(update) {
1081
1221
  }
1082
1222
 
1083
1223
  function dedupeRecentUpdates(updates) {
1084
- const seenUpdates = new Set();
1224
+ const seen = new Set();
1085
1225
  const result = [];
1086
1226
 
1087
1227
  for (const update of updates.filter(Boolean)) {
1088
1228
  const key = `${update.title}::${update.type}::${update.impact}`;
1089
1229
 
1090
- if (seenUpdates.has(key)) {
1230
+ if (seen.has(key)) {
1091
1231
  continue;
1092
1232
  }
1093
1233
 
1094
- seenUpdates.add(key);
1234
+ seen.add(key);
1095
1235
  result.push(update);
1096
1236
  }
1097
1237
 
@@ -1099,190 +1239,81 @@ function dedupeRecentUpdates(updates) {
1099
1239
  }
1100
1240
 
1101
1241
  function dedupeHistoryEntries(entries) {
1102
- const seenEntries = new Set();
1242
+ const seen = new Set();
1103
1243
  const result = [];
1104
1244
 
1105
1245
  for (const entry of entries.filter(Boolean)) {
1106
1246
  const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
1107
1247
 
1108
- if (seenEntries.has(key)) {
1248
+ if (seen.has(key)) {
1109
1249
  continue;
1110
1250
  }
1111
1251
 
1112
- seenEntries.add(key);
1252
+ seen.add(key);
1113
1253
  result.push(entry);
1114
1254
  }
1115
1255
 
1116
1256
  return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
1117
1257
  }
1118
1258
 
1119
- function promoteFeatures(history) {
1120
- if (!Array.isArray(history) || history.length === 0) {
1259
+ function promoteFeatures(historyEntries) {
1260
+ if (!Array.isArray(historyEntries) || historyEntries.length === 0) {
1121
1261
  return [];
1122
1262
  }
1123
1263
 
1124
- const featureStats = new Map();
1125
-
1126
- for (const entry of history) {
1127
- const featureKey = entry.feature_key || inferFeatureFromEntry(entry).featureKey;
1128
- const featureMeta = getFeatureMeta(featureKey);
1129
- const existing = featureStats.get(featureKey) || {
1130
- featureKey,
1131
- featureName: featureMeta.name,
1132
- count: 0,
1133
- score: 0,
1134
- lastTimestamp: new Date(0).toISOString()
1135
- };
1136
-
1137
- existing.count += 1;
1138
- existing.score += scoreFeatureEntry(entry);
1139
- if (new Date(entry.timestamp) > new Date(existing.lastTimestamp)) {
1140
- existing.lastTimestamp = entry.timestamp;
1141
- }
1142
-
1143
- featureStats.set(featureKey, existing);
1144
- }
1145
-
1146
- const rankedFeatures = Array.from(featureStats.values()).sort((left, right) => {
1147
- if (right.count !== left.count) {
1148
- return right.count - left.count;
1149
- }
1150
-
1151
- if (right.score !== left.score) {
1152
- return right.score - left.score;
1153
- }
1154
-
1155
- if (getFeaturePriority(right.featureKey) !== getFeaturePriority(left.featureKey)) {
1156
- return getFeaturePriority(right.featureKey) - getFeaturePriority(left.featureKey);
1157
- }
1158
-
1159
- return new Date(right.lastTimestamp) - new Date(left.lastTimestamp);
1160
- });
1161
-
1162
- const promoted = [];
1163
- const seenFeatureNames = new Set();
1164
-
1165
- for (const feature of rankedFeatures.filter((item) => item.count >= 3)) {
1166
- if (LOW_VALUE_FEATURE_KEYS.has(feature.featureKey)) {
1167
- continue;
1168
- }
1169
-
1170
- promoted.push(feature.featureName);
1171
- seenFeatureNames.add(feature.featureName);
1172
- }
1173
-
1174
- for (const feature of rankedFeatures) {
1175
- if (promoted.length >= MAX_KEY_FEATURES) {
1176
- break;
1177
- }
1178
-
1179
- if (LOW_VALUE_FEATURE_KEYS.has(feature.featureKey)) {
1180
- continue;
1181
- }
1182
-
1183
- if (seenFeatureNames.has(feature.featureName)) {
1184
- continue;
1185
- }
1186
-
1187
- promoted.push(feature.featureName);
1188
- seenFeatureNames.add(feature.featureName);
1189
- }
1264
+ const scores = new Map();
1190
1265
 
1191
- for (const feature of rankedFeatures) {
1192
- if (promoted.length >= MAX_KEY_FEATURES) {
1193
- break;
1194
- }
1266
+ for (const entry of historyEntries) {
1267
+ const featureName = entry.feature_name || FEATURE_CATALOG[entry.feature_key];
1195
1268
 
1196
- if (seenFeatureNames.has(feature.featureName)) {
1269
+ if (!featureName) {
1197
1270
  continue;
1198
1271
  }
1199
1272
 
1200
- promoted.push(feature.featureName);
1201
- seenFeatureNames.add(feature.featureName);
1273
+ scores.set(featureName, (scores.get(featureName) || 0) + scoreHistoryEntry(entry));
1202
1274
  }
1203
1275
 
1204
- return promoted.slice(0, MAX_KEY_FEATURES);
1276
+ return Array.from(scores.entries())
1277
+ .sort((left, right) => right[1] - left[1])
1278
+ .map(([featureName]) => featureName)
1279
+ .slice(0, MAX_KEY_FEATURES);
1205
1280
  }
1206
1281
 
1207
- function scoreFeatureEntry(entry) {
1208
- const typeWeights = {
1282
+ function scoreHistoryEntry(entry) {
1283
+ const weights = {
1209
1284
  feature: 4,
1210
1285
  refactor: 3,
1211
1286
  improvement: 2,
1212
1287
  fix: 2
1213
1288
  };
1214
1289
 
1215
- return typeWeights[normalizeUpdateType(entry.type)] || 1;
1216
- }
1217
-
1218
- function describeCanonicalSubject(featureKey) {
1219
- return getFeatureMeta(featureKey).subject;
1220
- }
1221
-
1222
- function createCapabilitySnapshotEntry(featureKey, fileCount, timestamp) {
1223
- const subject = describeCanonicalSubject(featureKey);
1224
- const type = fileCount > 2 ? 'refactor' : 'improvement';
1225
-
1226
- return {
1227
- timestamp,
1228
- scope: inferScopeFromFeature(featureKey),
1229
- title: buildIntentTitle(type, subject),
1230
- type,
1231
- impact: describeIntentImpact(type, featureKey, subject),
1232
- feature_key: featureKey,
1233
- feature_name: getFeatureMeta(featureKey).name,
1234
- source: 'project_snapshot'
1235
- };
1290
+ return weights[normalizeUpdateType(entry.type)] || 1;
1236
1291
  }
1237
1292
 
1238
- function inferScopeFromFeature(featureKey) {
1239
- if (
1240
- featureKey === 'github_sync' ||
1241
- featureKey === 'project_intelligence' ||
1242
- featureKey === 'change_tracking' ||
1243
- featureKey === 'project_setup'
1244
- ) {
1245
- return 'logic';
1246
- }
1247
-
1248
- if (featureKey === 'cli_workflow' || featureKey === 'cli_orchestration') {
1249
- return 'cli';
1250
- }
1251
-
1252
- if (featureKey === 'local_context_server' || featureKey === 'context_delivery_system') {
1253
- return 'backend';
1254
- }
1255
-
1256
- if (featureKey === 'package_configuration') {
1257
- return 'dependencies';
1258
- }
1259
-
1260
- if (featureKey === 'documentation') {
1261
- return 'documentation';
1262
- }
1263
-
1264
- return 'project';
1293
+ function mergeKeyFeatures(primaryFeatures, promotedFeatures) {
1294
+ return uniqueNonEmpty([].concat(primaryFeatures || [], promotedFeatures || [])).slice(0, MAX_KEY_FEATURES);
1265
1295
  }
1266
1296
 
1267
- function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
1297
+ function deriveKnownIssues(projectRoot, bootstrap) {
1268
1298
  const knownIssues = [];
1269
1299
 
1270
1300
  if (!hasTestIndicators(projectRoot)) {
1271
1301
  knownIssues.push('No automated test suite is detected yet.');
1272
1302
  }
1273
1303
 
1274
- if (!techStack.framework) {
1275
- knownIssues.push('No common application framework dependency is currently detected.');
1304
+ if (bootstrap.implementationDetails.length === 0) {
1305
+ knownIssues.push('Project structure exposes limited implementation signals, so deeper architecture details may still be missing.');
1276
1306
  }
1277
1307
 
1278
- if (keyFeatures.length < 2) {
1279
- knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
1308
+ if (!bootstrap.techStack.framework && bootstrap.techStack.language === 'Node.js') {
1309
+ knownIssues.push('No common Node.js application framework dependency is currently detected.');
1280
1310
  }
1281
1311
 
1282
1312
  return knownIssues;
1283
1313
  }
1284
1314
 
1285
1315
  function hasTestIndicators(projectRoot) {
1316
+ const resolvedRoot = resolveProjectRoot(projectRoot);
1286
1317
  const testPaths = [
1287
1318
  'test',
1288
1319
  'tests',
@@ -1293,11 +1324,11 @@ function hasTestIndicators(projectRoot) {
1293
1324
  'jest.config.mjs'
1294
1325
  ];
1295
1326
 
1296
- return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
1327
+ return testPaths.some((relativePath) => fs.existsSync(path.join(resolvedRoot, relativePath)));
1297
1328
  }
1298
1329
 
1299
- function determineCurrentStage(keyFeatures, historyEntries) {
1300
- if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
1330
+ function determineCurrentStage(keyFeatures, historyEntries, implementationDetails) {
1331
+ if (keyFeatures.length >= 4 && implementationDetails.length >= 3) {
1301
1332
  return 'Production-ready';
1302
1333
  }
1303
1334
 
@@ -1308,86 +1339,69 @@ function determineCurrentStage(keyFeatures, historyEntries) {
1308
1339
  return 'Early development';
1309
1340
  }
1310
1341
 
1311
- function generateAiSummary(state, historyEntries) {
1312
- const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
1313
- const projectType = featureSignals.has('cli_workflow') || featureSignals.has('cli_orchestration')
1314
- ? 'CLI tool'
1315
- : 'project system';
1316
- let coreCapability = 'maintains an AI-readable view of project progress';
1317
- let uniqueValue = 'so AI collaborators can understand the current project state immediately';
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);
1318
1349
 
1319
- if (featureSignals.has('project_intelligence') && featureSignals.has('change_tracking')) {
1320
- coreCapability = 'turns meaningful project activity into AI-readable context';
1321
- } else if (featureSignals.has('project_intelligence')) {
1322
- coreCapability = 'converts development work into AI-readable project state';
1323
- } else if (featureSignals.has('local_context_server')) {
1324
- coreCapability = 'delivers AI-readable project context through clear endpoints';
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.`;
1325
1352
  }
1326
1353
 
1327
1354
  if (
1328
- featureSignals.has('github_sync') ||
1329
- featureSignals.has('context_delivery_system') ||
1330
- featureSignals.has('cli_orchestration')
1355
+ features.includes(FEATURE_CATALOG.ai_context_generation) &&
1356
+ features.includes(FEATURE_CATALOG.public_sync)
1331
1357
  ) {
1332
- uniqueValue = 'and enables public AI collaboration through GitHub-synced context endpoints';
1333
- } else if (featureSignals.has('local_context_server')) {
1334
- uniqueValue = 'and keeps current context available through local AI endpoints';
1358
+ return `${normalizedType} that generates AI-readable project context, tracks meaningful changes, and can publish public project state through GitHub.`;
1335
1359
  }
1336
1360
 
1337
- return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
1338
- }
1339
-
1340
- function collectFeatureSignals(historyEntries, keyFeatures) {
1341
- const featureSignals = new Set();
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
+ }
1342
1367
 
1343
- for (const entry of historyEntries) {
1344
- if (entry.feature_key) {
1345
- featureSignals.add(entry.feature_key);
1346
- }
1368
+ if (features.length >= 2) {
1369
+ return `${normalizedType} focused on ${features.slice(0, 2).join(' and ').toLowerCase()}.`;
1347
1370
  }
1348
1371
 
1349
- for (const featureName of keyFeatures || []) {
1350
- for (const [featureKey, featureMeta] of Object.entries(FEATURE_CATALOG)) {
1351
- if (featureMeta.name === featureName) {
1352
- featureSignals.add(featureKey);
1353
- }
1354
- }
1372
+ if (details.length > 0) {
1373
+ return `${normalizedType} built around ${details[0].toLowerCase()}.`;
1355
1374
  }
1356
1375
 
1357
- return featureSignals;
1376
+ return `${normalizedType} with detectable project structure and implementation patterns.`;
1358
1377
  }
1359
1378
 
1360
- function generateNextSteps(state, historyEntries) {
1379
+ function generateNextSteps(state, bootstrap, historyEntries) {
1361
1380
  const nextSteps = [];
1362
- const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
1363
-
1364
- if (state.key_features.length === 0) {
1365
- nextSteps.push('Capture a few meaningful project milestones so stable AI-visible features can emerge from real development history.');
1366
- }
1367
1381
 
1368
1382
  if (state.known_issues.includes('No automated test suite is detected yet.')) {
1369
- nextSteps.push('Add automated tests for the intelligence engine, watcher, and GitHub sync workflow.');
1370
- }
1371
-
1372
- if (!featureSignals.has('project_intelligence')) {
1373
- nextSteps.push('Strengthen the intelligence engine so more project-level capabilities are captured automatically.');
1383
+ nextSteps.push('Add automated tests that cover the main application flow and critical integration points.');
1374
1384
  }
1375
1385
 
1376
- if (!featureSignals.has('local_context_server')) {
1377
- nextSteps.push('Expand context delivery coverage so AI consumers can reliably read current project state.');
1386
+ if (bootstrap.implementationDetails.length < 2) {
1387
+ nextSteps.push('Strengthen the project structure so major implementation patterns are easier to detect automatically.');
1378
1388
  }
1379
1389
 
1380
- if (!featureSignals.has('github_sync')) {
1381
- nextSteps.push('Validate public sync behavior so AI tools can safely consume the latest project context from GitHub.');
1390
+ if (historyEntries.length === 0) {
1391
+ nextSteps.push('Capture the next meaningful project update so recent evolution is reflected alongside the bootstrap analysis.');
1382
1392
  }
1383
1393
 
1384
1394
  if (state.current_stage === 'Early development') {
1385
- nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
1395
+ nextSteps.push('Ship the next core capability to move the project from initial structure into a functional prototype.');
1386
1396
  }
1387
1397
 
1388
1398
  return Array.from(new Set(nextSteps)).slice(0, 4);
1389
1399
  }
1390
1400
 
1401
+ function uniqueNonEmpty(values) {
1402
+ return Array.from(new Set((values || []).filter(Boolean)));
1403
+ }
1404
+
1391
1405
  function capitalize(value) {
1392
1406
  if (!value) {
1393
1407
  return '';
@@ -1459,6 +1473,7 @@ function createDebouncedStateUpdater(projectRoot, options) {
1459
1473
  module.exports = {
1460
1474
  CONTEXT_DIR_NAME,
1461
1475
  DEFAULT_CONFIG,
1476
+ bootstrapProjectAnalysis,
1462
1477
  createDebouncedStateUpdater,
1463
1478
  createDefaultChangelog,
1464
1479
  createDefaultState,
@@ -1466,7 +1481,6 @@ module.exports = {
1466
1481
  ensureContextDirectory,
1467
1482
  getContextPaths,
1468
1483
  groupEventsByIntent,
1469
- interpretChange,
1470
1484
  loadRuntimeConfig,
1471
1485
  promoteFeatures,
1472
1486
  readJsonFile,
@@ -1477,4 +1491,4 @@ module.exports = {
1477
1491
  updateProjectState,
1478
1492
  writeJsonAtomic,
1479
1493
  writeTextAtomic
1480
- };
1494
+ };