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.
- package/core/stateManager.js +698 -169
- package/package.json +1 -1
package/core/stateManager.js
CHANGED
|
@@ -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
|
+
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
|
-
|
|
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:
|
|
344
|
+
current_stage: 'Early development',
|
|
250
345
|
recent_updates: [],
|
|
251
|
-
key_features:
|
|
252
|
-
known_issues:
|
|
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
|
|
314
|
-
|
|
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
|
-
|
|
412
|
+
groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
|
|
323
413
|
).slice(0, MAX_CHANGELOG_ENTRIES);
|
|
324
|
-
const
|
|
325
|
-
|
|
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 ${
|
|
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 '
|
|
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
|
|
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 '
|
|
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
|
|
522
|
-
return '
|
|
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
|
|
530
|
-
return '
|
|
613
|
+
if (lowerPath.startsWith('server/')) {
|
|
614
|
+
return 'local_context_server';
|
|
531
615
|
}
|
|
532
616
|
|
|
533
|
-
if (lowerPath
|
|
534
|
-
return '
|
|
617
|
+
if (lowerPath.startsWith('templates/')) {
|
|
618
|
+
return 'context_templates';
|
|
535
619
|
}
|
|
536
620
|
|
|
537
|
-
if (lowerPath
|
|
538
|
-
return '
|
|
621
|
+
if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
|
|
622
|
+
return 'github_sync';
|
|
539
623
|
}
|
|
540
624
|
|
|
541
|
-
if (lowerPath
|
|
542
|
-
return '
|
|
625
|
+
if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
|
|
626
|
+
return 'change_tracking';
|
|
543
627
|
}
|
|
544
628
|
|
|
545
|
-
if (lowerPath
|
|
546
|
-
return '
|
|
629
|
+
if (lowerPath.includes('state') || lowerPath.includes('context')) {
|
|
630
|
+
return 'project_intelligence';
|
|
547
631
|
}
|
|
548
632
|
|
|
549
|
-
if (
|
|
550
|
-
return
|
|
633
|
+
if (lowerPath.includes('init')) {
|
|
634
|
+
return 'project_setup';
|
|
551
635
|
}
|
|
552
636
|
|
|
553
637
|
if (area === 'logic') {
|
|
554
|
-
return
|
|
638
|
+
return 'project_intelligence';
|
|
555
639
|
}
|
|
556
640
|
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
577
|
-
if (
|
|
578
|
-
return
|
|
713
|
+
function mergeCrossAreaIntentGroups(groups) {
|
|
714
|
+
if (groups.length < 2) {
|
|
715
|
+
return groups;
|
|
579
716
|
}
|
|
580
717
|
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
594
|
-
|
|
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
|
|
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
|
|
601
|
-
|
|
602
|
-
|
|
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 (
|
|
606
|
-
return '
|
|
823
|
+
if (hasFixSignals(group.events)) {
|
|
824
|
+
return 'fix';
|
|
607
825
|
}
|
|
608
826
|
|
|
609
|
-
if (
|
|
610
|
-
return '
|
|
827
|
+
if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
|
|
828
|
+
return 'refactor';
|
|
611
829
|
}
|
|
612
830
|
|
|
613
|
-
|
|
614
|
-
|
|
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 === '
|
|
618
|
-
return '
|
|
847
|
+
if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
|
|
848
|
+
return 'CLI workflow';
|
|
619
849
|
}
|
|
620
850
|
|
|
621
|
-
if (area === '
|
|
622
|
-
return '
|
|
851
|
+
if (group.area === 'backend' && group.events.length > 1) {
|
|
852
|
+
return 'AI context delivery service';
|
|
623
853
|
}
|
|
624
854
|
|
|
625
|
-
|
|
626
|
-
|
|
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 === '
|
|
630
|
-
return '
|
|
863
|
+
if (group.area === 'cli_system') {
|
|
864
|
+
return 'CLI';
|
|
631
865
|
}
|
|
632
866
|
|
|
633
|
-
|
|
634
|
-
|
|
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
|
|
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
|
-
|
|
693
|
-
title:
|
|
694
|
-
type:
|
|
695
|
-
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.
|
|
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
|
|
751
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
763
|
-
|
|
1240
|
+
featureKey === 'github_sync' ||
|
|
1241
|
+
featureKey === 'project_intelligence' ||
|
|
1242
|
+
featureKey === 'change_tracking' ||
|
|
1243
|
+
featureKey === 'project_setup'
|
|
764
1244
|
) {
|
|
765
|
-
|
|
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 (
|
|
769
|
-
|
|
1256
|
+
if (featureKey === 'package_configuration') {
|
|
1257
|
+
return 'dependencies';
|
|
770
1258
|
}
|
|
771
1259
|
|
|
772
|
-
if (
|
|
773
|
-
|
|
1260
|
+
if (featureKey === 'documentation') {
|
|
1261
|
+
return 'documentation';
|
|
774
1262
|
}
|
|
775
1263
|
|
|
776
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
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 (
|
|
832
|
-
|
|
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 (
|
|
836
|
-
|
|
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
|
|
1337
|
+
return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
|
|
840
1338
|
}
|
|
841
1339
|
|
|
842
|
-
function
|
|
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('
|
|
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
|
|
1369
|
+
nextSteps.push('Add automated tests for the intelligence engine, watcher, and GitHub sync workflow.');
|
|
851
1370
|
}
|
|
852
1371
|
|
|
853
|
-
if (
|
|
854
|
-
nextSteps.push('
|
|
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 (
|
|
858
|
-
nextSteps.push('
|
|
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 (!
|
|
862
|
-
nextSteps.push('
|
|
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.
|
|
866
|
-
nextSteps.push('
|
|
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