aibridge-context 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/core/stateManager.js +698 -169
  2. package/package.json +1 -1
@@ -5,10 +5,110 @@ 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
+ 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
+
50
+ 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
+ }
111
+ };
12
112
 
13
113
  const DEFAULT_CONFIG = {
14
114
  port: 3333,
@@ -230,31 +330,26 @@ async function writeJsonAtomic(filePath, value) {
230
330
  }
231
331
 
232
332
  async function writeTextAtomic(filePath, content) {
233
- const tempFilePath = `${filePath}.tmp`;
234
- await fsp.writeFile(tempFilePath, content, 'utf8');
235
- await fsp.rename(tempFilePath, filePath);
333
+ await safeWriteJSON(filePath, content);
236
334
  }
237
335
 
238
336
  function createDefaultState(projectRoot) {
239
337
  const metadata = detectProjectMetadata(projectRoot);
240
- const keyFeatures = deriveKeyFeatures(projectRoot);
241
- const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
242
- const currentStage = determineCurrentStage(keyFeatures);
243
338
  const state = {
244
339
  project: metadata.project,
245
340
  version: metadata.version,
246
341
  last_updated: new Date(0).toISOString(),
247
342
  ai_summary: '',
248
343
  tech_stack: metadata.techStack,
249
- current_stage: currentStage,
344
+ current_stage: 'Early development',
250
345
  recent_updates: [],
251
- key_features: keyFeatures,
252
- known_issues: knownIssues,
346
+ key_features: [],
347
+ known_issues: deriveKnownIssues(projectRoot, metadata.techStack, []),
253
348
  next_steps: []
254
349
  };
255
350
 
256
- state.ai_summary = generateAiSummary(state);
257
- state.next_steps = generateNextSteps(state);
351
+ state.ai_summary = generateAiSummary(state, []);
352
+ state.next_steps = generateNextSteps(state, []);
258
353
 
259
354
  return state;
260
355
  }
@@ -310,40 +405,39 @@ async function updateProjectState(projectRoot, changeEvent, options) {
310
405
  const meaningfulEvents = collapseEventsByFile(
311
406
  validEvents.filter((event) => isMeaningfulEvent(event))
312
407
  );
313
- const interpretedEvents = meaningfulEvents
314
- .map((event) => interpretChange(event))
315
- .filter(Boolean);
316
- const previousRecentUpdates = normalizeStoredUpdates(existingState.recent_updates);
408
+ const groupedUpdates = groupEventsByIntent(meaningfulEvents);
409
+ const capabilityHistory = buildProjectCapabilityHistory(projectRoot);
317
410
  const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
318
- const recentUpdates = dedupeRecentUpdates(
319
- interpretedEvents.map(toStateUpdate).concat(previousRecentUpdates)
320
- ).slice(0, MAX_RECENT_UPDATES);
321
411
  const historyEntries = dedupeHistoryEntries(
322
- interpretedEvents.concat(previousHistoryEntries)
412
+ groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
323
413
  ).slice(0, MAX_CHANGELOG_ENTRIES);
324
- const keyFeatures = deriveKeyFeatures(projectRoot);
325
- const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
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);
326
420
  const nextState = {
327
421
  project: metadata.project,
328
422
  version: metadata.version,
329
423
  last_updated: timestamp,
330
424
  ai_summary: '',
331
425
  tech_stack: metadata.techStack,
332
- current_stage: determineCurrentStage(keyFeatures),
426
+ current_stage: determineCurrentStage(keyFeatures, historyEntries),
333
427
  recent_updates: recentUpdates,
334
428
  key_features: keyFeatures,
335
429
  known_issues: knownIssues,
336
430
  next_steps: []
337
431
  };
338
432
 
339
- nextState.ai_summary = generateAiSummary(nextState);
340
- nextState.next_steps = generateNextSteps(nextState);
433
+ nextState.ai_summary = generateAiSummary(nextState, historyEntries);
434
+ nextState.next_steps = generateNextSteps(nextState, historyEntries);
341
435
 
342
436
  await writeJsonAtomic(contextPaths.stateFile, nextState);
343
437
  await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
344
438
 
345
439
  if (logger) {
346
- logger.debug(`Updated AI context with ${interpretedEvents.length} meaningful change(s).`);
440
+ logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
347
441
  }
348
442
 
349
443
  if (typeof settings.syncCallback === 'function') {
@@ -460,22 +554,6 @@ function collapseEventsByFile(events) {
460
554
  return Array.from(collapsedEvents.values());
461
555
  }
462
556
 
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
557
  function classifyChangeArea(filePath) {
480
558
  const lowerPath = filePath.toLowerCase();
481
559
 
@@ -496,7 +574,7 @@ function classifyChangeArea(filePath) {
496
574
  }
497
575
 
498
576
  if (lowerPath.startsWith('bin/')) {
499
- return 'CLI';
577
+ return 'cli';
500
578
  }
501
579
 
502
580
  if (lowerPath.startsWith('templates/')) {
@@ -506,135 +584,335 @@ function classifyChangeArea(filePath) {
506
584
  return 'project';
507
585
  }
508
586
 
509
- function describeChangeSubject(filePath, area) {
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) {
510
599
  const lowerPath = filePath.toLowerCase();
511
- const baseName = path.basename(filePath);
512
600
 
513
601
  if (lowerPath === 'package.json') {
514
- return 'dependency configuration';
602
+ return 'package_configuration';
515
603
  }
516
604
 
517
605
  if (lowerPath === 'readme.md') {
518
606
  return 'documentation';
519
607
  }
520
608
 
521
- if (lowerPath === 'core/gitsync.js') {
522
- return 'GitHub sync logic';
523
- }
524
-
525
- if (lowerPath === 'core/statemanager.js') {
526
- return 'state intelligence logic';
609
+ if (lowerPath.startsWith('bin/')) {
610
+ return 'cli_workflow';
527
611
  }
528
612
 
529
- if (lowerPath === 'core/watcher.js') {
530
- return 'watcher logic';
613
+ if (lowerPath.startsWith('server/')) {
614
+ return 'local_context_server';
531
615
  }
532
616
 
533
- if (lowerPath === 'core/init.js') {
534
- return 'initialization flow';
617
+ if (lowerPath.startsWith('templates/')) {
618
+ return 'context_templates';
535
619
  }
536
620
 
537
- if (lowerPath === 'server/server.js') {
538
- return 'backend server';
621
+ if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
622
+ return 'github_sync';
539
623
  }
540
624
 
541
- if (lowerPath === 'server/routes.js') {
542
- return 'backend routes';
625
+ if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
626
+ return 'change_tracking';
543
627
  }
544
628
 
545
- if (lowerPath === 'bin/cli.js') {
546
- return 'CLI workflow';
629
+ if (lowerPath.includes('state') || lowerPath.includes('context')) {
630
+ return 'project_intelligence';
547
631
  }
548
632
 
549
- if (area === 'templates') {
550
- return `${humanizeFileName(baseName)} template`;
633
+ if (lowerPath.includes('init')) {
634
+ return 'project_setup';
551
635
  }
552
636
 
553
637
  if (area === 'logic') {
554
- return `${humanizeFileName(baseName)} logic`;
638
+ return 'project_intelligence';
555
639
  }
556
640
 
557
- if (area === 'backend') {
558
- return `${humanizeFileName(baseName)} backend`;
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
+ };
655
+ }
656
+
657
+ function groupEventsByIntent(events) {
658
+ if (!Array.isArray(events) || events.length === 0) {
659
+ return [];
559
660
  }
560
661
 
561
- if (area === 'CLI') {
562
- return 'CLI workflow';
662
+ const groupedByArea = new Map();
663
+
664
+ 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
+ });
674
+ }
675
+
676
+ groupedByArea.get(groupKey).events.push(descriptor);
563
677
  }
564
678
 
565
- return humanizeFileName(baseName);
679
+ const mergedGroups = mergeCrossAreaIntentGroups(Array.from(groupedByArea.values()));
680
+
681
+ return mergedGroups
682
+ .map((group) => interpretIntentGroup(group))
683
+ .filter(Boolean)
684
+ .sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
566
685
  }
567
686
 
568
- function humanizeFileName(fileName) {
569
- return fileName
570
- .replace(path.extname(fileName), '')
571
- .replace(/[-_.]+/g, ' ')
572
- .replace(/\s+/g, ' ')
573
- .trim();
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);
574
711
  }
575
712
 
576
- function mapActionToType(action) {
577
- if (action === 'add') {
578
- return 'feature';
713
+ function mergeCrossAreaIntentGroups(groups) {
714
+ if (groups.length < 2) {
715
+ return groups;
579
716
  }
580
717
 
581
- if (action === 'delete') {
582
- return 'removal';
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');
583
724
  }
584
725
 
585
- return 'improvement';
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
+ 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) => {
752
+ 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);
756
+ const type = determineGroupedUpdateType(group);
757
+ const subject = describeIntentSubject(group, featureMeta.subject);
758
+
759
+ return {
760
+ timestamp: latestTimestamp,
761
+ scope: describeIntentScope(group),
762
+ title: buildIntentTitle(type, subject),
763
+ type,
764
+ impact: describeIntentImpact(type, featureKey, subject),
765
+ feature_key: featureKey,
766
+ feature_name: featureMeta.name,
767
+ source: 'event'
768
+ };
586
769
  }
587
770
 
588
- function mapActionToVerb(action) {
589
- if (action === 'add') {
590
- return 'Added';
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';
591
780
  }
592
781
 
593
- if (action === 'delete') {
594
- return 'Removed';
782
+ const themeCounts = new Map();
783
+
784
+ for (const event of group.events) {
785
+ themeCounts.set(event.theme, (themeCounts.get(event.theme) || 0) + 1);
595
786
  }
596
787
 
597
- return 'Updated';
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;
598
799
  }
599
800
 
600
- function describeImpact(area, subject, action) {
601
- if (action === 'delete') {
602
- return `Removes ${subject.toLowerCase()} from the project workflow.`;
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
810
+ };
811
+
812
+ return priorities[featureKey] || 0;
813
+ }
814
+
815
+ function determineGroupedUpdateType(group) {
816
+ const actions = new Set(group.events.map((event) => event.action));
817
+ const fileCount = group.events.length;
818
+
819
+ if (actions.has('add')) {
820
+ return 'feature';
603
821
  }
604
822
 
605
- if (subject === 'GitHub sync logic') {
606
- return 'Improves reliability of context syncing.';
823
+ if (hasFixSignals(group.events)) {
824
+ return 'fix';
607
825
  }
608
826
 
609
- if (subject === 'state intelligence logic') {
610
- return 'Improves the quality of AI-readable project state.';
827
+ if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
828
+ return 'refactor';
611
829
  }
612
830
 
613
- if (subject === 'watcher logic') {
614
- return 'Improves how meaningful project changes are detected.';
831
+ return 'improvement';
832
+ }
833
+
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';
615
845
  }
616
846
 
617
- if (area === 'backend') {
618
- return 'Improves local AI context delivery.';
847
+ if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
848
+ return 'CLI workflow';
619
849
  }
620
850
 
621
- if (area === 'CLI') {
622
- return 'Improves command-line workflow clarity and usability.';
851
+ if (group.area === 'backend' && group.events.length > 1) {
852
+ return 'AI context delivery service';
623
853
  }
624
854
 
625
- if (area === 'documentation') {
626
- return 'Improves onboarding and usage clarity.';
855
+ return fallbackSubject || 'project workflow';
856
+ }
857
+
858
+ function describeIntentScope(group) {
859
+ if (group.area === 'system') {
860
+ return 'system';
627
861
  }
628
862
 
629
- if (area === 'dependencies') {
630
- return 'Updates package behavior and dependency management.';
863
+ if (group.area === 'cli_system') {
864
+ return 'CLI';
631
865
  }
632
866
 
633
- if (area === 'templates') {
634
- return 'Improves generated AI context defaults.';
867
+ return group.area;
868
+ }
869
+
870
+ function buildIntentTitle(type, subject) {
871
+ const verbs = {
872
+ feature: 'Expanded',
873
+ improvement: 'Improved',
874
+ refactor: 'Refactored',
875
+ fix: 'Stabilized'
876
+ };
877
+
878
+ return `${verbs[type] || 'Improved'} ${subject}`;
879
+ }
880
+
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.'
895
+ };
896
+
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
+ if (type === 'fix') {
908
+ return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
635
909
  }
636
910
 
637
- return 'Improves core project intelligence and automation.';
911
+ return impactByFeature[featureKey] || 'Improves the overall project workflow for maintaining AI-readable context.';
912
+ }
913
+
914
+ function interpretChange(event) {
915
+ return groupEventsByIntent([event])[0] || null;
638
916
  }
639
917
 
640
918
  function normalizeStoredUpdates(updates) {
@@ -642,11 +920,7 @@ function normalizeStoredUpdates(updates) {
642
920
  return [];
643
921
  }
644
922
 
645
- return dedupeRecentUpdates(
646
- updates
647
- .map((update) => normalizeStoredUpdate(update))
648
- .filter(Boolean)
649
- );
923
+ return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
650
924
  }
651
925
 
652
926
  function normalizeStoredUpdate(update) {
@@ -657,7 +931,7 @@ function normalizeStoredUpdate(update) {
657
931
  if (update.title && update.type && update.impact) {
658
932
  return {
659
933
  title: update.title,
660
- type: update.type,
934
+ type: normalizeUpdateType(update.type),
661
935
  impact: update.impact
662
936
  };
663
937
  }
@@ -687,12 +961,20 @@ function normalizeStoredHistoryEntry(entry) {
687
961
  }
688
962
 
689
963
  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
+
690
969
  return {
691
970
  timestamp: entry.timestamp || new Date(0).toISOString(),
692
- file: normalizeProjectPath(entry.file || ''),
693
- title: entry.title,
694
- type: entry.type,
695
- impact: entry.impact
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'
696
978
  };
697
979
  }
698
980
 
@@ -703,10 +985,97 @@ function normalizeStoredHistoryEntry(entry) {
703
985
  return null;
704
986
  }
705
987
 
988
+ function inferFeatureFromEntry(entry) {
989
+ const combinedText = `${entry.title || ''} ${entry.impact || ''}`.toLowerCase();
990
+
991
+ if (combinedText.includes('github') || combinedText.includes('sync')) {
992
+ return {
993
+ featureKey: 'github_sync',
994
+ featureName: getFeatureMeta('github_sync').name,
995
+ scope: 'logic'
996
+ };
997
+ }
998
+
999
+ if (combinedText.includes('state') || combinedText.includes('intelligence')) {
1000
+ return {
1001
+ featureKey: 'project_intelligence',
1002
+ featureName: getFeatureMeta('project_intelligence').name,
1003
+ scope: 'logic'
1004
+ };
1005
+ }
1006
+
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
+ };
1013
+ }
1014
+
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
+ };
1026
+ }
1027
+
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
+ };
1034
+ }
1035
+
1036
+ if (combinedText.includes('documentation') || combinedText.includes('onboarding')) {
1037
+ return {
1038
+ featureKey: 'documentation',
1039
+ featureName: getFeatureMeta('documentation').name,
1040
+ scope: 'documentation'
1041
+ };
1042
+ }
1043
+
1044
+ if (combinedText.includes('package') || combinedText.includes('dependency')) {
1045
+ return {
1046
+ featureKey: 'package_configuration',
1047
+ featureName: getFeatureMeta('package_configuration').name,
1048
+ scope: 'dependencies'
1049
+ };
1050
+ }
1051
+
1052
+ return {
1053
+ featureKey: 'project_workflow',
1054
+ featureName: getFeatureMeta('project_workflow').name,
1055
+ scope: 'project'
1056
+ };
1057
+ }
1058
+
1059
+ function normalizeUpdateType(type) {
1060
+ if (type === 'removal') {
1061
+ return 'refactor';
1062
+ }
1063
+
1064
+ if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
1065
+ return type;
1066
+ }
1067
+
1068
+ return 'improvement';
1069
+ }
1070
+
706
1071
  function toStateUpdate(update) {
1072
+ if (!update) {
1073
+ return null;
1074
+ }
1075
+
707
1076
  return {
708
1077
  title: update.title,
709
- type: update.type,
1078
+ type: normalizeUpdateType(update.type),
710
1079
  impact: update.impact
711
1080
  };
712
1081
  }
@@ -715,7 +1084,7 @@ function dedupeRecentUpdates(updates) {
715
1084
  const seenUpdates = new Set();
716
1085
  const result = [];
717
1086
 
718
- for (const update of updates) {
1087
+ for (const update of updates.filter(Boolean)) {
719
1088
  const key = `${update.title}::${update.type}::${update.impact}`;
720
1089
 
721
1090
  if (seenUpdates.has(key)) {
@@ -733,8 +1102,8 @@ function dedupeHistoryEntries(entries) {
733
1102
  const seenEntries = new Set();
734
1103
  const result = [];
735
1104
 
736
- for (const entry of entries) {
737
- const key = `${entry.title}::${entry.type}::${entry.file}`;
1105
+ for (const entry of entries.filter(Boolean)) {
1106
+ const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
738
1107
 
739
1108
  if (seenEntries.has(key)) {
740
1109
  continue;
@@ -744,39 +1113,158 @@ function dedupeHistoryEntries(entries) {
744
1113
  result.push(entry);
745
1114
  }
746
1115
 
747
- return result;
1116
+ return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
748
1117
  }
749
1118
 
750
- function deriveKeyFeatures(projectRoot) {
751
- const features = [];
1119
+ function promoteFeatures(history) {
1120
+ if (!Array.isArray(history) || history.length === 0) {
1121
+ return [];
1122
+ }
1123
+
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();
752
1164
 
753
- if (fs.existsSync(path.join(projectRoot, 'bin', 'cli.js'))) {
754
- features.push('CLI commands for initializing, linking, starting, and updating AI context');
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);
755
1189
  }
756
1190
 
757
- if (fs.existsSync(path.join(projectRoot, 'core', 'watcher.js'))) {
758
- features.push('Noise-filtered watcher that turns file changes into meaningful project updates');
1191
+ for (const feature of rankedFeatures) {
1192
+ if (promoted.length >= MAX_KEY_FEATURES) {
1193
+ break;
1194
+ }
1195
+
1196
+ if (seenFeatureNames.has(feature.featureName)) {
1197
+ continue;
1198
+ }
1199
+
1200
+ promoted.push(feature.featureName);
1201
+ seenFeatureNames.add(feature.featureName);
759
1202
  }
760
1203
 
1204
+ return promoted.slice(0, MAX_KEY_FEATURES);
1205
+ }
1206
+
1207
+ function scoreFeatureEntry(entry) {
1208
+ const typeWeights = {
1209
+ feature: 4,
1210
+ refactor: 3,
1211
+ improvement: 2,
1212
+ fix: 2
1213
+ };
1214
+
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
+ };
1236
+ }
1237
+
1238
+ function inferScopeFromFeature(featureKey) {
761
1239
  if (
762
- fs.existsSync(path.join(projectRoot, 'server', 'server.js')) &&
763
- fs.existsSync(path.join(projectRoot, 'server', 'routes.js'))
1240
+ featureKey === 'github_sync' ||
1241
+ featureKey === 'project_intelligence' ||
1242
+ featureKey === 'change_tracking' ||
1243
+ featureKey === 'project_setup'
764
1244
  ) {
765
- features.push('Local Express server for AI-readable context endpoints');
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';
766
1254
  }
767
1255
 
768
- if (fs.existsSync(path.join(projectRoot, 'core', 'gitSync.js'))) {
769
- features.push('Optional GitHub sync for publishing public AI-readable context');
1256
+ if (featureKey === 'package_configuration') {
1257
+ return 'dependencies';
770
1258
  }
771
1259
 
772
- if (fs.existsSync(path.join(projectRoot, 'core', 'stateManager.js'))) {
773
- features.push('Intelligent state engine that summarizes project evolution for AI tools');
1260
+ if (featureKey === 'documentation') {
1261
+ return 'documentation';
774
1262
  }
775
1263
 
776
- return features;
1264
+ return 'project';
777
1265
  }
778
1266
 
779
- function deriveKnownIssues(projectRoot, techStack) {
1267
+ function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
780
1268
  const knownIssues = [];
781
1269
 
782
1270
  if (!hasTestIndicators(projectRoot)) {
@@ -787,6 +1275,10 @@ function deriveKnownIssues(projectRoot, techStack) {
787
1275
  knownIssues.push('No common application framework dependency is currently detected.');
788
1276
  }
789
1277
 
1278
+ if (keyFeatures.length < 2) {
1279
+ knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
1280
+ }
1281
+
790
1282
  return knownIssues;
791
1283
  }
792
1284
 
@@ -804,71 +1296,106 @@ function hasTestIndicators(projectRoot) {
804
1296
  return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
805
1297
  }
806
1298
 
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) {
1299
+ function determineCurrentStage(keyFeatures, historyEntries) {
1300
+ if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
815
1301
  return 'Production-ready';
816
1302
  }
817
1303
 
818
- if (hasCli && hasSync) {
1304
+ if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
819
1305
  return 'Functional prototype';
820
1306
  }
821
1307
 
822
1308
  return 'Early development';
823
1309
  }
824
1310
 
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'];
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';
830
1318
 
831
- if (hasServer) {
832
- clauses.push('serves project context locally through Express');
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';
833
1325
  }
834
1326
 
835
- if (hasSync) {
836
- clauses.push('can publish public AI-readable context through optional GitHub sync');
1327
+ if (
1328
+ featureSignals.has('github_sync') ||
1329
+ featureSignals.has('context_delivery_system') ||
1330
+ featureSignals.has('cli_orchestration')
1331
+ ) {
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';
837
1335
  }
838
1336
 
839
- return `AI-powered ${language} CLI that ${clauses.join(', ')}.`;
1337
+ return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
840
1338
  }
841
1339
 
842
- function generateNextSteps(state) {
1340
+ function collectFeatureSignals(historyEntries, keyFeatures) {
1341
+ const featureSignals = new Set();
1342
+
1343
+ for (const entry of historyEntries) {
1344
+ if (entry.feature_key) {
1345
+ featureSignals.add(entry.feature_key);
1346
+ }
1347
+ }
1348
+
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
+ }
1355
+ }
1356
+
1357
+ return featureSignals;
1358
+ }
1359
+
1360
+ function generateNextSteps(state, historyEntries) {
843
1361
  const nextSteps = [];
1362
+ const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
844
1363
 
845
1364
  if (state.key_features.length === 0) {
846
- nextSteps.push('Define core features for the AI context workflow.');
1365
+ nextSteps.push('Capture a few meaningful project milestones so stable AI-visible features can emerge from real development history.');
847
1366
  }
848
1367
 
849
1368
  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.');
1369
+ nextSteps.push('Add automated tests for the intelligence engine, watcher, and GitHub sync workflow.');
851
1370
  }
852
1371
 
853
- if (state.current_stage === 'Early development') {
854
- nextSteps.push('Implement the next core workflow milestone and document how AI should use it.');
1372
+ if (!featureSignals.has('project_intelligence')) {
1373
+ nextSteps.push('Strengthen the intelligence engine so more project-level capabilities are captured automatically.');
855
1374
  }
856
1375
 
857
- if (state.current_stage === 'Functional prototype') {
858
- nextSteps.push('Harden the current feature set with tests and release validation.');
1376
+ if (!featureSignals.has('local_context_server')) {
1377
+ nextSteps.push('Expand context delivery coverage so AI consumers can reliably read current project state.');
859
1378
  }
860
1379
 
861
- if (!state.tech_stack.framework) {
862
- nextSteps.push('Document the intended framework or extend stack detection for this project.');
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.');
863
1382
  }
864
1383
 
865
- if (state.recent_updates.length === 0) {
866
- nextSteps.push('Capture the first meaningful project milestone to seed AI context history.');
1384
+ if (state.current_stage === 'Early development') {
1385
+ nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
867
1386
  }
868
1387
 
869
1388
  return Array.from(new Set(nextSteps)).slice(0, 4);
870
1389
  }
871
1390
 
1391
+ function capitalize(value) {
1392
+ if (!value) {
1393
+ return '';
1394
+ }
1395
+
1396
+ return value.charAt(0).toUpperCase() + value.slice(1);
1397
+ }
1398
+
872
1399
  function createDebouncedStateUpdater(projectRoot, options) {
873
1400
  const settings = Object.assign(
874
1401
  {
@@ -938,8 +1465,10 @@ module.exports = {
938
1465
  detectProjectMetadata,
939
1466
  ensureContextDirectory,
940
1467
  getContextPaths,
1468
+ groupEventsByIntent,
941
1469
  interpretChange,
942
1470
  loadRuntimeConfig,
1471
+ promoteFeatures,
943
1472
  readJsonFile,
944
1473
  renderTemplate,
945
1474
  scoreEvent,
@@ -948,4 +1477,4 @@ module.exports = {
948
1477
  updateProjectState,
949
1478
  writeJsonAtomic,
950
1479
  writeTextAtomic
951
- };
1480
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aibridge-context",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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": {