aibridge-context 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,80 @@ const fsp = require('fs/promises');
5
5
  const path = require('path');
6
6
 
7
7
  const CONTEXT_DIR_NAME = '.ai-context';
8
- const MAX_RECENT_UPDATES = 50;
9
- const MAX_CHANGELOG_ENTRIES = 200;
8
+ const MAX_RECENT_UPDATES = 5;
9
+ const MAX_CHANGELOG_ENTRIES = 50;
10
+ const MAX_KEY_FEATURES = 6;
11
+ const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
12
+ 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'
18
+ ]);
19
+
20
+ 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
+ }
81
+ };
10
82
 
11
83
  const DEFAULT_CONFIG = {
12
84
  port: 3333,
@@ -59,10 +131,14 @@ function isObject(value) {
59
131
  function detectProjectMetadata(projectRoot) {
60
132
  const packageJsonPath = path.join(projectRoot, 'package.json');
61
133
  const packageManager = detectPackageManager(projectRoot);
134
+ const techStack = detectTechStack(projectRoot);
62
135
  const metadata = {
63
136
  project: path.basename(projectRoot),
64
137
  version: '0.1.0',
65
- stackLabel: 'Node.js project',
138
+ techStack: Object.assign({}, techStack, {
139
+ package_manager: packageManager
140
+ }),
141
+ stackLabel: buildStackLabel(techStack),
66
142
  packageManager
67
143
  };
68
144
 
@@ -73,53 +149,134 @@ function detectProjectMetadata(projectRoot) {
73
149
  try {
74
150
  const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
75
151
  const parsedPackage = JSON.parse(rawPackage);
76
- const dependencies = Object.assign(
77
- {},
78
- parsedPackage.dependencies || {},
79
- parsedPackage.devDependencies || {}
80
- );
81
152
 
82
153
  metadata.project = parsedPackage.name || metadata.project;
83
154
  metadata.version = parsedPackage.version || metadata.version;
84
- metadata.stackLabel = describeStack(dependencies);
85
- metadata.packageManager = packageManager;
86
155
  } catch (error) {
87
- metadata.stackLabel = 'Node.js project';
156
+ metadata.stackLabel = buildStackLabel(metadata.techStack);
88
157
  }
89
158
 
90
159
  return metadata;
91
160
  }
92
161
 
93
- function detectPackageManager(projectRoot) {
94
- if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
95
- return 'pnpm';
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 = {};
170
+
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
+ }
96
183
  }
97
184
 
98
- if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
99
- return 'yarn';
185
+ let language = '';
186
+ let runtime = '';
187
+
188
+ if (hasPackageJson || hasAnyFileExtension(projectRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
189
+ language = 'Node.js';
190
+ runtime = 'Node.js';
191
+ } else if (hasPythonMarker || hasAnyFileExtension(projectRoot, ['.py'])) {
192
+ language = 'Python';
193
+ runtime = 'Python';
100
194
  }
101
195
 
102
- return 'npm';
196
+ return {
197
+ language,
198
+ framework: detectFramework(dependencies),
199
+ runtime,
200
+ package_manager: detectPackageManager(projectRoot)
201
+ };
103
202
  }
104
203
 
105
- function describeStack(dependencies) {
204
+ function detectFramework(dependencies) {
106
205
  if (dependencies.next) {
107
- return 'Node.js + Next.js';
206
+ return 'Next.js';
108
207
  }
109
208
 
110
209
  if (dependencies.react) {
111
- return 'Node.js + React';
210
+ return 'React';
112
211
  }
113
212
 
114
213
  if (dependencies.express) {
115
- return 'Node.js + Express';
214
+ return 'Express';
116
215
  }
117
216
 
118
- if (dependencies.typescript) {
119
- return 'Node.js + TypeScript';
217
+ return '';
218
+ }
219
+
220
+ function buildStackLabel(techStack) {
221
+ const parts = [techStack.language, techStack.framework].filter(Boolean);
222
+ return parts.length > 0 ? parts.join(' + ') : 'Project';
223
+ }
224
+
225
+ function hasAnyFileExtension(projectRoot, extensions) {
226
+ return scanProjectFiles(projectRoot, 2).some((filePath) =>
227
+ extensions.includes(path.extname(filePath).toLowerCase())
228
+ );
229
+ }
230
+
231
+ function scanProjectFiles(projectRoot, maxDepth) {
232
+ const results = [];
233
+
234
+ function visit(currentDir, depth) {
235
+ if (depth > maxDepth) {
236
+ return;
237
+ }
238
+
239
+ let entries = [];
240
+
241
+ try {
242
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
243
+ } catch (error) {
244
+ return;
245
+ }
246
+
247
+ for (const entry of entries) {
248
+ const fullPath = path.join(currentDir, entry.name);
249
+ const relativePath = normalizeProjectPath(path.relative(projectRoot, fullPath));
250
+
251
+ if (entry.isDirectory()) {
252
+ if (shouldIgnoreProjectFile(relativePath)) {
253
+ continue;
254
+ }
255
+
256
+ visit(fullPath, depth + 1);
257
+ continue;
258
+ }
259
+
260
+ if (!shouldIgnoreProjectFile(relativePath)) {
261
+ results.push(relativePath);
262
+ }
263
+ }
264
+ }
265
+
266
+ visit(projectRoot, 0);
267
+ return results;
268
+ }
269
+
270
+ function detectPackageManager(projectRoot) {
271
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
272
+ return 'pnpm';
120
273
  }
121
274
 
122
- return 'Node.js project';
275
+ if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
276
+ return 'yarn';
277
+ }
278
+
279
+ return 'npm';
123
280
  }
124
281
 
125
282
  async function ensureContextDirectory(projectRoot) {
@@ -150,19 +307,23 @@ async function writeTextAtomic(filePath, content) {
150
307
 
151
308
  function createDefaultState(projectRoot) {
152
309
  const metadata = detectProjectMetadata(projectRoot);
153
-
154
- return {
310
+ const state = {
155
311
  project: metadata.project,
156
312
  version: metadata.version,
157
313
  last_updated: new Date(0).toISOString(),
158
- stats: {
159
- files_changed: 0,
160
- last_file: ''
161
- },
314
+ ai_summary: '',
315
+ tech_stack: metadata.techStack,
316
+ current_stage: 'Early development',
162
317
  recent_updates: [],
163
- features: [],
318
+ key_features: [],
319
+ known_issues: deriveKnownIssues(projectRoot, metadata.techStack, []),
164
320
  next_steps: []
165
321
  };
322
+
323
+ state.ai_summary = generateAiSummary(state, []);
324
+ state.next_steps = generateNextSteps(state, []);
325
+
326
+ return state;
166
327
  }
167
328
 
168
329
  function createDefaultChangelog() {
@@ -204,62 +365,1007 @@ async function updateProjectState(projectRoot, changeEvent, options) {
204
365
  );
205
366
  const logger = settings.logger;
206
367
  const contextPaths = getContextPaths(projectRoot);
207
- const state = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
208
- const changelog = await readJsonFile(contextPaths.changelogFile, createDefaultChangelog());
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
+ );
209
374
  const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
210
375
  const validEvents = normalizedEvents.filter(Boolean);
376
+ const timestamp = determineUpdateTimestamp(validEvents);
377
+ const meaningfulEvents = collapseEventsByFile(
378
+ validEvents.filter((event) => isMeaningfulEvent(event))
379
+ );
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
+ };
404
+
405
+ nextState.ai_summary = generateAiSummary(nextState, historyEntries);
406
+ nextState.next_steps = generateNextSteps(nextState, historyEntries);
407
+
408
+ await writeJsonAtomic(contextPaths.stateFile, nextState);
409
+ await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
410
+
411
+ if (logger) {
412
+ logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
413
+ }
414
+
415
+ if (typeof settings.syncCallback === 'function') {
416
+ await settings.syncCallback();
417
+ }
418
+
419
+ return nextState;
420
+ }
421
+
422
+ function determineUpdateTimestamp(events) {
423
+ if (!Array.isArray(events) || events.length === 0) {
424
+ return new Date().toISOString();
425
+ }
426
+
427
+ const latestEvent = events[events.length - 1];
428
+ return latestEvent.timestamp || new Date().toISOString();
429
+ }
430
+
431
+ function normalizeProjectPath(filePath) {
432
+ return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
433
+ }
434
+
435
+ function shouldIgnoreProjectFile(filePath) {
436
+ const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
437
+
438
+ if (!normalizedPath) {
439
+ return false;
440
+ }
441
+
442
+ const segments = normalizedPath.split('/');
443
+ const baseName = segments[segments.length - 1];
444
+
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;
453
+ }
454
+
455
+ if (baseName.startsWith('.start')) {
456
+ return true;
457
+ }
458
+
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;
471
+ }
472
+
473
+ return false;
474
+ }
475
+
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;
481
+
482
+ if (shouldIgnoreProjectFile(normalizedPath)) {
483
+ return -5;
484
+ }
485
+
486
+ if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
487
+ score += 3;
488
+ }
489
+
490
+ if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
491
+ score += 2;
492
+ }
493
+
494
+ if (lowerPath === 'package.json') {
495
+ score += 2;
496
+ }
497
+
498
+ if (lowerPath === 'readme.md') {
499
+ score += 1;
500
+ }
501
+
502
+ return score;
503
+ }
504
+
505
+ function isMeaningfulEvent(event) {
506
+ if (!event || !event.file) {
507
+ return false;
508
+ }
509
+
510
+ return scoreEvent(event.file) >= 2;
511
+ }
512
+
513
+ function collapseEventsByFile(events) {
514
+ const collapsedEvents = new Map();
515
+
516
+ for (const event of events) {
517
+ const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
518
+ collapsedEvents.set(
519
+ normalizedFile,
520
+ Object.assign({}, event, {
521
+ file: normalizeProjectPath(event.file)
522
+ })
523
+ );
524
+ }
525
+
526
+ return Array.from(collapsedEvents.values());
527
+ }
528
+
529
+ function classifyChangeArea(filePath) {
530
+ const lowerPath = filePath.toLowerCase();
531
+
532
+ if (lowerPath === 'package.json') {
533
+ return 'dependencies';
534
+ }
535
+
536
+ if (lowerPath === 'readme.md') {
537
+ return 'documentation';
538
+ }
539
+
540
+ if (lowerPath.startsWith('core/')) {
541
+ return 'logic';
542
+ }
543
+
544
+ if (lowerPath.startsWith('server/')) {
545
+ return 'backend';
546
+ }
547
+
548
+ if (lowerPath.startsWith('bin/')) {
549
+ return 'cli';
550
+ }
551
+
552
+ if (lowerPath.startsWith('templates/')) {
553
+ return 'templates';
554
+ }
555
+
556
+ return 'project';
557
+ }
558
+
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();
572
+
573
+ if (lowerPath === 'package.json') {
574
+ return 'package_configuration';
575
+ }
576
+
577
+ if (lowerPath === 'readme.md') {
578
+ return 'documentation';
579
+ }
580
+
581
+ if (lowerPath.startsWith('bin/')) {
582
+ return 'cli_workflow';
583
+ }
584
+
585
+ if (lowerPath.startsWith('server/')) {
586
+ return 'local_context_server';
587
+ }
588
+
589
+ if (lowerPath.startsWith('templates/')) {
590
+ return 'context_templates';
591
+ }
592
+
593
+ if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
594
+ return 'github_sync';
595
+ }
596
+
597
+ if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
598
+ return 'change_tracking';
599
+ }
600
+
601
+ if (lowerPath.includes('state') || lowerPath.includes('context')) {
602
+ return 'project_intelligence';
603
+ }
604
+
605
+ if (lowerPath.includes('init')) {
606
+ return 'project_setup';
607
+ }
608
+
609
+ if (area === 'logic') {
610
+ return 'project_intelligence';
611
+ }
612
+
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
+ };
627
+ }
628
+
629
+ function groupEventsByIntent(events) {
630
+ if (!Array.isArray(events) || events.length === 0) {
631
+ return [];
632
+ }
633
+
634
+ const groupedByArea = new Map();
635
+
636
+ 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
+ });
646
+ }
647
+
648
+ groupedByArea.get(groupKey).events.push(descriptor);
649
+ }
650
+
651
+ const mergedGroups = mergeCrossAreaIntentGroups(Array.from(groupedByArea.values()));
652
+
653
+ return mergedGroups
654
+ .map((group) => interpretIntentGroup(group))
655
+ .filter(Boolean)
656
+ .sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
657
+ }
658
+
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
+ 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) => {
724
+ 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);
728
+ const type = determineGroupedUpdateType(group);
729
+ const subject = describeIntentSubject(group, featureMeta.subject);
730
+
731
+ return {
732
+ timestamp: latestTimestamp,
733
+ scope: describeIntentScope(group),
734
+ title: buildIntentTitle(type, subject),
735
+ type,
736
+ impact: describeIntentImpact(type, featureKey, subject),
737
+ 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
782
+ };
783
+
784
+ return priorities[featureKey] || 0;
785
+ }
211
786
 
