neuronlayer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/index.js +38016 -0
- package/esbuild.config.js +26 -0
- package/package.json +63 -0
- package/src/cli/commands.ts +382 -0
- package/src/core/adr-exporter.ts +253 -0
- package/src/core/architecture/architecture-enforcement.ts +228 -0
- package/src/core/architecture/duplicate-detector.ts +288 -0
- package/src/core/architecture/index.ts +6 -0
- package/src/core/architecture/pattern-learner.ts +306 -0
- package/src/core/architecture/pattern-library.ts +403 -0
- package/src/core/architecture/pattern-validator.ts +324 -0
- package/src/core/change-intelligence/bug-correlator.ts +444 -0
- package/src/core/change-intelligence/change-intelligence.ts +221 -0
- package/src/core/change-intelligence/change-tracker.ts +334 -0
- package/src/core/change-intelligence/fix-suggester.ts +340 -0
- package/src/core/change-intelligence/index.ts +5 -0
- package/src/core/code-verifier.ts +843 -0
- package/src/core/confidence/confidence-scorer.ts +251 -0
- package/src/core/confidence/conflict-checker.ts +289 -0
- package/src/core/confidence/index.ts +5 -0
- package/src/core/confidence/source-tracker.ts +263 -0
- package/src/core/confidence/warning-detector.ts +241 -0
- package/src/core/context-rot/compaction.ts +284 -0
- package/src/core/context-rot/context-health.ts +243 -0
- package/src/core/context-rot/context-rot-prevention.ts +213 -0
- package/src/core/context-rot/critical-context.ts +221 -0
- package/src/core/context-rot/drift-detector.ts +255 -0
- package/src/core/context-rot/index.ts +7 -0
- package/src/core/context.ts +263 -0
- package/src/core/decision-extractor.ts +339 -0
- package/src/core/decisions.ts +69 -0
- package/src/core/deja-vu.ts +421 -0
- package/src/core/engine.ts +1455 -0
- package/src/core/feature-context.ts +726 -0
- package/src/core/ghost-mode.ts +412 -0
- package/src/core/learning.ts +485 -0
- package/src/core/living-docs/activity-tracker.ts +296 -0
- package/src/core/living-docs/architecture-generator.ts +428 -0
- package/src/core/living-docs/changelog-generator.ts +348 -0
- package/src/core/living-docs/component-generator.ts +230 -0
- package/src/core/living-docs/doc-engine.ts +110 -0
- package/src/core/living-docs/doc-validator.ts +282 -0
- package/src/core/living-docs/index.ts +8 -0
- package/src/core/project-manager.ts +297 -0
- package/src/core/summarizer.ts +267 -0
- package/src/core/test-awareness/change-validator.ts +499 -0
- package/src/core/test-awareness/index.ts +5 -0
- package/src/index.ts +49 -0
- package/src/indexing/ast.ts +563 -0
- package/src/indexing/embeddings.ts +85 -0
- package/src/indexing/indexer.ts +245 -0
- package/src/indexing/watcher.ts +78 -0
- package/src/server/gateways/aggregator.ts +374 -0
- package/src/server/gateways/index.ts +473 -0
- package/src/server/gateways/memory-ghost.ts +343 -0
- package/src/server/gateways/memory-query.ts +452 -0
- package/src/server/gateways/memory-record.ts +346 -0
- package/src/server/gateways/memory-review.ts +410 -0
- package/src/server/gateways/memory-status.ts +517 -0
- package/src/server/gateways/memory-verify.ts +392 -0
- package/src/server/gateways/router.ts +434 -0
- package/src/server/gateways/types.ts +610 -0
- package/src/server/mcp.ts +154 -0
- package/src/server/resources.ts +85 -0
- package/src/server/tools.ts +2261 -0
- package/src/storage/database.ts +262 -0
- package/src/storage/tier1.ts +135 -0
- package/src/storage/tier2.ts +764 -0
- package/src/storage/tier3.ts +123 -0
- package/src/types/documentation.ts +619 -0
- package/src/types/index.ts +222 -0
- package/src/utils/config.ts +193 -0
- package/src/utils/files.ts +117 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/tokens.ts +52 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
import type {
|
|
6
|
+
DailyChangelog,
|
|
7
|
+
ChangeEntry,
|
|
8
|
+
FileChangeInfo,
|
|
9
|
+
ChangeMetrics,
|
|
10
|
+
ChangelogOptions
|
|
11
|
+
} from '../../types/documentation.js';
|
|
12
|
+
|
|
13
|
+
interface CommitInfo {
|
|
14
|
+
hash: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
author: string;
|
|
17
|
+
date: Date;
|
|
18
|
+
files: string[];
|
|
19
|
+
additions: number;
|
|
20
|
+
deletions: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DayGroup {
|
|
24
|
+
date: Date;
|
|
25
|
+
commits: CommitInfo[];
|
|
26
|
+
entries: ChangeEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ChangelogGenerator {
|
|
30
|
+
private projectPath: string;
|
|
31
|
+
private db: Database.Database;
|
|
32
|
+
private isGitRepo: boolean;
|
|
33
|
+
|
|
34
|
+
constructor(projectPath: string, db: Database.Database) {
|
|
35
|
+
this.projectPath = projectPath;
|
|
36
|
+
this.db = db;
|
|
37
|
+
this.isGitRepo = existsSync(join(projectPath, '.git'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async generate(options: ChangelogOptions = {}): Promise<DailyChangelog[]> {
|
|
41
|
+
if (!this.isGitRepo) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const since = this.parseSinceString(options.since || 'this week');
|
|
46
|
+
const until = options.until || new Date();
|
|
47
|
+
|
|
48
|
+
const commits = this.getCommitsInRange(since, until);
|
|
49
|
+
const grouped = this.groupByDay(commits);
|
|
50
|
+
|
|
51
|
+
return grouped.map(day => this.createDailyChangelog(day, options.includeDecisions ?? false));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private parseSinceString(since: Date | string): Date {
|
|
55
|
+
if (since instanceof Date) {
|
|
56
|
+
return since;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const now = new Date();
|
|
60
|
+
const lower = since.toLowerCase();
|
|
61
|
+
|
|
62
|
+
if (lower === 'yesterday') {
|
|
63
|
+
const yesterday = new Date(now);
|
|
64
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
65
|
+
yesterday.setHours(0, 0, 0, 0);
|
|
66
|
+
return yesterday;
|
|
67
|
+
}
|
|
68
|
+
if (lower === 'today') {
|
|
69
|
+
const today = new Date(now);
|
|
70
|
+
today.setHours(0, 0, 0, 0);
|
|
71
|
+
return today;
|
|
72
|
+
}
|
|
73
|
+
if (lower === 'this week') {
|
|
74
|
+
const dayOfWeek = now.getDay();
|
|
75
|
+
const startOfWeek = new Date(now);
|
|
76
|
+
startOfWeek.setDate(now.getDate() - dayOfWeek);
|
|
77
|
+
startOfWeek.setHours(0, 0, 0, 0);
|
|
78
|
+
return startOfWeek;
|
|
79
|
+
}
|
|
80
|
+
if (lower === 'this month') {
|
|
81
|
+
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
82
|
+
}
|
|
83
|
+
if (lower === 'last week') {
|
|
84
|
+
const dayOfWeek = now.getDay();
|
|
85
|
+
const startOfLastWeek = new Date(now);
|
|
86
|
+
startOfLastWeek.setDate(now.getDate() - dayOfWeek - 7);
|
|
87
|
+
startOfLastWeek.setHours(0, 0, 0, 0);
|
|
88
|
+
return startOfLastWeek;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try to parse as date string
|
|
92
|
+
const parsed = new Date(since);
|
|
93
|
+
return isNaN(parsed.getTime()) ? new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) : parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getCommitsInRange(since: Date, until: Date): CommitInfo[] {
|
|
97
|
+
const commits: CommitInfo[] = [];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const sinceStr = since.toISOString().split('T')[0];
|
|
101
|
+
const untilStr = until.toISOString().split('T')[0];
|
|
102
|
+
|
|
103
|
+
// Get commit list with basic info
|
|
104
|
+
const output = execSync(
|
|
105
|
+
`git log --since="${sinceStr}" --until="${untilStr}" --format="%H|%s|%an|%ad" --date=iso-strict`,
|
|
106
|
+
{ cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
110
|
+
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const [hash, subject, author, dateStr] = line.split('|');
|
|
113
|
+
if (!hash || !subject) continue;
|
|
114
|
+
|
|
115
|
+
// Get file stats for this commit
|
|
116
|
+
const { files, additions, deletions } = this.getCommitStats(hash);
|
|
117
|
+
|
|
118
|
+
commits.push({
|
|
119
|
+
hash: hash.slice(0, 8),
|
|
120
|
+
subject: subject || '',
|
|
121
|
+
author: author || 'Unknown',
|
|
122
|
+
date: new Date(dateStr || Date.now()),
|
|
123
|
+
files,
|
|
124
|
+
additions,
|
|
125
|
+
deletions
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Git command failed
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return commits;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private getCommitStats(hash: string): { files: string[]; additions: number; deletions: number } {
|
|
136
|
+
try {
|
|
137
|
+
const output = execSync(
|
|
138
|
+
`git show --stat --format="" "${hash}"`,
|
|
139
|
+
{ cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const files: string[] = [];
|
|
143
|
+
let additions = 0;
|
|
144
|
+
let deletions = 0;
|
|
145
|
+
|
|
146
|
+
const lines = output.trim().split('\n');
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
// Match file lines like: src/file.ts | 10 +++++-----
|
|
149
|
+
const fileMatch = line.match(/^\s*([^\|]+)\s*\|/);
|
|
150
|
+
if (fileMatch && fileMatch[1]) {
|
|
151
|
+
files.push(fileMatch[1].trim());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Match insertions/deletions from summary line
|
|
155
|
+
const insertMatch = line.match(/(\d+)\s+insertion/);
|
|
156
|
+
const deleteMatch = line.match(/(\d+)\s+deletion/);
|
|
157
|
+
if (insertMatch) additions = parseInt(insertMatch[1]!, 10);
|
|
158
|
+
if (deleteMatch) deletions = parseInt(deleteMatch[1]!, 10);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { files, additions, deletions };
|
|
162
|
+
} catch {
|
|
163
|
+
return { files: [], additions: 0, deletions: 0 };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private groupByDay(commits: CommitInfo[]): DayGroup[] {
|
|
168
|
+
const groups = new Map<string, DayGroup>();
|
|
169
|
+
|
|
170
|
+
for (const commit of commits) {
|
|
171
|
+
const dateKey = commit.date.toISOString().split('T')[0]!;
|
|
172
|
+
|
|
173
|
+
if (!groups.has(dateKey)) {
|
|
174
|
+
const dayDate = new Date(dateKey);
|
|
175
|
+
groups.set(dateKey, {
|
|
176
|
+
date: dayDate,
|
|
177
|
+
commits: [],
|
|
178
|
+
entries: []
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const group = groups.get(dateKey)!;
|
|
183
|
+
group.commits.push(commit);
|
|
184
|
+
group.entries.push(this.commitToEntry(commit));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Sort by date descending (most recent first)
|
|
188
|
+
return Array.from(groups.values()).sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private commitToEntry(commit: CommitInfo): ChangeEntry {
|
|
192
|
+
return {
|
|
193
|
+
type: this.categorizeCommit(commit.subject),
|
|
194
|
+
description: this.cleanCommitMessage(commit.subject),
|
|
195
|
+
files: commit.files,
|
|
196
|
+
commit: commit.hash
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private categorizeCommit(subject: string): ChangeEntry['type'] {
|
|
201
|
+
const lower = subject.toLowerCase();
|
|
202
|
+
|
|
203
|
+
// Check conventional commit prefixes
|
|
204
|
+
if (lower.startsWith('feat') || lower.includes('add ') || lower.includes('implement')) {
|
|
205
|
+
return 'feature';
|
|
206
|
+
}
|
|
207
|
+
if (lower.startsWith('fix') || lower.includes('bug') || lower.includes('resolve')) {
|
|
208
|
+
return 'fix';
|
|
209
|
+
}
|
|
210
|
+
if (lower.startsWith('refactor') || lower.includes('cleanup') || lower.includes('reorganize')) {
|
|
211
|
+
return 'refactor';
|
|
212
|
+
}
|
|
213
|
+
if (lower.startsWith('docs') || lower.includes('documentation') || lower.includes('readme')) {
|
|
214
|
+
return 'docs';
|
|
215
|
+
}
|
|
216
|
+
if (lower.startsWith('test') || lower.includes('spec') || lower.includes('coverage')) {
|
|
217
|
+
return 'test';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return 'chore';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private cleanCommitMessage(subject: string): string {
|
|
224
|
+
// Remove conventional commit prefix
|
|
225
|
+
return subject
|
|
226
|
+
.replace(/^(?:feat|fix|refactor|docs|test|chore|perf|build|ci|style)\([^)]*\):\s*/i, '')
|
|
227
|
+
.replace(/^(?:feat|fix|refactor|docs|test|chore|perf|build|ci|style):\s*/i, '')
|
|
228
|
+
.trim();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private createDailyChangelog(day: DayGroup, includeDecisions: boolean): DailyChangelog {
|
|
232
|
+
const features = day.entries.filter(e => e.type === 'feature');
|
|
233
|
+
const fixes = day.entries.filter(e => e.type === 'fix');
|
|
234
|
+
const refactors = day.entries.filter(e => e.type === 'refactor');
|
|
235
|
+
|
|
236
|
+
const filesModified = this.aggregateFileChanges(day.commits);
|
|
237
|
+
const metrics = this.calculateMetrics(day.commits);
|
|
238
|
+
const decisions = includeDecisions ? this.getDecisionsForDate(day.date) : [];
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
date: day.date,
|
|
242
|
+
summary: this.generateSummary(day.entries, metrics),
|
|
243
|
+
features,
|
|
244
|
+
fixes,
|
|
245
|
+
refactors,
|
|
246
|
+
filesModified,
|
|
247
|
+
decisions,
|
|
248
|
+
metrics
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private aggregateFileChanges(commits: CommitInfo[]): FileChangeInfo[] {
|
|
253
|
+
const fileMap = new Map<string, FileChangeInfo>();
|
|
254
|
+
|
|
255
|
+
for (const commit of commits) {
|
|
256
|
+
for (const file of commit.files) {
|
|
257
|
+
if (!fileMap.has(file)) {
|
|
258
|
+
fileMap.set(file, {
|
|
259
|
+
file,
|
|
260
|
+
added: 0,
|
|
261
|
+
removed: 0,
|
|
262
|
+
type: 'modified'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Distribute additions/deletions proportionally (rough estimate)
|
|
269
|
+
for (const commit of commits) {
|
|
270
|
+
const fileCount = commit.files.length || 1;
|
|
271
|
+
const addPerFile = Math.round(commit.additions / fileCount);
|
|
272
|
+
const delPerFile = Math.round(commit.deletions / fileCount);
|
|
273
|
+
|
|
274
|
+
for (const file of commit.files) {
|
|
275
|
+
const info = fileMap.get(file);
|
|
276
|
+
if (info) {
|
|
277
|
+
info.added += addPerFile;
|
|
278
|
+
info.removed += delPerFile;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return Array.from(fileMap.values());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private calculateMetrics(commits: CommitInfo[]): ChangeMetrics {
|
|
287
|
+
const filesSet = new Set<string>();
|
|
288
|
+
let linesAdded = 0;
|
|
289
|
+
let linesRemoved = 0;
|
|
290
|
+
|
|
291
|
+
for (const commit of commits) {
|
|
292
|
+
for (const file of commit.files) {
|
|
293
|
+
filesSet.add(file);
|
|
294
|
+
}
|
|
295
|
+
linesAdded += commit.additions;
|
|
296
|
+
linesRemoved += commit.deletions;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
commits: commits.length,
|
|
301
|
+
filesChanged: filesSet.size,
|
|
302
|
+
linesAdded,
|
|
303
|
+
linesRemoved
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private generateSummary(entries: ChangeEntry[], metrics: ChangeMetrics): string {
|
|
308
|
+
const parts: string[] = [];
|
|
309
|
+
|
|
310
|
+
const features = entries.filter(e => e.type === 'feature').length;
|
|
311
|
+
const fixes = entries.filter(e => e.type === 'fix').length;
|
|
312
|
+
const refactors = entries.filter(e => e.type === 'refactor').length;
|
|
313
|
+
|
|
314
|
+
if (features > 0) {
|
|
315
|
+
parts.push(`${features} feature${features > 1 ? 's' : ''}`);
|
|
316
|
+
}
|
|
317
|
+
if (fixes > 0) {
|
|
318
|
+
parts.push(`${fixes} fix${fixes > 1 ? 'es' : ''}`);
|
|
319
|
+
}
|
|
320
|
+
if (refactors > 0) {
|
|
321
|
+
parts.push(`${refactors} refactor${refactors > 1 ? 's' : ''}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (parts.length === 0) {
|
|
325
|
+
return `${metrics.commits} commit${metrics.commits !== 1 ? 's' : ''} affecting ${metrics.filesChanged} file${metrics.filesChanged !== 1 ? 's' : ''}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return `${parts.join(', ')} across ${metrics.filesChanged} file${metrics.filesChanged !== 1 ? 's' : ''}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getDecisionsForDate(date: Date): string[] {
|
|
332
|
+
try {
|
|
333
|
+
const startOfDay = Math.floor(new Date(date).setHours(0, 0, 0, 0) / 1000);
|
|
334
|
+
const endOfDay = Math.floor(new Date(date).setHours(23, 59, 59, 999) / 1000);
|
|
335
|
+
|
|
336
|
+
const stmt = this.db.prepare(`
|
|
337
|
+
SELECT title FROM decisions
|
|
338
|
+
WHERE created_at >= ? AND created_at <= ?
|
|
339
|
+
ORDER BY created_at
|
|
340
|
+
`);
|
|
341
|
+
|
|
342
|
+
const rows = stmt.all(startOfDay, endOfDay) as Array<{ title: string }>;
|
|
343
|
+
return rows.map(r => r.title);
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { basename, extname, join } from 'path';
|
|
4
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
5
|
+
import type { CodeSymbol } from '../../types/index.js';
|
|
6
|
+
import type {
|
|
7
|
+
ComponentDoc,
|
|
8
|
+
SymbolDoc,
|
|
9
|
+
DependencyDoc,
|
|
10
|
+
DependentDoc,
|
|
11
|
+
ChangeHistoryEntry
|
|
12
|
+
} from '../../types/documentation.js';
|
|
13
|
+
|
|
14
|
+
export class ComponentGenerator {
|
|
15
|
+
private projectPath: string;
|
|
16
|
+
private tier2: Tier2Storage;
|
|
17
|
+
private isGitRepo: boolean;
|
|
18
|
+
|
|
19
|
+
constructor(projectPath: string, tier2: Tier2Storage) {
|
|
20
|
+
this.projectPath = projectPath;
|
|
21
|
+
this.tier2 = tier2;
|
|
22
|
+
this.isGitRepo = existsSync(join(projectPath, '.git'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async generate(filePath: string): Promise<ComponentDoc> {
|
|
26
|
+
const file = this.tier2.getFile(filePath);
|
|
27
|
+
if (!file) {
|
|
28
|
+
throw new Error(`File not found: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const symbols = this.tier2.getSymbolsByFile(file.id);
|
|
32
|
+
const imports = this.tier2.getImportsByFile(file.id);
|
|
33
|
+
const dependents = this.tier2.getFileDependents(filePath);
|
|
34
|
+
const history = this.getChangeHistory(filePath);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
file: filePath,
|
|
38
|
+
name: basename(filePath, extname(filePath)),
|
|
39
|
+
purpose: this.inferPurpose(filePath, symbols),
|
|
40
|
+
lastModified: new Date(file.lastModified * 1000),
|
|
41
|
+
publicInterface: this.extractPublicInterface(symbols),
|
|
42
|
+
dependencies: this.formatDependencies(imports),
|
|
43
|
+
dependents: this.formatDependents(dependents),
|
|
44
|
+
changeHistory: history,
|
|
45
|
+
contributors: this.extractContributors(history),
|
|
46
|
+
complexity: this.calculateComplexity(symbols),
|
|
47
|
+
documentationScore: this.calculateDocScore(symbols)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private inferPurpose(filePath: string, symbols: CodeSymbol[]): string {
|
|
52
|
+
const name = basename(filePath, extname(filePath));
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Infer from filename
|
|
56
|
+
if (name.toLowerCase().includes('test')) {
|
|
57
|
+
return `Test file for ${name.replace(/\.test|\.spec|Test|Spec/gi, '')}`;
|
|
58
|
+
}
|
|
59
|
+
if (name.toLowerCase().includes('util') || name.toLowerCase().includes('helper')) {
|
|
60
|
+
return 'Utility functions and helpers';
|
|
61
|
+
}
|
|
62
|
+
if (name === 'index') {
|
|
63
|
+
return 'Module entry point / barrel export';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Infer from exported symbols
|
|
67
|
+
const exported = symbols.filter(s => s.exported);
|
|
68
|
+
const classes = exported.filter(s => s.kind === 'class');
|
|
69
|
+
const interfaces = exported.filter(s => s.kind === 'interface');
|
|
70
|
+
const functions = exported.filter(s => s.kind === 'function');
|
|
71
|
+
|
|
72
|
+
if (classes.length === 1) {
|
|
73
|
+
return `Defines the ${classes[0]!.name} class`;
|
|
74
|
+
}
|
|
75
|
+
if (interfaces.length > 0 && functions.length === 0) {
|
|
76
|
+
return `Type definitions: ${interfaces.map(i => i.name).slice(0, 3).join(', ')}`;
|
|
77
|
+
}
|
|
78
|
+
if (functions.length > 0) {
|
|
79
|
+
parts.push(`Functions: ${functions.map(f => f.name).slice(0, 3).join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Infer from directory
|
|
83
|
+
const pathParts = filePath.split(/[/\\]/);
|
|
84
|
+
const parentDir = pathParts[pathParts.length - 2];
|
|
85
|
+
if (parentDir) {
|
|
86
|
+
const dirPurposes: Record<string, string> = {
|
|
87
|
+
'server': 'Server-side code',
|
|
88
|
+
'api': 'API layer',
|
|
89
|
+
'core': 'Core business logic',
|
|
90
|
+
'storage': 'Data storage',
|
|
91
|
+
'utils': 'Utilities',
|
|
92
|
+
'types': 'Type definitions',
|
|
93
|
+
'components': 'UI components',
|
|
94
|
+
'hooks': 'React hooks',
|
|
95
|
+
'services': 'Service layer'
|
|
96
|
+
};
|
|
97
|
+
if (dirPurposes[parentDir]) {
|
|
98
|
+
parts.unshift(dirPurposes[parentDir]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return parts.length > 0 ? parts.join('. ') : `Module: ${name}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private extractPublicInterface(symbols: CodeSymbol[]): SymbolDoc[] {
|
|
106
|
+
return symbols
|
|
107
|
+
.filter(s => s.exported)
|
|
108
|
+
.map(s => ({
|
|
109
|
+
name: s.name,
|
|
110
|
+
kind: s.kind,
|
|
111
|
+
signature: s.signature,
|
|
112
|
+
description: s.docstring,
|
|
113
|
+
lineStart: s.lineStart,
|
|
114
|
+
lineEnd: s.lineEnd,
|
|
115
|
+
exported: true
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private formatDependencies(imports: Array<{
|
|
120
|
+
importedFrom: string;
|
|
121
|
+
importedSymbols: string[];
|
|
122
|
+
}>): DependencyDoc[] {
|
|
123
|
+
return imports.map(i => ({
|
|
124
|
+
file: i.importedFrom,
|
|
125
|
+
symbols: i.importedSymbols
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private formatDependents(dependents: Array<{
|
|
130
|
+
file: string;
|
|
131
|
+
imports: string[];
|
|
132
|
+
}>): DependentDoc[] {
|
|
133
|
+
return dependents.map(d => ({
|
|
134
|
+
file: d.file,
|
|
135
|
+
symbols: d.imports
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getChangeHistory(filePath: string): ChangeHistoryEntry[] {
|
|
140
|
+
const history: ChangeHistoryEntry[] = [];
|
|
141
|
+
|
|
142
|
+
if (!this.isGitRepo) {
|
|
143
|
+
return history;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const output = execSync(
|
|
148
|
+
`git log --oneline -20 --format="%H|%s|%an|%ad" --date=short -- "${filePath}"`,
|
|
149
|
+
{ cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
153
|
+
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const [hash, subject, author, dateStr] = line.split('|');
|
|
156
|
+
if (!hash || !subject) continue;
|
|
157
|
+
|
|
158
|
+
const { added, removed } = this.getCommitLineChanges(hash, filePath);
|
|
159
|
+
|
|
160
|
+
history.push({
|
|
161
|
+
date: new Date(dateStr || Date.now()),
|
|
162
|
+
change: subject,
|
|
163
|
+
author: author || 'Unknown',
|
|
164
|
+
commit: hash.slice(0, 8),
|
|
165
|
+
linesChanged: { added, removed }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Git command failed
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return history;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getCommitLineChanges(hash: string, filePath: string): { added: number; removed: number } {
|
|
176
|
+
try {
|
|
177
|
+
const output = execSync(
|
|
178
|
+
`git show --numstat --format="" "${hash}" -- "${filePath}"`,
|
|
179
|
+
{ cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const match = output.trim().match(/^(\d+)\s+(\d+)/);
|
|
183
|
+
if (match) {
|
|
184
|
+
return {
|
|
185
|
+
added: parseInt(match[1]!, 10),
|
|
186
|
+
removed: parseInt(match[2]!, 10)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Git command failed
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { added: 0, removed: 0 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private extractContributors(history: ChangeHistoryEntry[]): string[] {
|
|
197
|
+
const contributors = new Set<string>();
|
|
198
|
+
for (const entry of history) {
|
|
199
|
+
contributors.add(entry.author);
|
|
200
|
+
}
|
|
201
|
+
return Array.from(contributors);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private calculateComplexity(symbols: CodeSymbol[]): 'low' | 'medium' | 'high' {
|
|
205
|
+
const totalSymbols = symbols.length;
|
|
206
|
+
const exportedSymbols = symbols.filter(s => s.exported).length;
|
|
207
|
+
const avgLineSpan = symbols.length > 0
|
|
208
|
+
? symbols.reduce((sum, s) => sum + (s.lineEnd - s.lineStart), 0) / symbols.length
|
|
209
|
+
: 0;
|
|
210
|
+
|
|
211
|
+
// Simple heuristic based on symbol count and size
|
|
212
|
+
if (totalSymbols > 20 || avgLineSpan > 50) {
|
|
213
|
+
return 'high';
|
|
214
|
+
}
|
|
215
|
+
if (totalSymbols > 8 || avgLineSpan > 25) {
|
|
216
|
+
return 'medium';
|
|
217
|
+
}
|
|
218
|
+
return 'low';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private calculateDocScore(symbols: CodeSymbol[]): number {
|
|
222
|
+
const exported = symbols.filter(s => s.exported);
|
|
223
|
+
if (exported.length === 0) {
|
|
224
|
+
return 100; // No public API, considered fully documented
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const documented = exported.filter(s => s.docstring && s.docstring.length > 0);
|
|
228
|
+
return Math.round((documented.length / exported.length) * 100);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
3
|
+
import { ArchitectureGenerator } from './architecture-generator.js';
|
|
4
|
+
import { ComponentGenerator } from './component-generator.js';
|
|
5
|
+
import { ChangelogGenerator } from './changelog-generator.js';
|
|
6
|
+
import { DocValidator } from './doc-validator.js';
|
|
7
|
+
import { ActivityTracker } from './activity-tracker.js';
|
|
8
|
+
import type {
|
|
9
|
+
ArchitectureDoc,
|
|
10
|
+
ComponentDoc,
|
|
11
|
+
DailyChangelog,
|
|
12
|
+
ChangelogOptions,
|
|
13
|
+
ValidationResult,
|
|
14
|
+
ActivityResult,
|
|
15
|
+
UndocumentedItem
|
|
16
|
+
} from '../../types/documentation.js';
|
|
17
|
+
|
|
18
|
+
export class LivingDocumentationEngine {
|
|
19
|
+
private archGen: ArchitectureGenerator;
|
|
20
|
+
private compGen: ComponentGenerator;
|
|
21
|
+
private changeGen: ChangelogGenerator;
|
|
22
|
+
private validator: DocValidator;
|
|
23
|
+
private activityTracker: ActivityTracker;
|
|
24
|
+
private db: Database.Database;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
projectPath: string,
|
|
28
|
+
dataDir: string,
|
|
29
|
+
db: Database.Database,
|
|
30
|
+
tier2: Tier2Storage
|
|
31
|
+
) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.archGen = new ArchitectureGenerator(projectPath, tier2);
|
|
34
|
+
this.compGen = new ComponentGenerator(projectPath, tier2);
|
|
35
|
+
this.changeGen = new ChangelogGenerator(projectPath, db);
|
|
36
|
+
this.validator = new DocValidator(projectPath, tier2, db);
|
|
37
|
+
this.activityTracker = new ActivityTracker(projectPath, db, tier2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async generateArchitectureDocs(): Promise<ArchitectureDoc> {
|
|
41
|
+
const doc = await this.archGen.generate();
|
|
42
|
+
|
|
43
|
+
// Log activity
|
|
44
|
+
this.activityTracker.logActivity(
|
|
45
|
+
'doc_generation',
|
|
46
|
+
'Generated architecture documentation',
|
|
47
|
+
undefined,
|
|
48
|
+
{ type: 'architecture' }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return doc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async generateComponentDoc(filePath: string): Promise<ComponentDoc> {
|
|
55
|
+
const doc = await this.compGen.generate(filePath);
|
|
56
|
+
|
|
57
|
+
// Store in documentation table for tracking
|
|
58
|
+
this.storeDocumentation(filePath, 'component', JSON.stringify(doc));
|
|
59
|
+
|
|
60
|
+
// Log activity
|
|
61
|
+
this.activityTracker.logActivity(
|
|
62
|
+
'doc_generation',
|
|
63
|
+
`Generated component documentation for ${filePath}`,
|
|
64
|
+
filePath,
|
|
65
|
+
{ type: 'component' }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async generateChangelog(options: ChangelogOptions = {}): Promise<DailyChangelog[]> {
|
|
72
|
+
return this.changeGen.generate(options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async validateDocs(): Promise<ValidationResult> {
|
|
76
|
+
return this.validator.validate();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async whatHappened(since: string, scope?: string): Promise<ActivityResult> {
|
|
80
|
+
return this.activityTracker.whatHappened(since, scope);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async findUndocumented(options?: {
|
|
84
|
+
importance?: 'low' | 'medium' | 'high' | 'all';
|
|
85
|
+
type?: 'file' | 'function' | 'class' | 'interface' | 'all';
|
|
86
|
+
}): Promise<UndocumentedItem[]> {
|
|
87
|
+
return this.validator.findUndocumented(options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private storeDocumentation(filePath: string, docType: string, content: string): void {
|
|
91
|
+
try {
|
|
92
|
+
// Get file ID
|
|
93
|
+
const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
|
|
94
|
+
const fileRow = fileStmt.get(filePath) as { id: number } | undefined;
|
|
95
|
+
|
|
96
|
+
if (fileRow) {
|
|
97
|
+
const stmt = this.db.prepare(`
|
|
98
|
+
INSERT INTO documentation (file_id, doc_type, content, generated_at)
|
|
99
|
+
VALUES (?, ?, ?, unixepoch())
|
|
100
|
+
ON CONFLICT(file_id, doc_type) DO UPDATE SET
|
|
101
|
+
content = excluded.content,
|
|
102
|
+
generated_at = unixepoch()
|
|
103
|
+
`);
|
|
104
|
+
stmt.run(fileRow.id, docType, content);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore storage errors
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|