aibridge-context 1.3.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.
Files changed (2) hide show
  1. package/core/stateManager.js +666 -165
  2. package/package.json +1 -1
@@ -5,10 +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 = 8;
8
+ const MAX_RECENT_UPDATES = 5;
9
9
  const MAX_CHANGELOG_ENTRIES = 50;
10
+ const MAX_KEY_FEATURES = 6;
10
11
  const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
11
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
+ };
12
82
 
13
83
  const DEFAULT_CONFIG = {
14
84
  port: 3333,
@@ -237,24 +307,21 @@ async function writeTextAtomic(filePath, content) {
237
307
 
238
308
  function createDefaultState(projectRoot) {
239
309
  const metadata = detectProjectMetadata(projectRoot);
240
- const keyFeatures = deriveKeyFeatures(projectRoot);
241
- const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
242
- const currentStage = determineCurrentStage(keyFeatures);
243
310
  const state = {
244
311
  project: metadata.project,
245
312
  version: metadata.version,
246
313
  last_updated: new Date(0).toISOString(),
247
314
  ai_summary: '',
248
315
  tech_stack: metadata.techStack,
249
- current_stage: currentStage,
316
+ current_stage: 'Early development',
250
317
  recent_updates: [],
251
- key_features: keyFeatures,
252
- known_issues: knownIssues,
318
+ key_features: [],
319
+ known_issues: deriveKnownIssues(projectRoot, metadata.techStack, []),
253
320
  next_steps: []
254
321
  };
255
322
 
256
- state.ai_summary = generateAiSummary(state);
257
- state.next_steps = generateNextSteps(state);
323
+ state.ai_summary = generateAiSummary(state, []);
324
+ state.next_steps = generateNextSteps(state, []);
258
325
 
259
326
  return state;
260
327
  }
@@ -310,40 +377,39 @@ async function updateProjectState(projectRoot, changeEvent, options) {
310
377
  const meaningfulEvents = collapseEventsByFile(
311
378
  validEvents.filter((event) => isMeaningfulEvent(event))
312
379
  );
313
- const interpretedEvents = meaningfulEvents
314
- .map((event) => interpretChange(event))
315
- .filter(Boolean);
316
- const previousRecentUpdates = normalizeStoredUpdates(existingState.recent_updates);
380
+ const groupedUpdates = groupEventsByIntent(meaningfulEvents);
381
+ const capabilityHistory = buildProjectCapabilityHistory(projectRoot);
317
382
  const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
318
- const recentUpdates = dedupeRecentUpdates(
319
- interpretedEvents.map(toStateUpdate).concat(previousRecentUpdates)
320
- ).slice(0, MAX_RECENT_UPDATES);
321
383
  const historyEntries = dedupeHistoryEntries(
322
- interpretedEvents.concat(previousHistoryEntries)
384
+ groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
323
385
  ).slice(0, MAX_CHANGELOG_ENTRIES);
324
- const keyFeatures = deriveKeyFeatures(projectRoot);
325
- const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
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);
326
392
  const nextState = {
327
393
  project: metadata.project,
328
394
  version: metadata.version,
329
395
  last_updated: timestamp,
330
396
  ai_summary: '',
331
397
  tech_stack: metadata.techStack,
332
- current_stage: determineCurrentStage(keyFeatures),
398
+ current_stage: determineCurrentStage(keyFeatures, historyEntries),
333
399
  recent_updates: recentUpdates,
334
400
  key_features: keyFeatures,
335
401
  known_issues: knownIssues,
336
402
  next_steps: []
337
403
  };
338
404
 
339
- nextState.ai_summary = generateAiSummary(nextState);
340
- nextState.next_steps = generateNextSteps(nextState);
405
+ nextState.ai_summary = generateAiSummary(nextState, historyEntries);
406
+ nextState.next_steps = generateNextSteps(nextState, historyEntries);
341
407
 
342
408
  await writeJsonAtomic(contextPaths.stateFile, nextState);
343
409
  await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
344
410
 
345
411
  if (logger) {
346
- logger.debug(`Updated AI context with ${interpretedEvents.length} meaningful change(s).`);
412
+ logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
347
413
  }
348
414
 
349
415
  if (typeof settings.syncCallback === 'function') {
@@ -460,22 +526,6 @@ function collapseEventsByFile(events) {
460
526
  return Array.from(collapsedEvents.values());
461
527
  }
462
528
 
463
- function interpretChange(event) {
464
- const filePath = normalizeProjectPath(event.file);
465
- const area = classifyChangeArea(filePath);
466
- const subject = describeChangeSubject(filePath, area);
467
- const type = mapActionToType(event.action);
468
- const title = `${mapActionToVerb(event.action)} ${subject}`;
469
-
470
- return {
471
- timestamp: event.timestamp || new Date().toISOString(),
472
- file: filePath,
473
- title,
474
- type,
475
- impact: describeImpact(area, subject, event.action)
476
- };
477
- }
478
-
479
529
  function classifyChangeArea(filePath) {
480
530
  const lowerPath = filePath.toLowerCase();
481
531
 
@@ -496,7 +546,7 @@ function classifyChangeArea(filePath) {
496
546
  }
497
547
 
498
548
  if (lowerPath.startsWith('bin/')) {
499
- return 'CLI';
549
+ return 'cli';
500
550
  }
501
551
 
502
552
  if (lowerPath.startsWith('templates/')) {
@@ -506,135 +556,335 @@ function classifyChangeArea(filePath) {
506
556
  return 'project';
507
557
  }
508
558
 
509
- function describeChangeSubject(filePath, area) {
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) {
510
571
  const lowerPath = filePath.toLowerCase();
511
- const baseName = path.basename(filePath);
512
572
 
513
573
  if (lowerPath === 'package.json') {
514
- return 'dependency configuration';
574
+ return 'package_configuration';
515
575
  }
516
576
 
517
577
  if (lowerPath === 'readme.md') {
518
578
  return 'documentation';
519
579
  }
520
580
 
521
- if (lowerPath === 'core/gitsync.js') {
522
- return 'GitHub sync logic';
523
- }
524
-
525
- if (lowerPath === 'core/statemanager.js') {
526
- return 'state intelligence logic';
581
+ if (lowerPath.startsWith('bin/')) {
582
+ return 'cli_workflow';
527
583
  }
528
584
 
529
- if (lowerPath === 'core/watcher.js') {
530
- return 'watcher logic';
585
+ if (lowerPath.startsWith('server/')) {
586
+ return 'local_context_server';
531
587
  }
532
588
 
533
- if (lowerPath === 'core/init.js') {
534
- return 'initialization flow';
589
+ if (lowerPath.startsWith('templates/')) {
590
+ return 'context_templates';
535
591
  }
536
592
 
537
- if (lowerPath === 'server/server.js') {
538
- return 'backend server';
593
+ if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
594
+ return 'github_sync';
539
595
  }
540
596
 
541
- if (lowerPath === 'server/routes.js') {
542
- return 'backend routes';
597
+ if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
598
+ return 'change_tracking';
543
599
  }
544
600
 
545
- if (lowerPath === 'bin/cli.js') {
546
- return 'CLI workflow';
601
+ if (lowerPath.includes('state') || lowerPath.includes('context')) {
602
+ return 'project_intelligence';
547
603
  }
548
604
 
549
- if (area === 'templates') {
550
- return `${humanizeFileName(baseName)} template`;
605
+ if (lowerPath.includes('init')) {
606
+ return 'project_setup';
551
607
  }
552
608
 
553
609
  if (area === 'logic') {
554
- return `${humanizeFileName(baseName)} logic`;
610
+ return 'project_intelligence';
555
611
  }
556
612
 
557
- if (area === 'backend') {
558
- return `${humanizeFileName(baseName)} backend`;
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 [];
559
632
  }
560
633
 
561
- if (area === 'CLI') {
562
- return 'CLI workflow';
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);
563
649
  }
564
650
 
565
- return humanizeFileName(baseName);
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));
566
657
  }
567
658
 
568
- function humanizeFileName(fileName) {
569
- return fileName
570
- .replace(path.extname(fileName), '')
571
- .replace(/[-_.]+/g, ' ')
572
- .replace(/\s+/g, ' ')
573
- .trim();
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);
574
683
  }
575
684
 
576
- function mapActionToType(action) {
577
- if (action === 'add') {
578
- return 'feature';
685
+ function mergeCrossAreaIntentGroups(groups) {
686
+ if (groups.length < 2) {
687
+ return groups;
579
688
  }
580
689
 
581
- if (action === 'delete') {
582
- return 'removal';
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');
583
696
  }
584
697
 
585
- return 'improvement';
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;
586
716
  }
587
717
 
588
- function mapActionToVerb(action) {
589
- if (action === 'add') {
590
- return 'Added';
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';
591
752
  }
592
753
 
593
- if (action === 'delete') {
594
- return 'Removed';
754
+ const themeCounts = new Map();
755
+
756
+ for (const event of group.events) {
757
+ themeCounts.set(event.theme, (themeCounts.get(event.theme) || 0) + 1);
595
758
  }
596
759
 
597
- return 'Updated';
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;
598
771
  }
599
772
 
600
- function describeImpact(area, subject, action) {
601
- if (action === 'delete') {
602
- return `Removes ${subject.toLowerCase()} from the project workflow.`;
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
+ }
786
+
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';
603
793
  }
604
794
 
605
- if (subject === 'GitHub sync logic') {
606
- return 'Improves reliability of context syncing.';
795
+ if (hasFixSignals(group.events)) {
796
+ return 'fix';
607
797
  }
608
798
 
609
- if (subject === 'state intelligence logic') {
610
- return 'Improves the quality of AI-readable project state.';
799
+ if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
800
+ return 'refactor';
801
+ }
802
+
803
+ return 'improvement';
804
+ }
805
+
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';
611
817
  }
612
818
 
613
- if (subject === 'watcher logic') {
614
- return 'Improves how meaningful project changes are detected.';
819
+ if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
820
+ return 'CLI workflow';
615
821
  }
616
822
 
617
- if (area === 'backend') {
618
- return 'Improves local AI context delivery.';
823
+ if (group.area === 'backend' && group.events.length > 1) {
824
+ return 'AI context delivery service';
619
825
  }
620
826
 
621
- if (area === 'CLI') {
622
- return 'Improves command-line workflow clarity and usability.';
827
+ return fallbackSubject || 'project workflow';
828
+ }
829
+
830
+ function describeIntentScope(group) {
831
+ if (group.area === 'system') {
832
+ return 'system';
623
833
  }
624
834
 
625
- if (area === 'documentation') {
626
- return 'Improves onboarding and usage clarity.';
835
+ if (group.area === 'cli_system') {
836
+ return 'CLI';
627
837
  }
628
838
 
629
- if (area === 'dependencies') {
630
- return 'Updates package behavior and dependency management.';
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 ');
631
877
  }
632
878
 
633
- if (area === 'templates') {
634
- return 'Improves generated AI context defaults.';
879
+ if (type === 'fix') {
880
+ return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
635
881
  }
636
882
 
637
- return 'Improves core project intelligence and automation.';
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;
638
888
  }
639
889
 
640
890
  function normalizeStoredUpdates(updates) {
@@ -642,11 +892,7 @@ function normalizeStoredUpdates(updates) {
642
892
  return [];
643
893
  }
644
894
 
645
- return dedupeRecentUpdates(
646
- updates
647
- .map((update) => normalizeStoredUpdate(update))
648
- .filter(Boolean)
649
- );
895
+ return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
650
896
  }
651
897
 
652
898
  function normalizeStoredUpdate(update) {
@@ -657,7 +903,7 @@ function normalizeStoredUpdate(update) {
657
903
  if (update.title && update.type && update.impact) {
658
904
  return {
659
905
  title: update.title,
660
- type: update.type,
906
+ type: normalizeUpdateType(update.type),
661
907
  impact: update.impact
662
908
  };
663
909
  }
@@ -687,12 +933,20 @@ function normalizeStoredHistoryEntry(entry) {
687
933
  }
688
934
 
689
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
+
690
941
  return {
691
942
  timestamp: entry.timestamp || new Date(0).toISOString(),
692
- file: normalizeProjectPath(entry.file || ''),
693
- title: entry.title,
694
- type: entry.type,
695
- impact: entry.impact
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'
696
950
  };
697
951
  }
698
952
 
@@ -703,10 +957,97 @@ function normalizeStoredHistoryEntry(entry) {
703
957
  return null;
704
958
  }
705
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'
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
+ }
1039
+
1040
+ return 'improvement';
1041
+ }
1042
+
706
1043
  function toStateUpdate(update) {
1044
+ if (!update) {
1045
+ return null;
1046
+ }
1047
+
707
1048
  return {
708
1049
  title: update.title,
709
- type: update.type,
1050
+ type: normalizeUpdateType(update.type),
710
1051
  impact: update.impact
711
1052
  };
712
1053
  }
@@ -715,7 +1056,7 @@ function dedupeRecentUpdates(updates) {
715
1056
  const seenUpdates = new Set();
716
1057
  const result = [];
717
1058
 
718
- for (const update of updates) {
1059
+ for (const update of updates.filter(Boolean)) {
719
1060
  const key = `${update.title}::${update.type}::${update.impact}`;
720
1061
 
721
1062
  if (seenUpdates.has(key)) {
@@ -733,8 +1074,8 @@ function dedupeHistoryEntries(entries) {
733
1074
  const seenEntries = new Set();
734
1075
  const result = [];
735
1076
 
736
- for (const entry of entries) {
737
- const key = `${entry.title}::${entry.type}::${entry.file}`;
1077
+ for (const entry of entries.filter(Boolean)) {
1078
+ const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
738
1079
 
739
1080
  if (seenEntries.has(key)) {
740
1081
  continue;
@@ -744,39 +1085,158 @@ function dedupeHistoryEntries(entries) {
744
1085
  result.push(entry);
745
1086
  }
746
1087
 
747
- return result;
1088
+ return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
748
1089
  }
749
1090
 
750
- function deriveKeyFeatures(projectRoot) {
751
- const features = [];
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
+ }
752
1114
 
753
- if (fs.existsSync(path.join(projectRoot, 'bin', 'cli.js'))) {
754
- features.push('CLI commands for initializing, linking, starting, and updating AI context');
1115
+ featureStats.set(featureKey, existing);
755
1116
  }
756
1117
 
757
- if (fs.existsSync(path.join(projectRoot, 'core', 'watcher.js'))) {
758
- features.push('Noise-filtered watcher that turns file changes into meaningful project updates');
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);
759
1144
  }
760
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) {
761
1211
  if (
762
- fs.existsSync(path.join(projectRoot, 'server', 'server.js')) &&
763
- fs.existsSync(path.join(projectRoot, 'server', 'routes.js'))
1212
+ featureKey === 'github_sync' ||
1213
+ featureKey === 'project_intelligence' ||
1214
+ featureKey === 'change_tracking' ||
1215
+ featureKey === 'project_setup'
764
1216
  ) {
765
- features.push('Local Express server for AI-readable context endpoints');
1217
+ return 'logic';
766
1218
  }
767
1219
 
768
- if (fs.existsSync(path.join(projectRoot, 'core', 'gitSync.js'))) {
769
- features.push('Optional GitHub sync for publishing public AI-readable context');
1220
+ if (featureKey === 'cli_workflow' || featureKey === 'cli_orchestration') {
1221
+ return 'cli';
770
1222
  }
771
1223
 
772
- if (fs.existsSync(path.join(projectRoot, 'core', 'stateManager.js'))) {
773
- features.push('Intelligent state engine that summarizes project evolution for AI tools');
1224
+ if (featureKey === 'local_context_server' || featureKey === 'context_delivery_system') {
1225
+ return 'backend';
1226
+ }
1227
+
1228
+ if (featureKey === 'package_configuration') {
1229
+ return 'dependencies';
774
1230
  }
775
1231
 
776
- return features;
1232
+ if (featureKey === 'documentation') {
1233
+ return 'documentation';
1234
+ }
1235
+
1236
+ return 'project';
777
1237
  }
778
1238
 
779
- function deriveKnownIssues(projectRoot, techStack) {
1239
+ function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
780
1240
  const knownIssues = [];
781
1241
 
782
1242
  if (!hasTestIndicators(projectRoot)) {
@@ -787,6 +1247,10 @@ function deriveKnownIssues(projectRoot, techStack) {
787
1247
  knownIssues.push('No common application framework dependency is currently detected.');
788
1248
  }
789
1249
 
1250
+ if (keyFeatures.length < 2) {
1251
+ knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
1252
+ }
1253
+
790
1254
  return knownIssues;
791
1255
  }
792
1256
 
@@ -804,71 +1268,106 @@ function hasTestIndicators(projectRoot) {
804
1268
  return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
805
1269
  }
806
1270
 
807
- function determineCurrentStage(keyFeatures) {
808
- const hasCli = keyFeatures.some((feature) => feature.includes('CLI commands'));
809
- const hasSync = keyFeatures.some((feature) => feature.includes('GitHub sync'));
810
- const hasServer = keyFeatures.some((feature) => feature.includes('Express server'));
811
- const hasWatcher = keyFeatures.some((feature) => feature.includes('watcher'));
812
- const hasIntelligence = keyFeatures.some((feature) => feature.includes('Intelligent state engine'));
813
-
814
- if (hasCli && hasSync && hasServer && hasWatcher && hasIntelligence) {
1271
+ function determineCurrentStage(keyFeatures, historyEntries) {
1272
+ if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
815
1273
  return 'Production-ready';
816
1274
  }
817
1275
 
818
- if (hasCli && hasSync) {
1276
+ if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
819
1277
  return 'Functional prototype';
820
1278
  }
821
1279
 
822
1280
  return 'Early development';
823
1281
  }
824
1282
 
825
- function generateAiSummary(state) {
826
- const language = state.tech_stack.language || 'Project';
827
- const hasServer = state.key_features.some((feature) => feature.includes('Express server'));
828
- const hasSync = state.key_features.some((feature) => feature.includes('GitHub sync'));
829
- const clauses = ['tracks meaningful project evolution', 'generates structured AI-readable context'];
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';
830
1290
 
831
- if (hasServer) {
832
- clauses.push('serves project context locally through Express');
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';
833
1297
  }
834
1298
 
835
- if (hasSync) {
836
- clauses.push('can publish public AI-readable context through optional GitHub sync');
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';
837
1307
  }
838
1308
 
839
- return `AI-powered ${language} CLI that ${clauses.join(', ')}.`;
1309
+ return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
840
1310
  }
841
1311
 
842
- function generateNextSteps(state) {
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) {
843
1333
  const nextSteps = [];
1334
+ const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
844
1335
 
845
1336
  if (state.key_features.length === 0) {
846
- nextSteps.push('Define core features for the AI context workflow.');
1337
+ nextSteps.push('Capture a few meaningful project milestones so stable AI-visible features can emerge from real development history.');
847
1338
  }
848
1339
 
849
1340
  if (state.known_issues.includes('No automated test suite is detected yet.')) {
850
- nextSteps.push('Add automated tests for the state engine, watcher, and GitHub sync flows.');
1341
+ nextSteps.push('Add automated tests for the intelligence engine, watcher, and GitHub sync workflow.');
851
1342
  }
852
1343
 
853
- if (state.current_stage === 'Early development') {
854
- nextSteps.push('Implement the next core workflow milestone and document how AI should use it.');
1344
+ if (!featureSignals.has('project_intelligence')) {
1345
+ nextSteps.push('Strengthen the intelligence engine so more project-level capabilities are captured automatically.');
855
1346
  }
856
1347
 
857
- if (state.current_stage === 'Functional prototype') {
858
- nextSteps.push('Harden the current feature set with tests and release validation.');
1348
+ if (!featureSignals.has('local_context_server')) {
1349
+ nextSteps.push('Expand context delivery coverage so AI consumers can reliably read current project state.');
859
1350
  }
860
1351
 
861
- if (!state.tech_stack.framework) {
862
- nextSteps.push('Document the intended framework or extend stack detection for this project.');
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.');
863
1354
  }
864
1355
 
865
- if (state.recent_updates.length === 0) {
866
- nextSteps.push('Capture the first meaningful project milestone to seed AI context history.');
1356
+ if (state.current_stage === 'Early development') {
1357
+ nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
867
1358
  }
868
1359
 
869
1360
  return Array.from(new Set(nextSteps)).slice(0, 4);
870
1361
  }
871
1362
 
1363
+ function capitalize(value) {
1364
+ if (!value) {
1365
+ return '';
1366
+ }
1367
+
1368
+ return value.charAt(0).toUpperCase() + value.slice(1);
1369
+ }
1370
+
872
1371
  function createDebouncedStateUpdater(projectRoot, options) {
873
1372
  const settings = Object.assign(
874
1373
  {
@@ -938,8 +1437,10 @@ module.exports = {
938
1437
  detectProjectMetadata,
939
1438
  ensureContextDirectory,
940
1439
  getContextPaths,
1440
+ groupEventsByIntent,
941
1441
  interpretChange,
942
1442
  loadRuntimeConfig,
1443
+ promoteFeatures,
943
1444
  readJsonFile,
944
1445
  renderTemplate,
945
1446
  scoreEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aibridge-context",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Zero-config CLI and library for generating AI-readable project context, serving it locally, and syncing it with git.",
5
5
  "main": "./index.js",
6
6
  "bin": {