aibridge-context 1.4.1 → 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 +831 -817
- 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,488 @@ 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
|
|
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
|
+
|
|
358
436
|
return {
|
|
359
|
-
|
|
437
|
+
projectType,
|
|
438
|
+
techStack,
|
|
439
|
+
architecturePatterns,
|
|
440
|
+
implementationDetails,
|
|
441
|
+
keyFeatures,
|
|
442
|
+
signals: implementationSignals
|
|
360
443
|
};
|
|
361
444
|
}
|
|
362
445
|
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return accumulator.split(`{{${key}}}`).join(safeValue);
|
|
367
|
-
}, template);
|
|
368
|
-
}
|
|
446
|
+
function collectAnalysisInputs(projectRoot) {
|
|
447
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
448
|
+
const selectedFiles = new Set();
|
|
369
449
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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);
|
|
374
482
|
}
|
|
375
483
|
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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);
|
|
381
499
|
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
};
|
|
384
557
|
}
|
|
385
558
|
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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'
|
|
393
575
|
);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const meaningfulEvents = collapseEventsByFile(
|
|
406
|
-
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'
|
|
407
587
|
);
|
|
408
|
-
|
|
409
|
-
const capabilityHistory = buildProjectCapabilityHistory(projectRoot);
|
|
410
|
-
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
411
|
-
const historyEntries = dedupeHistoryEntries(
|
|
412
|
-
groupedUpdates.concat(previousHistoryEntries, capabilityHistory)
|
|
413
|
-
).slice(0, MAX_CHANGELOG_ENTRIES);
|
|
414
|
-
const recentUpdates = historyEntries
|
|
415
|
-
.filter((entry) => entry.source !== 'project_snapshot')
|
|
416
|
-
.slice(0, MAX_RECENT_UPDATES)
|
|
417
|
-
.map(toStateUpdate);
|
|
418
|
-
const keyFeatures = promoteFeatures(historyEntries);
|
|
419
|
-
const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack, keyFeatures);
|
|
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: []
|
|
431
|
-
};
|
|
588
|
+
}
|
|
432
589
|
|
|
433
|
-
|
|
434
|
-
|
|
590
|
+
function buildArchitecturePatterns(signals, analysisInputs, techStack, rootPackage) {
|
|
591
|
+
const patterns = [];
|
|
435
592
|
|
|
436
|
-
|
|
437
|
-
|
|
593
|
+
if (signals.hasCliEntry) {
|
|
594
|
+
patterns.push('Command-line automation workflow');
|
|
595
|
+
}
|
|
438
596
|
|
|
439
|
-
if (
|
|
440
|
-
|
|
597
|
+
if (signals.hasWatcher) {
|
|
598
|
+
patterns.push('Event-driven file watching pipeline');
|
|
441
599
|
}
|
|
442
600
|
|
|
443
|
-
if (
|
|
444
|
-
|
|
601
|
+
if (signals.hasExpress && signals.hasRestRoutes) {
|
|
602
|
+
patterns.push('REST API architecture');
|
|
445
603
|
}
|
|
446
604
|
|
|
447
|
-
|
|
448
|
-
|
|
605
|
+
if (signals.hasMiddleware) {
|
|
606
|
+
patterns.push('Middleware-driven request pipeline');
|
|
607
|
+
}
|
|
449
608
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return new Date().toISOString();
|
|
609
|
+
if (signals.hasControllers && signals.hasServices) {
|
|
610
|
+
patterns.push('Layered controller-service architecture');
|
|
453
611
|
}
|
|
454
612
|
|
|
455
|
-
|
|
456
|
-
|
|
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);
|
|
457
644
|
}
|
|
458
645
|
|
|
459
|
-
function
|
|
460
|
-
|
|
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);
|
|
461
690
|
}
|
|
462
691
|
|
|
463
|
-
function
|
|
464
|
-
const
|
|
692
|
+
function buildKeyFeatures(signals, techStack) {
|
|
693
|
+
const features = [];
|
|
465
694
|
|
|
466
|
-
if (
|
|
467
|
-
|
|
695
|
+
if (signals.hasAiContextArtifacts) {
|
|
696
|
+
features.push(FEATURE_CATALOG.ai_context_generation);
|
|
468
697
|
}
|
|
469
698
|
|
|
470
|
-
|
|
471
|
-
|
|
699
|
+
if (signals.hasCliEntry) {
|
|
700
|
+
features.push(FEATURE_CATALOG.cli_automation);
|
|
701
|
+
}
|
|
472
702
|
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
segments.includes('.git') ||
|
|
476
|
-
segments.includes('.ai-context') ||
|
|
477
|
-
segments.includes('dist') ||
|
|
478
|
-
segments.includes('build')
|
|
479
|
-
) {
|
|
480
|
-
return true;
|
|
703
|
+
if (signals.hasWatcher) {
|
|
704
|
+
features.push(FEATURE_CATALOG.change_tracking);
|
|
481
705
|
}
|
|
482
706
|
|
|
483
|
-
if (
|
|
484
|
-
|
|
707
|
+
if (signals.hasGitAutomation) {
|
|
708
|
+
features.push(FEATURE_CATALOG.public_sync);
|
|
485
709
|
}
|
|
486
710
|
|
|
487
|
-
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)
|
|
497
|
-
) {
|
|
498
|
-
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);
|
|
499
715
|
}
|
|
500
716
|
|
|
501
|
-
|
|
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);
|
|
502
746
|
}
|
|
503
747
|
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
let score = 0;
|
|
748
|
+
function determineProjectType(signals, techStack, rootPackage) {
|
|
749
|
+
if (signals.hasNext) {
|
|
750
|
+
return 'Next.js application';
|
|
751
|
+
}
|
|
509
752
|
|
|
510
|
-
if (
|
|
511
|
-
return
|
|
753
|
+
if (signals.hasCliEntry && signals.hasAiContextArtifacts) {
|
|
754
|
+
return 'CLI tool';
|
|
512
755
|
}
|
|
513
756
|
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
}
|
|
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);
|
|
517
845
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
846
|
+
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
847
|
+
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
521
848
|
|
|
522
|
-
if (
|
|
523
|
-
|
|
849
|
+
if (logger) {
|
|
850
|
+
logger.debug(`Updated AI context with ${groupedUpdates.length} grouped project intent(s).`);
|
|
524
851
|
}
|
|
525
852
|
|
|
526
|
-
if (
|
|
527
|
-
|
|
853
|
+
if (typeof settings.syncCallback === 'function') {
|
|
854
|
+
await settings.syncCallback();
|
|
528
855
|
}
|
|
529
856
|
|
|
530
|
-
return
|
|
857
|
+
return nextState;
|
|
531
858
|
}
|
|
532
859
|
|
|
533
860
|
function isMeaningfulEvent(event) {
|
|
@@ -554,365 +881,225 @@ function collapseEventsByFile(events) {
|
|
|
554
881
|
return Array.from(collapsedEvents.values());
|
|
555
882
|
}
|
|
556
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
|
+
|
|
557
893
|
function classifyChangeArea(filePath) {
|
|
558
|
-
const lowerPath = filePath.toLowerCase();
|
|
894
|
+
const lowerPath = normalizeProjectPath(filePath).toLowerCase();
|
|
559
895
|
|
|
560
896
|
if (lowerPath === 'package.json') {
|
|
561
|
-
return '
|
|
897
|
+
return 'configuration';
|
|
562
898
|
}
|
|
563
899
|
|
|
564
900
|
if (lowerPath === 'readme.md') {
|
|
565
901
|
return 'documentation';
|
|
566
902
|
}
|
|
567
903
|
|
|
568
|
-
if (lowerPath.startsWith('
|
|
569
|
-
return '
|
|
904
|
+
if (lowerPath.startsWith('bin/')) {
|
|
905
|
+
return 'cli';
|
|
570
906
|
}
|
|
571
907
|
|
|
572
|
-
if (lowerPath.startsWith('server/')) {
|
|
908
|
+
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
573
909
|
return 'backend';
|
|
574
910
|
}
|
|
575
911
|
|
|
576
|
-
if (
|
|
577
|
-
|
|
912
|
+
if (
|
|
913
|
+
lowerPath.startsWith('controllers/') ||
|
|
914
|
+
lowerPath.startsWith('services/') ||
|
|
915
|
+
lowerPath.startsWith('middleware/') ||
|
|
916
|
+
lowerPath.startsWith('config/')
|
|
917
|
+
) {
|
|
918
|
+
return 'application';
|
|
578
919
|
}
|
|
579
920
|
|
|
580
|
-
if (lowerPath.startsWith('
|
|
581
|
-
return '
|
|
921
|
+
if (lowerPath.startsWith('core/') || lowerPath.startsWith('src/')) {
|
|
922
|
+
return 'logic';
|
|
582
923
|
}
|
|
583
924
|
|
|
584
925
|
return 'project';
|
|
585
926
|
}
|
|
586
927
|
|
|
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();
|
|
928
|
+
function detectEventFeatureKey(filePath, bootstrap) {
|
|
929
|
+
const lowerPath = normalizeProjectPath(filePath).toLowerCase();
|
|
930
|
+
const signals = bootstrap.signals;
|
|
600
931
|
|
|
601
932
|
if (lowerPath === 'package.json') {
|
|
602
|
-
|
|
603
|
-
|
|
933
|
+
if (signals.hasGitAutomation) {
|
|
934
|
+
return 'public_sync';
|
|
935
|
+
}
|
|
604
936
|
|
|
605
|
-
|
|
606
|
-
|
|
937
|
+
if (signals.hasCliEntry) {
|
|
938
|
+
return 'cli_automation';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return 'config_management';
|
|
607
942
|
}
|
|
608
943
|
|
|
609
944
|
if (lowerPath.startsWith('bin/')) {
|
|
610
|
-
return '
|
|
945
|
+
return 'cli_automation';
|
|
611
946
|
}
|
|
612
947
|
|
|
613
|
-
if (lowerPath.startsWith('server/')) {
|
|
614
|
-
|
|
615
|
-
|
|
948
|
+
if (lowerPath.startsWith('server/') || lowerPath.startsWith('routes/')) {
|
|
949
|
+
if (signals.hasAiContextArtifacts) {
|
|
950
|
+
return 'local_context_server';
|
|
951
|
+
}
|
|
616
952
|
|
|
617
|
-
|
|
618
|
-
return 'context_templates';
|
|
953
|
+
return 'rest_api';
|
|
619
954
|
}
|
|
620
955
|
|
|
621
956
|
if (lowerPath.includes('gitsync') || lowerPath.includes('sync')) {
|
|
622
|
-
return '
|
|
957
|
+
return 'public_sync';
|
|
623
958
|
}
|
|
624
959
|
|
|
625
|
-
if (lowerPath.includes('
|
|
960
|
+
if (lowerPath.includes('watch')) {
|
|
626
961
|
return 'change_tracking';
|
|
627
962
|
}
|
|
628
963
|
|
|
629
964
|
if (lowerPath.includes('state') || lowerPath.includes('context')) {
|
|
630
|
-
return '
|
|
965
|
+
return 'ai_context_generation';
|
|
631
966
|
}
|
|
632
967
|
|
|
633
|
-
if (lowerPath.includes('
|
|
634
|
-
return '
|
|
968
|
+
if (lowerPath.includes('auth') || lowerPath.includes('jwt')) {
|
|
969
|
+
return 'auth';
|
|
635
970
|
}
|
|
636
971
|
|
|
637
|
-
if (
|
|
638
|
-
return '
|
|
972
|
+
if (lowerPath.includes('service') || lowerPath.includes('controller')) {
|
|
973
|
+
return signals.hasAxiosOrFetch ? 'external_api' : 'rest_api';
|
|
639
974
|
}
|
|
640
975
|
|
|
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
|
-
};
|
|
976
|
+
return 'ai_context_generation';
|
|
655
977
|
}
|
|
656
978
|
|
|
657
|
-
function groupEventsByIntent(events) {
|
|
979
|
+
function groupEventsByIntent(events, bootstrap) {
|
|
658
980
|
if (!Array.isArray(events) || events.length === 0) {
|
|
659
981
|
return [];
|
|
660
982
|
}
|
|
661
983
|
|
|
662
|
-
const
|
|
984
|
+
const buckets = new Map();
|
|
663
985
|
|
|
664
986
|
for (const event of events) {
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
rootDirectory: descriptor.rootDirectory,
|
|
672
|
-
events: []
|
|
673
|
-
});
|
|
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, []);
|
|
674
993
|
}
|
|
675
994
|
|
|
676
|
-
|
|
995
|
+
buckets.get(key).push(
|
|
996
|
+
Object.assign({}, event, {
|
|
997
|
+
area,
|
|
998
|
+
featureKey
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
677
1001
|
}
|
|
678
1002
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return mergedGroups
|
|
1003
|
+
return Array.from(buckets.values())
|
|
682
1004
|
.map((group) => interpretIntentGroup(group))
|
|
683
1005
|
.filter(Boolean)
|
|
684
1006
|
.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
685
1007
|
}
|
|
686
1008
|
|
|
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
1009
|
function interpretIntentGroup(group) {
|
|
747
|
-
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const latestTimestamp = group.events.reduce((latest, event) => {
|
|
1010
|
+
const latestTimestamp = group.reduce((latest, event) => {
|
|
752
1011
|
return new Date(event.timestamp) > new Date(latest) ? event.timestamp : latest;
|
|
753
|
-
}, group
|
|
754
|
-
const featureKey =
|
|
755
|
-
const
|
|
1012
|
+
}, group[0].timestamp || new Date().toISOString());
|
|
1013
|
+
const featureKey = group[0].featureKey;
|
|
1014
|
+
const area = group[0].area;
|
|
756
1015
|
const type = determineGroupedUpdateType(group);
|
|
757
|
-
const subject = describeIntentSubject(
|
|
1016
|
+
const subject = describeIntentSubject(featureKey);
|
|
758
1017
|
|
|
759
1018
|
return {
|
|
760
1019
|
timestamp: latestTimestamp,
|
|
761
|
-
scope:
|
|
1020
|
+
scope: area,
|
|
762
1021
|
title: buildIntentTitle(type, subject),
|
|
763
1022
|
type,
|
|
764
|
-
impact: describeIntentImpact(type, featureKey
|
|
1023
|
+
impact: describeIntentImpact(type, featureKey),
|
|
765
1024
|
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
|
|
1025
|
+
feature_name: FEATURE_CATALOG[featureKey] || subject
|
|
810
1026
|
};
|
|
811
|
-
|
|
812
|
-
return priorities[featureKey] || 0;
|
|
813
1027
|
}
|
|
814
1028
|
|
|
815
1029
|
function determineGroupedUpdateType(group) {
|
|
816
|
-
const
|
|
817
|
-
const
|
|
1030
|
+
const hasAdd = group.some((event) => event.action === 'add');
|
|
1031
|
+
const hasDelete = group.some((event) => event.action === 'delete');
|
|
818
1032
|
|
|
819
|
-
if (
|
|
1033
|
+
if (hasAdd) {
|
|
820
1034
|
return 'feature';
|
|
821
1035
|
}
|
|
822
1036
|
|
|
823
|
-
if (
|
|
1037
|
+
if (hasDelete) {
|
|
824
1038
|
return 'fix';
|
|
825
1039
|
}
|
|
826
1040
|
|
|
827
|
-
if (
|
|
1041
|
+
if (group.length > 2) {
|
|
828
1042
|
return 'refactor';
|
|
829
1043
|
}
|
|
830
1044
|
|
|
831
1045
|
return 'improvement';
|
|
832
1046
|
}
|
|
833
1047
|
|
|
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
|
-
}
|
|
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
|
+
};
|
|
866
1063
|
|
|
867
|
-
return
|
|
1064
|
+
return subjectMap[featureKey] || 'project workflow';
|
|
868
1065
|
}
|
|
869
1066
|
|
|
870
1067
|
function buildIntentTitle(type, subject) {
|
|
871
1068
|
const verbs = {
|
|
872
1069
|
feature: 'Expanded',
|
|
873
1070
|
improvement: 'Improved',
|
|
874
|
-
refactor: '
|
|
1071
|
+
refactor: 'Refined',
|
|
875
1072
|
fix: 'Stabilized'
|
|
876
1073
|
};
|
|
877
1074
|
|
|
878
1075
|
return `${verbs[type] || 'Improved'} ${subject}`;
|
|
879
1076
|
}
|
|
880
1077
|
|
|
881
|
-
function describeIntentImpact(type, featureKey
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
local_context_server: 'Improves how
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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.'
|
|
895
1092
|
};
|
|
896
1093
|
|
|
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
1094
|
if (type === 'fix') {
|
|
908
|
-
return
|
|
1095
|
+
return impactMap[featureKey].replace(/^Improves /, 'Resolves issues in ');
|
|
909
1096
|
}
|
|
910
1097
|
|
|
911
|
-
|
|
912
|
-
|
|
1098
|
+
if (type === 'feature') {
|
|
1099
|
+
return impactMap[featureKey].replace(/^Improves /, 'Adds ');
|
|
1100
|
+
}
|
|
913
1101
|
|
|
914
|
-
|
|
915
|
-
return groupEventsByIntent([event])[0] || null;
|
|
1102
|
+
return impactMap[featureKey] || 'Improves the overall project workflow.';
|
|
916
1103
|
}
|
|
917
1104
|
|
|
918
1105
|
function normalizeStoredUpdates(updates) {
|
|
@@ -920,7 +1107,11 @@ function normalizeStoredUpdates(updates) {
|
|
|
920
1107
|
return [];
|
|
921
1108
|
}
|
|
922
1109
|
|
|
923
|
-
return dedupeRecentUpdates(
|
|
1110
|
+
return dedupeRecentUpdates(
|
|
1111
|
+
updates
|
|
1112
|
+
.map((update) => normalizeStoredUpdate(update))
|
|
1113
|
+
.filter(Boolean)
|
|
1114
|
+
);
|
|
924
1115
|
}
|
|
925
1116
|
|
|
926
1117
|
function normalizeStoredUpdate(update) {
|
|
@@ -936,10 +1127,6 @@ function normalizeStoredUpdate(update) {
|
|
|
936
1127
|
};
|
|
937
1128
|
}
|
|
938
1129
|
|
|
939
|
-
if (update.file && update.action && isMeaningfulEvent(update)) {
|
|
940
|
-
return toStateUpdate(interpretChange(update));
|
|
941
|
-
}
|
|
942
|
-
|
|
943
1130
|
return null;
|
|
944
1131
|
}
|
|
945
1132
|
|
|
@@ -961,118 +1148,71 @@ function normalizeStoredHistoryEntry(entry) {
|
|
|
961
1148
|
}
|
|
962
1149
|
|
|
963
1150
|
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
1151
|
return {
|
|
970
1152
|
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'
|
|
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'
|
|
978
1159
|
};
|
|
979
1160
|
}
|
|
980
1161
|
|
|
981
|
-
if (entry.file && entry.action && isMeaningfulEvent(entry)) {
|
|
982
|
-
return interpretChange(entry);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
1162
|
return null;
|
|
986
1163
|
}
|
|
987
1164
|
|
|
988
|
-
function
|
|
989
|
-
const combinedText = `${
|
|
1165
|
+
function inferFeatureKeyFromText(title, impact) {
|
|
1166
|
+
const combinedText = `${title || ''} ${impact || ''}`.toLowerCase();
|
|
990
1167
|
|
|
991
|
-
if (combinedText.includes('
|
|
992
|
-
return
|
|
993
|
-
featureKey: 'github_sync',
|
|
994
|
-
featureName: getFeatureMeta('github_sync').name,
|
|
995
|
-
scope: 'logic'
|
|
996
|
-
};
|
|
1168
|
+
if (combinedText.includes('jwt') || combinedText.includes('auth')) {
|
|
1169
|
+
return 'auth';
|
|
997
1170
|
}
|
|
998
1171
|
|
|
999
|
-
if (combinedText.includes('
|
|
1000
|
-
return
|
|
1001
|
-
featureKey: 'project_intelligence',
|
|
1002
|
-
featureName: getFeatureMeta('project_intelligence').name,
|
|
1003
|
-
scope: 'logic'
|
|
1004
|
-
};
|
|
1172
|
+
if (combinedText.includes('mongo') || combinedText.includes('mongoose') || combinedText.includes('persist')) {
|
|
1173
|
+
return 'persistence';
|
|
1005
1174
|
}
|
|
1006
1175
|
|
|
1007
|
-
if (combinedText.includes('
|
|
1008
|
-
return
|
|
1009
|
-
featureKey: 'change_tracking',
|
|
1010
|
-
featureName: getFeatureMeta('change_tracking').name,
|
|
1011
|
-
scope: 'logic'
|
|
1012
|
-
};
|
|
1176
|
+
if (combinedText.includes('real-time') || combinedText.includes('socket')) {
|
|
1177
|
+
return 'realtime';
|
|
1013
1178
|
}
|
|
1014
1179
|
|
|
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
|
-
};
|
|
1180
|
+
if (combinedText.includes('api') || combinedText.includes('route')) {
|
|
1181
|
+
return 'rest_api';
|
|
1026
1182
|
}
|
|
1027
1183
|
|
|
1028
|
-
if (combinedText.includes('
|
|
1029
|
-
return
|
|
1030
|
-
featureKey: 'cli_workflow',
|
|
1031
|
-
featureName: getFeatureMeta('cli_workflow').name,
|
|
1032
|
-
scope: 'cli'
|
|
1033
|
-
};
|
|
1184
|
+
if (combinedText.includes('git') || combinedText.includes('publish') || combinedText.includes('sync')) {
|
|
1185
|
+
return 'public_sync';
|
|
1034
1186
|
}
|
|
1035
1187
|
|
|
1036
|
-
if (combinedText.includes('
|
|
1037
|
-
return
|
|
1038
|
-
featureKey: 'documentation',
|
|
1039
|
-
featureName: getFeatureMeta('documentation').name,
|
|
1040
|
-
scope: 'documentation'
|
|
1041
|
-
};
|
|
1188
|
+
if (combinedText.includes('watch') || combinedText.includes('change')) {
|
|
1189
|
+
return 'change_tracking';
|
|
1042
1190
|
}
|
|
1043
1191
|
|
|
1044
|
-
if (combinedText.includes('
|
|
1045
|
-
return
|
|
1046
|
-
featureKey: 'package_configuration',
|
|
1047
|
-
featureName: getFeatureMeta('package_configuration').name,
|
|
1048
|
-
scope: 'dependencies'
|
|
1049
|
-
};
|
|
1192
|
+
if (combinedText.includes('command line') || combinedText.includes('cli')) {
|
|
1193
|
+
return 'cli_automation';
|
|
1050
1194
|
}
|
|
1051
1195
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1196
|
+
if (combinedText.includes('http') || combinedText.includes('context delivery')) {
|
|
1197
|
+
return 'local_context_server';
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return 'ai_context_generation';
|
|
1057
1201
|
}
|
|
1058
1202
|
|
|
1059
1203
|
function normalizeUpdateType(type) {
|
|
1060
|
-
if (type === 'removal') {
|
|
1061
|
-
return 'refactor';
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1204
|
if (type === 'feature' || type === 'improvement' || type === 'refactor' || type === 'fix') {
|
|
1065
1205
|
return type;
|
|
1066
1206
|
}
|
|
1067
1207
|
|
|
1208
|
+
if (type === 'removal') {
|
|
1209
|
+
return 'fix';
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1068
1212
|
return 'improvement';
|
|
1069
1213
|
}
|
|
1070
1214
|
|
|
1071
1215
|
function toStateUpdate(update) {
|
|
1072
|
-
if (!update) {
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
1216
|
return {
|
|
1077
1217
|
title: update.title,
|
|
1078
1218
|
type: normalizeUpdateType(update.type),
|
|
@@ -1081,17 +1221,17 @@ function toStateUpdate(update) {
|
|
|
1081
1221
|
}
|
|
1082
1222
|
|
|
1083
1223
|
function dedupeRecentUpdates(updates) {
|
|
1084
|
-
const
|
|
1224
|
+
const seen = new Set();
|
|
1085
1225
|
const result = [];
|
|
1086
1226
|
|
|
1087
1227
|
for (const update of updates.filter(Boolean)) {
|
|
1088
1228
|
const key = `${update.title}::${update.type}::${update.impact}`;
|
|
1089
1229
|
|
|
1090
|
-
if (
|
|
1230
|
+
if (seen.has(key)) {
|
|
1091
1231
|
continue;
|
|
1092
1232
|
}
|
|
1093
1233
|
|
|
1094
|
-
|
|
1234
|
+
seen.add(key);
|
|
1095
1235
|
result.push(update);
|
|
1096
1236
|
}
|
|
1097
1237
|
|
|
@@ -1099,190 +1239,81 @@ function dedupeRecentUpdates(updates) {
|
|
|
1099
1239
|
}
|
|
1100
1240
|
|
|
1101
1241
|
function dedupeHistoryEntries(entries) {
|
|
1102
|
-
const
|
|
1242
|
+
const seen = new Set();
|
|
1103
1243
|
const result = [];
|
|
1104
1244
|
|
|
1105
1245
|
for (const entry of entries.filter(Boolean)) {
|
|
1106
1246
|
const key = `${entry.title}::${entry.type}::${entry.feature_key}`;
|
|
1107
1247
|
|
|
1108
|
-
if (
|
|
1248
|
+
if (seen.has(key)) {
|
|
1109
1249
|
continue;
|
|
1110
1250
|
}
|
|
1111
1251
|
|
|
1112
|
-
|
|
1252
|
+
seen.add(key);
|
|
1113
1253
|
result.push(entry);
|
|
1114
1254
|
}
|
|
1115
1255
|
|
|
1116
1256
|
return result.sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp));
|
|
1117
1257
|
}
|
|
1118
1258
|
|
|
1119
|
-
function promoteFeatures(
|
|
1120
|
-
if (!Array.isArray(
|
|
1259
|
+
function promoteFeatures(historyEntries) {
|
|
1260
|
+
if (!Array.isArray(historyEntries) || historyEntries.length === 0) {
|
|
1121
1261
|
return [];
|
|
1122
1262
|
}
|
|
1123
1263
|
|
|
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
|
-
}
|
|
1264
|
+
const scores = new Map();
|
|
1190
1265
|
|
|
1191
|
-
for (const
|
|
1192
|
-
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1266
|
+
for (const entry of historyEntries) {
|
|
1267
|
+
const featureName = entry.feature_name || FEATURE_CATALOG[entry.feature_key];
|
|
1195
1268
|
|
|
1196
|
-
if (
|
|
1269
|
+
if (!featureName) {
|
|
1197
1270
|
continue;
|
|
1198
1271
|
}
|
|
1199
1272
|
|
|
1200
|
-
|
|
1201
|
-
seenFeatureNames.add(feature.featureName);
|
|
1273
|
+
scores.set(featureName, (scores.get(featureName) || 0) + scoreHistoryEntry(entry));
|
|
1202
1274
|
}
|
|
1203
1275
|
|
|
1204
|
-
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);
|
|
1205
1280
|
}
|
|
1206
1281
|
|
|
1207
|
-
function
|
|
1208
|
-
const
|
|
1282
|
+
function scoreHistoryEntry(entry) {
|
|
1283
|
+
const weights = {
|
|
1209
1284
|
feature: 4,
|
|
1210
1285
|
refactor: 3,
|
|
1211
1286
|
improvement: 2,
|
|
1212
1287
|
fix: 2
|
|
1213
1288
|
};
|
|
1214
1289
|
|
|
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
|
-
};
|
|
1290
|
+
return weights[normalizeUpdateType(entry.type)] || 1;
|
|
1236
1291
|
}
|
|
1237
1292
|
|
|
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';
|
|
1293
|
+
function mergeKeyFeatures(primaryFeatures, promotedFeatures) {
|
|
1294
|
+
return uniqueNonEmpty([].concat(primaryFeatures || [], promotedFeatures || [])).slice(0, MAX_KEY_FEATURES);
|
|
1265
1295
|
}
|
|
1266
1296
|
|
|
1267
|
-
function deriveKnownIssues(projectRoot,
|
|
1297
|
+
function deriveKnownIssues(projectRoot, bootstrap) {
|
|
1268
1298
|
const knownIssues = [];
|
|
1269
1299
|
|
|
1270
1300
|
if (!hasTestIndicators(projectRoot)) {
|
|
1271
1301
|
knownIssues.push('No automated test suite is detected yet.');
|
|
1272
1302
|
}
|
|
1273
1303
|
|
|
1274
|
-
if (
|
|
1275
|
-
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.');
|
|
1276
1306
|
}
|
|
1277
1307
|
|
|
1278
|
-
if (
|
|
1279
|
-
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.');
|
|
1280
1310
|
}
|
|
1281
1311
|
|
|
1282
1312
|
return knownIssues;
|
|
1283
1313
|
}
|
|
1284
1314
|
|
|
1285
1315
|
function hasTestIndicators(projectRoot) {
|
|
1316
|
+
const resolvedRoot = resolveProjectRoot(projectRoot);
|
|
1286
1317
|
const testPaths = [
|
|
1287
1318
|
'test',
|
|
1288
1319
|
'tests',
|
|
@@ -1293,11 +1324,11 @@ function hasTestIndicators(projectRoot) {
|
|
|
1293
1324
|
'jest.config.mjs'
|
|
1294
1325
|
];
|
|
1295
1326
|
|
|
1296
|
-
return testPaths.some((relativePath) => fs.existsSync(path.join(
|
|
1327
|
+
return testPaths.some((relativePath) => fs.existsSync(path.join(resolvedRoot, relativePath)));
|
|
1297
1328
|
}
|
|
1298
1329
|
|
|
1299
|
-
function determineCurrentStage(keyFeatures, historyEntries) {
|
|
1300
|
-
if (keyFeatures.length >= 4 &&
|
|
1330
|
+
function determineCurrentStage(keyFeatures, historyEntries, implementationDetails) {
|
|
1331
|
+
if (keyFeatures.length >= 4 && implementationDetails.length >= 3) {
|
|
1301
1332
|
return 'Production-ready';
|
|
1302
1333
|
}
|
|
1303
1334
|
|
|
@@ -1308,86 +1339,69 @@ function determineCurrentStage(keyFeatures, historyEntries) {
|
|
|
1308
1339
|
return 'Early development';
|
|
1309
1340
|
}
|
|
1310
1341
|
|
|
1311
|
-
function generateAiSummary(state,
|
|
1312
|
-
const
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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);
|
|
1318
1349
|
|
|
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';
|
|
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.`;
|
|
1325
1352
|
}
|
|
1326
1353
|
|
|
1327
1354
|
if (
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
featureSignals.has('cli_orchestration')
|
|
1355
|
+
features.includes(FEATURE_CATALOG.ai_context_generation) &&
|
|
1356
|
+
features.includes(FEATURE_CATALOG.public_sync)
|
|
1331
1357
|
) {
|
|
1332
|
-
|
|
1333
|
-
} else if (featureSignals.has('local_context_server')) {
|
|
1334
|
-
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.`;
|
|
1335
1359
|
}
|
|
1336
1360
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
+
}
|
|
1342
1367
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
featureSignals.add(entry.feature_key);
|
|
1346
|
-
}
|
|
1368
|
+
if (features.length >= 2) {
|
|
1369
|
+
return `${normalizedType} focused on ${features.slice(0, 2).join(' and ').toLowerCase()}.`;
|
|
1347
1370
|
}
|
|
1348
1371
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (featureMeta.name === featureName) {
|
|
1352
|
-
featureSignals.add(featureKey);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1372
|
+
if (details.length > 0) {
|
|
1373
|
+
return `${normalizedType} built around ${details[0].toLowerCase()}.`;
|
|
1355
1374
|
}
|
|
1356
1375
|
|
|
1357
|
-
return
|
|
1376
|
+
return `${normalizedType} with detectable project structure and implementation patterns.`;
|
|
1358
1377
|
}
|
|
1359
1378
|
|
|
1360
|
-
function generateNextSteps(state, historyEntries) {
|
|
1379
|
+
function generateNextSteps(state, bootstrap, historyEntries) {
|
|
1361
1380
|
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
1381
|
|
|
1368
1382
|
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.');
|
|
1383
|
+
nextSteps.push('Add automated tests that cover the main application flow and critical integration points.');
|
|
1374
1384
|
}
|
|
1375
1385
|
|
|
1376
|
-
if (
|
|
1377
|
-
nextSteps.push('
|
|
1386
|
+
if (bootstrap.implementationDetails.length < 2) {
|
|
1387
|
+
nextSteps.push('Strengthen the project structure so major implementation patterns are easier to detect automatically.');
|
|
1378
1388
|
}
|
|
1379
1389
|
|
|
1380
|
-
if (
|
|
1381
|
-
nextSteps.push('
|
|
1390
|
+
if (historyEntries.length === 0) {
|
|
1391
|
+
nextSteps.push('Capture the next meaningful project update so recent evolution is reflected alongside the bootstrap analysis.');
|
|
1382
1392
|
}
|
|
1383
1393
|
|
|
1384
1394
|
if (state.current_stage === 'Early development') {
|
|
1385
|
-
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.');
|
|
1386
1396
|
}
|
|
1387
1397
|
|
|
1388
1398
|
return Array.from(new Set(nextSteps)).slice(0, 4);
|
|
1389
1399
|
}
|
|
1390
1400
|
|
|
1401
|
+
function uniqueNonEmpty(values) {
|
|
1402
|
+
return Array.from(new Set((values || []).filter(Boolean)));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1391
1405
|
function capitalize(value) {
|
|
1392
1406
|
if (!value) {
|
|
1393
1407
|
return '';
|
|
@@ -1459,6 +1473,7 @@ function createDebouncedStateUpdater(projectRoot, options) {
|
|
|
1459
1473
|
module.exports = {
|
|
1460
1474
|
CONTEXT_DIR_NAME,
|
|
1461
1475
|
DEFAULT_CONFIG,
|
|
1476
|
+
bootstrapProjectAnalysis,
|
|
1462
1477
|
createDebouncedStateUpdater,
|
|
1463
1478
|
createDefaultChangelog,
|
|
1464
1479
|
createDefaultState,
|
|
@@ -1466,7 +1481,6 @@ module.exports = {
|
|
|
1466
1481
|
ensureContextDirectory,
|
|
1467
1482
|
getContextPaths,
|
|
1468
1483
|
groupEventsByIntent,
|
|
1469
|
-
interpretChange,
|
|
1470
1484
|
loadRuntimeConfig,
|
|
1471
1485
|
promoteFeatures,
|
|
1472
1486
|
readJsonFile,
|
|
@@ -1477,4 +1491,4 @@ module.exports = {
|
|
|
1477
1491
|
updateProjectState,
|
|
1478
1492
|
writeJsonAtomic,
|
|
1479
1493
|
writeTextAtomic
|
|
1480
|
-
};
|
|
1494
|
+
};
|