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