aibridge-context 1.4.1 → 1.5.2

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,549 @@ 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() {
358
- return {
359
- entries: []
360
- };
361
- }
419
+ function mergePreferredArray(preferredValue, fallbackValue) {
420
+ if (Array.isArray(preferredValue) && preferredValue.length > 0) {
421
+ return preferredValue;
422
+ }
362
423
 
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
- }
424
+ if (Array.isArray(fallbackValue) && fallbackValue.length > 0) {
425
+ return fallbackValue;
426
+ }
369
427
 
370
- async function loadRuntimeConfig(projectRoot) {
371
- const { configFile } = getContextPaths(projectRoot);
372
- const userConfig = await readJsonFile(configFile, {});
373
- return deepMerge(DEFAULT_CONFIG, userConfig);
428
+ return [];
374
429
  }
375
430
 
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 || {});
431
+ function mergePreferredObject(preferredValue, fallbackValue) {
432
+ const preferredObject = isObject(preferredValue) ? preferredValue : {};
433
+ const fallbackObject = isObject(fallbackValue) ? fallbackValue : {};
381
434
 
382
- await writeJsonAtomic(configFile, nextConfig);
383
- return nextConfig;
435
+ return Object.assign({}, fallbackObject, preferredObject);
384
436
  }
385
437
 
386
- async function updateProjectState(projectRoot, changeEvent, options) {
387
- const settings = Object.assign(
438
+ function composeStateFromAnalysis(existingState, metadata, bootstrap, overrides) {
439
+ const baseState = isObject(existingState) ? existingState : {};
440
+ const nextState = Object.assign(
388
441
  {
389
- logger: null,
390
- syncCallback: null
442
+ project: metadata.project,
443
+ version: metadata.version,
444
+ last_updated: baseState.last_updated || new Date(0).toISOString(),
445
+ ai_summary: '',
446
+ tech_stack: mergePreferredObject(bootstrap.techStack, baseState.tech_stack),
447
+ architecture_patterns: mergePreferredArray(
448
+ bootstrap.architecturePatterns,
449
+ baseState.architecture_patterns
450
+ ),
451
+ implementation_details: mergePreferredArray(
452
+ bootstrap.implementationDetails,
453
+ baseState.implementation_details
454
+ ),
455
+ current_stage: '',
456
+ recent_updates: Array.isArray(baseState.recent_updates) ? baseState.recent_updates : [],
457
+ key_features: mergePreferredArray(bootstrap.keyFeatures, baseState.key_features),
458
+ known_issues: [],
459
+ next_steps: []
391
460
  },
392
- options
393
- );
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()
461
+ overrides || {}
401
462
  );
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))
463
+
464
+ return nextState;
465
+ }
466
+
467
+ function bootstrapProjectAnalysis(projectRoot) {
468
+ const resolvedRoot = resolveProjectRoot(projectRoot);
469
+ const metadata = detectProjectMetadata(resolvedRoot);
470
+ const rootPackage = readRootPackageJson(resolvedRoot);
471
+ const techStack = metadata.techStack;
472
+ const analysisInputs = collectAnalysisInputs(resolvedRoot);
473
+ const implementationSignals = detectImplementationSignals(analysisInputs, rootPackage, techStack);
474
+ const architecturePatterns = buildArchitecturePatterns(
475
+ implementationSignals,
476
+ analysisInputs,
477
+ techStack,
478
+ rootPackage
407
479
  );
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: []
480
+ const implementationDetails = buildImplementationDetails(implementationSignals, techStack);
481
+ const keyFeatures = buildKeyFeatures(implementationSignals, techStack);
482
+ const projectType = determineProjectType(implementationSignals, techStack, rootPackage);
483
+
484
+ return {
485
+ projectType,
486
+ techStack,
487
+ architecturePatterns,
488
+ implementationDetails,
489
+ keyFeatures,
490
+ signals: implementationSignals
431
491
  };
492
+ }
432
493
 
