aibridge-context 1.4.0 → 1.5.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/init.js +13 -2
- package/core/stateManager.js +827 -785
- package/index.js +1 -0
- package/package.json +1 -1
- package/templates/state.template.json +2 -0
package/core/stateManager.js
CHANGED
|
@@ -8,78 +8,54 @@ const CONTEXT_DIR_NAME = '.ai-context';
|
|
|
8
8
|
const MAX_RECENT_UPDATES = 5;
|
|
9
9
|
const MAX_CHANGELOG_ENTRIES = 50;
|
|
10
10
|
const MAX_KEY_FEATURES = 6;
|
|
11
|
-
const
|
|
11
|
+
const MAX_IMPLEMENTATION_DETAILS = 8;
|
|
12
|
+
const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/', 'src/', 'routes/', 'controllers/', 'services/'];
|
|
12
13
|
const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
|
|
13
|
-
const
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
14
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'.ai-context',
|
|
18
|
+
'dist',
|
|
19
|
+
'build',
|
|
20
|
+
'coverage',
|
|
21
|
+
'.tmp',
|
|
22
|
+
'logs'
|
|
18
23
|
]);
|
|
19
|
-
|
|
24
|
+
const ANALYSIS_ROOT_FILES = [
|
|
25
|
+
'package.json',
|
|
26
|
+
'app.js',
|
|
27
|
+
'app.ts',
|
|
28
|
+
'index.js',
|
|
29
|
+
'index.ts',
|
|
30
|
+
'main.py',
|
|
31
|
+
'requirements.txt',
|
|
32
|
+
'tsconfig.json'
|
|
33
|
+
];
|
|
34
|
+
const ANALYSIS_DIRECTORIES = [
|
|
35
|
+
'routes',
|
|
36
|
+
'server',
|
|
37
|
+
'controllers',
|
|
38
|
+
'services',
|
|
39
|
+
'middleware',
|
|
40
|
+
'config',
|
|
41
|
+
'src',
|
|
42
|
+
'core',
|
|
43
|
+
'bin'
|
|
44
|
+
];
|
|
20
45
|
const FEATURE_CATALOG = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
46
|
+
ai_context_generation: 'AI-readable project context generation',
|
|
47
|
+
cli_automation: 'Command-line workflow for project setup and automation',
|
|
48
|
+
change_tracking: 'Automatic change tracking with noise filtering',
|
|
49
|
+
public_sync: 'Optional GitHub sync for publishing project context',
|
|
50
|
+
local_context_server: 'HTTP endpoints for accessing current project context',
|
|
51
|
+
rest_api: 'REST-style API surface',
|
|
52
|
+
auth: 'Authenticated workflows secured with JWT',
|
|
53
|
+
persistence: 'MongoDB-backed data persistence',
|
|
54
|
+
realtime: 'Real-time communication channel',
|
|
55
|
+
external_api: 'External service integration',
|
|
56
|
+
middleware: 'Middleware-driven request processing',
|
|
57
|
+
config_management: 'Centralized project configuration management'
|
|
81
58
|
};
|
|
82
|
-
|
|
83
59
|
const DEFAULT_CONFIG = {
|
|
84
60
|
port: 3333,
|
|
85
61
|
debounceMs: 600,
|
|
@@ -93,8 +69,13 @@ const DEFAULT_CONFIG = {
|
|
|
93
69
|
}
|
|
94
70
|
};
|
|
95
71
|
|
|
72
|
+
function resolveProjectRoot(projectRoot) {
|
|
73
|
+
return path.resolve(projectRoot || process.cwd());
|
|
74
|
+
}
|
|
75
|
+
|
|
96
76
|
function getContextPaths(projectRoot) {
|
|
97
|
-
const
|
|
77
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
78
|
+
const contextDir = path.join(resolvedRoot, CONTEXT_DIR_NAME);
|
|
98
79
|
|
|
99
80
|
return {
|
|
100
81
|
contextDir,
|
|
@@ -128,67 +109,72 @@ function isObject(value) {
|
|
|
128
109
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
129
110
|
}
|
|
130
111
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
112
|
+
function normalizeProjectPath(filePath) {
|
|
113
|
+
return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isInsideProjectRoot(projectRoot, targetPath) {
|
|
117
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
118
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
119
|
+
const relativePath = path.relative(resolvedRoot, resolvedTarget);
|
|
120
|
+
|
|
121
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getRootPackageJsonPath(projectRoot) {
|
|
125
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
126
|
+
return path.join(resolvedRoot, 'package.json');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readRootPackageJson(projectRoot) {
|
|
130
|
+
const packageJsonPath = getRootPackageJsonPath(projectRoot);
|
|
144
131
|
|
|
145
132
|
if (!fs.existsSync(packageJsonPath)) {
|
|
146
|
-
return
|
|
133
|
+
return null;
|
|
147
134
|
}
|
|
148
135
|
|
|
149
136
|
try {
|
|
150
|
-
|
|
151
|
-
const parsedPackage = JSON.parse(rawPackage);
|
|
152
|
-
|
|
153
|
-
metadata.project = parsedPackage.name || metadata.project;
|
|
154
|
-
metadata.version = parsedPackage.version || metadata.version;
|
|
137
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
155
138
|
} catch (error) {
|
|
156
|
-
|
|
139
|
+
return null;
|
|
157
140
|
}
|
|
158
|
-
|
|
159
|
-
return metadata;
|
|
160
141
|
}
|
|
161
142
|
|
|
162
|
-
function
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
fs.existsSync(path.join(projectRoot, marker))
|
|
168
|
-
);
|
|
169
|
-
let dependencies = {};
|
|
143
|
+
function detectProjectMetadata(projectRoot) {
|
|
144
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
145
|
+
const packageJson = readRootPackageJson(resolvedRoot);
|
|
146
|
+
const techStack = detectTechStack(resolvedRoot, packageJson);
|
|
147
|
+
const packageManager = detectPackageManager(resolvedRoot);
|
|
170
148
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
dependencies = {};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
149
|
+
return {
|
|
150
|
+
project: packageJson && packageJson.name ? packageJson.name : path.basename(resolvedRoot),
|
|
151
|
+
version: packageJson && packageJson.version ? packageJson.version : '0.1.0',
|
|
152
|
+
techStack: Object.assign({}, techStack, {
|
|
153
|
+
package_manager: packageManager
|
|
154
|
+
}),
|
|
155
|
+
stackLabel: buildStackLabel(techStack),
|
|
156
|
+
packageManager
|
|
157
|
+
};
|
|
158
|
+
}
|
|
184
159
|
|
|
160
|
+
function detectTechStack(projectRoot, packageJson) {
|
|
161
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
162
|
+
const rootPackage = packageJson || readRootPackageJson(resolvedRoot);
|
|
163
|
+
const dependencies = Object.assign(
|
|
164
|
+
{},
|
|
165
|
+
(rootPackage && rootPackage.dependencies) || {},
|
|
166
|
+
(rootPackage && rootPackage.devDependencies) || {}
|
|
167
|
+
);
|
|
168
|
+
const hasPythonMarker = ['pyproject.toml', 'requirements.txt', 'setup.py'].some((marker) =>
|
|
169
|
+
fs.existsSync(path.join(resolvedRoot, marker))
|
|
170
|
+
);
|
|
185
171
|
let language = '';
|
|
186
172
|
let runtime = '';
|
|
187
173
|
|
|
188
|
-
if (
|
|
174
|
+
if (rootPackage || hasAnyFileExtension(resolvedRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
|
|
189
175
|
language = 'Node.js';
|
|
190
176
|
runtime = 'Node.js';
|
|
191
|
-
} else if (hasPythonMarker || hasAnyFileExtension(
|
|
177
|
+
} else if (hasPythonMarker || hasAnyFileExtension(resolvedRoot, ['.py'])) {
|
|
192
178
|
language = 'Python';
|
|
193
179
|
runtime = 'Python';
|
|
194
180
|
}
|
|
@@ -197,7 +183,7 @@ function detectTechStack(projectRoot) {
|
|
|
197
183
|
language,
|
|
198
184
|
framework: detectFramework(dependencies),
|
|
199
185
|
runtime,
|
|
200
|
-
package_manager: detectPackageManager(
|
|
186
|
+
package_manager: detectPackageManager(resolvedRoot)
|
|
201
187
|
};
|
|
202
188
|
}
|
|
203
189
|
|
|
@@ -214,6 +200,14 @@ function detectFramework(dependencies) {
|
|
|
214
200
|
return 'Express';
|
|
215
201
|
}
|
|
216
202
|
|
|
203
|
+
if (dependencies.fastify) {
|
|
204
|
+
return 'Fastify';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (dependencies.koa) {
|
|
208
|
+
return 'Koa';
|
|
209
|
+
}
|
|
210
|
+
|
|
217
211
|
return '';
|
|
218
212
|
}
|
|
219
213
|
|
|
@@ -228,11 +222,30 @@ function hasAnyFileExtension(projectRoot, extensions) {
|
|
|
228
222
|
);
|
|
229
223
|
}
|
|
230
224
|
|
|
231
|
-
function
|
|
225
|
+
function detectPackageManager(projectRoot) {
|
|
226
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
227
|
+
|
|
228
|
+
if (fs.existsSync(path.join(resolvedRoot, 'pnpm-lock.yaml'))) {
|
|
229
|
+
return 'pnpm';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (fs.existsSync(path.join(resolvedRoot, 'yarn.lock'))) {
|
|
233
|
+
return 'yarn';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return 'npm';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scanProjectFiles(projectRoot, maxDepth, options) {
|
|
240
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
241
|
+
const settings = Object.assign({ includeDirectories: null }, options);
|
|
242
|
+
const includeDirectories = settings.includeDirectories
|
|
243
|
+
? new Set(settings.includeDirectories.map((entry) => normalizeProjectPath(entry).toLowerCase()))
|
|
244
|
+
: null;
|
|
232
245
|
const results = [];
|
|
233
246
|
|
|
234
247
|
function visit(currentDir, depth) {
|
|
235
|
-
if (depth > maxDepth) {
|
|
248
|
+
if (depth > maxDepth || !isInsideProjectRoot(resolvedRoot, currentDir)) {
|
|
236
249
|
return;
|
|
237
250
|
}
|
|
238
251
|
|
|
@@ -246,13 +259,22 @@ function scanProjectFiles(projectRoot, maxDepth) {
|
|
|
246
259
|
|
|
247
260
|
for (const entry of entries) {
|
|
248
261
|
const fullPath = path.join(currentDir, entry.name);
|
|
249
|
-
|
|
262
|
+
|
|
263
|
+
if (!isInsideProjectRoot(resolvedRoot, fullPath)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const relativePath = normalizeProjectPath(path.relative(resolvedRoot, fullPath));
|
|
250
268
|
|
|
251
269
|
if (entry.isDirectory()) {
|
|
252
270
|
if (shouldIgnoreProjectFile(relativePath)) {
|
|
253
271
|
continue;
|
|
254
272
|
}
|
|
255
273
|
|
|
274
|
+
if (includeDirectories && depth === 0 && !includeDirectories.has(relativePath.toLowerCase())) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
256
278
|
visit(fullPath, depth + 1);
|
|
257
279
|
continue;
|
|
258
280
|
}
|
|
@@ -263,20 +285,71 @@ function scanProjectFiles(projectRoot, maxDepth) {
|
|
|
263
285
|
}
|
|
264
286
|
}
|
|
265
287
|
|
|
266
|
-
visit(
|
|
288
|
+
visit(resolvedRoot, 0);
|
|
267
289
|
return results;
|
|
268
290
|
}
|
|
269
291
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
292
|
+
function shouldIgnoreProjectFile(filePath) {
|
|
293
|
+
const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
|
|
294
|
+
|
|
295
|
+
if (!normalizedPath) {
|
|
296
|
+
return false;
|
|
273
297
|
}
|
|
274
298
|
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
300
|
+
const baseName = segments[segments.length - 1] || '';
|
|
301
|
+
|
|
302
|
+
if (segments.some((segment) => IGNORED_DIRECTORY_NAMES.has(segment))) {
|
|
303
|
+
return true;
|
|
277
304
|
}
|
|
278
305
|
|
|
279
|
-
|
|
306
|
+
if (baseName.startsWith('.start')) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
baseName.endsWith('.log') ||
|
|
312
|
+
baseName.endsWith('.tmp') ||
|
|
313
|
+
baseName.endsWith('.lock') ||
|
|
314
|
+
baseName === 'package-lock.json' ||
|
|
315
|
+
baseName === 'yarn.lock' ||
|
|
316
|
+
baseName === 'pnpm-lock.yaml' ||
|
|
317
|
+
/^tmp[._-]/i.test(baseName) ||
|
|
318
|
+
/^temp[._-]/i.test(baseName) ||
|
|
319
|
+
/^debug[._-]/i.test(baseName)
|
|
320
|
+
) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function scoreEvent(filePath) {
|
|
328
|
+
const normalizedPath = normalizeProjectPath(filePath);
|
|
329
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
330
|
+
let score = 0;
|
|
331
|
+
|
|
332
|
+
if (shouldIgnoreProjectFile(normalizedPath)) {
|
|
333
|
+
return -5;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
|
|
337
|
+
score += 3;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (IMPORTANT_EXTENSIONS.has(path.extname(lowerPath))) {
|
|
341
|
+
score += 2;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (lowerPath === 'package.json') {
|
|
345
|
+
score += 2;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (lowerPath === 'readme.md') {
|
|
349
|
+
score += 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return score;
|
|
280
353
|
}
|
|
281
354
|
|
|
282
355
|
async function ensureContextDirectory(projectRoot) {
|
|
@@ -301,205 +374,487 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
301
374
|
|
|
302
375
|
async function writeTextAtomic(filePath, content) {
|
|
303
376
|
const tempFilePath = `${filePath}.tmp`;
|
|
377
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
304
378
|
await fsp.writeFile(tempFilePath, content, 'utf8');
|
|
305
379
|
await fsp.rename(tempFilePath, filePath);
|
|
306
380
|
}
|
|
307
381
|
|
|
382
|
+
function renderTemplate(template, variables) {
|
|
383
|
+
return Object.entries(variables).reduce((accumulator, [key, value]) => {
|
|
384
|
+
const safeValue = value == null ? '' : String(value);
|
|
385
|
+
return accumulator.split(`{{${key}}}`).join(safeValue);
|
|
386
|
+
}, template);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createDefaultChangelog() {
|
|
390
|
+
return {
|
|
391
|
+
entries: []
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
308
395
|
function createDefaultState(projectRoot) {
|
|
309
396
|
const metadata = detectProjectMetadata(projectRoot);
|
|
397
|
+
const bootstrap = bootstrapProjectAnalysis(projectRoot);
|
|
310
398
|
const state = {
|
|
311
399
|
project: metadata.project,
|
|
312
400
|
version: metadata.version,
|
|
313
401
|
last_updated: new Date(0).toISOString(),
|
|
314
402
|
ai_summary: '',
|
|
315
|
-
tech_stack:
|
|
316
|
-
|
|
403
|
+
tech_stack: bootstrap.techStack,
|
|
404
|
+
architecture_patterns: bootstrap.architecturePatterns,
|
|
405
|
+
implementation_details: bootstrap.implementationDetails,
|
|
406
|
+
current_stage: determineCurrentStage(bootstrap.keyFeatures, [], bootstrap.implementationDetails),
|
|
317
407
|
recent_updates: [],
|
|
318
|
-
key_features:
|
|
319
|
-
known_issues: deriveKnownIssues(projectRoot,
|
|
408
|
+
key_features: bootstrap.keyFeatures,
|
|
409
|
+
known_issues: deriveKnownIssues(projectRoot, bootstrap),
|
|
320
410
|
next_steps: []
|
|
321
411
|
};
|
|
322
412
|
|
|
323
|
-
state.ai_summary = generateAiSummary(state,
|
|
324
|
-
state.next_steps = generateNextSteps(state, []);
|
|
413
|
+
state.ai_summary = generateAiSummary(state, bootstrap);
|
|
414
|
+
state.next_steps = generateNextSteps(state, bootstrap, []);
|
|
325
415
|
|
|
326
416
|
return state;
|
|
327
417
|
}
|
|
328
418
|
|
|
329
|
-
function
|
|
419
|
+
function bootstrapProjectAnalysis(projectRoot) {
|
|
420
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
421
|
+
const metadata = detectProjectMetadata(resolvedRoot);
|
|
422
|
+
const rootPackage = readRootPackageJson(resolvedRoot);
|
|
423
|
+
const techStack = metadata.techStack;
|
|
424
|
+
const analysisInputs = collectAnalysisInputs(resolvedRoot);
|
|
425
|
+
const implementationSignals = detectImplementationSignals(analysisInputs, rootPackage, techStack);
|
|
426
|
+
const architecturePatterns = buildArchitecturePatterns(
|
|
427
|
+
implementationSignals,
|
|
428
|
+
analysisInputs,
|
|
429
|
+
techStack,
|
|
430
|
+
rootPackage
|
|
431
|
+
);
|
|
432
|
+
const implementationDetails = buildImplementationDetails(implementationSignals, techStack);
|
|
433
|
+
const keyFeatures = buildKeyFeatures(implementationSignals, techStack);
|
|
434
|
+
const projectType = determineProjectType(implementationSignals, techStack, rootPackage);
|
|
435
|
+
|
|
330
436
|
return {
|
|
331
|
-
|
|
437
|
+
projectType,
|
|
438
|
+
techStack,
|
|
439
|
+
architecturePatterns,
|
|
440
|
+
implementationDetails,
|
|
441
|
+
keyFeatures,
|
|
442
|
+
signals: implementationSignals
|
|
332
443
|
};
|
|
333
444
|
}
|
|
334
445
|
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return accumulator.split(`{{${key}}}`).join(safeValue);
|
|
339
|
-
}, template);
|
|
340
|
-
}
|
|
446
|
+
function collectAnalysisInputs(projectRoot) {
|
|
447
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
448
|
+
const selectedFiles = new Set();
|
|
341
449
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
450
|
+
for (const relativeFile of ANALYSIS_ROOT_FILES) {
|
|
451
|
+
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
452
|
+
|
|
453
|
+
if (fs.existsSync(absoluteFile) && isInsideProjectRoot(resolvedRoot, absoluteFile)) {
|
|
454
|
+
selectedFiles.add(relativeFile);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const discoveredFiles = scanProjectFiles(resolvedRoot, 4, {
|
|
459
|
+
includeDirectories: ANALYSIS_DIRECTORIES
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
for (const relativeFile of discoveredFiles) {
|
|
463
|
+
selectedFiles.add(relativeFile);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return Array.from(selectedFiles)
|
|
467
|
+
.sort()
|
|
468
|
+
.map((relativeFile) => {
|
|
469
|
+
const absoluteFile = path.join(resolvedRoot, relativeFile);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const content = fs.readFileSync(absoluteFile, 'utf8');
|
|
473
|
+
return {
|
|
474
|
+
path: relativeFile,
|
|
475
|
+
content: content.slice(0, 50000)
|
|
476
|
+
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
.filter(Boolean);
|
|
346
482
|
}
|
|
347
483
|
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
484
|
+
function detectImplementationSignals(analysisInputs, rootPackage, techStack) {
|
|
485
|
+
const dependencyNames = new Set([
|
|
486
|
+
...Object.keys((rootPackage && rootPackage.dependencies) || {}),
|
|
487
|
+
...Object.keys((rootPackage && rootPackage.devDependencies) || {})
|
|
488
|
+
]);
|
|
489
|
+
const runtimeInputs = analysisInputs.filter((input) => isRuntimeImplementationFile(input.path));
|
|
490
|
+
const automationInputs = analysisInputs.filter((input) => isAutomationImplementationFile(input.path));
|
|
491
|
+
const runtimeContent = runtimeInputs.map((input) => input.content).join('\n');
|
|
492
|
+
const automationContent = automationInputs.map((input) => input.content).join('\n');
|
|
493
|
+
const allContent = analysisInputs.map((input) => input.content).join('\n');
|
|
494
|
+
const hasDirectory = (directoryName) =>
|
|
495
|
+
analysisInputs.some((input) => normalizeProjectPath(input.path).startsWith(`${directoryName}/`));
|
|
496
|
+
const hasRuntimePattern = (pattern) => pattern.test(runtimeContent);
|
|
497
|
+
const hasAutomationPattern = (pattern) => pattern.test(automationContent);
|
|
498
|
+
const hasAnyPattern = (pattern) => pattern.test(allContent);
|
|
353
499
|
|
|
354
|
-
|
|
355
|
-
|
|
500
|
+
return {
|
|
501
|
+
hasPackageJson: Boolean(rootPackage),
|
|
502
|
+
hasCliEntry:
|
|
503
|
+
Boolean(rootPackage && rootPackage.bin && Object.keys(rootPackage.bin).length > 0) ||
|
|
504
|
+
hasAutomationPattern(/^#!\/usr\/bin\/env node/m) ||
|
|
505
|
+
hasAutomationPattern(/\bprocess\.argv\b/),
|
|
506
|
+
hasExpress:
|
|
507
|
+
dependencyNames.has('express') ||
|
|
508
|
+
hasRuntimePattern(/\brequire\(['"]express['"]\)/) ||
|
|
509
|
+
hasRuntimePattern(/\bfrom ['"]express['"]/) ||
|
|
510
|
+
hasRuntimePattern(/\bexpress\(\)/),
|
|
511
|
+
hasNext: dependencyNames.has('next'),
|
|
512
|
+
hasReact: dependencyNames.has('react'),
|
|
513
|
+
hasFastify: dependencyNames.has('fastify'),
|
|
514
|
+
hasKoa: dependencyNames.has('koa'),
|
|
515
|
+
hasRestRoutes: hasRuntimePattern(/\b(router|app)\.(get|post|put|patch|delete)\s*\(/),
|
|
516
|
+
hasMiddleware: hasRuntimePattern(/\bapp\.use\s*\(/) || hasDirectory('middleware'),
|
|
517
|
+
hasJwt:
|
|
518
|
+
dependencyNames.has('jsonwebtoken') ||
|
|
519
|
+
hasRuntimePattern(/\bjwt\.(sign|verify)\s*\(/) ||
|
|
520
|
+
hasRuntimePattern(/\brequire\(['"]jsonwebtoken['"]\)/),
|
|
521
|
+
hasMongoose:
|
|
522
|
+
dependencyNames.has('mongoose') ||
|
|
523
|
+
hasRuntimePattern(/\bmongoose\.connect\s*\(/) ||
|
|
524
|
+
hasRuntimePattern(/\brequire\(['"]mongoose['"]\)/),
|
|
525
|
+
hasSocketIO:
|
|
526
|
+
dependencyNames.has('socket.io') ||
|
|
527
|
+
hasRuntimePattern(/\bsocket\.io\b/) ||
|
|
528
|
+
hasRuntimePattern(/\brequire\(['"]socket\.io['"]\)/),
|
|
529
|
+
hasAxiosOrFetch:
|
|
530
|
+
dependencyNames.has('axios') ||
|
|
531
|
+
hasRuntimePattern(/\baxios\./) ||
|
|
532
|
+
hasRuntimePattern(/\bfetch\s*\(/),
|
|
533
|
+
hasWatcher:
|
|
534
|
+
dependencyNames.has('chokidar') ||
|
|
535
|
+
hasAutomationPattern(/\bchokidar\.watch\s*\(/) ||
|
|
536
|
+
hasAutomationPattern(/\bfs\.watch\s*\(/),
|
|
537
|
+
hasGitAutomation:
|
|
538
|
+
hasAutomationPattern(/\bgit\s+(add|commit|push)\b/) ||
|
|
539
|
+
hasAutomationPattern(/\b(syncContextToGit|linkGithubRepository)\b/),
|
|
540
|
+
hasAiContextArtifacts:
|
|
541
|
+
hasAutomationPattern(/\bstate\.json\b/) ||
|
|
542
|
+
hasAutomationPattern(/\bbrain\.txt\b/) ||
|
|
543
|
+
hasAutomationPattern(/\bcontext\.md\b/) ||
|
|
544
|
+
hasAutomationPattern(/\bchangelog\.json\b/),
|
|
545
|
+
hasControllers: hasDirectory('controllers'),
|
|
546
|
+
hasServices: hasDirectory('services'),
|
|
547
|
+
hasRoutes: hasDirectory('routes'),
|
|
548
|
+
hasServerDirectory: hasDirectory('server'),
|
|
549
|
+
hasConfigDirectory: hasDirectory('config'),
|
|
550
|
+
hasTypeScriptConfig: analysisInputs.some((input) => input.path === 'tsconfig.json'),
|
|
551
|
+
hasPythonRequirements: analysisInputs.some((input) => input.path === 'requirements.txt'),
|
|
552
|
+
hasNodeRuntime: techStack.language === 'Node.js',
|
|
553
|
+
hasPythonRuntime: techStack.language === 'Python',
|
|
554
|
+
hasApplicationEntries: runtimeInputs.length > 0,
|
|
555
|
+
hasAnyContent: hasAnyPattern(/\S/)
|
|
556
|
+
};
|
|
356
557
|
}
|
|
357
558
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
559
|
+
function isRuntimeImplementationFile(relativePath) {
|
|
560
|
+
const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
normalizedPath.startsWith('routes/') ||
|
|
564
|
+
normalizedPath.startsWith('server/') ||
|
|
565
|
+
normalizedPath.startsWith('controllers/') ||
|
|
566
|
+
normalizedPath.startsWith('services/') ||
|
|
567
|
+
normalizedPath.startsWith('middleware/') ||
|
|
568
|
+
normalizedPath.startsWith('config/') ||
|
|
569
|
+
normalizedPath.startsWith('src/') ||
|
|
570
|
+
normalizedPath === 'app.js' ||
|
|
571
|
+
normalizedPath === 'app.ts' ||
|
|
572
|
+
normalizedPath === 'index.js' ||
|
|
573
|
+
normalizedPath === 'index.ts' ||
|
|
574
|
+
normalizedPath === 'main.py'
|
|
365
575
|
);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const meaningfulEvents = collapseEventsByFile(
|
|
378
|
-
validEvents.filter((event) => isMeaningfulEvent(event))
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function isAutomationImplementationFile(relativePath) {
|
|
579
|
+
const normalizedPath = normalizeProjectPath(relativePath).toLowerCase();
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
normalizedPath.startsWith('core/') ||
|
|
583
|
+
normalizedPath.startsWith('bin/') ||
|
|
584
|
+
normalizedPath === 'package.json' ||
|
|
585
|
+
normalizedPath === 'index.js' ||
|
|
586
|
+
normalizedPath === 'index.ts'
|
|
379
587
|
);
|
|
380
|
-
|
|
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
|
-
};
|
|
588
|
+
}
|
|
404
589
|
|
|
405
|
-
|
|
406
|
-
|
|
590
|
+
function buildArchitecturePatterns(signals, analysisInputs, techStack, rootPackage) {
|
|
591
|
+
const patterns = [];
|
|
407
592
|
|
|
408
|
-
|
|
409
|
-
|
|
593
|
+
if (signals.hasCliEntry) {
|
|
594
|
+
patterns.push('Command-line automation workflow');
|
|
595
|
+
}
|
|
410
596
|
|
|
411
|
-
if (
|
|
412
|
-
|
|
597
|
+
if (signals.hasWatcher) {
|
|
598
|
+
patterns.push('Event-driven file watching pipeline');
|
|
413
599
|
}
|
|
414
600
|
|
|
415
|
-
if (
|
|
416
|
-
|
|
601
|
+
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
602
|
+
patterns.push('REST API architecture');
|
|
417
603
|
}
|
|
418
604
|
|
|
419
|
-
|
|
420
|
-
|
|
605
|
+
if (signals.hasMiddleware) {
|
|
606
|
+
patterns.push('Middleware-driven request pipeline');
|
|
607
|
+
}
|
|
421
608
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return new Date().toISOString();
|
|
609
|
+
if (signals.hasControllers && signals.hasServices) {
|
|
610
|
+
patterns.push('Layered controller-service architecture');
|
|
425
611
|
}
|
|
426
612
|
|
|
427
|
-
|
|
428
|
-
|
|
613
|
+
if (signals.hasServerDirectory && signals.hasRoutes) {
|
|
614
|
+
patterns.push('Separated server bootstrap and route handling');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (signals.hasConfigDirectory) {
|
|
618
|
+
patterns.push('Centralized configuration layer');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (signals.hasSocketIO) {
|
|
622
|
+
patterns.push('Real-time event architecture');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (signals.hasNext) {
|
|
626
|
+
patterns.push('Framework-driven web application structure');
|
|
627
|
+
} else if (signals.hasReact) {
|
|
628
|
+
patterns.push('Component-based frontend architecture');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (
|
|
632
|
+
signals.hasAiContextArtifacts &&
|
|
633
|
+
signals.hasExpress &&
|
|
634
|
+
analysisInputs.some((input) => /\bstate\.json\b|\bbrain\.txt\b|\bcontext\.md\b/.test(input.content))
|
|
635
|
+
) {
|
|
636
|
+
patterns.push('Structured AI context delivery workflow');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (rootPackage && Array.isArray(rootPackage.keywords) && rootPackage.keywords.includes('cli')) {
|
|
640
|
+
patterns.push('Package-distributed CLI architecture');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return uniqueNonEmpty(patterns).slice(0, 6);
|
|
429
644
|
}
|
|
430
645
|
|
|
431
|
-
function
|
|
432
|
-
|
|
646
|
+
function buildImplementationDetails(signals, techStack) {
|
|
647
|
+
const details = [];
|
|
648
|
+
|
|
649
|
+
if (signals.hasJwt) {
|
|
650
|
+
details.push('JWT-based authentication system');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (signals.hasMongoose) {
|
|
654
|
+
details.push('MongoDB integration through Mongoose ORM');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
658
|
+
details.push('REST API architecture using Express routing');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (signals.hasMiddleware) {
|
|
662
|
+
details.push('Express middleware pipeline for request handling');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (signals.hasSocketIO) {
|
|
666
|
+
details.push('Socket.IO-based real-time communication');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (signals.hasAxiosOrFetch) {
|
|
670
|
+
details.push('External API integration for outbound service calls');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (signals.hasWatcher) {
|
|
674
|
+
details.push('File system monitoring for automatic project state updates');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (signals.hasAiContextArtifacts) {
|
|
678
|
+
details.push('Structured AI context generation across JSON, Markdown, and instruction files');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (signals.hasGitAutomation) {
|
|
682
|
+
details.push('Git-backed synchronization workflow for publishing project state');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (signals.hasCliEntry && techStack.language === 'Node.js') {
|
|
686
|
+
details.push('Node.js CLI entrypoint for developer-facing automation');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return uniqueNonEmpty(details).slice(0, MAX_IMPLEMENTATION_DETAILS);
|
|
433
690
|
}
|
|
434
691
|
|
|
435
|
-
function
|
|
436
|
-
const
|
|
692
|
+
function buildKeyFeatures(signals, techStack) {
|
|
693
|
+
const features = [];
|
|
437
694
|
|
|
438
|
-
if (
|
|
439
|
-
|
|
695
|
+
if (signals.hasAiContextArtifacts) {
|
|
696
|
+
features.push(FEATURE_CATALOG.ai_context_generation);
|
|
440
697
|
}
|
|
441
698
|
|
|
442
|
-
|
|
443
|
-
|
|
699
|
+
if (signals.hasCliEntry) {
|
|
700
|
+
features.push(FEATURE_CATALOG.cli_automation);
|
|
701
|
+
}
|
|
444
702
|
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
segments.includes('.git') ||
|
|
448
|
-
segments.includes('.ai-context') ||
|
|
449
|
-
segments.includes('dist') ||
|
|
450
|
-
segments.includes('build')
|
|
451
|
-
) {
|
|
452
|
-
return true;
|
|
703
|
+
if (signals.hasWatcher) {
|
|
704
|
+
features.push(FEATURE_CATALOG.change_tracking);
|
|
453
705
|
}
|
|
454
706
|
|
|
455
|
-
if (
|
|
456
|
-
|
|
707
|
+
if (signals.hasGitAutomation) {
|
|
708
|
+
features.push(FEATURE_CATALOG.public_sync);
|
|
457
709
|
}
|
|
458
710
|
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
711
|
+
if (signals.hasExpress && signals.hasAiContextArtifacts) {
|
|
712
|
+
features.push(FEATURE_CATALOG.local_context_server);
|
|
713
|
+
} else if (signals.hasExpress && signals.hasRestRoutes) {
|
|
714
|
+
features.push(FEATURE_CATALOG.rest_api);
|
|
471
715
|
}
|
|
472
716
|
|
|
473
|
-
|
|
717
|
+
if (signals.hasJwt) {
|
|
718
|
+
features.push(FEATURE_CATALOG.auth);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (signals.hasMongoose) {
|
|
722
|
+
features.push(FEATURE_CATALOG.persistence);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (signals.hasSocketIO) {
|
|
726
|
+
features.push(FEATURE_CATALOG.realtime);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (signals.hasAxiosOrFetch) {
|
|
730
|
+
features.push(FEATURE_CATALOG.external_api);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (signals.hasMiddleware) {
|
|
734
|
+
features.push(FEATURE_CATALOG.middleware);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (signals.hasConfigDirectory) {
|
|
738
|
+
features.push(FEATURE_CATALOG.config_management);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (features.length === 0 && techStack.language) {
|
|
742
|
+
features.push(`${techStack.language} project structure with detectable application entry points`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return uniqueNonEmpty(features).slice(0, MAX_KEY_FEATURES);
|
|
474
746
|
}
|
|
475
747
|
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
let score = 0;
|
|
748
|
+
function determineProjectType(signals, techStack, rootPackage) {
|
|
749
|
+
if (signals.hasNext) {
|
|
750
|
+
return 'Next.js application';
|
|
751
|
+
}
|
|
481
752
|
|
|
482
|
-
if (
|
|
483
|
-
return
|
|
753
|
+
if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
|
|
754
|
+
return 'CLI tool';
|
|
484
755
|
}
|
|
485
756
|
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
}
|
|
757
|
+
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
758
|
+
return 'backend API platform';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (signals.hasReact) {
|
|
762
|
+
return 'frontend application';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (signals.hasPythonRuntime) {
|
|
766
|
+
return 'Python application';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (rootPackage && rootPackage.bin) {
|
|
770
|
+
return 'CLI tool';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (techStack.language === 'Node.js') {
|
|
774
|
+
return 'Node.js application';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return 'software project';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function loadRuntimeConfig(projectRoot) {
|
|
781
|
+
const { configFile } = getContextPaths(projectRoot);
|
|
782
|
+
const userConfig = await readJsonFile(configFile, {});
|
|
783
|
+
return deepMerge(DEFAULT_CONFIG, userConfig);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function updateRuntimeConfig(projectRoot, updates) {
|
|
787
|
+
const { configFile } = getContextPaths(projectRoot);
|
|
788
|
+
const currentConfig = await readJsonFile(configFile, {});
|
|
789
|
+
const mergedCurrentConfig = deepMerge(DEFAULT_CONFIG, currentConfig);
|
|
790
|
+
const nextConfig = deepMerge(mergedCurrentConfig, updates || {});
|
|
791
|
+
|
|
792
|
+
await writeJsonAtomic(configFile, nextConfig);
|
|
793
|
+
return nextConfig;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function updateProjectState(projectRoot, changeEvent, options) {
|
|
797
|
+
const settings = Object.assign(
|
|
798
|
+
{
|
|
799
|
+
logger: null,
|
|
800
|
+
syncCallback: null
|
|
801
|
+
},
|
|
802
|
+
options
|
|
803
|
+
);
|
|
804
|
+
const logger = settings.logger;
|
|
805
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
806
|
+
const contextPaths = getContextPaths(resolvedRoot);
|
|
807
|
+
const metadata = detectProjectMetadata(resolvedRoot);
|
|
808
|
+
const bootstrap = bootstrapProjectAnalysis(resolvedRoot);
|
|
809
|
+
const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(resolvedRoot));
|
|
810
|
+
const existingChangelog = await readJsonFile(
|
|
811
|
+
contextPaths.changelogFile,
|
|
812
|
+
createDefaultChangelog()
|
|
813
|
+
);
|
|
814
|
+
const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
|
|
815
|
+
const validEvents = normalizedEvents.filter(Boolean);
|
|
816
|
+
const timestamp = determineUpdateTimestamp(validEvents);
|
|
817
|
+
const meaningfulEvents = collapseEventsByFile(validEvents.filter((event) => isMeaningfulEvent(event)));
|
|
818
|
+
const groupedUpdates = groupEventsByIntent(meaningfulEvents, bootstrap);
|
|
819
|
+
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
820
|
+
const historyEntries = dedupeHistoryEntries(groupedUpdates.concat(previousHistoryEntries))
|
|
821
|
+
.slice(0, MAX_CHANGELOG_ENTRIES);
|
|
822
|
+
const promotedFeatures = promoteFeatures(historyEntries);
|
|
823
|
+
const keyFeatures = mergeKeyFeatures(bootstrap.keyFeatures, promotedFeatures);
|
|
824
|
+
const recentUpdates = groupedUpdates.length > 0
|
|
825
|
+
? dedupeRecentUpdates(groupedUpdates.map(toStateUpdate).concat(normalizeStoredUpdates(existingState.recent_updates)))
|
|
826
|
+
.slice(0, MAX_RECENT_UPDATES)
|
|
827
|
+
: normalizeStoredUpdates(existingState.recent_updates).slice(0, MAX_RECENT_UPDATES);
|
|
828
|
+
const nextState = {
|
|
829
|
+
project: metadata.project,
|
|
830
|
+
version: metadata.version,
|
|
831
|
+
last_updated: timestamp,
|
|
832
|
+
ai_summary: '',
|
|
833
|
+
tech_stack: bootstrap.techStack,
|
|
834
|
+
architecture_patterns: bootstrap.architecturePatterns,
|
|
835
|
+
implementation_details: bootstrap.implementationDetails,
|
|
836
|
+
current_stage: determineCurrentStage(keyFeatures, historyEntries, bootstrap.implementationDetails),
|
|
837
|
+
recent_updates: recentUpdates,
|
|
838
|
+
key_features: keyFeatures,
|
|
839
|
+
known_issues: deriveKnownIssues(resolvedRoot, bootstrap),
|
|
840
|
+
next_steps: []
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
nextState.ai_summary = generateAiSummary(nextState, bootstrap);
|
|
844
|
+
nextState.next_steps = generateNextSteps(nextState, bootstrap, historyEntries);
|
|
489
845
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
846
|
+
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
847
|
+
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
493
848
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
849
|
+
if (logger) {
|
|
850
|
+
logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
|
|
496
851
|
}
|
|
497
852
|
|
|
498
|
-
if (
|
|
499
|
-
|
|
853
|
+
if (typeof settings.syncCallback === 'function') {
|
|
854
|
+
await settings.syncCallback();
|
|
500
855
|
}
|
|
501
856
|
|
|
502
|
-
return
|
|
857
|
+
return nextState;
|
|
503
858
|
}
|
|
504
859
|
|
|
505
860
|
function isMeaningfulEvent(event) {
|
|
@@ -526,365 +881,225 @@ function collapseEventsByFile(events) {
|
|
|
526
881
|
return Array.from(collapsedEvents.values());
|
|
527
882
|
}
|
|
528
883
|
|
|
884
|
+
function determineUpdateTimestamp(events) {
|
|
885
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
886
|
+
return new Date().toISOString();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const latestEvent = events[events.length - 1];
|
|
890
|
+
return latestEvent.timestamp || new Date().toISOString();
|
|
891
|
+
}
|
|
892
|
+
|
|
529
893
|
function classifyChangeArea(filePath) {
|
|
530
|
-
const lowerPath = filePath.toLowerCase();
|
|
894
|
+
const lowerPath = normalizeProjectPath(filePath).toLowerCase();
|
|
531
895
|
|
|
532
896
|
if (lowerPath === 'package.json') {
|
|
533
|
-
return '
|
|
897
|
+
return 'configuration';
|
|
534
898
|
}
|
|
535
899
|
|
|
536
900
|
if (lowerPath === 'readme.md') {
|
|
537
901
|
return 'documentation';
|
|
538
902
|
}
|
|
539
903
|
|
|
540
|
-
if (lowerPath.startsWith('
|
|
541
|
-
return '
|
|
904
|
+
if (lowerPath.startsWith('bin/')) {
|
|
905
|
+
return 'cli';
|
|
542
906
|
}
|
|
543
907
|
|
|
544
|
-
if (lowerPath.startsWith('server/')) {
|
|
908
|
+
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
545
909
|
return 'backend';
|
|
546
910
|
}
|
|
547
911
|
|
|
548
|
-
if (
|
|
549
|
-
|
|
912
|
+
if (
|
|
913
|
+
lowerPath.startsWith('controllers/') ||
|
|
914
|
+
lowerPath.startsWith('services/') ||
|
|
915
|
+
lowerPath.startsWith('middleware/') ||
|
|
916
|
+
lowerPath.startsWith('config/')
|
|
917
|
+
) {
|
|
918
|
+
return 'application';
|
|
550
919
|
}
|
|
551
920
|
|
|
552
|
-
if (lowerPath.startsWith('
|
|
553
|
-
return '
|
|
921
|
+
if (lowerPath.startsWith('core/') || lowerPath.startsWith('src/')) {
|
|
922
|
+
return 'logic';
|
|
554
923
|
}
|
|
555
924
|
|
|
556
925
|
return 'project';
|
|
557
926
|
}
|
|
558
927
|
|
|
559
|
-
function
|
|
560
|
-
const
|
|
561
|
-
const
|
|
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();
|
|
928
|
+
function detectEventFeatureKey(filePath, bootstrap) {
|
|
929
|
+
const lowerPath = normalizeProjectPath(filePath).toLowerCase();
|
|
930
|
+
const signals = bootstrap.signals;
|
|
572
931
|
|
|
573
932
|
if (lowerPath === 'package.json') {
|
|
574
|
-
|
|
575
|
-
|
|
933
|
+
if (signals.hasGitAutomation) {
|
|
934
|
+
return 'public_sync';
|
|
935
|
+
}
|
|
576
936
|
|
|
577
|
-
|
|
578
|
-
|
|
937
|
+
if (signals.hasCliEntry) {
|
|
938
|
+
return 'cli_automation';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return 'config_management';
|
|
579
942
|
}
|
|
580
943
|
|
|
581
944
|
if (lowerPath.startsWith('bin/')) {
|
|
582
|
-
return '
|
|
945
|
+
return 'cli_automation';
|
|
583
946
|
}
|
|
584
947
|
|
|
585
|
-
if (lowerPath.startsWith('server/')) {
|
|
586
|
-
|
|
587
|
-
|
|
948
|
+
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
949
|
+
if (signals.hasAiContextArtifacts) {
|
|
950
|
+
return 'local_context_server';
|
|
951
|
+
}
|
|
588
952
|
|
|
589
|
-
|
|
590
|
-
return 'context_templates';
|
|
953
|
+
return 'rest_api';
|
|
591
954
|
}
|
|
592
955
|
|
|
593
956
|
if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
|
|
594
|
-
return '
|
|
957
|
+
return 'public_sync';
|
|
595
958
|
}
|
|
596
959
|
|
|
597
|
-
if (lowerPath.includes('
|
|
960
|
+
if (lowerPath.includes('watch')) {
|
|
598
961
|
return 'change_tracking';
|
|
599
962
|
}
|
|
600
963
|
|
|
601
964
|
if (lowerPath.includes('state') || lowerPath.includes('context')) {
|
|
602
|
-
return '
|
|
965
|
+
return 'ai_context_generation';
|
|
603
966
|
}
|
|
604
967
|
|
|
605
|
-
if (lowerPath.includes('
|
|
606
|
-
return '
|
|
968
|
+
if (lowerPath.includes('auth') || lowerPath.includes('jwt')) {
|
|
969
|
+
return 'auth';
|
|
607
970
|
}
|
|
608
971
|
|
|
609
|
-
if (
|
|
610
|
-
return '
|
|
972
|
+
if (lowerPath.includes('service') || lowerPath.includes('controller')) {
|
|
973
|
+
return signals.hasAxiosOrFetch ? 'external_api' : 'rest_api';
|
|
611
974
|
}
|
|
612
975
|
|
|
613
|
-
return '
|
|
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
|
-
};
|
|
976
|
+
return 'ai_context_generation';
|
|
627
977
|
}
|
|
628
978
|
|
|
629
|
-
function groupEventsByIntent(events) {
|
|
979
|
+
function groupEventsByIntent(events, bootstrap) {
|
|
630
980
|
if (!Array.isArray(events) || events.length === 0) {
|
|
631
981
|
return [];
|
|
632
982
|
}
|
|
633
983
|
|
|
634
|
-
const
|
|
984
|
+
const buckets = new Map();
|
|
635
985
|
|
|
636
986
|
for (const event of events) {
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
rootDirectory: descriptor.rootDirectory,
|
|
644
|
-
events: []
|
|
645
|
-
});
|
|
987
|
+
const area = classifyChangeArea(event.file);
|
|
988
|
+
const featureKey = detectEventFeatureKey(event.file, bootstrap);
|
|
989
|
+
const key = `${area}:${featureKey}`;
|
|
990
|
+
|
|
991
|
+
if (!buckets.has(key)) {
|
|
992
|
+
buckets.set(key, []);
|
|
646
993
|
}
|
|
647
994
|
|
|
648
|
-
|
|
995
|
+
buckets.get(key).push(
|
|
996
|
+
Object.assign({}, event, {
|
|
997
|
+
area,
|
|
998
|
+
featureKey
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
649
1001
|
}
|
|
650
1002
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return mergedGroups
|
|
1003
|
+
return Array.from(buckets.values())
|
|
654
1004
|
.map((group) => interpretIntentGroup(group))
|
|
655
1005
|
.filter(Boolean)
|
|
656
1006
|
.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
657
1007
|
}
|
|
658
1008
|
|
|
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
1009
|
function interpretIntentGroup(group) {
|
|
719
|
-
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const latestTimestamp = group.events.reduce((latest, event) => {
|
|
1010
|
+
const latestTimestamp = group.reduce((latest, event) => {
|
|
724
1011
|
return new Date(event.timestamp) > new Date(latest) ? event.timestamp : latest;
|
|
725
|
-
}, group
|
|
726
|
-
const featureKey =
|
|
727
|
-
const
|
|
1012
|
+
}, group[0].timestamp || new Date().toISOString());
|
|
1013
|
+
const featureKey = group[0].featureKey;
|
|
1014
|
+
const area = group[0].area;
|
|
728
1015
|
const type = determineGroupedUpdateType(group);
|
|
729
|
-
const subject = describeIntentSubject(
|
|
1016
|
+
const subject = describeIntentSubject(featureKey);
|
|
730
1017
|
|
|
731
1018
|
return {
|
|
732
1019
|
timestamp: latestTimestamp,
|
|
733
|
-
scope:
|
|
1020
|
+
scope: area,
|
|
734
1021
|
title: buildIntentTitle(type, subject),
|
|
735
1022
|
type,
|
|
736
|
-
impact: describeIntentImpact(type, featureKey
|
|
1023
|
+
impact: describeIntentImpact(type, featureKey),
|
|
737
1024
|
feature_key: featureKey,
|
|
738
|
-
feature_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
|
|
1025
|
+
feature_name: FEATURE_CATALOG[featureKey] || subject
|
|
782
1026
|
};
|
|
783
|
-
|
|
784
|
-
return priorities[featureKey] || 0;
|
|
785
1027
|
}
|
|
786
1028
|
|
|
787
1029
|
function determineGroupedUpdateType(group) {
|
|
788
|
-
const
|
|
789
|
-
const
|
|
1030
|
+
const hasAdd = group.some((event) => event.action === 'add');
|
|
1031
|
+
const hasDelete = group.some((event) => event.action === 'delete');
|
|
790
1032
|
|
|
791
|
-
if (
|
|
1033
|
+
if (hasAdd) {
|
|
792
1034
|
return 'feature';
|
|
793
1035
|
}
|
|
794
1036
|
|
|
795
|
-
if (
|
|
1037
|
+
if (hasDelete) {
|
|
796
1038
|
return 'fix';
|
|
797
1039
|
}
|
|
798
1040
|
|
|
799
|
-
if (
|
|
1041
|
+
if (group.length > 2) {
|
|
800
1042
|
return 'refactor';
|
|
801
1043
|
}
|
|
802
1044
|
|
|
803
1045
|
return 'improvement';
|
|
804
1046
|
}
|
|
805
1047
|
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
}
|
|
1048
|
+
function describeIntentSubject(featureKey) {
|
|
1049
|
+
const subjectMap = {
|
|
1050
|
+
ai_context_generation: 'AI context generation workflow',
|
|
1051
|
+
cli_automation: 'CLI automation workflow',
|
|
1052
|
+
change_tracking: 'change tracking workflow',
|
|
1053
|
+
public_sync: 'GitHub sync workflow',
|
|
1054
|
+
local_context_server: 'context delivery service',
|
|
1055
|
+
rest_api: 'API architecture',
|
|
1056
|
+
auth: 'authentication workflow',
|
|
1057
|
+
persistence: 'data persistence layer',
|
|
1058
|
+
realtime: 'real-time communication layer',
|
|
1059
|
+
external_api: 'external integration workflow',
|
|
1060
|
+
middleware: 'request processing workflow',
|
|
1061
|
+
config_management: 'project configuration workflow'
|
|
1062
|
+
};
|
|
838
1063
|
|
|
839
|
-
return
|
|
1064
|
+
return subjectMap[featureKey] || 'project workflow';
|
|
840
1065
|
}
|
|
841
1066
|
|
|
842
1067
|
function buildIntentTitle(type, subject) {
|
|
843
1068
|
const verbs = {
|
|
844
1069
|
feature: 'Expanded',
|
|
845
1070
|
improvement: 'Improved',
|
|
846
|
-
refactor: '
|
|
1071
|
+
refactor: 'Refined',
|
|
847
1072
|
fix: 'Stabilized'
|
|
848
1073
|
};
|
|
849
1074
|
|
|
850
1075
|
return `${verbs[type] || 'Improved'} ${subject}`;
|
|
851
1076
|
}
|
|
852
1077
|
|
|
853
|
-
function describeIntentImpact(type, featureKey
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
local_context_server: 'Improves how
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1078
|
+
function describeIntentImpact(type, featureKey) {
|
|
1079
|
+
const impactMap = {
|
|
1080
|
+
ai_context_generation: 'Improves the quality of generated AI-readable project context.',
|
|
1081
|
+
cli_automation: 'Improves how developers control the project workflow from the command line.',
|
|
1082
|
+
change_tracking: 'Improves how meaningful project changes are detected without noise.',
|
|
1083
|
+
public_sync: 'Improves how project state is published for external AI consumption.',
|
|
1084
|
+
local_context_server: 'Improves how current project context is delivered over HTTP endpoints.',
|
|
1085
|
+
rest_api: 'Improves the structure and clarity of the project API surface.',
|
|
1086
|
+
auth: 'Improves authentication reliability and access control.',
|
|
1087
|
+
persistence: 'Improves how project data is persisted and retrieved.',
|
|
1088
|
+
realtime: 'Improves real-time communication behavior.',
|
|
1089
|
+
external_api: 'Improves outbound integration reliability.',
|
|
1090
|
+
middleware: 'Improves request handling and middleware orchestration.',
|
|
1091
|
+
config_management: 'Improves project configuration and packaging reliability.'
|
|
867
1092
|
};
|
|
868
1093
|
|
|
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
1094
|
if (type === 'fix') {
|
|
880
|
-
return
|
|
1095
|
+
return impactMap[featureKey].replace(/^Improves /, 'Resolves issues in ');
|
|
881
1096
|
}
|
|
882
1097
|
|
|
883
|
-
|
|
884
|
-
|
|
1098
|
+
if (type === 'feature') {
|
|
1099
|
+
return impactMap[featureKey].replace(/^Improves /, 'Adds ');
|
|
1100
|
+
}
|
|
885
1101
|
|
|
886
|
-
|
|
887
|
-
return groupEventsByIntent([event])[0] || null;
|
|
1102
|
+
return impactMap[featureKey] || 'Improves the overall project workflow.';
|
|
888
1103
|
}
|
|
889
1104
|
|
|
890
1105
|
function normalizeStoredUpdates(updates) {
|
|
@@ -892,7 +1107,11 @@ function normalizeStoredUpdates(updates) {
|
|
|
892
1107
|
return [];
|
|
893
1108
|
}
|
|
894
1109
|
|
|
895
|
-
return dedupeRecentUpdates(
|
|
1110
|
+
return dedupeRecentUpdates(
|
|
1111
|
+
updates
|
|
1112
|
+
.map((update) => normalizeStoredUpdate(update))
|
|
1113
|
+
.filter(Boolean)
|
|
1114
|
+
);
|
|
896
1115
|
}
|
|
897
1116
|
|
|
898
1117
|
function normalizeStoredUpdate(update) {
|
|
@@ -908,10 +1127,6 @@ function normalizeStoredUpdate(update) {
|
|
|
908
1127
|
};
|
|
909
1128
|
}
|
|
910
1129
|
|
|
911
|
-
if (update.file && update.action && isMeaningfulEvent(update)) {
|
|
912
|
-
return toStateUpdate(interpretChange(update));
|
|
913
|
-
}
|
|
914
|
-
|
|
915
1130
|
return null;
|
|
916
1131
|
}
|
|
917
1132
|
|
|
@@ -933,118 +1148,71 @@ function normalizeStoredHistoryEntry(entry) {
|
|
|
933
1148
|
}
|
|
934
1149
|
|
|
935
1150
|
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
1151
|
return {
|
|
942
1152
|
timestamp: entry.timestamp || new Date(0).toISOString(),
|
|
943
|
-
scope: entry.scope ||
|
|
944
|
-
title:
|
|
945
|
-
type:
|
|
946
|
-
impact:
|
|
947
|
-
feature_key:
|
|
948
|
-
feature_name: entry.feature_name ||
|
|
949
|
-
source: entry.source || 'history'
|
|
1153
|
+
scope: entry.scope || 'project',
|
|
1154
|
+
title: entry.title,
|
|
1155
|
+
type: normalizeUpdateType(entry.type),
|
|
1156
|
+
impact: entry.impact,
|
|
1157
|
+
feature_key: entry.feature_key || inferFeatureKeyFromText(entry.title, entry.impact),
|
|
1158
|
+
feature_name: entry.feature_name || FEATURE_CATALOG[inferFeatureKeyFromText(entry.title, entry.impact)] || 'Project capability'
|
|
950
1159
|
};
|
|
951
1160
|
}
|
|
952
1161
|
|
|
953
|
-
if (entry.file && entry.action && isMeaningfulEvent(entry)) {
|
|
954
|
-
return interpretChange(entry);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
1162
|
return null;
|
|
958
1163
|
}
|
|
959
1164
|
|
|
960
|
-
function
|
|
961
|
-
const combinedText = `${
|
|
1165
|
+
function inferFeatureKeyFromText(title, impact) {
|
|
1166
|
+
const combinedText = `${title || ''} ${impact || ''}`.toLowerCase();
|
|
962
1167
|
|
|
963
|
-
if (combinedText.includes('
|
|
964
|
-
return
|
|
965
|
-
featureKey: 'github_sync',
|
|
966
|
-
featureName: getFeatureMeta('github_sync').name,
|
|
967
|
-
scope: 'logic'
|
|
968
|
-
};
|
|
1168
|
+
if (combinedText.includes('jwt') || combinedText.includes('auth')) {
|
|
1169
|
+
return 'auth';
|
|
969
1170
|
}
|
|
970
1171
|
|
|
971
|
-
if (combinedText.includes('
|
|
972
|
-
return
|
|
973
|
-
featureKey: 'project_intelligence',
|
|
974
|
-
featureName: getFeatureMeta('project_intelligence').name,
|
|
975
|
-
scope: 'logic'
|
|
976
|
-
};
|
|
1172
|
+
if (combinedText.includes('mongo') || combinedText.includes('mongoose') || combinedText.includes('persist')) {
|
|
1173
|
+
return 'persistence';
|
|
977
1174
|
}
|
|
978
1175
|
|
|
979
|
-
if (combinedText.includes('
|
|
980
|
-
return
|
|
981
|
-
featureKey: 'change_tracking',
|
|
982
|
-
featureName: getFeatureMeta('change_tracking').name,
|
|
983
|
-
scope: 'logic'
|
|
984
|
-
};
|
|
1176
|
+
if (combinedText.includes('real-time') || combinedText.includes('socket')) {
|
|
1177
|
+
return 'realtime';
|
|
985
1178
|
}
|
|
986
1179
|
|
|
987
|
-
if (
|
|
988
|
-
|
|
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
|
-
};
|
|
1180
|
+
if (combinedText.includes('api') || combinedText.includes('route')) {
|
|
1181
|
+
return 'rest_api';
|
|
998
1182
|
}
|
|
999
1183
|
|
|
1000
|
-
if (combinedText.includes('
|
|
1001
|
-
return
|
|
1002
|
-
featureKey: 'cli_workflow',
|
|
1003
|
-
featureName: getFeatureMeta('cli_workflow').name,
|
|
1004
|
-
scope: 'cli'
|
|
1005
|
-
};
|
|
1184
|
+
if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
|
|
1185
|
+
return 'public_sync';
|
|
1006
1186
|
}
|
|
1007
1187
|
|
|
1008
|
-
if (combinedText.includes('
|
|
1009
|
-
return
|
|
1010
|
-
featureKey: 'documentation',
|
|
1011
|
-
featureName: getFeatureMeta('documentation').name,
|
|
1012
|
-
scope: 'documentation'
|
|
1013
|
-
};
|
|
1188
|
+
if (combinedText.includes('watch') || combinedText.includes('change')) {
|
|
1189
|
+
return 'change_tracking';
|
|
1014
1190
|
}
|
|
1015
1191
|
|
|
1016
|
-
if (combinedText.includes('
|
|
1017
|
-
return
|
|
1018
|
-
featureKey: 'package_configuration',
|
|
1019
|
-
featureName: getFeatureMeta('package_configuration').name,
|
|
1020
|
-
scope: 'dependencies'
|
|
1021
|
-
};
|
|
1192
|
+
if (combinedText.includes('command line') || combinedText.includes('cli')) {
|
|
1193
|
+
return 'cli_automation';
|
|
1022
1194
|
}
|
|
1023
1195
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1196
|
+
if (combinedText.includes('http') || combinedText.includes('context delivery')) {
|
|
1197
|
+
return 'local_context_server';
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return 'ai_context_generation';
|
|
1029
1201
|
}
|
|
1030
1202
|
|
|
1031
1203
|
function normalizeUpdateType(type) {
|
|
1032
|
-
if (type === 'removal') {
|
|
1033
|
-
return 'refactor';
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
1204
|
if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
|
|
1037
1205
|
return type;
|
|
1038
1206
|
}
|
|
1039
1207
|
|
|
1208
|
+
if (type === 'removal') {
|
|
1209
|
+
return 'fix';
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1040
1212
|
return 'improvement';
|
|
1041
1213
|
}
|
|
1042
1214
|
|
|
1043
1215
|
function toStateUpdate(update) {
|
|
1044
|
-
if (!update) {
|
|
1045
|
-
return null;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
1216
|
return {
|
|
1049
1217
|
title: update.title,
|
|
1050
1218
|
type: normalizeUpdateType(update.type),
|
|
@@ -1053,17 +1221,17 @@ function toStateUpdate(update) {
|
|
|
1053
1221
|
}
|
|
1054
1222
|
|
|
1055
1223
|
function dedupeRecentUpdates(updates) {
|
|
1056
|
-
const
|
|
1224
|
+
const seen = new Set();
|
|
1057
1225
|
const result = [];
|
|
1058
1226
|
|
|
1059
1227
|
for (const update of updates.filter(Boolean)) {
|
|
1060
1228
|
const key = `${update.title}::${update.type}::${update.impact}`;
|
|
1061
1229
|
|
|
1062
|
-
if (
|
|
1230
|
+
if (seen.has(key)) {
|
|
1063
1231
|
continue;
|
|
1064
1232
|
}
|
|
1065
1233
|
|
|
1066
|
-
|
|
1234
|
+
seen.add(key);
|
|
1067
1235
|
result.push(update);
|
|
1068
1236
|
}
|
|
1069
1237
|
|
|
@@ -1071,190 +1239,81 @@ function dedupeRecentUpdates(updates) {
|
|
|
1071
1239
|
}
|
|
1072
1240
|
|
|
1073
1241
|
function dedupeHistoryEntries(entries) {
|
|
1074
|
-
const
|
|
1242
|
+
const seen = new Set();
|
|
1075
1243
|
const result = [];
|
|
1076
1244
|
|
|
1077
1245
|
for (const entry of entries.filter(Boolean)) {
|
|
1078
1246
|
const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
|
|
1079
1247
|
|
|
1080
|
-
if (
|
|
1248
|
+
if (seen.has(key)) {
|
|
1081
1249
|
continue;
|
|
1082
1250
|
}
|
|
1083
1251
|
|
|
1084
|
-
|
|
1252
|
+
seen.add(key);
|
|
1085
1253
|
result.push(entry);
|
|
1086
1254
|
}
|
|
1087
1255
|
|
|
1088
1256
|
return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
1089
1257
|
}
|
|
1090
1258
|
|
|
1091
|
-
function promoteFeatures(
|
|
1092
|
-
if (!Array.isArray(
|
|
1259
|
+
function promoteFeatures(historyEntries) {
|
|
1260
|
+
if (!Array.isArray(historyEntries) || historyEntries.length === 0) {
|
|
1093
1261
|
return [];
|
|
1094
1262
|
}
|
|
1095
1263
|
|
|
1096
|
-
const
|
|
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
|
-
}
|
|
1264
|
+
const scores = new Map();
|
|
1162
1265
|
|
|
1163
|
-
for (const
|
|
1164
|
-
|
|
1165
|
-
break;
|
|
1166
|
-
}
|
|
1266
|
+
for (const entry of historyEntries) {
|
|
1267
|
+
const featureName = entry.feature_name || FEATURE_CATALOG[entry.feature_key];
|
|
1167
1268
|
|
|
1168
|
-
if (
|
|
1269
|
+
if (!featureName) {
|
|
1169
1270
|
continue;
|
|
1170
1271
|
}
|
|
1171
1272
|
|
|
1172
|
-
|
|
1173
|
-
seenFeatureNames.add(feature.featureName);
|
|
1273
|
+
scores.set(featureName, (scores.get(featureName) || 0) + scoreHistoryEntry(entry));
|
|
1174
1274
|
}
|
|
1175
1275
|
|
|
1176
|
-
return
|
|
1276
|
+
return Array.from(scores.entries())
|
|
1277
|
+
.sort((left, right) => right[1] - left[1])
|
|
1278
|
+
.map(([featureName]) => featureName)
|
|
1279
|
+
.slice(0, MAX_KEY_FEATURES);
|
|
1177
1280
|
}
|
|
1178
1281
|
|
|
1179
|
-
function
|
|
1180
|
-
const
|
|
1282
|
+
function scoreHistoryEntry(entry) {
|
|
1283
|
+
const weights = {
|
|
1181
1284
|
feature: 4,
|
|
1182
1285
|
refactor: 3,
|
|
1183
1286
|
improvement: 2,
|
|
1184
1287
|
fix: 2
|
|
1185
1288
|
};
|
|
1186
1289
|
|
|
1187
|
-
return
|
|
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
|
-
};
|
|
1290
|
+
return weights[normalizeUpdateType(entry.type)] || 1;
|
|
1208
1291
|
}
|
|
1209
1292
|
|
|
1210
|
-
function
|
|
1211
|
-
|
|
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';
|
|
1293
|
+
function mergeKeyFeatures(primaryFeatures, promotedFeatures) {
|
|
1294
|
+
return uniqueNonEmpty([].concat(primaryFeatures || [], promotedFeatures || [])).slice(0, MAX_KEY_FEATURES);
|
|
1237
1295
|
}
|
|
1238
1296
|
|
|
1239
|
-
function deriveKnownIssues(projectRoot,
|
|
1297
|
+
function deriveKnownIssues(projectRoot, bootstrap) {
|
|
1240
1298
|
const knownIssues = [];
|
|
1241
1299
|
|
|
1242
1300
|
if (!hasTestIndicators(projectRoot)) {
|
|
1243
1301
|
knownIssues.push('No automated test suite is detected yet.');
|
|
1244
1302
|
}
|
|
1245
1303
|
|
|
1246
|
-
if (
|
|
1247
|
-
knownIssues.push('
|
|
1304
|
+
if (bootstrap.implementationDetails.length === 0) {
|
|
1305
|
+
knownIssues.push('Project structure exposes limited implementation signals, so deeper architecture details may still be missing.');
|
|
1248
1306
|
}
|
|
1249
1307
|
|
|
1250
|
-
if (
|
|
1251
|
-
knownIssues.push('
|
|
1308
|
+
if (!bootstrap.techStack.framework && bootstrap.techStack.language === 'Node.js') {
|
|
1309
|
+
knownIssues.push('No common Node.js application framework dependency is currently detected.');
|
|
1252
1310
|
}
|
|
1253
1311
|
|
|
1254
1312
|
return knownIssues;
|
|
1255
1313
|
}
|
|
1256
1314
|
|
|
1257
1315
|
function hasTestIndicators(projectRoot) {
|
|
1316
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
1258
1317
|
const testPaths = [
|
|
1259
1318
|
'test',
|
|
1260
1319
|
'tests',
|
|
@@ -1265,11 +1324,11 @@ function hasTestIndicators(projectRoot) {
|
|
|
1265
1324
|
'jest.config.mjs'
|
|
1266
1325
|
];
|
|
1267
1326
|
|
|
1268
|
-
return testPaths.some((relativePath) => fs.existsSync(path.join(
|
|
1327
|
+
return testPaths.some((relativePath) => fs.existsSync(path.join(resolvedRoot, relativePath)));
|
|
1269
1328
|
}
|
|
1270
1329
|
|
|
1271
|
-
function determineCurrentStage(keyFeatures, historyEntries) {
|
|
1272
|
-
if (keyFeatures.length >= 4 &&
|
|
1330
|
+
function determineCurrentStage(keyFeatures, historyEntries, implementationDetails) {
|
|
1331
|
+
if (keyFeatures.length >= 4 && implementationDetails.length >= 3) {
|
|
1273
1332
|
return 'Production-ready';
|
|
1274
1333
|
}
|
|
1275
1334
|
|
|
@@ -1280,86 +1339,69 @@ function determineCurrentStage(keyFeatures, historyEntries) {
|
|
|
1280
1339
|
return 'Early development';
|
|
1281
1340
|
}
|
|
1282
1341
|
|
|
1283
|
-
function generateAiSummary(state,
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1342
|
+
function generateAiSummary(state, bootstrap) {
|
|
1343
|
+
const features = state.key_features || [];
|
|
1344
|
+
const details = bootstrap.implementationDetails || [];
|
|
1345
|
+
const projectType = bootstrap.projectType || 'software project';
|
|
1346
|
+
const normalizedType = projectType === 'backend API platform'
|
|
1347
|
+
? 'Backend API platform'
|
|
1348
|
+
: capitalize(projectType);
|
|
1290
1349
|
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
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';
|
|
1350
|
+
if (details.some((detail) => detail.includes('JWT')) && details.some((detail) => detail.includes('MongoDB'))) {
|
|
1351
|
+
return `${normalizedType} with JWT authentication and MongoDB-backed application logic.`;
|
|
1297
1352
|
}
|
|
1298
1353
|
|
|
1299
1354
|
if (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
featureSignals.has('cli_orchestration')
|
|
1355
|
+
features.includes(FEATURE_CATALOG.ai_context_generation) &&
|
|
1356
|
+
features.includes(FEATURE_CATALOG.public_sync)
|
|
1303
1357
|
) {
|
|
1304
|
-
|
|
1305
|
-
} else if (featureSignals.has('local_context_server')) {
|
|
1306
|
-
uniqueValue = 'and keeps current context available through local AI endpoints';
|
|
1358
|
+
return `${normalizedType} that generates AI-readable project context, tracks meaningful changes, and can publish public project state through GitHub.`;
|
|
1307
1359
|
}
|
|
1308
1360
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1361
|
+
if (
|
|
1362
|
+
features.includes(FEATURE_CATALOG.rest_api) &&
|
|
1363
|
+
details.some((detail) => detail.includes('Express'))
|
|
1364
|
+
) {
|
|
1365
|
+
return `${normalizedType} with RESTful request handling and structured server-side workflow orchestration.`;
|
|
1366
|
+
}
|
|
1314
1367
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
featureSignals.add(entry.feature_key);
|
|
1318
|
-
}
|
|
1368
|
+
if (features.length >= 2) {
|
|
1369
|
+
return `${normalizedType} focused on ${features.slice(0, 2).join(' and ').toLowerCase()}.`;
|
|
1319
1370
|
}
|
|
1320
1371
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
if (featureMeta.name === featureName) {
|
|
1324
|
-
featureSignals.add(featureKey);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1372
|
+
if (details.length > 0) {
|
|
1373
|
+
return `${normalizedType} built around ${details[0].toLowerCase()}.`;
|
|
1327
1374
|
}
|
|
1328
1375
|
|
|
1329
|
-
return
|
|
1376
|
+
return `${normalizedType} with detectable project structure and implementation patterns.`;
|
|
1330
1377
|
}
|
|
1331
1378
|
|
|
1332
|
-
function generateNextSteps(state, historyEntries) {
|
|
1379
|
+
function generateNextSteps(state, bootstrap, historyEntries) {
|
|
1333
1380
|
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
1381
|
|
|
1340
1382
|
if (state.known_issues.includes('No automated test suite is detected yet.')) {
|
|
1341
|
-
nextSteps.push('Add automated tests
|
|
1383
|
+
nextSteps.push('Add automated tests that cover the main application flow and critical integration points.');
|
|
1342
1384
|
}
|
|
1343
1385
|
|
|
1344
|
-
if (
|
|
1345
|
-
nextSteps.push('Strengthen the
|
|
1386
|
+
if (bootstrap.implementationDetails.length < 2) {
|
|
1387
|
+
nextSteps.push('Strengthen the project structure so major implementation patterns are easier to detect automatically.');
|
|
1346
1388
|
}
|
|
1347
1389
|
|
|
1348
|
-
if (
|
|
1349
|
-
nextSteps.push('
|
|
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.');
|
|
1390
|
+
if (historyEntries.length === 0) {
|
|
1391
|
+
nextSteps.push('Capture the next meaningful project update so recent evolution is reflected alongside the bootstrap analysis.');
|
|
1354
1392
|
}
|
|
1355
1393
|
|
|
1356
1394
|
if (state.current_stage === 'Early development') {
|
|
1357
|
-
nextSteps.push('Ship the next core
|
|
1395
|
+
nextSteps.push('Ship the next core capability to move the project from initial structure into a functional prototype.');
|
|
1358
1396
|
}
|
|
1359
1397
|
|
|
1360
1398
|
return Array.from(new Set(nextSteps)).slice(0, 4);
|
|
1361
1399
|
}
|
|
1362
1400
|
|
|
1401
|
+
function uniqueNonEmpty(values) {
|
|
1402
|
+
return Array.from(new Set((values || []).filter(Boolean)));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1363
1405
|
function capitalize(value) {
|
|
1364
1406
|
if (!value) {
|
|
1365
1407
|
return '';
|
|
@@ -1431,6 +1473,7 @@ function createDebouncedStateUpdater(projectRoot, options) {
|
|
|
1431
1473
|
module.exports = {
|
|
1432
1474
|
CONTEXT_DIR_NAME,
|
|
1433
1475
|
DEFAULT_CONFIG,
|
|
1476
|
+
bootstrapProjectAnalysis,
|
|
1434
1477
|
createDebouncedStateUpdater,
|
|
1435
1478
|
createDefaultChangelog,
|
|
1436
1479
|
createDefaultState,
|
|
@@ -1438,7 +1481,6 @@ module.exports = {
|
|
|
1438
1481
|
ensureContextDirectory,
|
|
1439
1482
|
getContextPaths,
|
|
1440
1483
|
groupEventsByIntent,
|
|
1441
|
-
interpretChange,
|
|
1442
1484
|
loadRuntimeConfig,
|
|
1443
1485
|
promoteFeatures,
|
|
1444
1486
|
readJsonFile,
|