aibridge-context 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/stateManager.js +1179 -68
- package/core/watcher.js +9 -9
- package/package.json +1 -1
- package/templates/state.template.json +9 -4
package/core/stateManager.js
CHANGED
|
@@ -5,8 +5,80 @@ const fsp = require('fs/promises');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const CONTEXT_DIR_NAME = '.ai-context';
|
|
8
|
-
const MAX_RECENT_UPDATES =
|
|
9
|
-
const MAX_CHANGELOG_ENTRIES =
|
|
8
|
+
const MAX_RECENT_UPDATES = 5;
|
|
9
|
+
const MAX_CHANGELOG_ENTRIES = 50;
|
|
10
|
+
const MAX_KEY_FEATURES = 6;
|
|
11
|
+
const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
|
|
12
|
+
const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
|
|
13
|
+
const LOW_VALUE_FEATURE_KEYS = new Set([
|
|
14
|
+
'documentation',
|
|
15
|
+
'package_configuration',
|
|
16
|
+
'context_templates',
|
|
17
|
+
'project_workflow'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const FEATURE_CATALOG = {
|
|
21
|
+
cli_workflow: {
|
|
22
|
+
name: 'CLI workflow for initializing, updating, and linking AI context',
|
|
23
|
+
subject: 'CLI workflow',
|
|
24
|
+
projectType: 'CLI tool'
|
|
25
|
+
},
|
|
26
|
+
github_sync: {
|
|
27
|
+
name: 'Public GitHub sync for AI-readable project context',
|
|
28
|
+
subject: 'GitHub sync system',
|
|
29
|
+
projectType: 'CLI tool'
|
|
30
|
+
},
|
|
31
|
+
project_intelligence: {
|
|
32
|
+
name: 'Project intelligence engine that turns development activity into AI-readable state',
|
|
33
|
+
subject: 'project intelligence engine',
|
|
34
|
+
projectType: 'project intelligence engine'
|
|
35
|
+
},
|
|
36
|
+
change_tracking: {
|
|
37
|
+
name: 'Meaningful change tracking that filters noise from project activity',
|
|
38
|
+
subject: 'change tracking engine',
|
|
39
|
+
projectType: 'change tracking engine'
|
|
40
|
+
},
|
|
41
|
+
local_context_server: {
|
|
42
|
+
name: 'Local server for AI-readable project context endpoints',
|
|
43
|
+
subject: 'AI context delivery service',
|
|
44
|
+
projectType: 'context delivery service'
|
|
45
|
+
},
|
|
46
|
+
context_delivery_system: {
|
|
47
|
+
name: 'Unified context delivery system connecting project intelligence and serving layers',
|
|
48
|
+
subject: 'AI context delivery system',
|
|
49
|
+
projectType: 'AI context system'
|
|
50
|
+
},
|
|
51
|
+
cli_orchestration: {
|
|
52
|
+
name: 'Command workflow that connects project intelligence with developer actions',
|
|
53
|
+
subject: 'CLI workflow',
|
|
54
|
+
projectType: 'CLI tool'
|
|
55
|
+
},
|
|
56
|
+
project_setup: {
|
|
57
|
+
name: 'Guided setup flow for safe AI context initialization',
|
|
58
|
+
subject: 'project setup flow',
|
|
59
|
+
projectType: 'CLI tool'
|
|
60
|
+
},
|
|
61
|
+
documentation: {
|
|
62
|
+
name: 'Developer guidance for adopting the AI context workflow',
|
|
63
|
+
subject: 'developer guidance',
|
|
64
|
+
projectType: 'project'
|
|
65
|
+
},
|
|
66
|
+
package_configuration: {
|
|
67
|
+
name: 'Package configuration for distributing the AI context CLI',
|
|
68
|
+
subject: 'package configuration',
|
|
69
|
+
projectType: 'package'
|
|
70
|
+
},
|
|
71
|
+
context_templates: {
|
|
72
|
+
name: 'Generated templates for bootstrapping AI-readable project context',
|
|
73
|
+
subject: 'generated AI context templates',
|
|
74
|
+
projectType: 'template set'
|
|
75
|
+
},
|
|
76
|
+
project_workflow: {
|
|
77
|
+
name: 'Core project workflow for maintaining AI-readable project state',
|
|
78
|
+
subject: 'project workflow',
|
|
79
|
+
projectType: 'project'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
10
82
|
|
|
11
83
|
const DEFAULT_CONFIG = {
|
|
12
84
|
port: 3333,
|
|
@@ -59,10 +131,14 @@ function isObject(value) {
|
|
|
59
131
|
function detectProjectMetadata(projectRoot) {
|
|
60
132
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
61
133
|
const packageManager = detectPackageManager(projectRoot);
|
|
134
|
+
const techStack = detectTechStack(projectRoot);
|
|
62
135
|
const metadata = {
|
|
63
136
|
project: path.basename(projectRoot),
|
|
64
137
|
version: '0.1.0',
|
|
65
|
-
|
|
138
|
+
techStack: Object.assign({}, techStack, {
|
|
139
|
+
package_manager: packageManager
|
|
140
|
+
}),
|
|
141
|
+
stackLabel: buildStackLabel(techStack),
|
|
66
142
|
packageManager
|
|
67
143
|
};
|
|
68
144
|
|
|
@@ -73,53 +149,134 @@ function detectProjectMetadata(projectRoot) {
|
|
|
73
149
|
try {
|
|
74
150
|
const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
|
|
75
151
|
const parsedPackage = JSON.parse(rawPackage);
|
|
76
|
-
const dependencies = Object.assign(
|
|
77
|
-
{},
|
|
78
|
-
parsedPackage.dependencies || {},
|
|
79
|
-
parsedPackage.devDependencies || {}
|
|
80
|
-
);
|
|
81
152
|
|
|
82
153
|
metadata.project = parsedPackage.name || metadata.project;
|
|
83
154
|
metadata.version = parsedPackage.version || metadata.version;
|
|
84
|
-
metadata.stackLabel = describeStack(dependencies);
|
|
85
|
-
metadata.packageManager = packageManager;
|
|
86
155
|
} catch (error) {
|
|
87
|
-
metadata.stackLabel =
|
|
156
|
+
metadata.stackLabel = buildStackLabel(metadata.techStack);
|
|
88
157
|
}
|
|
89
158
|
|
|
90
159
|
return metadata;
|
|
91
160
|
}
|
|
92
161
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
162
|
+
function detectTechStack(projectRoot) {
|
|
163
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
164
|
+
const pythonMarkers = ['pyproject.toml', 'requirements.txt', 'setup.py'];
|
|
165
|
+
const hasPackageJson = fs.existsSync(packageJsonPath);
|
|
166
|
+
const hasPythonMarker = pythonMarkers.some((marker) =>
|
|
167
|
+
fs.existsSync(path.join(projectRoot, marker))
|
|
168
|
+
);
|
|
169
|
+
let dependencies = {};
|
|
170
|
+
|
|
171
|
+
if (hasPackageJson) {
|
|
172
|
+
try {
|
|
173
|
+
const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
|
|
174
|
+
const parsedPackage = JSON.parse(rawPackage);
|
|
175
|
+
dependencies = Object.assign(
|
|
176
|
+
{},
|
|
177
|
+
parsedPackage.dependencies || {},
|
|
178
|
+
parsedPackage.devDependencies || {}
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
dependencies = {};
|
|
182
|
+
}
|
|
96
183
|
}
|
|
97
184
|
|
|
98
|
-
|
|
99
|
-
|
|
185
|
+
let language = '';
|
|
186
|
+
let runtime = '';
|
|
187
|
+
|
|
188
|
+
if (hasPackageJson || hasAnyFileExtension(projectRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
|
|
189
|
+
language = 'Node.js';
|
|
190
|
+
runtime = 'Node.js';
|
|
191
|
+
} else if (hasPythonMarker || hasAnyFileExtension(projectRoot, ['.py'])) {
|
|
192
|
+
language = 'Python';
|
|
193
|
+
runtime = 'Python';
|
|
100
194
|
}
|
|
101
195
|
|
|
102
|
-
return
|
|
196
|
+
return {
|
|
197
|
+
language,
|
|
198
|
+
framework: detectFramework(dependencies),
|
|
199
|
+
runtime,
|
|
200
|
+
package_manager: detectPackageManager(projectRoot)
|
|
201
|
+
};
|
|
103
202
|
}
|
|
104
203
|
|
|
105
|
-
function
|
|
204
|
+
function detectFramework(dependencies) {
|
|
106
205
|
if (dependencies.next) {
|
|
107
|
-
return '
|
|
206
|
+
return 'Next.js';
|
|
108
207
|
}
|
|
109
208
|
|
|
110
209
|
if (dependencies.react) {
|
|
111
|
-
return '
|
|
210
|
+
return 'React';
|
|
112
211
|
}
|
|
113
212
|
|
|
114
213
|
if (dependencies.express) {
|
|
115
|
-
return '
|
|
214
|
+
return 'Express';
|
|
116
215
|
}
|
|
117
216
|
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildStackLabel(techStack) {
|
|
221
|
+
const parts = [techStack.language, techStack.framework].filter(Boolean);
|
|
222
|
+
return parts.length > 0 ? parts.join(' + ') : 'Project';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasAnyFileExtension(projectRoot, extensions) {
|
|
226
|
+
return scanProjectFiles(projectRoot, 2).some((filePath) =>
|
|
227
|
+
extensions.includes(path.extname(filePath).toLowerCase())
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function scanProjectFiles(projectRoot, maxDepth) {
|
|
232
|
+
const results = [];
|
|
233
|
+
|
|
234
|
+
function visit(currentDir, depth) {
|
|
235
|
+
if (depth > maxDepth) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let entries = [];
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
249
|
+
const relativePath = normalizeProjectPath(path.relative(projectRoot, fullPath));
|
|
250
|
+
|
|
251
|
+
if (entry.isDirectory()) {
|
|
252
|
+
if (shouldIgnoreProjectFile(relativePath)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
visit(fullPath, depth + 1);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!shouldIgnoreProjectFile(relativePath)) {
|
|
261
|
+
results.push(relativePath);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
visit(projectRoot, 0);
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function detectPackageManager(projectRoot) {
|
|
271
|
+
if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
|
|
272
|
+
return 'pnpm';
|
|
120
273
|
}
|
|
121
274
|
|
|
122
|
-
|
|
275
|
+
if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
|
|
276
|
+
return 'yarn';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return 'npm';
|
|
123
280
|
}
|
|
124
281
|
|
|
125
282
|
async function ensureContextDirectory(projectRoot) {
|
|
@@ -150,19 +307,23 @@ async function writeTextAtomic(filePath, content) {
|
|
|
150
307
|
|
|
151
308
|
function createDefaultState(projectRoot) {
|
|
152
309
|
const metadata = detectProjectMetadata(projectRoot);
|
|
153
|
-
|
|
154
|
-
return {
|
|
310
|
+
const state = {
|
|
155
311
|
project: metadata.project,
|
|
156
312
|
version: metadata.version,
|
|
157
313
|
last_updated: new Date(0).toISOString(),
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
314
|
+
ai_summary: '',
|
|
315
|
+
tech_stack: metadata.techStack,
|
|
316
|
+
current_stage: 'Early development',
|
|
162
317
|
recent_updates: [],
|
|
163
|
-
|
|
318
|
+
key_features: [],
|
|
319
|
+
known_issues: deriveKnownIssues(projectRoot, metadata.techStack, []),
|
|
164
320
|
next_steps: []
|
|
165
321
|
};
|
|
322
|
+
|
|
323
|
+
state.ai_summary = generateAiSummary(state, []);
|
|
324
|
+
state.next_steps = generateNextSteps(state, []);
|
|
325
|
+
|
|
326
|
+
return state;
|
|
166
327
|
}
|
|
167
328
|
|
|
168
329
|
function createDefaultChangelog() {
|
|
@@ -204,62 +365,1007 @@ async function updateProjectState(projectRoot, changeEvent, options) {
|
|
|
204
365
|
);
|
|
205
366
|
const logger = settings.logger;
|
|
206
367
|
const contextPaths = getContextPaths(projectRoot);
|
|
207
|
-
const
|
|
208
|
-
const
|
|
368
|
+
const metadata = detectProjectMetadata(projectRoot);
|
|
369
|
+
const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
|
|
370
|
+
const existingChangelog = await readJsonFile(
|
|
371
|
+
contextPaths.changelogFile,
|
|
372
|
+
createDefaultChangelog()
|
|
373
|
+
);
|
|
209
374
|
const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
|
|
210
375
|
const validEvents = normalizedEvents.filter(Boolean);
|
|
376
|
+
const timestamp = determineUpdateTimestamp(validEvents);
|
|
377
|
+
const meaningfulEvents = collapseEventsByFile(
|
|
378
|
+
validEvents.filter((event) => isMeaningfulEvent(event))
|
|
379
|
+
);
|
|
380
|
+
const groupedUpdates = groupEventsByIntent(meaningfulEvents);
|
|
381
|
+
const capabilityHistory = buildProjectCapabilityHistory(projectRoot);
|
|
382
|
+
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
383
|
+
const historyEntries = dedupeHistoryEntries(
|
|
384
|
+
groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
|
|
385
|
+
).slice(0, MAX_CHANGELOG_ENTRIES);
|
|
386
|
+
const recentUpdates = historyEntries
|
|
387
|
+
.filter((entry) => entry.source !== 'project_snapshot')
|
|
388
|
+
.slice(0, MAX_RECENT_UPDATES)
|
|
389
|
+
.map(toStateUpdate);
|
|
390
|
+
const keyFeatures = promoteFeatures(historyEntries);
|
|
391
|
+
const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack, keyFeatures);
|
|
392
|
+
const nextState = {
|
|
393
|
+
project: metadata.project,
|
|
394
|
+
version: metadata.version,
|
|
395
|
+
last_updated: timestamp,
|
|
396
|
+
ai_summary: '',
|
|
397
|
+
tech_stack: metadata.techStack,
|
|
398
|
+
current_stage: determineCurrentStage(keyFeatures, historyEntries),
|
|
399
|
+
recent_updates: recentUpdates,
|
|
400
|
+
key_features: keyFeatures,
|
|
401
|
+
known_issues: knownIssues,
|
|
402
|
+
next_steps: []
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
nextState.ai_summary = generateAiSummary(nextState, historyEntries);
|
|
406
|
+
nextState.next_steps = generateNextSteps(nextState, historyEntries);
|
|
407
|
+
|
|
408
|
+
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
409
|
+
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
410
|
+
|
|
411
|
+
if (logger) {
|
|
412
|
+
logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (typeof settings.syncCallback === 'function') {
|
|
416
|
+
await settings.syncCallback();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return nextState;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function determineUpdateTimestamp(events) {
|
|
423
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
424
|
+
return new Date().toISOString();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const latestEvent = events[events.length - 1];
|
|
428
|
+
return latestEvent.timestamp || new Date().toISOString();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function normalizeProjectPath(filePath) {
|
|
432
|
+
return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function shouldIgnoreProjectFile(filePath) {
|
|
436
|
+
const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
|
|
437
|
+
|
|
438
|
+
if (!normalizedPath) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const segments = normalizedPath.split('/');
|
|
443
|
+
const baseName = segments[segments.length - 1];
|
|
444
|
+
|
|
445
|
+
if (
|
|
446
|
+
segments.includes('node_modules') ||
|
|
447
|
+
segments.includes('.git') ||
|
|
448
|
+
segments.includes('.ai-context') ||
|
|
449
|
+
segments.includes('dist') ||
|
|
450
|
+
segments.includes('build')
|
|
451
|
+
) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (baseName.startsWith('.start')) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (
|
|
460
|
+
baseName.endsWith('.log') ||
|
|
461
|
+
baseName.endsWith('.tmp') ||
|
|
462
|
+
baseName.endsWith('.lock') ||
|
|
463
|
+
baseName === 'package-lock.json' ||
|
|
464
|
+
baseName === 'yarn.lock' ||
|
|
465
|
+
baseName === 'pnpm-lock.yaml' ||
|
|
466
|
+
/^tmp[._-]/i.test(baseName) ||
|
|
467
|
+
/^temp[._-]/i.test(baseName) ||
|
|
468
|
+
/^debug[._-]/i.test(baseName)
|
|
469
|
+
) {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function scoreEvent(filePath) {
|
|
477
|
+
const normalizedPath = normalizeProjectPath(filePath);
|
|
478
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
479
|
+
const baseName = path.basename(normalizedPath).toLowerCase();
|
|
480
|
+
let score = 0;
|
|
481
|
+
|
|
482
|
+
if (shouldIgnoreProjectFile(normalizedPath)) {
|
|
483
|
+
return -5;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
|
|
487
|
+
score += 3;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
|
|
491
|
+
score += 2;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (lowerPath === 'package.json') {
|
|
495
|
+
score += 2;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (lowerPath === 'readme.md') {
|
|
499
|
+
score += 1;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return score;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isMeaningfulEvent(event) {
|
|
506
|
+
if (!event || !event.file) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return scoreEvent(event.file) >= 2;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function collapseEventsByFile(events) {
|
|
514
|
+
const collapsedEvents = new Map();
|
|
515
|
+
|
|
516
|
+
for (const event of events) {
|
|
517
|
+
const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
|
|
518
|
+
collapsedEvents.set(
|
|
519
|
+
normalizedFile,
|
|
520
|
+
Object.assign({}, event, {
|
|
521
|
+
file: normalizeProjectPath(event.file)
|
|
522
|
+
})
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return Array.from(collapsedEvents.values());
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function classifyChangeArea(filePath) {
|
|
530
|
+
const lowerPath = filePath.toLowerCase();
|
|
531
|
+
|
|
532
|
+
if (lowerPath === 'package.json') {
|
|
533
|
+
return 'dependencies';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (lowerPath === 'readme.md') {
|
|
537
|
+
return 'documentation';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (lowerPath.startsWith('core/')) {
|
|
541
|
+
return 'logic';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (lowerPath.startsWith('server/')) {
|
|
545
|
+
return 'backend';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (lowerPath.startsWith('bin/')) {
|
|
549
|
+
return 'cli';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (lowerPath.startsWith('templates/')) {
|
|
553
|
+
return 'templates';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return 'project';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function describeRootDirectory(filePath) {
|
|
560
|
+
const normalizedPath = normalizeProjectPath(filePath);
|
|
561
|
+
const segments = normalizedPath.split('/');
|
|
562
|
+
|
|
563
|
+
if (segments.length === 1) {
|
|
564
|
+
return segments[0] || 'project';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return segments[0] || 'project';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function detectIntentTheme(filePath, area) {
|
|
571
|
+
const lowerPath = filePath.toLowerCase();
|
|
572
|
+
|
|
573
|
+
if (lowerPath === 'package.json') {
|
|
574
|
+
return 'package_configuration';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (lowerPath === 'readme.md') {
|
|
578
|
+
return 'documentation';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (lowerPath.startsWith('bin/')) {
|
|
582
|
+
return 'cli_workflow';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (lowerPath.startsWith('server/')) {
|
|
586
|
+
return 'local_context_server';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (lowerPath.startsWith('templates/')) {
|
|
590
|
+
return 'context_templates';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
|
|
594
|
+
return 'github_sync';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (lowerPath.includes('watcher') || lowerPath.includes('watch')) {
|
|
598
|
+
return 'change_tracking';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (lowerPath.includes('state') || lowerPath.includes('context')) {
|
|
602
|
+
return 'project_intelligence';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (lowerPath.includes('init')) {
|
|
606
|
+
return 'project_setup';
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (area === 'logic') {
|
|
610
|
+
return 'project_intelligence';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return 'project_workflow';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function createEventDescriptor(event) {
|
|
617
|
+
const normalizedPath = normalizeProjectPath(event.file);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
621
|
+
action: event.action || 'change',
|
|
622
|
+
file: normalizedPath,
|
|
623
|
+
area: classifyChangeArea(normalizedPath),
|
|
624
|
+
rootDirectory: describeRootDirectory(normalizedPath),
|
|
625
|
+
theme: detectIntentTheme(normalizedPath, classifyChangeArea(normalizedPath))
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function groupEventsByIntent(events) {
|
|
630
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const groupedByArea = new Map();
|
|
635
|
+
|
|
636
|
+
for (const event of events) {
|
|
637
|
+
const descriptor = createEventDescriptor(event);
|
|
638
|
+
const groupKey = `${descriptor.area}:${descriptor.rootDirectory}`;
|
|
639
|
+
|
|
640
|
+
if (!groupedByArea.has(groupKey)) {
|
|
641
|
+
groupedByArea.set(groupKey, {
|
|
642
|
+
area: descriptor.area,
|
|
643
|
+
rootDirectory: descriptor.rootDirectory,
|
|
644
|
+
events: []
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
groupedByArea.get(groupKey).events.push(descriptor);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const mergedGroups = mergeCrossAreaIntentGroups(Array.from(groupedByArea.values()));
|
|
652
|
+
|
|
653
|
+
return mergedGroups
|
|
654
|
+
.map((group) => interpretIntentGroup(group))
|
|
655
|
+
.filter(Boolean)
|
|
656
|
+
.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function buildProjectCapabilityHistory(projectRoot) {
|
|
660
|
+
const snapshotTimestamp = new Date(0).toISOString();
|
|
661
|
+
const projectFiles = scanProjectFiles(projectRoot, 2).filter((filePath) => scoreEvent(filePath) >= 2);
|
|
662
|
+
|
|
663
|
+
if (projectFiles.length === 0) {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const capabilityBuckets = new Map();
|
|
668
|
+
|
|
669
|
+
for (const filePath of projectFiles) {
|
|
670
|
+
const area = classifyChangeArea(filePath);
|
|
671
|
+
const featureKey = detectIntentTheme(filePath, area);
|
|
672
|
+
|
|
673
|
+
if (!capabilityBuckets.has(featureKey)) {
|
|
674
|
+
capabilityBuckets.set(featureKey, []);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
capabilityBuckets.get(featureKey).push(filePath);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return Array.from(capabilityBuckets.entries())
|
|
681
|
+
.map(([featureKey, files]) => createCapabilitySnapshotEntry(featureKey, files.length, snapshotTimestamp))
|
|
682
|
+
.filter(Boolean);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function mergeCrossAreaIntentGroups(groups) {
|
|
686
|
+
if (groups.length < 2) {
|
|
687
|
+
return groups;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const logicGroup = groups.find((group) => group.area === 'logic');
|
|
691
|
+
const backendGroup = groups.find((group) => group.area === 'backend');
|
|
692
|
+
const cliGroup = groups.find((group) => group.area === 'cli');
|
|
693
|
+
|
|
694
|
+
if (logicGroup && backendGroup && groups.length <= 3) {
|
|
695
|
+
return mergeSelectedGroups(groups, [logicGroup, backendGroup], 'system');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (logicGroup && cliGroup && groups.length <= 3) {
|
|
699
|
+
return mergeSelectedGroups(groups, [logicGroup, cliGroup], 'cli_system');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return groups;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function mergeSelectedGroups(groups, groupsToMerge, mergedArea) {
|
|
706
|
+
const mergeSet = new Set(groupsToMerge);
|
|
707
|
+
const remainingGroups = groups.filter((group) => !mergeSet.has(group));
|
|
708
|
+
const mergedGroup = {
|
|
709
|
+
area: mergedArea,
|
|
710
|
+
rootDirectory: mergedArea,
|
|
711
|
+
events: groupsToMerge.flatMap((group) => group.events)
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
remainingGroups.push(mergedGroup);
|
|
715
|
+
return remainingGroups;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function interpretIntentGroup(group) {
|
|
719
|
+
if (!group || !Array.isArray(group.events) || group.events.length === 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const latestTimestamp = group.events.reduce((latest, event) => {
|
|
724
|
+
return new Date(event.timestamp) > new Date(latest) ? event.timestamp : latest;
|
|
725
|
+
}, group.events[0].timestamp);
|
|
726
|
+
const featureKey = determineFeatureKey(group);
|
|
727
|
+
const featureMeta = getFeatureMeta(featureKey);
|
|
728
|
+
const type = determineGroupedUpdateType(group);
|
|
729
|
+
const subject = describeIntentSubject(group, featureMeta.subject);
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
timestamp: latestTimestamp,
|
|
733
|
+
scope: describeIntentScope(group),
|
|
734
|
+
title: buildIntentTitle(type, subject),
|
|
735
|
+
type,
|
|
736
|
+
impact: describeIntentImpact(type, featureKey, subject),
|
|
737
|
+
feature_key: featureKey,
|
|
738
|
+
feature_name: featureMeta.name,
|
|
739
|
+
source: 'event'
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function determineFeatureKey(group) {
|
|
744
|
+
const areas = new Set(group.events.map((event) => event.area));
|
|
745
|
+
|
|
746
|
+
if (group.area === 'system' || (areas.has('logic') && areas.has('backend'))) {
|
|
747
|
+
return 'context_delivery_system';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
|
|
751
|
+
return 'cli_orchestration';
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const themeCounts = new Map();
|
|
755
|
+
|
|
756
|
+
for (const event of group.events) {
|
|
757
|
+
themeCounts.set(event.theme, (themeCounts.get(event.theme) || 0) + 1);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return Array.from(themeCounts.entries()).sort((left, right) => {
|
|
761
|
+
if (right[1] !== left[1]) {
|
|
762
|
+
return right[1] - left[1];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return getFeaturePriority(right[0]) - getFeaturePriority(left[0]);
|
|
766
|
+
})[0][0];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function getFeatureMeta(featureKey) {
|
|
770
|
+
return FEATURE_CATALOG[featureKey] || FEATURE_CATALOG.project_workflow;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function getFeaturePriority(featureKey) {
|
|
774
|
+
const priorities = {
|
|
775
|
+
project_intelligence: 7,
|
|
776
|
+
github_sync: 6,
|
|
777
|
+
local_context_server: 5,
|
|
778
|
+
change_tracking: 4,
|
|
779
|
+
cli_workflow: 3,
|
|
780
|
+
project_setup: 2,
|
|
781
|
+
project_workflow: 1
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
return priorities[featureKey] || 0;
|
|
785
|
+
}
|
|
211
786
|
|
|
212
|
-
|
|
213
|
-
|
|
787
|
+
function determineGroupedUpdateType(group) {
|
|
788
|
+
const actions = new Set(group.events.map((event) => event.action));
|
|
789
|
+
const fileCount = group.events.length;
|
|
790
|
+
|
|
791
|
+
if (actions.has('add')) {
|
|
792
|
+
return 'feature';
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (hasFixSignals(group.events)) {
|
|
796
|
+
return 'fix';
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (fileCount > 2 || group.area === 'system' || group.area === 'cli_system') {
|
|
800
|
+
return 'refactor';
|
|
214
801
|
}
|
|
215
802
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const recentUpdates = Array.isArray(state.recent_updates) ? state.recent_updates.slice() : [];
|
|
219
|
-
const changelogEntries = Array.isArray(changelog.entries) ? changelog.entries.slice() : [];
|
|
803
|
+
return 'improvement';
|
|
804
|
+
}
|
|
220
805
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
806
|
+
function hasFixSignals(events) {
|
|
807
|
+
return events.some((event) =>
|
|
808
|
+
/(fix|bug|error|guard|validate|sanitize|safe|stabilize)/i.test(event.file)
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function describeIntentSubject(group, fallbackSubject) {
|
|
813
|
+
const areas = new Set(group.events.map((event) => event.area));
|
|
814
|
+
|
|
815
|
+
if (group.area === 'system' || (areas.has('logic') && areas.has('backend'))) {
|
|
816
|
+
return 'AI context delivery system';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (group.area === 'cli_system' || (areas.has('logic') && areas.has('cli'))) {
|
|
820
|
+
return 'CLI workflow';
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (group.area === 'backend' && group.events.length > 1) {
|
|
824
|
+
return 'AI context delivery service';
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return fallbackSubject || 'project workflow';
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function describeIntentScope(group) {
|
|
831
|
+
if (group.area === 'system') {
|
|
832
|
+
return 'system';
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (group.area === 'cli_system') {
|
|
836
|
+
return 'CLI';
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return group.area;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function buildIntentTitle(type, subject) {
|
|
843
|
+
const verbs = {
|
|
844
|
+
feature: 'Expanded',
|
|
845
|
+
improvement: 'Improved',
|
|
846
|
+
refactor: 'Refactored',
|
|
847
|
+
fix: 'Stabilized'
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
return `${verbs[type] || 'Improved'} ${subject}`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function describeIntentImpact(type, featureKey, subject) {
|
|
854
|
+
const impactByFeature = {
|
|
855
|
+
cli_workflow: 'Improves how developers initialize and manage AI context from the command line.',
|
|
856
|
+
github_sync: 'Improves reliability of publishing AI-readable project context to GitHub.',
|
|
857
|
+
project_intelligence: 'Improves how project progress is summarized for AI systems.',
|
|
858
|
+
change_tracking: 'Improves how meaningful project evolution is detected without noise.',
|
|
859
|
+
local_context_server: 'Improves how AI tools consume project context through local endpoints.',
|
|
860
|
+
context_delivery_system: 'Improves reliability and structure of the end-to-end AI context delivery system.',
|
|
861
|
+
cli_orchestration: 'Improves how CLI actions drive the project intelligence workflow.',
|
|
862
|
+
project_setup: 'Improves first-run setup and configuration clarity for teams adopting AI context.',
|
|
863
|
+
documentation: 'Improves onboarding and usage clarity for developers and AI collaborators.',
|
|
864
|
+
package_configuration: 'Improves package installation and distribution behavior.',
|
|
865
|
+
context_templates: 'Improves the default AI context generated for new projects.',
|
|
866
|
+
project_workflow: 'Improves the overall project workflow for maintaining AI-readable context.'
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
if (type === 'feature') {
|
|
870
|
+
return impactByFeature[featureKey]
|
|
871
|
+
.replace(/^Improves /, 'Adds ')
|
|
872
|
+
.replace(/^Improves how /, 'Adds ')
|
|
873
|
+
.replace(/^Improves reliability of /, 'Adds ')
|
|
874
|
+
.replace(/^Improves first-run setup and configuration clarity for teams adopting /, 'Adds ')
|
|
875
|
+
.replace(/^Improves the default AI context generated for /, 'Adds ')
|
|
876
|
+
.replace(/^Improves the overall project workflow for maintaining /, 'Adds ');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (type === 'fix') {
|
|
880
|
+
return `Resolves reliability issues in the ${subject.toLowerCase()}.`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return impactByFeature[featureKey] || 'Improves the overall project workflow for maintaining AI-readable context.';
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function interpretChange(event) {
|
|
887
|
+
return groupEventsByIntent([event])[0] || null;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function normalizeStoredUpdates(updates) {
|
|
891
|
+
if (!Array.isArray(updates)) {
|
|
892
|
+
return [];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return dedupeRecentUpdates(updates.map((update) => normalizeStoredUpdate(update)).filter(Boolean));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function normalizeStoredUpdate(update) {
|
|
899
|
+
if (!update) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (update.title && update.type && update.impact) {
|
|
904
|
+
return {
|
|
905
|
+
title: update.title,
|
|
906
|
+
type: normalizeUpdateType(update.type),
|
|
907
|
+
impact: update.impact
|
|
226
908
|
};
|
|
909
|
+
}
|
|
227
910
|
|
|
228
|
-
|
|
229
|
-
|
|
911
|
+
if (update.file && update.action && isMeaningfulEvent(update)) {
|
|
912
|
+
return toStateUpdate(interpretChange(update));
|
|
230
913
|
}
|
|
231
914
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function normalizeStoredHistoryEntries(entries) {
|
|
919
|
+
if (!Array.isArray(entries)) {
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return dedupeHistoryEntries(
|
|
924
|
+
entries
|
|
925
|
+
.map((entry) => normalizeStoredHistoryEntry(entry))
|
|
926
|
+
.filter(Boolean)
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function normalizeStoredHistoryEntry(entry) {
|
|
931
|
+
if (!entry) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (entry.title && entry.type && entry.impact) {
|
|
936
|
+
const inferredFeature = inferFeatureFromEntry(entry);
|
|
937
|
+
const featureKey = entry.feature_key || inferredFeature.featureKey;
|
|
938
|
+
const normalizedType = normalizeUpdateType(entry.type);
|
|
939
|
+
const subject = describeCanonicalSubject(featureKey);
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
timestamp: entry.timestamp || new Date(0).toISOString(),
|
|
943
|
+
scope: entry.scope || inferredFeature.scope,
|
|
944
|
+
title: buildIntentTitle(normalizedType, subject),
|
|
945
|
+
type: normalizedType,
|
|
946
|
+
impact: describeIntentImpact(normalizedType, featureKey, subject),
|
|
947
|
+
feature_key: featureKey,
|
|
948
|
+
feature_name: entry.feature_name || getFeatureMeta(featureKey).name,
|
|
949
|
+
source: entry.source || 'history'
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (entry.file && entry.action && isMeaningfulEvent(entry)) {
|
|
954
|
+
return interpretChange(entry);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function inferFeatureFromEntry(entry) {
|
|
961
|
+
const combinedText = `${entry.title || ''} ${entry.impact || ''}`.toLowerCase();
|
|
962
|
+
|
|
963
|
+
if (combinedText.includes('github') || combinedText.includes('sync')) {
|
|
964
|
+
return {
|
|
965
|
+
featureKey: 'github_sync',
|
|
966
|
+
featureName: getFeatureMeta('github_sync').name,
|
|
967
|
+
scope: 'logic'
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (combinedText.includes('state') || combinedText.includes('intelligence')) {
|
|
972
|
+
return {
|
|
973
|
+
featureKey: 'project_intelligence',
|
|
974
|
+
featureName: getFeatureMeta('project_intelligence').name,
|
|
975
|
+
scope: 'logic'
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (combinedText.includes('watch') || combinedText.includes('change tracking')) {
|
|
980
|
+
return {
|
|
981
|
+
featureKey: 'change_tracking',
|
|
982
|
+
featureName: getFeatureMeta('change_tracking').name,
|
|
983
|
+
scope: 'logic'
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (
|
|
988
|
+
combinedText.includes('server') ||
|
|
989
|
+
combinedText.includes('backend') ||
|
|
990
|
+
combinedText.includes('endpoint') ||
|
|
991
|
+
combinedText.includes('delivery')
|
|
992
|
+
) {
|
|
993
|
+
return {
|
|
994
|
+
featureKey: 'local_context_server',
|
|
995
|
+
featureName: getFeatureMeta('local_context_server').name,
|
|
996
|
+
scope: 'backend'
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (combinedText.includes('cli') || combinedText.includes('command line')) {
|
|
1001
|
+
return {
|
|
1002
|
+
featureKey: 'cli_workflow',
|
|
1003
|
+
featureName: getFeatureMeta('cli_workflow').name,
|
|
1004
|
+
scope: 'cli'
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (combinedText.includes('documentation') || combinedText.includes('onboarding')) {
|
|
1009
|
+
return {
|
|
1010
|
+
featureKey: 'documentation',
|
|
1011
|
+
featureName: getFeatureMeta('documentation').name,
|
|
1012
|
+
scope: 'documentation'
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (combinedText.includes('package') || combinedText.includes('dependency')) {
|
|
1017
|
+
return {
|
|
1018
|
+
featureKey: 'package_configuration',
|
|
1019
|
+
featureName: getFeatureMeta('package_configuration').name,
|
|
1020
|
+
scope: 'dependencies'
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
featureKey: 'project_workflow',
|
|
1026
|
+
featureName: getFeatureMeta('project_workflow').name,
|
|
1027
|
+
scope: 'project'
|
|
245
1028
|
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function normalizeUpdateType(type) {
|
|
1032
|
+
if (type === 'removal') {
|
|
1033
|
+
return 'refactor';
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
|
|
1037
|
+
return type;
|
|
1038
|
+
}
|
|
246
1039
|
|
|
247
|
-
|
|
248
|
-
|
|
1040
|
+
return 'improvement';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function toStateUpdate(update) {
|
|
1044
|
+
if (!update) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
title: update.title,
|
|
1050
|
+
type: normalizeUpdateType(update.type),
|
|
1051
|
+
impact: update.impact
|
|
249
1052
|
};
|
|
1053
|
+
}
|
|
250
1054
|
|
|
251
|
-
|
|
252
|
-
|
|
1055
|
+
function dedupeRecentUpdates(updates) {
|
|
1056
|
+
const seenUpdates = new Set();
|
|
1057
|
+
const result = [];
|
|
253
1058
|
|
|
254
|
-
|
|
255
|
-
|
|
1059
|
+
for (const update of updates.filter(Boolean)) {
|
|
1060
|
+
const key = `${update.title}::${update.type}::${update.impact}`;
|
|
1061
|
+
|
|
1062
|
+
if (seenUpdates.has(key)) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
seenUpdates.add(key);
|
|
1067
|
+
result.push(update);
|
|
256
1068
|
}
|
|
257
1069
|
|
|
258
|
-
|
|
259
|
-
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function dedupeHistoryEntries(entries) {
|
|
1074
|
+
const seenEntries = new Set();
|
|
1075
|
+
const result = [];
|
|
1076
|
+
|
|
1077
|
+
for (const entry of entries.filter(Boolean)) {
|
|
1078
|
+
const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
|
|
1079
|
+
|
|
1080
|
+
if (seenEntries.has(key)) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
seenEntries.add(key);
|
|
1085
|
+
result.push(entry);
|
|
260
1086
|
}
|
|
261
1087
|
|
|
262
|
-
return
|
|
1088
|
+
return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function promoteFeatures(history) {
|
|
1092
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
1093
|
+
return [];
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const featureStats = new Map();
|
|
1097
|
+
|
|
1098
|
+
for (const entry of history) {
|
|
1099
|
+
const featureKey = entry.feature_key || inferFeatureFromEntry(entry).featureKey;
|
|
1100
|
+
const featureMeta = getFeatureMeta(featureKey);
|
|
1101
|
+
const existing = featureStats.get(featureKey) || {
|
|
1102
|
+
featureKey,
|
|
1103
|
+
featureName: featureMeta.name,
|
|
1104
|
+
count: 0,
|
|
1105
|
+
score: 0,
|
|
1106
|
+
lastTimestamp: new Date(0).toISOString()
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
existing.count += 1;
|
|
1110
|
+
existing.score += scoreFeatureEntry(entry);
|
|
1111
|
+
if (new Date(entry.timestamp) > new Date(existing.lastTimestamp)) {
|
|
1112
|
+
existing.lastTimestamp = entry.timestamp;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
featureStats.set(featureKey, existing);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const rankedFeatures = Array.from(featureStats.values()).sort((left, right) => {
|
|
1119
|
+
if (right.count !== left.count) {
|
|
1120
|
+
return right.count - left.count;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (right.score !== left.score) {
|
|
1124
|
+
return right.score - left.score;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (getFeaturePriority(right.featureKey) !== getFeaturePriority(left.featureKey)) {
|
|
1128
|
+
return getFeaturePriority(right.featureKey) - getFeaturePriority(left.featureKey);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return new Date(right.lastTimestamp) - new Date(left.lastTimestamp);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
const promoted = [];
|
|
1135
|
+
const seenFeatureNames = new Set();
|
|
1136
|
+
|
|
1137
|
+
for (const feature of rankedFeatures.filter((item) => item.count >= 3)) {
|
|
1138
|
+
if (LOW_VALUE_FEATURE_KEYS.has(feature.featureKey)) {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
promoted.push(feature.featureName);
|
|
1143
|
+
seenFeatureNames.add(feature.featureName);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
for (const feature of rankedFeatures) {
|
|
1147
|
+
if (promoted.length >= MAX_KEY_FEATURES) {
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (LOW_VALUE_FEATURE_KEYS.has(feature.featureKey)) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (seenFeatureNames.has(feature.featureName)) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
promoted.push(feature.featureName);
|
|
1160
|
+
seenFeatureNames.add(feature.featureName);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
for (const feature of rankedFeatures) {
|
|
1164
|
+
if (promoted.length >= MAX_KEY_FEATURES) {
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (seenFeatureNames.has(feature.featureName)) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
promoted.push(feature.featureName);
|
|
1173
|
+
seenFeatureNames.add(feature.featureName);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return promoted.slice(0, MAX_KEY_FEATURES);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function scoreFeatureEntry(entry) {
|
|
1180
|
+
const typeWeights = {
|
|
1181
|
+
feature: 4,
|
|
1182
|
+
refactor: 3,
|
|
1183
|
+
improvement: 2,
|
|
1184
|
+
fix: 2
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
return typeWeights[normalizeUpdateType(entry.type)] || 1;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function describeCanonicalSubject(featureKey) {
|
|
1191
|
+
return getFeatureMeta(featureKey).subject;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function createCapabilitySnapshotEntry(featureKey, fileCount, timestamp) {
|
|
1195
|
+
const subject = describeCanonicalSubject(featureKey);
|
|
1196
|
+
const type = fileCount > 2 ? 'refactor' : 'improvement';
|
|
1197
|
+
|
|
1198
|
+
return {
|
|
1199
|
+
timestamp,
|
|
1200
|
+
scope: inferScopeFromFeature(featureKey),
|
|
1201
|
+
title: buildIntentTitle(type, subject),
|
|
1202
|
+
type,
|
|
1203
|
+
impact: describeIntentImpact(type, featureKey, subject),
|
|
1204
|
+
feature_key: featureKey,
|
|
1205
|
+
feature_name: getFeatureMeta(featureKey).name,
|
|
1206
|
+
source: 'project_snapshot'
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function inferScopeFromFeature(featureKey) {
|
|
1211
|
+
if (
|
|
1212
|
+
featureKey === 'github_sync' ||
|
|
1213
|
+
featureKey === 'project_intelligence' ||
|
|
1214
|
+
featureKey === 'change_tracking' ||
|
|
1215
|
+
featureKey === 'project_setup'
|
|
1216
|
+
) {
|
|
1217
|
+
return 'logic';
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (featureKey === 'cli_workflow' || featureKey === 'cli_orchestration') {
|
|
1221
|
+
return 'cli';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (featureKey === 'local_context_server' || featureKey === 'context_delivery_system') {
|
|
1225
|
+
return 'backend';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (featureKey === 'package_configuration') {
|
|
1229
|
+
return 'dependencies';
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (featureKey === 'documentation') {
|
|
1233
|
+
return 'documentation';
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return 'project';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function deriveKnownIssues(projectRoot, techStack, keyFeatures) {
|
|
1240
|
+
const knownIssues = [];
|
|
1241
|
+
|
|
1242
|
+
if (!hasTestIndicators(projectRoot)) {
|
|
1243
|
+
knownIssues.push('No automated test suite is detected yet.');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (!techStack.framework) {
|
|
1247
|
+
knownIssues.push('No common application framework dependency is currently detected.');
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (keyFeatures.length < 2) {
|
|
1251
|
+
knownIssues.push('Project intelligence history is still sparse, so AI context may omit mature capabilities.');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return knownIssues;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function hasTestIndicators(projectRoot) {
|
|
1258
|
+
const testPaths = [
|
|
1259
|
+
'test',
|
|
1260
|
+
'tests',
|
|
1261
|
+
'__tests__',
|
|
1262
|
+
'vitest.config.js',
|
|
1263
|
+
'jest.config.js',
|
|
1264
|
+
'jest.config.cjs',
|
|
1265
|
+
'jest.config.mjs'
|
|
1266
|
+
];
|
|
1267
|
+
|
|
1268
|
+
return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function determineCurrentStage(keyFeatures, historyEntries) {
|
|
1272
|
+
if (keyFeatures.length >= 4 && historyEntries.length >= 4) {
|
|
1273
|
+
return 'Production-ready';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (keyFeatures.length >= 2 || historyEntries.length >= 2) {
|
|
1277
|
+
return 'Functional prototype';
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
return 'Early development';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function generateAiSummary(state, historyEntries) {
|
|
1284
|
+
const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
|
|
1285
|
+
const projectType = featureSignals.has('cli_workflow') || featureSignals.has('cli_orchestration')
|
|
1286
|
+
? 'CLI tool'
|
|
1287
|
+
: 'project system';
|
|
1288
|
+
let coreCapability = 'maintains an AI-readable view of project progress';
|
|
1289
|
+
let uniqueValue = 'so AI collaborators can understand the current project state immediately';
|
|
1290
|
+
|
|
1291
|
+
if (featureSignals.has('project_intelligence') && featureSignals.has('change_tracking')) {
|
|
1292
|
+
coreCapability = 'turns meaningful project activity into AI-readable context';
|
|
1293
|
+
} else if (featureSignals.has('project_intelligence')) {
|
|
1294
|
+
coreCapability = 'converts development work into AI-readable project state';
|
|
1295
|
+
} else if (featureSignals.has('local_context_server')) {
|
|
1296
|
+
coreCapability = 'delivers AI-readable project context through clear endpoints';
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (
|
|
1300
|
+
featureSignals.has('github_sync') ||
|
|
1301
|
+
featureSignals.has('context_delivery_system') ||
|
|
1302
|
+
featureSignals.has('cli_orchestration')
|
|
1303
|
+
) {
|
|
1304
|
+
uniqueValue = 'and enables public AI collaboration through GitHub-synced context endpoints';
|
|
1305
|
+
} else if (featureSignals.has('local_context_server')) {
|
|
1306
|
+
uniqueValue = 'and keeps current context available through local AI endpoints';
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return `${capitalize(projectType)} that ${coreCapability} ${uniqueValue}.`;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function collectFeatureSignals(historyEntries, keyFeatures) {
|
|
1313
|
+
const featureSignals = new Set();
|
|
1314
|
+
|
|
1315
|
+
for (const entry of historyEntries) {
|
|
1316
|
+
if (entry.feature_key) {
|
|
1317
|
+
featureSignals.add(entry.feature_key);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
for (const featureName of keyFeatures || []) {
|
|
1322
|
+
for (const [featureKey, featureMeta] of Object.entries(FEATURE_CATALOG)) {
|
|
1323
|
+
if (featureMeta.name === featureName) {
|
|
1324
|
+
featureSignals.add(featureKey);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return featureSignals;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function generateNextSteps(state, historyEntries) {
|
|
1333
|
+
const nextSteps = [];
|
|
1334
|
+
const featureSignals = collectFeatureSignals(historyEntries, state.key_features);
|
|
1335
|
+
|
|
1336
|
+
if (state.key_features.length === 0) {
|
|
1337
|
+
nextSteps.push('Capture a few meaningful project milestones so stable AI-visible features can emerge from real development history.');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (state.known_issues.includes('No automated test suite is detected yet.')) {
|
|
1341
|
+
nextSteps.push('Add automated tests for the intelligence engine, watcher, and GitHub sync workflow.');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (!featureSignals.has('project_intelligence')) {
|
|
1345
|
+
nextSteps.push('Strengthen the intelligence engine so more project-level capabilities are captured automatically.');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (!featureSignals.has('local_context_server')) {
|
|
1349
|
+
nextSteps.push('Expand context delivery coverage so AI consumers can reliably read current project state.');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (!featureSignals.has('github_sync')) {
|
|
1353
|
+
nextSteps.push('Validate public sync behavior so AI tools can safely consume the latest project context from GitHub.');
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (state.current_stage === 'Early development') {
|
|
1357
|
+
nextSteps.push('Ship the next core workflow milestone to turn the project into a functional prototype.');
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return Array.from(new Set(nextSteps)).slice(0, 4);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function capitalize(value) {
|
|
1364
|
+
if (!value) {
|
|
1365
|
+
return '';
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
263
1369
|
}
|
|
264
1370
|
|
|
265
1371
|
function createDebouncedStateUpdater(projectRoot, options) {
|
|
@@ -331,9 +1437,14 @@ module.exports = {
|
|
|
331
1437
|
detectProjectMetadata,
|
|
332
1438
|
ensureContextDirectory,
|
|
333
1439
|
getContextPaths,
|
|
1440
|
+
groupEventsByIntent,
|
|
1441
|
+
interpretChange,
|
|
334
1442
|
loadRuntimeConfig,
|
|
1443
|
+
promoteFeatures,
|
|
335
1444
|
readJsonFile,
|
|
336
1445
|
renderTemplate,
|
|
1446
|
+
scoreEvent,
|
|
1447
|
+
shouldIgnoreProjectFile,
|
|
337
1448
|
updateRuntimeConfig,
|
|
338
1449
|
updateProjectState,
|
|
339
1450
|
writeJsonAtomic,
|