212
- if (validEvents.length === 0) {
213
- return state;
787
+ function determineGroupedUpdateType(group) {
788
+ const actions = new Set(group.events.map((event) => event.action));
789
+ const fileCount = group.events.length;
790
+
791
+ if (actions.has('add')) {
792
+ return 'feature';
793
+ }
794
+
795
+ if (hasFixSignals(group.events)) {
796
+ return 'fix';
797
+ }
798
+
799
+ if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
800
+ return 'refactor';
214
801
  }
215
802
 
216
- const latestEvent = validEvents[validEvents.length - 1];
217
- const timestamp = latestEvent.timestamp || new Date().toISOString();
218
- const recentUpdates = Array.isArray(state.recent_updates) ? state.recent_updates.slice() : [];
219
- const changelogEntries = Array.isArray(changelog.entries) ? changelog.entries.slice() : [];
803
+ return 'improvement';
804
+ }
220
805
 
221
- for (const event of validEvents) {
222
- const normalizedEvent = {
223
- timestamp: event.timestamp || timestamp,
224
- action: event.action || 'updated',
225
- file: event.file || ''
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
+ }
838
+
839
+ return group.area;
840
+ }
841
+
842
+ function buildIntentTitle(type, subject) {
843
+ const verbs = {
844
+ feature: 'Expanded',
845
+ improvement: 'Improved',
846
+ refactor: 'Refactored',
847
+ fix: 'Stabilized'
848
+ };
849
+
850
+ return `${verbs[type] || 'Improved'} ${subject}`;
851
+ }
852
+
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.'
867
+ };
868
+
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
+ if (type === 'fix') {
880
+ return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
881
+ }
882
+
883
+ return impactByFeature[featureKey] || 'Improves the overall project workflow for maintaining AI-readable context.';
884
+ }
885
+
886
+ function interpretChange(event) {
887
+ return groupEventsByIntent([event])[0] || null;
888
+ }
889
+
890
+ function normalizeStoredUpdates(updates) {
891
+ if (!Array.isArray(updates)) {
892
+ return [];
893
+ }
894
+
895
+ return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
896
+ }
897
+
898
+ function normalizeStoredUpdate(update) {
899
+ if (!update) {
900
+ return null;
901
+ }
902
+
903
+ if (update.title && update.type && update.impact) {
904
+ return {
905
+ title: update.title,
906
+ type: normalizeUpdateType(update.type),
907
+ impact: update.impact
226
908
  };
909
+ }
227
910
 
228
- recentUpdates.push(normalizedEvent);
229
- changelogEntries.push(normalizedEvent);
911
+ if (update.file && update.action && isMeaningfulEvent(update)) {
912
+ return toStateUpdate(interpretChange(update));
230
913
  }
231
914
 
232
- const nextState = {
233
- project: state.project || createDefaultState(projectRoot).project,
234
- version: state.version || createDefaultState(projectRoot).version,
235
- last_updated: timestamp,
236
- stats: {
237
- files_changed: (state.stats && typeof state.stats.files_changed === 'number'
238
- ? state.stats.files_changed
239
- : 0) + validEvents.length,
240
- last_file: latestEvent.file || ''
241
- },
242
- recent_updates: recentUpdates.slice(-MAX_RECENT_UPDATES),
243
- features: Array.isArray(state.features) ? state.features : [],
244
- next_steps: Array.isArray(state.next_steps) ? state.next_steps : []
915
+ return null;
916
+ }
917
+
918
+ function normalizeStoredHistoryEntries(entries) {
919
+ if (!Array.isArray(entries)) {
920
+ return [];
921
+ }
922
+
923
+ return dedupeHistoryEntries(
924
+ entries
925
+ .map((entry) => normalizeStoredHistoryEntry(entry))
926
+ .filter(Boolean)
927
+ );
928
+ }
929
+
930
+ function normalizeStoredHistoryEntry(entry) {
931
+ if (!entry) {
932
+ return null;
933
+ }
934
+
935
+ 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
+ return {
942
+ 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'
950
+ };
951
+ }
952
+
953
+ if (entry.file && entry.action && isMeaningfulEvent(entry)) {
954
+ return interpretChange(entry);
955
+ }
956
+
957
+ return null;
958
+ }
959
+
960
+ function inferFeatureFromEntry(entry) {
961
+ const combinedText = `${entry.title || ''} ${entry.impact || ''}`.toLowerCase();
962
+
963
+ if (combinedText.includes('github') || combinedText.includes('sync')) {
964
+ return {
965
+ featureKey: 'github_sync',
966
+ featureName: getFeatureMeta('github_sync').name,
967
+ scope: 'logic'
968
+ };
969
+ }
970
+
971
+ if (combinedText.includes('state') || combinedText.includes('intelligence')) {
972
+ return {
973
+ featureKey: 'project_intelligence',
974
+ featureName: getFeatureMeta('project_intelligence').name,
975
+ scope: 'logic'
976
+ };
977
+ }
978
+
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
+ };
985
+ }
986
+
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
+ };
998
+ }
999
+
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
+ };
1006
+ }
1007
+
1008
+ if (combinedText.includes('documentation') || combinedText.includes('onboarding')) {
1009
+ return {
1010
+ featureKey: 'documentation',
1011
+ featureName: getFeatureMeta('documentation').name,
1012
+ scope: 'documentation'
1013
+ };
1014
+ }
1015
+
1016
+ if (combinedText.includes('package') || combinedText.includes('dependency')) {
1017
+ return {
1018
+ featureKey: 'package_configuration',
1019
+ featureName: getFeatureMeta('package_configuration').name,
1020
+ scope: 'dependencies'
1021
+ };
1022
+ }
1023
+
1024
+ return {
1025
+ featureKey: 'project_workflow',
1026
+ featureName: getFeatureMeta('project_workflow').name,
1027
+ scope: 'project'
245
1028
  };