433
- nextState.ai_summary = generateAiSummary(nextState, historyEntries);
434
- nextState.next_steps = generateNextSteps(nextState, historyEntries);
494
+ function collectAnalysisInputs(projectRoot) {
495
+ const resolvedRoot = resolveProjectRoot(projectRoot);
496
+ const selectedFiles = new Set();
435
497
 
436
- await writeJsonAtomic(contextPaths.stateFile, nextState);
437
- await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
498
+ for (const relativeFile of ANALYSIS_ROOT_FILES) {
499
+ const absoluteFile = path.join(resolvedRoot, relativeFile);
438
500
 
439
- if (logger) {
440
- logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
501
+ if (fs.existsSync(absoluteFile) && isInsideProjectRoot(resolvedRoot, absoluteFile)) {
502
+ selectedFiles.add(relativeFile);
503
+ }
441
504
  }
442
505
 
443
- if (typeof settings.syncCallback === 'function') {
444
- await settings.syncCallback();
506
+ const discoveredFiles = scanProjectFiles(resolvedRoot, 4, {
507
+ includeDirectories: ANALYSIS_DIRECTORIES
508
+ });
509
+
510
+ for (const relativeFile of discoveredFiles) {
511
+ selectedFiles.add(relativeFile);
445
512
  }
446
513
 
447
- return nextState;
514
+ return Array.from(selectedFiles)
515
+ .sort()
516
+ .map((relativeFile) => {
517
+ const absoluteFile = path.join(resolvedRoot, relativeFile);
518
+
519
+ try {
520
+ const content = fs.readFileSync(absoluteFile, 'utf8');
521
+ return {
522
+ path: relativeFile,
523
+ content: content.slice(0, 50000)
524
+ };
525
+ } catch (error) {
526
+ return null;
527
+ }
528
+ })
529
+ .filter(Boolean);
448
530
  }
449
531
 
450
- function determineUpdateTimestamp(events) {
451
- if (!Array.isArray(events) || events.length === 0) {
452
- return new Date().toISOString();
453
- }
532
+ function detectImplementationSignals(analysisInputs, rootPackage, techStack) {
533
+ const dependencyNames = new Set([
534
+ ...Object.keys((rootPackage && rootPackage.dependencies) || {}),
535
+ ...Object.keys((rootPackage && rootPackage.devDependencies) || {})
536
+ ]);
537
+ const runtimeInputs = analysisInputs.filter((input) => isRuntimeImplementationFile(input.path));
538
+ const automationInputs = analysisInputs.filter((input) => isAutomationImplementationFile(input.path));
539
+ const runtimeContent = runtimeInputs.map((input) => input.content).join('\n');
540
+ const automationContent = automationInputs.map((input) => input.content).join('\n');
541
+ const allContent = analysisInputs.map((input) => input.content).join('\n');
542
+ const hasDirectory = (directoryName) =>
543
+ analysisInputs.some((input) => normalizeProjectPath(input.path).startsWith(`${directoryName}/`));
544
+ const hasRuntimePattern = (pattern) => pattern.test(runtimeContent);
545
+ const hasAutomationPattern = (pattern) => pattern.test(automationContent);
546
+ const hasAnyPattern = (pattern) => pattern.test(allContent);
454
547
 
455
- const latestEvent = events[events.length - 1];
456
- return latestEvent.timestamp || new Date().toISOString();
548
+ return {
549
+ hasPackageJson: Boolean(rootPackage),
550
+ hasCliEntry:
551
+ Boolean(rootPackage && rootPackage.bin && Object.keys(rootPackage.bin).length > 0) ||
552
+ hasAutomationPattern(/^#!\/usr\/bin\/env node/m) ||
553
+ hasAutomationPattern(/\bprocess\.argv\b/),
554
+ hasExpress:
555
+ dependencyNames.has('express') ||
556
+ hasRuntimePattern(/\brequire\(['"]express['"]\)/) ||
557
+ hasRuntimePattern(/\bfrom ['"]express['"]/) ||
558
+ hasRuntimePattern(/\bexpress\(\)/),
559
+ hasNext: dependencyNames.has('next'),
560
+ hasReact: dependencyNames.has('react'),
561
+ hasFastify: dependencyNames.has('fastify'),
562
+ hasKoa: dependencyNames.has('koa'),
563
+ hasRestRoutes: hasRuntimePattern(/\b(router|app)\.(get|post|put|patch|delete)\s*\(/),
564
+ hasMiddleware: hasRuntimePattern(/\bapp\.use\s*\(/) || hasDirectory('middleware'),
565
+ hasJwt:
566
+ dependencyNames.has('jsonwebtoken') ||
567
+ hasRuntimePattern(/\bjwt\.(sign|verify)\s*\(/) ||
568
+ hasRuntimePattern(/\brequire\(['"]jsonwebtoken['"]\)/),
569
+ hasMongoose:
570
+ dependencyNames.has('mongoose') ||
571
+ hasRuntimePattern(/\bmongoose\.connect\s*\(/) ||
572
+ hasRuntimePattern(/\brequire\(['"]mongoose['"]\)/),
573
+ hasSocketIO:
574
+ dependencyNames.has('socket.io') ||
575
+ hasRuntimePattern(/\bsocket\.io\b/) ||
576
+ hasRuntimePattern(/\brequire\(['"]socket\.io['"]\)/),
577
+ hasAxiosOrFetch:
578
+ dependencyNames.has('axios') ||
579
+ hasRuntimePattern(/\baxios\./) ||
580
+ hasRuntimePattern(/\bfetch\s*\(/),
581
+ hasWatcher:
582
+ dependencyNames.has('chokidar') ||
583
+ hasAutomationPattern(/\bchokidar\.watch\s*\(/) ||
584
+ hasAutomationPattern(/\bfs\.watch\s*\(/),
585
+ hasGitAutomation:
586
+ hasAutomationPattern(/\bgit\s+(add|commit|push)\b/) ||
587
+ hasAutomationPattern(/\b(syncContextToGit|linkGithubRepository)\b/),
588
+ hasAiContextArtifacts:
589
+ hasAutomationPattern(/\bstate\.json\b/) ||
590
+ hasAutomationPattern(/\bbrain\.txt\b/) ||
591
+ hasAutomationPattern(/\bcontext\.md\b/) ||
592
+ hasAutomationPattern(/\bchangelog\.json\b/),
593
+ hasControllers: hasDirectory('controllers'),
594
+ hasServices: hasDirectory('services'),
595
+ hasRoutes: hasDirectory('routes'),
596
+ hasServerDirectory: hasDirectory('server'),
597
+ hasConfigDirectory: hasDirectory('config'),
598
+ hasTypeScriptConfig: analysisInputs.some((input) => input.path === 'tsconfig.json'),
599
+ hasPythonRequirements: analysisInputs.some((input) => input.path === 'requirements.txt'),
600
+ hasNodeRuntime: techStack.language === 'Node.js',
601
+ hasPythonRuntime: techStack.language === 'Python',
602
+ hasApplicationEntries: runtimeInputs.length > 0,
603
+ hasAnyContent: hasAnyPattern(/\S/)
604
+ };
457
605
  }
458
606
 
459
- function normalizeProjectPath(filePath) {
460
- return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
607
+ function isRuntimeImplementationFile(relativePath) {
608
+ const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
609
+
610
+ return (
611
+ normalizedPath.startsWith('routes/') ||
612
+ normalizedPath.startsWith('server/') ||
613
+ normalizedPath.startsWith('controllers/') ||
614
+ normalizedPath.startsWith('services/') ||
615
+ normalizedPath.startsWith('middleware/') ||
616
+ normalizedPath.startsWith('config/') ||
617
+ normalizedPath.startsWith('src/') ||
618
+ normalizedPath === 'app.js' ||
619
+ normalizedPath === 'app.ts' ||
620
+ normalizedPath === 'index.js' ||
621
+ normalizedPath === 'index.ts' ||
622
+ normalizedPath === 'main.py'
623
+ );
461
624
  }
462
625
 
463
- function shouldIgnoreProjectFile(filePath) {
464
- const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
626
+ function isAutomationImplementationFile(relativePath) {
627
+ const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
465
628
 
466
- if (!normalizedPath) {
467
- return false;
629
+ return (
630
+ normalizedPath.startsWith('core/') ||
631
+ normalizedPath.startsWith('bin/') ||
632
+ normalizedPath === 'package.json' ||
633
+ normalizedPath === 'index.js' ||
634
+ normalizedPath === 'index.ts'
635
+ );
636
+ }
637
+
638
+ function buildArchitecturePatterns(signals, analysisInputs, techStack, rootPackage) {
639
+ const patterns = [];
640
+
641
+ if (signals.hasCliEntry) {
642
+ patterns.push('Command-line automation workflow');
468
643
  }
469
644
 
470
- const segments = normalizedPath.split('/');
471
- const baseName = segments[segments.length - 1];
645
+ if (signals.hasWatcher) {
646
+ patterns.push('Event-driven file watching pipeline');
647
+ }
472
648
 
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;
649
+ if (signals.hasExpress && signals.hasRestRoutes) {
650
+ patterns.push('REST API architecture');
481
651
  }
482
652
 
483
- if (baseName.startsWith('.start')) {
484
- return true;
653
+ if (signals.hasMiddleware) {
654
+ patterns.push('Middleware-driven request pipeline');
655
+ }
656
+
657
+ if (signals.hasControllers && signals.hasServices) {
658
+ patterns.push('Layered controller-service architecture');
659
+ }
660
+
661
+ if (signals.hasServerDirectory && signals.hasRoutes) {
662
+ patterns.push('Separated server bootstrap and route handling');
663
+ }
664
+
665
+ if (signals.hasConfigDirectory) {
666
+ patterns.push('Centralized configuration layer');
667
+ }
668
+
669
+ if (signals.hasSocketIO) {
670
+ patterns.push('Real-time event architecture');
671
+ }
672
+
673
+ if (signals.hasNext) {
674
+ patterns.push('Framework-driven web application structure');
675
+ } else if (signals.hasReact) {
676
+ patterns.push('Component-based frontend architecture');
485
677
  }
486
678
 
487
679
  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)
680
+ signals.hasAiContextArtifacts &&
681
+ signals.hasExpress &&
682
+ analysisInputs.some((input) => /\bstate\.json\b|\bbrain\.txt\b|\bcontext\.md\b/.test(input.content))
497
683
  ) {
498
- return true;
684
+ patterns.push('Structured AI context delivery workflow');
499
685
  }
500
686
 
501
- return false;
687
+ if (rootPackage && Array.isArray(rootPackage.keywords) && rootPackage.keywords.includes('cli')) {
688
+ patterns.push('Package-distributed CLI architecture');
689
+ }
690
+
691
+ return uniqueNonEmpty(patterns).slice(0, 6);
502
692
  }
503
693
 
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;
694
+ function buildImplementationDetails(signals, techStack) {
695
+ const details = [];
509
696
 
510
- if (shouldIgnoreProjectFile(normalizedPath)) {
511
- return -5;
697
+ if (signals.hasJwt) {
698
+ details.push('JWT-based authentication system');
512
699
  }
513
700
 
514
- if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
515
- score += 3;
516
- }
701
+ if (signals.hasMongoose) {
702
+ details.push('MongoDB integration through Mongoose ORM');
703
+ }
704
+
705
+ if (signals.hasExpress && signals.hasRestRoutes) {
706
+ details.push('REST API architecture using Express routing');
707
+ }
708
+
709
+ if (signals.hasMiddleware) {
710
+ details.push('Express middleware pipeline for request handling');
711
+ }
712
+
713
+ if (signals.hasSocketIO) {
714
+ details.push('Socket.IO-based real-time communication');
715
+ }
716
+
717
+ if (signals.hasAxiosOrFetch) {
718
+ details.push('External API integration for outbound service calls');
719
+ }
720
+
721
+ if (signals.hasWatcher) {
722
+ details.push('File system monitoring for automatic project state updates');
723
+ }
724
+
725
+ if (signals.hasAiContextArtifacts) {
726
+ details.push('Structured AI context generation across JSON, Markdown, and instruction files');
727
+ }
728
+
729
+ if (signals.hasGitAutomation) {
730
+ details.push('Git-backed synchronization workflow for publishing project state');
731
+ }
732
+
733
+ if (signals.hasCliEntry && techStack.language === 'Node.js') {
734
+ details.push('Node.js CLI entrypoint for developer-facing automation');
735
+ }
736
+
737
+ return uniqueNonEmpty(details).slice(0, MAX_IMPLEMENTATION_DETAILS);
738
+ }
739
+
740
+ function buildKeyFeatures(signals, techStack) {
741
+ const features = [];
742
+
743
+ if (signals.hasAiContextArtifacts) {
744
+ features.push(FEATURE_CATALOG.ai_context_generation);
745
+ }
746
+
747
+ if (signals.hasCliEntry) {
748
+ features.push(FEATURE_CATALOG.cli_automation);
749
+ }
750
+
751
+ if (signals.hasWatcher) {
752
+ features.push(FEATURE_CATALOG.change_tracking);
753
+ }
754
+
755
+ if (signals.hasGitAutomation) {
756
+ features.push(FEATURE_CATALOG.public_sync);
757
+ }
758
+
759
+ if (signals.hasExpress && signals.hasAiContextArtifacts) {
760
+ features.push(FEATURE_CATALOG.local_context_server);
761
+ } else if (signals.hasExpress && signals.hasRestRoutes) {
762
+ features.push(FEATURE_CATALOG.rest_api);
763
+ }
764
+
765
+ if (signals.hasJwt) {
766
+ features.push(FEATURE_CATALOG.auth);
767
+ }
768
+
769
+ if (signals.hasMongoose) {
770
+ features.push(FEATURE_CATALOG.persistence);
771
+ }
772
+
773
+ if (signals.hasSocketIO) {
774
+ features.push(FEATURE_CATALOG.realtime);
775
+ }
776
+
777
+ if (signals.hasAxiosOrFetch) {
778
+ features.push(FEATURE_CATALOG.external_api);
779
+ }
780
+
781
+ if (signals.hasMiddleware) {
782
+ features.push(FEATURE_CATALOG.middleware);
783
+ }
784
+
785
+ if (signals.hasConfigDirectory) {
786
+ features.push(FEATURE_CATALOG.config_management);
787
+ }
788
+
789
+ if (features.length === 0 && techStack.language) {
790
+ features.push(`${techStack.language} project structure with detectable application entry points`);
791
+ }
792
+
793
+ return uniqueNonEmpty(features).slice(0, MAX_KEY_FEATURES);
794
+ }
795
+
796
+ function determineProjectType(signals, techStack, rootPackage) {
797
+ if (signals.hasNext) {
798
+ return 'Next.js application';
799
+ }
800
+
801
+ if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
802
+ return 'CLI tool';
803
+ }
804
+
805
+ if (signals.hasExpress && signals.hasRestRoutes) {
806
+ return 'backend API platform';
807
+ }
808
+
809
+ if (signals.hasReact) {
810
+ return 'frontend application';
811
+ }
812
+
813
+ if (signals.hasPythonRuntime) {
814
+ return 'Python application';
815
+ }
816
+
817
+ if (rootPackage && rootPackage.bin) {
818
+ return 'CLI tool';
819
+ }
820
+
821
+ if (techStack.language === 'Node.js') {
822
+ return 'Node.js application';
823
+ }
824
+
825
+ return 'software project';
826
+ }
827
+
828
+ async function loadRuntimeConfig(projectRoot) {
829
+ const { configFile } = getContextPaths(projectRoot);
830
+ const userConfig = await readJsonFile(configFile, {});
831
+ return deepMerge(DEFAULT_CONFIG, userConfig);
832
+ }
833
+
834
+ async function updateRuntimeConfig(projectRoot, updates) {
835
+ const { configFile } = getContextPaths(projectRoot);
836
+ const currentConfig = await readJsonFile(configFile, {});
837
+ const mergedCurrentConfig = deepMerge(DEFAULT_CONFIG, currentConfig);
838
+ const nextConfig = deepMerge(mergedCurrentConfig, updates || {});
839
+
840
+ await writeJsonAtomic(configFile, nextConfig);
841
+ return nextConfig;
842
+ }
843
+
844
+ async function updateProjectState(projectRoot, changeEvent, options) {
845
+ const settings = Object.assign(
846
+ {
847
+ logger: null,
848
+ syncCallback: null
849
+ },
850
+ options
851
+ );
852
+ const logger = settings.logger;
853
+ const resolvedRoot = resolveProjectRoot(projectRoot);
854
+ const contextPaths = getContextPaths(resolvedRoot);
855
+ const metadata = detectProjectMetadata(resolvedRoot);
856
+ const bootstrap = bootstrapProjectAnalysis(resolvedRoot);
857
+ const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(resolvedRoot));
858
+ const existingChangelog = await readJsonFile(
859
+ contextPaths.changelogFile,
860
+ createDefaultChangelog()
861
+ );
862
+ const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
863
+ const validEvents = normalizedEvents.filter(Boolean);
864
+ const timestamp = determineUpdateTimestamp(validEvents);
865
+ const meaningfulEvents = collapseEventsByFile(validEvents.filter((event) => isMeaningfulEvent(event)));
866
+ const groupedUpdates = groupEventsByIntent(meaningfulEvents, bootstrap);
867
+ const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
868
+ const historyEntries = dedupeHistoryEntries(groupedUpdates.concat(previousHistoryEntries))
869
+ .slice(0, MAX_CHANGELOG_ENTRIES);
870
+ const promotedFeatures = promoteFeatures(historyEntries);
871
+ const keyFeatures = mergeKeyFeatures(bootstrap.keyFeatures, promotedFeatures);
872
+ const recentUpdates = groupedUpdates.length > 0
873
+ ? dedupeRecentUpdates(groupedUpdates.map(toStateUpdate).concat(normalizeStoredUpdates(existingState.recent_updates)))
874
+ .slice(0, MAX_RECENT_UPDATES)
875
+ : normalizeStoredUpdates(existingState.recent_updates).slice(0, MAX_RECENT_UPDATES);
876
+ const mergedArchitecturePatterns = mergePreferredArray(
877
+ bootstrap.architecturePatterns,
878
+ existingState.architecture_patterns
879
+ );
880
+ const mergedImplementationDetails = mergePreferredArray(
881
+ bootstrap.implementationDetails,
882
+ existingState.implementation_details
883
+ );
884
+ const nextState = composeStateFromAnalysis(existingState, metadata, bootstrap, {
885
+ last_updated: timestamp,
886
+ tech_stack: mergePreferredObject(bootstrap.techStack, existingState.tech_stack),
887
+ architecture_patterns: mergedArchitecturePatterns,
888
+ implementation_details: mergedImplementationDetails,
889
+ current_stage: determineCurrentStage(
890
+ keyFeatures,
891
+ historyEntries,
892
+ mergedImplementationDetails
893
+ ),
894
+ recent_updates: recentUpdates,
895
+ key_features: keyFeatures,
896
+ known_issues: deriveKnownIssues(resolvedRoot, Object.assign({}, bootstrap, {
897
+ architecturePatterns: mergedArchitecturePatterns,
898
+ implementationDetails: mergedImplementationDetails,
899
+ keyFeatures
900
+ })),
901
+ next_steps: []
902
+ });
903
+
904
+ nextState.ai_summary = generateAiSummary(nextState, bootstrap);
905
+ nextState.next_steps = generateNextSteps(nextState, bootstrap, historyEntries);
517
906
 
518
- if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
519
- score += 2;
520
- }
907
+ await writeJsonAtomic(contextPaths.stateFile, nextState);
908
+ await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
521
909
 
522
- if (lowerPath === 'package.json') {
523
- score += 2;
910
+ if (logger) {
911
+ logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
524
912
  }
525
913
 
526
- if (lowerPath === 'readme.md') {
527
- score += 1;
914
+ if (typeof settings.syncCallback === 'function') {
915
+ await settings.syncCallback();
528
916
  }
529
917
 
530
- return score;
918
+ return nextState;
531
919
  }
532
920
 
533
921
  function isMeaningfulEvent(event) {
@@ -554,365 +942,225 @@ function collapseEventsByFile(events) {
554
942
  return Array.from(collapsedEvents.values());
555
943
  }
556
944
 
945
+ function determineUpdateTimestamp(events) {
946
+ if (!Array.isArray(events) || events.length === 0) {
947
+ return new Date().toISOString();
948
+ }
949
+
950
+ const latestEvent = events[events.length - 1];
951
+ return latestEvent.timestamp || new Date().toISOString();
952
+ }
953
+
557
954
  function classifyChangeArea(filePath) {
558
- const lowerPath = filePath.toLowerCase();
955
+ const lowerPath = normalizeProjectPath(filePath).toLowerCase();
559
956
 
560
957
  if (lowerPath === 'package.json') {
561
- return 'dependencies';
958
+ return 'configuration';
562
959
  }
563
960
 
564
961
  if (lowerPath === 'readme.md') {
565
962
  return 'documentation';
566
963
  }
567
964
 
568
- if (lowerPath.startsWith('core/')) {
569
- return 'logic';
965
+ if (lowerPath.startsWith('bin/')) {
966
+ return 'cli';
570
967
  }
571
968
 
572
- if (lowerPath.startsWith('server/')) {
969
+ if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
573
970
  return 'backend';
574
971
  }
575
972
 
576
- if (lowerPath.startsWith('bin/')) {
577
- return 'cli';
973
+ if (
974
+ lowerPath.startsWith('controllers/') ||
975
+ lowerPath.startsWith('services/') ||
976
+ lowerPath.startsWith('middleware/') ||
977
+ lowerPath.startsWith('config/')
978
+ ) {
979
+ return 'application';
578
980
  }
579
981
 
580
- if (lowerPath.startsWith('templates/')) {
581
- return 'templates';
982
+ if (lowerPath.startsWith('core/') || lowerPath.startsWith('src/')) {
983
+ return 'logic';
582
984
  }
583
985
 
584
986
  return 'project';
585
987
  }
586
988
 
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();
989
+ function detectEventFeatureKey(filePath, bootstrap) {
990
+ const lowerPath = normalizeProjectPath(filePath).toLowerCase();
991
+ const signals = bootstrap.signals;
600
992
 
601
993
  if (lowerPath === 'package.json') {
602
- return 'package_configuration';
603
- }
994
+ if (signals.hasGitAutomation) {
995
+ return 'public_sync';
996
+ }
604
997
 
605
- if (lowerPath === 'readme.md') {
606
- return 'documentation';
998
+ if (signals.hasCliEntry) {
999
+ return 'cli_automation';
1000
+ }
1001
+
1002
+ return 'config_management';
607
1003
  }
608
1004
 
609
1005
  if (lowerPath.startsWith('bin/')) {
610
- return 'cli_workflow';
1006
+ return 'cli_automation';
611
1007
  }
612
1008
 
613
- if (lowerPath.startsWith('server/')) {
614
- return 'local_context_server';
615
- }
1009
+ if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
1010
+ if (signals.hasAiContextArtifacts) {
1011
+ return 'local_context_server';
1012
+ }
616
1013
 
617
- if (lowerPath.startsWith('templates/')) {
618
- return 'context_templates';
1014
+ return 'rest_api';
619
1015
  }
620
1016
 
621
1017
  if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
622
- return 'github_sync';
1018
+ return 'public_sync';
623
1019
  }
624
1020
 
625
- if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
1021
+ if (lowerPath.includes('watch')) {
626
1022
  return 'change_tracking';
627
1023
  }
628
1024
 
629
1025
  if (lowerPath.includes('state') || lowerPath.includes('context')) {
630
- return 'project_intelligence';
1026
+ return 'ai_context_generation';
631
1027
  }
632
1028
 
633
- if (lowerPath.includes('init')) {
634
- return 'project_setup';
1029
+ if (lowerPath.includes('auth') || lowerPath.includes('jwt')) {
1030
+ return 'auth';
635
1031
  }
636
1032
 
637
- if (area === 'logic') {
638
- return 'project_intelligence';
1033
+ if (lowerPath.includes('service') || lowerPath.includes('controller')) {
1034
+ return signals.hasAxiosOrFetch ? 'external_api' : 'rest_api';
639
1035
  }
640
1036
 
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
- };
1037
+ return 'ai_context_generation';
655
1038
  }
656
1039
 
657
- function groupEventsByIntent(events) {
1040
+ function groupEventsByIntent(events, bootstrap) {
658
1041
  if (!Array.isArray(events) || events.length === 0) {
659
1042
  return [];
660
1043
  }
661
1044
 
662
- const groupedByArea = new Map();
1045
+ const buckets = new Map();
663
1046
 
664
1047
  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
- });
1048
+ const area = classifyChangeArea(event.file);
1049
+ const featureKey = detectEventFeatureKey(event.file, bootstrap);
1050
+ const key = `${area}:${featureKey}`;
1051
+
1052
+ if (!buckets.has(key)) {
1053
+ buckets.set(key, []);
674
1054
  }
675
1055
 
676
- groupedByArea.get(groupKey).events.push(descriptor);
1056
+ buckets.get(key).push(
1057
+ Object.assign({}, event, {
1058
+ area,
1059
+ featureKey
1060
+ })
1061
+ );
677
1062
  }
678
1063
 
679
- const mergedGroups = mergeCrossAreaIntentGroups(Array.from(groupedByArea.values()));
680
-
681
- return mergedGroups
1064
+ return Array.from(buckets.values())
682
1065
  .map((group) => interpretIntentGroup(group))
683
1066
  .filter(Boolean)
684
1067
  .sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
685
1068
  }
686
1069
 
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
1070
  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) => {
1071
+ const latestTimestamp = group.reduce((latest, event) => {
752
1072
  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);
1073
+ }, group[0].timestamp || new Date().toISOString());
1074
+ const featureKey = group[0].featureKey;
1075
+ const area = group[0].area;
756
1076
  const type = determineGroupedUpdateType(group);
757
- const subject = describeIntentSubject(group, featureMeta.subject);
1077
+ const subject = describeIntentSubject(featureKey);
758
1078
 
759
1079
  return {
760
1080
  timestamp: latestTimestamp,
761
- scope: describeIntentScope(group),
1081
+ scope: area,
762
1082
  title: buildIntentTitle(type, subject),
763
1083
  type,
764
- impact: describeIntentImpact(type, featureKey, subject),
1084
+ impact: describeIntentImpact(type, featureKey),
765
1085
  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
1086
+ feature_name: FEATURE_CATALOG[featureKey] || subject
810
1087
  };
811
-
812
- return priorities[featureKey] || 0;
813
1088
  }
814
1089
 
815
1090
  function determineGroupedUpdateType(group) {
816
- const actions = new Set(group.events.map((event) => event.action));
817
- const fileCount = group.events.length;
1091
+ const hasAdd = group.some((event) => event.action === 'add');
1092
+ const hasDelete = group.some((event) => event.action === 'delete');
818
1093
 
819
- if (actions.has('add')) {
1094
+ if (hasAdd) {
820
1095
  return 'feature';
821
1096
  }
822
1097
 
823
- if (hasFixSignals(group.events)) {
1098
+ if (hasDelete) {
824
1099
  return 'fix';
825
1100
  }
826
1101
 
827
- if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
1102
+ if (group.length > 2) {
828
1103
  return 'refactor';
829
1104
  }
830
1105
 
831
1106
  return 'improvement';
832
1107
  }
833
1108
 
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
- }
1109
+ function describeIntentSubject(featureKey) {
1110
+ const subjectMap = {
1111
+ ai_context_generation: 'AI context generation workflow',
1112
+ cli_automation: 'CLI automation workflow',
1113
+ change_tracking: 'change tracking workflow',
1114
+ public_sync: 'GitHub sync workflow',
1115
+ local_context_server: 'context delivery service',
1116
+ rest_api: 'API architecture',
1117
+ auth: 'authentication workflow',
1118
+ persistence: 'data persistence layer',
1119
+ realtime: 'real-time communication layer',
1120
+ external_api: 'external integration workflow',
1121
+ middleware: 'request processing workflow',
1122
+ config_management: 'project configuration workflow'
1123
+ };
866
1124
 
867
- return group.area;
1125
+ return subjectMap[featureKey] || 'project workflow';
868
1126
  }
869
1127
 
870
1128
  function buildIntentTitle(type, subject) {
871
1129
  const verbs = {
872
1130
  feature: 'Expanded',
873
1131
  improvement: 'Improved',
874
- refactor: 'Refactored',
1132
+ refactor: 'Refined',
875
1133
  fix: 'Stabilized'
876
1134
  };
877
1135
 
878
1136
  return `${verbs[type] || 'Improved'} ${subject}`;
879
1137
  }
880
1138
 
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.'
1139
+ function describeIntentImpact(type, featureKey) {
1140
+ const impactMap = {
1141
+ ai_context_generation: 'Improves the quality of generated AI-readable project context.',
1142
+ cli_automation: 'Improves how developers control the project workflow from the command line.',
1143
+ change_tracking: 'Improves how meaningful project changes are detected without noise.',
1144
+ public_sync: 'Improves how project state is published for external AI consumption.',
1145
+ local_context_server: 'Improves how current project context is delivered over HTTP endpoints.',
1146
+ rest_api: 'Improves the structure and clarity of the project API surface.',
1147
+ auth: 'Improves authentication reliability and access control.',
1148
+ persistence: 'Improves how project data is persisted and retrieved.',
1149
+ realtime: 'Improves real-time communication behavior.',
1150
+ external_api: 'Improves outbound integration reliability.',
1151
+ middleware: 'Improves request handling and middleware orchestration.',
1152
+ config_management: 'Improves project configuration and packaging reliability.'
895
1153
  };
896
1154
 
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
1155
  if (type === 'fix') {
908
- return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
1156
+ return impactMap[featureKey].replace(/^Improves /, 'Resolves issues in ');
909
1157
  }
910
1158
 
911
- return impactByFeature[featureKey] || 'Improves the overall project workflow for maintaining AI-readable context.';
912
- }
1159
+ if (type === 'feature') {
1160
+ return impactMap[featureKey].replace(/^Improves /, 'Adds ');
1161
+ }
913
1162
 
914
- function interpretChange(event) {
915
- return groupEventsByIntent([event])[0] || null;
1163
+ return impactMap[featureKey] || 'Improves the overall project workflow.';
916
1164
  }
917
1165
 
918
1166
  function normalizeStoredUpdates(updates) {
@@ -920,7 +1168,11 @@ function normalizeStoredUpdates(updates) {
920
1168
  return [];
921
1169
  }
922
1170
 
923
- return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
1171
+ return dedupeRecentUpdates(
1172
+ updates
1173
+ .map((update) => normalizeStoredUpdate(update))
1174
+ .filter(Boolean)
1175
+ );
924
1176
  }
925
1177
 
926
1178
  function normalizeStoredUpdate(update) {
@@ -936,10 +1188,6 @@ function normalizeStoredUpdate(update) {
936
1188
  };
937
1189
  }
938
1190
 
939
- if (update.file && update.action && isMeaningfulEvent(update)) {
940
- return toStateUpdate(interpretChange(update));
941
- }
942
-
943
1191
  return null;
944
1192
  }
945
1193
 
@@ -961,118 +1209,71 @@ function normalizeStoredHistoryEntry(entry) {
961
1209
  }
962
1210
 
963
1211
  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
1212
  return {
970
1213
  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'
1214
+ scope: entry.scope || 'project',
1215
+ title: entry.title,
1216
+ type: normalizeUpdateType(entry.type),
1217
+ impact: entry.impact,
1218
+ feature_key: entry.feature_key || inferFeatureKeyFromText(entry.title, entry.impact),
1219
+ feature_name: entry.feature_name || FEATURE_CATALOG[inferFeatureKeyFromText(entry.title, entry.impact)] || 'Project capability'
978
1220
  };
979
1221
  }
980
1222
 
981
- if (entry.file && entry.action && isMeaningfulEvent(entry)) {
982
- return interpretChange(entry);
983
- }
984
-
985
1223
  return null;
986
1224
  }
987
1225
 
988
- function inferFeatureFromEntry(entry) {
989
- const combinedText = `${entry.title || ''} ${entry.impact || ''}`.toLowerCase();
1226
+ function inferFeatureKeyFromText(title, impact) {
1227
+ const combinedText = `${title || ''} ${impact || ''}`.toLowerCase();
990
1228
 
991
- if (combinedText.includes('github') || combinedText.includes('sync')) {
992
- return {
993
- featureKey: 'github_sync',
994
- featureName: getFeatureMeta('github_sync').name,
995
- scope: 'logic'
996
- };
1229
+ if (combinedText.includes('jwt') || combinedText.includes('auth')) {
1230
+ return 'auth';
997
1231
  }
998
1232
 
999
- if (combinedText.includes('state') || combinedText.includes('intelligence')) {
1000
- return {
1001
- featureKey: 'project_intelligence',
1002
- featureName: getFeatureMeta('project_intelligence').name,
1003
- scope: 'logic'
1004
- };
1233
+ if (combinedText.includes('mongo') || combinedText.includes('mongoose') || combinedText.includes('persist')) {
1234
+ return 'persistence';
1005
1235
  }
1006
1236
 
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
- };
1237
+ if (combinedText.includes('real-time') || combinedText.includes('socket')) {
1238
+ return 'realtime';
1013
1239
  }
1014
1240
 
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
- };
1241
+ if (combinedText.includes('api') || combinedText.includes('route')) {
1242
+ return 'rest_api';
1026
1243
  }
1027
1244
 
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
- };
1245
+ if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
1246
+ return 'public_sync';
1034
1247
  }
1035
1248
 
1036
- if (combinedText.includes('documentation') || combinedText.includes('onboarding')) {
1037
- return {
1038
- featureKey: 'documentation',
1039
- featureName: getFeatureMeta('documentation').name,
1040
- scope: 'documentation'
1041
- };
1249
+ if (combinedText.includes('watch') || combinedText.includes('change')) {
1250
+ return 'change_tracking';
1042
1251
  }
1043
1252
 
1044
- if (combinedText.includes('package') || combinedText.includes('dependency')) {
1045
- return {
1046
- featureKey: 'package_configuration',
1047
- featureName: getFeatureMeta('package_configuration').name,
1048
- scope: 'dependencies'
1049
- };
1253
+ if (combinedText.includes('command line') || combinedText.includes('cli')) {
1254
+ return 'cli_automation';
1050
1255
  }
1051
1256
 
1052
- return {
1053
- featureKey: 'project_workflow',
1054
- featureName: getFeatureMeta('project_workflow').name,
1055
- scope: 'project'
1056
- };
1257
+ if (combinedText.includes('http') || combinedText.includes('context delivery')) {
1258
+ return 'local_context_server';
1259
+ }
1260
+
1261
+ return 'ai_context_generation';
1057
1262
  }
1058
1263
 
1059
1264
  function normalizeUpdateType(type) {
1060
- if (type === 'removal') {
1061
- return 'refactor';
1062
- }
1063
-
1064
1265
  if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
1065
1266
  return type;
1066
1267
  }
1067
1268
 
1269
+ if (type === 'removal') {
1270
+ return 'fix';
1271
+ }
1272
+
1068
1273
  return 'improvement';
1069
1274
  }
1070
1275
 
1071
1276
  function toStateUpdate(update) {
1072
- if (!update) {
1073
- return null;
1074
- }
1075
-
1076
1277
  return {
1077
1278
  title: update.title,
1078
1279
  type: normalizeUpdateType(update.type),
@@ -1081,17 +1282,17 @@ function toStateUpdate(update) {
1081
1282
  }
1082
1283
 
1083
1284
  function dedupeRecentUpdates(updates) {
1084
- const seenUpdates = new Set();
1285
+ const seen = new Set();
1085
1286
  const result = [];
1086
1287
 
1087
1288
  for (const update of updates.filter(Boolean)) {
1088
1289
  const key = `${update.title}::${update.type}::${update.impact}`;
1089
1290
 
1090
- if (seenUpdates.has(key)) {
1291
+ if (seen.has(key)) {
1091
1292
  continue;
1092
1293
  }
1093
1294
 
1094
- seenUpdates.add(key);
1295
+ seen.add(key);
1095
1296
  result.push(update);
1096
1297
  }
1097
1298
 
@@ -1099,190 +1300,81 @@ function dedupeRecentUpdates(updates) {
1099
1300
  }
1100
1301
 
1101
1302
  function dedupeHistoryEntries(entries) {
1102
- const seenEntries = new Set();
1303
+ const seen = new Set();
1103
1304
  const result = [];
1104
1305
 
1105
1306
  for (const entry of entries.filter(Boolean)) {
1106
1307
  const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
1107
1308
 
1108
- if (seenEntries.has(key)) {
1309
+ if (seen.has(key)) {
1109
1310
  continue;
1110
1311
  }
1111
1312
 
1112
- seenEntries.add(key);
1313
+ seen.add(key);
1113
1314
  result.push(entry);
1114
1315
  }
1115
1316
 
1116
1317
  return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
1117
1318
  }
1118
1319
 
1119
- function promoteFeatures(history) {
1120
- if (!Array.isArray(history) || history.length === 0) {
1320
+ function promoteFeatures(historyEntries) {
1321
+ if (!Array.isArray(historyEntries) || historyEntries.length === 0) {
1121
1322
  return [];
1122
1323
  }
1123
1324
 
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
- }
1325
+ const scores = new Map();
1190
1326
 
1191
- for (const feature of rankedFeatures) {
1192
- if (promoted.length >= MAX_KEY_FEATURES) {
1193
- break;
1194
- }
1327
+ for (const entry of historyEntries) {
1328
+ const featureName = entry.feature_name || FEATURE_CATALOG[entry.feature_key];
1195
1329
 
1196
- if (seenFeatureNames.has(feature.featureName)) {
1330
+ if (!featureName) {
1197
1331
  continue;
1198
1332
  }
1199
1333
 
1200
- promoted.push(feature.featureName);
1201
- seenFeatureNames.add(feature.featureName);
1334
+ scores.set(featureName, (scores.get(featureName) || 0) + scoreHistoryEntry(entry));
1202
1335
  }
1203
1336
 
1204
- return promoted.slice(0, MAX_KEY_FEATURES);
1337
+ return Array.from(scores.entries())
1338
+ .sort((left, right) => right[1] - left[1])
1339
+ .map(([featureName]) => featureName)
1340
+ .slice(0, MAX_KEY_FEATURES);
1205
1341
  }
1206
1342
 
1207
- function scoreFeatureEntry(entry) {
1208
- const typeWeights = {
1343
+ function scoreHistoryEntry(entry) {
1344
+ const weights = {
1209
1345
  feature: 4,
1210
1346
  refactor: 3,
1211
1347
  improvement: 2,
1212
1348
  fix: 2
1213
1349
  };
1214
1350
 
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
- };
1351
+ return weights[normalizeUpdateType(entry.type)] || 1;
1236
1352
  }
1237
1353
 
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';
1354
+ function mergeKeyFeatures(primaryFeatures, promotedFeatures) {
1355
+ return uniqueNonEmpty([].concat(primaryFeatures || [], promotedFeatures || [])).slice(0, MAX_KEY_FEATURES);
1265
1356
  }
1266
1357
 
1267
- function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
1358
+ function deriveKnownIssues(projectRoot, bootstrap) {
1268
1359
  const knownIssues = [];
1269
1360
 
1270
1361
  if (!hasTestIndicators(projectRoot)) {
1271
1362
  knownIssues.push('No automated test suite is detected yet.');
1272
1363
  }
1273
1364
 
1274
- if (!techStack.framework) {
1275
- knownIssues.push('No common application framework dependency is currently detected.');
1365
+ if (bootstrap.implementationDetails.length === 0) {
1366
+ knownIssues.push('Project structure exposes limited implementation signals, so deeper architecture details may still be missing.');
1276
1367
  }
1277
1368
 
1278
- if (keyFeatures.length < 2) {
1279
- knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
1369
+ if (!bootstrap.techStack.framework && bootstrap.techStack.language === 'Node.js') {
1370
+ knownIssues.push('No common Node.js application framework dependency is currently detected.');
1280
1371
  }
1281
1372
 
1282
1373
  return knownIssues;
1283
1374
  }
1284
1375
 
1285
1376
  function hasTestIndicators(projectRoot) {
1377
+ const resolvedRoot = resolveProjectRoot(projectRoot);
1286
1378
  const testPaths = [
1287
1379
  'test',
1288
1380
  'tests',
@@ -1293,11 +1385,11 @@ function hasTestIndicators(projectRoot) {
1293
1385
  'jest.config.mjs'
1294
1386
  ];
1295
1387
 
1296
- return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
1388
+ return testPaths.some((relativePath) => fs.existsSync(path.join(resolvedRoot, relativePath)));
1297
1389
  }
1298
1390
 
1299
- function determineCurrentStage(keyFeatures, historyEntries) {
1300
- if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
1391
+ function determineCurrentStage(keyFeatures, historyEntries, implementationDetails) {
1392
+ if (keyFeatures.length >= 4 && implementationDetails.length >= 3) {
1301
1393
  return 'Production-ready';
1302
1394
  }
1303
1395
 
@@ -1308,86 +1400,69 @@ function determineCurrentStage(keyFeatures, historyEntries) {
1308
1400
  return 'Early development';
1309
1401
  }
1310
1402
 
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';
1403
+ function generateAiSummary(state, bootstrap) {
1404
+ const features = state.key_features || [];
1405
+ const details = bootstrap.implementationDetails || [];
1406
+ const projectType = bootstrap.projectType || 'software project';
1407
+ const normalizedType = projectType === 'backend API platform'
1408
+ ? 'Backend API platform'
1409
+ : capitalize(projectType);
1318
1410
 
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';
1411
+ if (details.some((detail) => detail.includes('JWT')) && details.some((detail) => detail.includes('MongoDB'))) {
1412
+ return `${normalizedType} with JWT authentication and MongoDB-backed application logic.`;
1325
1413
  }
1326
1414
 
1327
1415
  if (
1328
- featureSignals.has('github_sync') ||
1329
- featureSignals.has('context_delivery_system') ||
1330
- featureSignals.has('cli_orchestration')
1416
+ features.includes(FEATURE_CATALOG.ai_context_generation) &&
1417
+ features.includes(FEATURE_CATALOG.public_sync)
1331
1418
  ) {
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';
1419
+ return `${normalizedType} that generates AI-readable project context, tracks meaningful changes, and can publish public project state through GitHub.`;
1335
1420
  }
1336
1421
 
1337
- return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
1338
- }
1339
-
1340
- function collectFeatureSignals(historyEntries, keyFeatures) {
1341
- const featureSignals = new Set();
1422
+ if (
1423
+ features.includes(FEATURE_CATALOG.rest_api) &&
1424
+ details.some((detail) => detail.includes('Express'))
1425
+ ) {
1426
+ return `${normalizedType} with RESTful request handling and structured server-side workflow orchestration.`;
1427
+ }
1342
1428
 
1343
- for (const entry of historyEntries) {
1344
- if (entry.feature_key) {
1345
- featureSignals.add(entry.feature_key);
1346
- }
1429
+ if (features.length >= 2) {
1430
+ return `${normalizedType} focused on ${features.slice(0, 2).join(' and ').toLowerCase()}.`;
1347
1431
  }
1348
1432
 
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
- }
1433
+ if (details.length > 0) {
1434
+ return `${normalizedType} built around ${details[0].toLowerCase()}.`;
1355
1435
  }
1356
1436
 
1357
- return featureSignals;
1437
+ return `${normalizedType} with detectable project structure and implementation patterns.`;
1358
1438
  }
1359
1439
 
1360
- function generateNextSteps(state, historyEntries) {
1440
+ function generateNextSteps(state, bootstrap, historyEntries) {
1361
1441
  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
1442
 
1368
1443
  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.');
1444
+ nextSteps.push('Add automated tests that cover the main application flow and critical integration points.');
1374
1445
  }
1375
1446
 
1376
- if (!featureSignals.has('local_context_server')) {
1377
- nextSteps.push('Expand context delivery coverage so AI consumers can reliably read current project state.');
1447
+ if (bootstrap.implementationDetails.length < 2) {
1448
+ nextSteps.push('Strengthen the project structure so major implementation patterns are easier to detect automatically.');
1378
1449
  }
1379
1450
 
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.');
1451
+ if (historyEntries.length === 0) {
1452
+ nextSteps.push('Capture the next meaningful project update so recent evolution is reflected alongside the bootstrap analysis.');
1382
1453
  }
1383
1454
 
1384
1455
  if (state.current_stage === 'Early development') {
1385
- nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
1456
+ nextSteps.push('Ship the next core capability to move the project from initial structure into a functional prototype.');
1386
1457
  }
1387
1458
 
1388
1459
  return Array.from(new Set(nextSteps)).slice(0, 4);
1389
1460
  }
1390
1461
 
1462
+ function uniqueNonEmpty(values) {
1463
+ return Array.from(new Set((values || []).filter(Boolean)));
1464
+ }
1465
+
1391
1466
  function capitalize(value) {
1392
1467
  if (!value) {
1393
1468
  return '';
@@ -1459,6 +1534,7 @@ function createDebouncedStateUpdater(projectRoot, options) {
1459
1534
  module.exports = {
1460
1535
  CONTEXT_DIR_NAME,
1461
1536
  DEFAULT_CONFIG,
1537
+ bootstrapProjectAnalysis,
1462
1538
  createDebouncedStateUpdater,
1463
1539
  createDefaultChangelog,
1464
1540
  createDefaultState,
@@ -1466,7 +1542,6 @@ module.exports = {
1466
1542
  ensureContextDirectory,
1467
1543
  getContextPaths,
1468
1544
  groupEventsByIntent,
1469
- interpretChange,
1470
1545
  loadRuntimeConfig,
1471
1546
  promoteFeatures,
1472
1547
  readJsonFile,
@@ -1477,4 +1552,4 @@ module.exports = {
1477
1552
  updateProjectState,
1478
1553
  writeJsonAtomic,
1479
1554
  writeTextAtomic
1480
- };
1555
+ };