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