1029
+ }
1030
+
1031
+ function normalizeUpdateType(type) {
1032
+ if (type === 'removal') {
1033
+ return 'refactor';
1034
+ }
1035
+
1036
+ if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
1037
+ return type;
1038
+ }
246
1039
 
247
- const nextChangelog = {
248
- entries: changelogEntries.slice(-MAX_CHANGELOG_ENTRIES)
1040
+ return 'improvement';
1041
+ }
1042
+
1043
+ function toStateUpdate(update) {
1044
+ if (!update) {
1045
+ return null;
1046
+ }
1047
+
1048
+ return {
1049
+ title: update.title,
1050
+ type: normalizeUpdateType(update.type),
1051
+ impact: update.impact
249
1052
  };
1053
+ }
250
1054
 
251
- await writeJsonAtomic(contextPaths.stateFile, nextState);
252
- await writeJsonAtomic(contextPaths.changelogFile, nextChangelog);
1055
+ function dedupeRecentUpdates(updates) {
1056
+ const seenUpdates = new Set();
1057
+ const result = [];
253
1058
 
254
- if (logger) {
255
- logger.debug(`Updated AI context for ${validEvents.length} change(s).`);
1059
+ for (const update of updates.filter(Boolean)) {
1060
+ const key = `${update.title}::${update.type}::${update.impact}`;
1061
+
1062
+ if (seenUpdates.has(key)) {
1063
+ continue;
1064
+ }
1065
+
1066
+ seenUpdates.add(key);
1067
+ result.push(update);
256
1068
  }
257
1069
 
258
- if (typeof settings.syncCallback === 'function') {
259
- await settings.syncCallback();
1070
+ return result;
1071
+ }
1072
+
1073
+ function dedupeHistoryEntries(entries) {
1074
+ const seenEntries = new Set();
1075
+ const result = [];
1076
+
1077
+ for (const entry of entries.filter(Boolean)) {
1078
+ const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
1079
+
1080
+ if (seenEntries.has(key)) {
1081
+ continue;
1082
+ }
1083
+
1084
+ seenEntries.add(key);
1085
+ result.push(entry);
260
1086
  }
261
1087
 
262
- return nextState;
1088
+ return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
1089
+ }
1090
+
1091
+ function promoteFeatures(history) {
1092
+ if (!Array.isArray(history) || history.length === 0) {
1093
+ return [];
1094
+ }
1095
+
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
+ }
1162
+
1163
+ for (const feature of rankedFeatures) {
1164
+ if (promoted.length >= MAX_KEY_FEATURES) {
1165
+ break;
1166
+ }
1167
+
1168
+ if (seenFeatureNames.has(feature.featureName)) {
1169
+ continue;
1170
+ }
1171
+
1172
+ promoted.push(feature.featureName);
1173
+ seenFeatureNames.add(feature.featureName);
1174
+ }
1175
+
1176
+ return promoted.slice(0, MAX_KEY_FEATURES);
1177
+ }
1178
+
1179
+ function scoreFeatureEntry(entry) {
1180
+ const typeWeights = {
1181
+ feature: 4,
1182
+ refactor: 3,
1183
+ improvement: 2,
1184
+ fix: 2
1185
+ };
1186
+
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
+ };
1208
+ }
1209
+
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';
1237
+ }
1238
+
1239
+ function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
1240
+ const knownIssues = [];
1241
+
1242
+ if (!hasTestIndicators(projectRoot)) {
1243
+ knownIssues.push('No automated test suite is detected yet.');
1244
+ }
1245
+
1246
+ if (!techStack.framework) {
1247
+ knownIssues.push('No common application framework dependency is currently detected.');
1248
+ }
1249
+
1250
+ if (keyFeatures.length < 2) {
1251
+ knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
1252
+ }
1253
+
1254
+ return knownIssues;
1255
+ }
1256
+
1257
+ function hasTestIndicators(projectRoot) {
1258
+ const testPaths = [
1259
+ 'test',
1260
+ 'tests',
1261
+ '__tests__',
1262
+ 'vitest.config.js',
1263
+ 'jest.config.js',
1264
+ 'jest.config.cjs',
1265
+ 'jest.config.mjs'
1266
+ ];
1267
+
1268
+ return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
1269
+ }
1270
+
1271
+ function determineCurrentStage(keyFeatures, historyEntries) {
1272
+ if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
1273
+ return 'Production-ready';
1274
+ }
1275
+
1276
+ if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
1277
+ return 'Functional prototype';
1278
+ }
1279
+
1280
+ return 'Early development';
1281
+ }
1282
+
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';
1290
+
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';
1297
+ }
1298
+
1299
+ if (
1300
+ featureSignals.has('github_sync') ||
1301
+ featureSignals.has('context_delivery_system') ||
1302
+ featureSignals.has('cli_orchestration')
1303
+ ) {
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';
1307
+ }
1308
+
1309
+ return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
1310
+ }
1311
+
1312
+ function collectFeatureSignals(historyEntries, keyFeatures) {
1313
+ const featureSignals = new Set();
1314
+
1315
+ for (const entry of historyEntries) {
1316
+ if (entry.feature_key) {
1317
+ featureSignals.add(entry.feature_key);
1318
+ }
1319
+ }
1320
+
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
+ }
1327
+ }
1328
+
1329
+ return featureSignals;
1330
+ }
1331
+
1332
+ function generateNextSteps(state, historyEntries) {
1333
+ 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
+
1340
+ 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.');
1342
+ }
1343
+
1344
+ if (!featureSignals.has('project_intelligence')) {
1345
+ nextSteps.push('Strengthen the intelligence engine so more project-level capabilities are captured automatically.');
1346
+ }
1347
+
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.');
1354
+ }
1355
+
1356
+ if (state.current_stage === 'Early development') {
1357
+ nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
1358
+ }
1359
+
1360
+ return Array.from(new Set(nextSteps)).slice(0, 4);
1361
+ }
1362
+
1363
+ function capitalize(value) {
1364
+ if (!value) {
1365
+ return '';
1366
+ }
1367
+
1368
+ return value.charAt(0).toUpperCase() + value.slice(1);
263
1369
  }
264
1370
 
265
1371
  function createDebouncedStateUpdater(projectRoot, options) {
@@ -331,9 +1437,14 @@ module.exports = {
331
1437
  detectProjectMetadata,
332
1438
  ensureContextDirectory,
333
1439
  getContextPaths,
1440
+ groupEventsByIntent,
1441
+ interpretChange,
334
1442
  loadRuntimeConfig,
1443
+ promoteFeatures,
335
1444
  readJsonFile,
336
1445
  renderTemplate,
1446
+ scoreEvent,
1447
+ shouldIgnoreProjectFile,
337
1448
  updateRuntimeConfig,
338
1449
  updateProjectState,
339
1450
  writeJsonAtomic,