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.
Files changed (78) hide show
  1. package/CONTRIBUTING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/index.js +38016 -0
  5. package/esbuild.config.js +26 -0
  6. package/package.json +63 -0
  7. package/src/cli/commands.ts +382 -0
  8. package/src/core/adr-exporter.ts +253 -0
  9. package/src/core/architecture/architecture-enforcement.ts +228 -0
  10. package/src/core/architecture/duplicate-detector.ts +288 -0
  11. package/src/core/architecture/index.ts +6 -0
  12. package/src/core/architecture/pattern-learner.ts +306 -0
  13. package/src/core/architecture/pattern-library.ts +403 -0
  14. package/src/core/architecture/pattern-validator.ts +324 -0
  15. package/src/core/change-intelligence/bug-correlator.ts +444 -0
  16. package/src/core/change-intelligence/change-intelligence.ts +221 -0
  17. package/src/core/change-intelligence/change-tracker.ts +334 -0
  18. package/src/core/change-intelligence/fix-suggester.ts +340 -0
  19. package/src/core/change-intelligence/index.ts +5 -0
  20. package/src/core/code-verifier.ts +843 -0
  21. package/src/core/confidence/confidence-scorer.ts +251 -0
  22. package/src/core/confidence/conflict-checker.ts +289 -0
  23. package/src/core/confidence/index.ts +5 -0
  24. package/src/core/confidence/source-tracker.ts +263 -0
  25. package/src/core/confidence/warning-detector.ts +241 -0
  26. package/src/core/context-rot/compaction.ts +284 -0
  27. package/src/core/context-rot/context-health.ts +243 -0
  28. package/src/core/context-rot/context-rot-prevention.ts +213 -0
  29. package/src/core/context-rot/critical-context.ts +221 -0
  30. package/src/core/context-rot/drift-detector.ts +255 -0
  31. package/src/core/context-rot/index.ts +7 -0
  32. package/src/core/context.ts +263 -0
  33. package/src/core/decision-extractor.ts +339 -0
  34. package/src/core/decisions.ts +69 -0
  35. package/src/core/deja-vu.ts +421 -0
  36. package/src/core/engine.ts +1455 -0
  37. package/src/core/feature-context.ts +726 -0
  38. package/src/core/ghost-mode.ts +412 -0
  39. package/src/core/learning.ts +485 -0
  40. package/src/core/living-docs/activity-tracker.ts +296 -0
  41. package/src/core/living-docs/architecture-generator.ts +428 -0
  42. package/src/core/living-docs/changelog-generator.ts +348 -0
  43. package/src/core/living-docs/component-generator.ts +230 -0
  44. package/src/core/living-docs/doc-engine.ts +110 -0
  45. package/src/core/living-docs/doc-validator.ts +282 -0
  46. package/src/core/living-docs/index.ts +8 -0
  47. package/src/core/project-manager.ts +297 -0
  48. package/src/core/summarizer.ts +267 -0
  49. package/src/core/test-awareness/change-validator.ts +499 -0
  50. package/src/core/test-awareness/index.ts +5 -0
  51. package/src/index.ts +49 -0
  52. package/src/indexing/ast.ts +563 -0
  53. package/src/indexing/embeddings.ts +85 -0
  54. package/src/indexing/indexer.ts +245 -0
  55. package/src/indexing/watcher.ts +78 -0
  56. package/src/server/gateways/aggregator.ts +374 -0
  57. package/src/server/gateways/index.ts +473 -0
  58. package/src/server/gateways/memory-ghost.ts +343 -0
  59. package/src/server/gateways/memory-query.ts +452 -0
  60. package/src/server/gateways/memory-record.ts +346 -0
  61. package/src/server/gateways/memory-review.ts +410 -0
  62. package/src/server/gateways/memory-status.ts +517 -0
  63. package/src/server/gateways/memory-verify.ts +392 -0
  64. package/src/server/gateways/router.ts +434 -0
  65. package/src/server/gateways/types.ts +610 -0
  66. package/src/server/mcp.ts +154 -0
  67. package/src/server/resources.ts +85 -0
  68. package/src/server/tools.ts +2261 -0
  69. package/src/storage/database.ts +262 -0
  70. package/src/storage/tier1.ts +135 -0
  71. package/src/storage/tier2.ts +764 -0
  72. package/src/storage/tier3.ts +123 -0
  73. package/src/types/documentation.ts +619 -0
  74. package/src/types/index.ts +222 -0
  75. package/src/utils/config.ts +193 -0
  76. package/src/utils/files.ts +117 -0
  77. package/src/utils/time.ts +37 -0
  78. package/src/utils/tokens.ts +52 -0
@@ -0,0 +1,245 @@
1
+ import { readFileSync, statSync } from 'fs';
2
+ import { glob } from 'glob';
3
+ import { join, relative } from 'path';
4
+ import { EventEmitter } from 'events';
5
+ import { EmbeddingGenerator } from './embeddings.js';
6
+ import { ASTParser } from './ast.js';
7
+ import { FileWatcher, type FileEvent } from './watcher.js';
8
+ import { Tier2Storage } from '../storage/tier2.js';
9
+ import { isCodeFile, detectLanguage, hashContent, getPreview, countLines } from '../utils/files.js';
10
+ import type { MemoryLayerConfig, IndexingProgress } from '../types/index.js';
11
+
12
+ export class Indexer extends EventEmitter {
13
+ private config: MemoryLayerConfig;
14
+ private embeddingGenerator: EmbeddingGenerator;
15
+ private astParser: ASTParser;
16
+ private watcher: FileWatcher;
17
+ private tier2: Tier2Storage;
18
+ private isIndexing = false;
19
+ private pendingFiles: Set<string> = new Set();
20
+ private processTimeout: NodeJS.Timeout | null = null;
21
+
22
+ constructor(config: MemoryLayerConfig, tier2: Tier2Storage) {
23
+ super();
24
+ this.config = config;
25
+ this.tier2 = tier2;
26
+ this.embeddingGenerator = new EmbeddingGenerator(config.embeddingModel);
27
+ this.astParser = new ASTParser(config.dataDir);
28
+ this.watcher = new FileWatcher(config.projectPath, config.watchIgnore);
29
+
30
+ this.setupWatcher();
31
+ }
32
+
33
+ private setupWatcher(): void {
34
+ this.watcher.on('file', (event: FileEvent) => {
35
+ this.handleFileEvent(event);
36
+ });
37
+
38
+ this.watcher.on('ready', () => {
39
+ this.emit('watcherReady');
40
+ });
41
+
42
+ this.watcher.on('error', (error) => {
43
+ this.emit('error', error);
44
+ });
45
+ }
46
+
47
+ private handleFileEvent(event: FileEvent): void {
48
+ // Only process code files
49
+ if (!isCodeFile(event.path)) {
50
+ return;
51
+ }
52
+
53
+ if (event.type === 'unlink') {
54
+ // File deleted
55
+ this.tier2.deleteFile(event.relativePath);
56
+ this.emit('fileRemoved', event.relativePath);
57
+ return;
58
+ }
59
+
60
+ // Add or change - queue for processing
61
+ this.pendingFiles.add(event.path);
62
+ this.schedulePendingProcessing();
63
+ }
64
+
65
+ private schedulePendingProcessing(): void {
66
+ // Debounce processing
67
+ if (this.processTimeout) {
68
+ clearTimeout(this.processTimeout);
69
+ }
70
+
71
+ this.processTimeout = setTimeout(() => {
72
+ this.processPendingFiles();
73
+ }, 500);
74
+ }
75
+
76
+ private async processPendingFiles(): Promise<void> {
77
+ if (this.isIndexing || this.pendingFiles.size === 0) {
78
+ return;
79
+ }
80
+
81
+ const files = Array.from(this.pendingFiles);
82
+ this.pendingFiles.clear();
83
+
84
+ for (const file of files) {
85
+ try {
86
+ await this.indexFile(file);
87
+ } catch (error) {
88
+ console.error(`Error indexing ${file}:`, error);
89
+ }
90
+ }
91
+ }
92
+
93
+ async indexFile(absolutePath: string): Promise<boolean> {
94
+ try {
95
+ const content = readFileSync(absolutePath, 'utf-8');
96
+ const stats = statSync(absolutePath);
97
+ const relativePath = relative(this.config.projectPath, absolutePath);
98
+
99
+ const contentHash = hashContent(content);
100
+ const existingFile = this.tier2.getFile(relativePath);
101
+
102
+ // Skip if content hasn't changed
103
+ if (existingFile && existingFile.contentHash === contentHash) {
104
+ return false; // Not indexed, skipped
105
+ }
106
+
107
+ const language = detectLanguage(absolutePath);
108
+ const preview = getPreview(content);
109
+ const lineCount = countLines(content);
110
+
111
+ // Store file metadata
112
+ const fileId = this.tier2.upsertFile(
113
+ relativePath,
114
+ contentHash,
115
+ preview,
116
+ language,
117
+ stats.size,
118
+ lineCount,
119
+ Math.floor(stats.mtimeMs)
120
+ );
121
+
122
+ // Generate and store embedding
123
+ const embedding = await this.embeddingGenerator.embed(content);
124
+ this.tier2.upsertEmbedding(fileId, embedding);
125
+
126
+ // Phase 2: Parse AST and extract symbols
127
+ try {
128
+ const parsed = await this.astParser.parseFile(relativePath, content);
129
+ if (parsed) {
130
+ // Clear old symbols/imports/exports for this file
131
+ this.tier2.clearSymbols(fileId);
132
+ this.tier2.clearImports(fileId);
133
+ this.tier2.clearExports(fileId);
134
+
135
+ // Insert new symbols with fileId
136
+ if (parsed.symbols.length > 0) {
137
+ const symbolsWithFileId = parsed.symbols.map(s => ({ ...s, fileId }));
138
+ this.tier2.insertSymbols(symbolsWithFileId);
139
+ }
140
+
141
+ // Insert imports with fileId
142
+ if (parsed.imports.length > 0) {
143
+ const importsWithFileId = parsed.imports.map(i => ({ ...i, fileId }));
144
+ this.tier2.insertImports(importsWithFileId);
145
+ }
146
+
147
+ // Insert exports with fileId
148
+ if (parsed.exports.length > 0) {
149
+ const exportsWithFileId = parsed.exports.map(e => ({ ...e, fileId }));
150
+ this.tier2.insertExports(exportsWithFileId);
151
+ }
152
+ }
153
+ } catch (astError) {
154
+ // AST parsing is optional, don't fail the whole index
155
+ console.error(`AST parsing failed for ${relativePath}:`, astError);
156
+ }
157
+
158
+ this.emit('fileIndexed', relativePath);
159
+ return true; // Actually indexed
160
+ } catch (error) {
161
+ console.error(`Error indexing ${absolutePath}:`, error);
162
+ this.emit('indexError', { path: absolutePath, error });
163
+ return false;
164
+ }
165
+ }
166
+
167
+ async performInitialIndex(): Promise<void> {
168
+ if (this.isIndexing) {
169
+ return;
170
+ }
171
+
172
+ this.isIndexing = true;
173
+ this.emit('indexingStarted');
174
+
175
+ try {
176
+ // Find all code files
177
+ const patterns = [
178
+ '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs',
179
+ '**/*.py', '**/*.rb', '**/*.go', '**/*.rs', '**/*.java', '**/*.kt',
180
+ '**/*.cs', '**/*.cpp', '**/*.c', '**/*.h', '**/*.hpp',
181
+ '**/*.php', '**/*.swift', '**/*.vue', '**/*.svelte',
182
+ '**/*.md', '**/*.json', '**/*.yaml', '**/*.yml',
183
+ '**/*.sql', '**/*.sh', '**/*.dockerfile',
184
+ '**/*.prisma', '**/*.graphql'
185
+ ];
186
+
187
+ const files: string[] = [];
188
+
189
+ for (const pattern of patterns) {
190
+ const matches = await glob(pattern, {
191
+ cwd: this.config.projectPath,
192
+ ignore: this.config.watchIgnore,
193
+ absolute: true,
194
+ nodir: true
195
+ });
196
+ files.push(...matches);
197
+ }
198
+
199
+ // Deduplicate
200
+ const uniqueFiles = [...new Set(files)];
201
+
202
+ let checked = 0;
203
+ let indexed = 0;
204
+ const total = uniqueFiles.length;
205
+
206
+ // Index files (only shows progress for actually indexed files)
207
+ for (const file of uniqueFiles) {
208
+ try {
209
+ const wasIndexed = await this.indexFile(file);
210
+ checked++;
211
+ if (wasIndexed) {
212
+ indexed++;
213
+ this.emit('progress', { total, indexed, current: relative(this.config.projectPath, file) });
214
+ }
215
+ } catch (error) {
216
+ console.error(`Error indexing ${file}:`, error);
217
+ }
218
+ }
219
+
220
+ this.emit('indexingComplete', {
221
+ total: checked,
222
+ indexed,
223
+ skipped: checked - indexed
224
+ });
225
+ } finally {
226
+ this.isIndexing = false;
227
+ }
228
+ }
229
+
230
+ startWatching(): void {
231
+ this.watcher.start();
232
+ }
233
+
234
+ stopWatching(): void {
235
+ this.watcher.stop();
236
+ }
237
+
238
+ getEmbeddingGenerator(): EmbeddingGenerator {
239
+ return this.embeddingGenerator;
240
+ }
241
+
242
+ isCurrentlyIndexing(): boolean {
243
+ return this.isIndexing;
244
+ }
245
+ }
@@ -0,0 +1,78 @@
1
+ import chokidar, { type FSWatcher } from 'chokidar';
2
+ import { EventEmitter } from 'events';
3
+ import { relative } from 'path';
4
+
5
+ export interface FileEvent {
6
+ type: 'add' | 'change' | 'unlink';
7
+ path: string;
8
+ relativePath: string;
9
+ }
10
+
11
+ export class FileWatcher extends EventEmitter {
12
+ private watcher: FSWatcher | null = null;
13
+ private projectPath: string;
14
+ private ignorePatterns: string[];
15
+
16
+ constructor(projectPath: string, ignorePatterns: string[] = []) {
17
+ super();
18
+ this.projectPath = projectPath;
19
+ this.ignorePatterns = ignorePatterns;
20
+ }
21
+
22
+ start(): void {
23
+ if (this.watcher) {
24
+ return;
25
+ }
26
+
27
+ this.watcher = chokidar.watch(this.projectPath, {
28
+ ignored: [
29
+ /(^|[\/\\])\../, // dotfiles
30
+ ...this.ignorePatterns
31
+ ],
32
+ persistent: true,
33
+ ignoreInitial: false, // We want initial add events for indexing
34
+ awaitWriteFinish: {
35
+ stabilityThreshold: 300,
36
+ pollInterval: 100
37
+ },
38
+ usePolling: false, // Use native events when possible
39
+ depth: 20 // Limit recursion depth
40
+ });
41
+
42
+ this.watcher
43
+ .on('add', (path) => this.handleEvent('add', path))
44
+ .on('change', (path) => this.handleEvent('change', path))
45
+ .on('unlink', (path) => this.handleEvent('unlink', path))
46
+ .on('error', (error) => {
47
+ console.error('File watcher error:', error);
48
+ this.emit('error', error);
49
+ })
50
+ .on('ready', () => {
51
+ console.error('File watcher ready');
52
+ this.emit('ready');
53
+ });
54
+ }
55
+
56
+ private handleEvent(type: 'add' | 'change' | 'unlink', path: string): void {
57
+ const relativePath = relative(this.projectPath, path);
58
+
59
+ const event: FileEvent = {
60
+ type,
61
+ path,
62
+ relativePath
63
+ };
64
+
65
+ this.emit('file', event);
66
+ }
67
+
68
+ stop(): void {
69
+ if (this.watcher) {
70
+ this.watcher.close();
71
+ this.watcher = null;
72
+ }
73
+ }
74
+
75
+ isRunning(): boolean {
76
+ return this.watcher !== null;
77
+ }
78
+ }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Result Aggregation Utilities
3
+ *
4
+ * Combines results from multiple internal tools into unified gateway responses.
5
+ */
6
+
7
+ import type {
8
+ MemoryQueryResponse,
9
+ MemoryReviewResponse,
10
+ MemoryStatusResponse,
11
+ } from './types.js';
12
+
13
+ // ============================================================================
14
+ // Query Result Aggregation
15
+ // ============================================================================
16
+
17
+ export interface RawContextResult {
18
+ context: string;
19
+ sources: string[];
20
+ tokenCount: number;
21
+ decisions: Array<{
22
+ id: string;
23
+ title: string;
24
+ description: string;
25
+ createdAt: Date;
26
+ }>;
27
+ }
28
+
29
+ export interface RawSearchResult {
30
+ file: string;
31
+ preview: string;
32
+ similarity: number;
33
+ lineStart?: number;
34
+ lineEnd?: number;
35
+ }
36
+
37
+ /**
38
+ * Aggregate context and search results into a unified query response
39
+ */
40
+ export function aggregateQueryResults(
41
+ contextResult: RawContextResult | null,
42
+ searchResults: RawSearchResult[] | null,
43
+ sourcesUsed: string[]
44
+ ): Partial<MemoryQueryResponse> {
45
+ const response: Partial<MemoryQueryResponse> = {
46
+ sources_used: sourcesUsed,
47
+ };
48
+
49
+ if (contextResult) {
50
+ response.context = {
51
+ content: contextResult.context,
52
+ sources: contextResult.sources,
53
+ token_count: contextResult.tokenCount,
54
+ decisions: contextResult.decisions.map(d => ({
55
+ id: d.id,
56
+ title: d.title,
57
+ description: d.description,
58
+ created_at: d.createdAt.toISOString(),
59
+ })),
60
+ };
61
+ }
62
+
63
+ if (searchResults && searchResults.length > 0) {
64
+ // Deduplicate search results by file
65
+ const seen = new Set<string>();
66
+ const deduped: RawSearchResult[] = [];
67
+
68
+ for (const result of searchResults) {
69
+ const key = `${result.file}:${result.lineStart || 0}`;
70
+ if (!seen.has(key)) {
71
+ seen.add(key);
72
+ deduped.push(result);
73
+ }
74
+ }
75
+
76
+ response.search_results = deduped.map(r => ({
77
+ file: r.file,
78
+ preview: r.preview,
79
+ relevance: r.similarity,
80
+ line_start: r.lineStart,
81
+ line_end: r.lineEnd,
82
+ }));
83
+ }
84
+
85
+ return response;
86
+ }
87
+
88
+ /**
89
+ * Merge search results from multiple sources, deduplicating and sorting by relevance
90
+ */
91
+ export function mergeSearchResults(
92
+ ...resultSets: (RawSearchResult[] | null)[]
93
+ ): RawSearchResult[] {
94
+ const merged: Map<string, RawSearchResult> = new Map();
95
+
96
+ for (const results of resultSets) {
97
+ if (!results) continue;
98
+
99
+ for (const result of results) {
100
+ const key = `${result.file}:${result.lineStart || 0}`;
101
+ const existing = merged.get(key);
102
+
103
+ if (!existing || existing.similarity < result.similarity) {
104
+ merged.set(key, result);
105
+ }
106
+ }
107
+ }
108
+
109
+ // Sort by relevance descending
110
+ return Array.from(merged.values()).sort((a, b) => b.similarity - a.similarity);
111
+ }
112
+
113
+ // ============================================================================
114
+ // Review Result Aggregation
115
+ // ============================================================================
116
+
117
+ export interface PatternValidationResult {
118
+ valid: boolean;
119
+ score: number;
120
+ matchedPattern?: string;
121
+ violations: Array<{
122
+ rule: string;
123
+ message: string;
124
+ severity: string;
125
+ suggestion?: string;
126
+ }>;
127
+ }
128
+
129
+ export interface ConflictCheckResult {
130
+ hasConflicts: boolean;
131
+ conflicts: Array<{
132
+ decisionId: string;
133
+ decisionTitle: string;
134
+ conflictDescription: string;
135
+ severity: string;
136
+ decisionDate: Date;
137
+ }>;
138
+ }
139
+
140
+ export interface ConfidenceCheckResult {
141
+ confidence: string;
142
+ score: number;
143
+ reasoning: string;
144
+ }
145
+
146
+ export interface TestCheckResult {
147
+ safe: boolean;
148
+ coveragePercent: number;
149
+ wouldFail: Array<{
150
+ test: { id: string; name: string; file: string };
151
+ reason: string;
152
+ suggestedFix?: string;
153
+ }>;
154
+ suggestedTestUpdates: Array<{
155
+ file: string;
156
+ testName: string;
157
+ before: string;
158
+ after: string;
159
+ reason: string;
160
+ }>;
161
+ }
162
+
163
+ export interface ExistingFunctionResult {
164
+ name: string;
165
+ file: string;
166
+ line: number;
167
+ signature: string;
168
+ similarity: number;
169
+ }
170
+
171
+ /**
172
+ * Calculate a unified risk score from review results
173
+ *
174
+ * Scoring factors:
175
+ * - Pattern validation: 0-30 points (based on violations)
176
+ * - Conflicts: 0-40 points (critical if conflicts exist)
177
+ * - Test impact: 0-20 points (based on failing tests)
178
+ * - Confidence: 0-10 points (low confidence adds risk)
179
+ */
180
+ export function calculateRiskScore(
181
+ patternResult: PatternValidationResult | null,
182
+ conflicts: ConflictCheckResult | null,
183
+ testResult: TestCheckResult | null,
184
+ confidence: ConfidenceCheckResult | null
185
+ ): number {
186
+ let riskScore = 0;
187
+
188
+ // Pattern violations (0-30 points)
189
+ if (patternResult) {
190
+ const violationRisk = Math.min(30, patternResult.violations.length * 10);
191
+ // Higher severity violations count more
192
+ const severityBonus = patternResult.violations.filter(
193
+ v => v.severity === 'high' || v.severity === 'error'
194
+ ).length * 5;
195
+ riskScore += Math.min(30, violationRisk + severityBonus);
196
+ }
197
+
198
+ // Conflicts (0-40 points) - conflicts with past decisions are serious
199
+ if (conflicts && conflicts.hasConflicts) {
200
+ const conflictRisk = Math.min(40, conflicts.conflicts.length * 20);
201
+ riskScore += conflictRisk;
202
+ }
203
+
204
+ // Test impact (0-20 points)
205
+ if (testResult && !testResult.safe) {
206
+ const testRisk = Math.min(20, testResult.wouldFail.length * 10);
207
+ riskScore += testRisk;
208
+ }
209
+
210
+ // Low confidence (0-10 points)
211
+ if (confidence) {
212
+ if (confidence.confidence === 'low') {
213
+ riskScore += 10;
214
+ } else if (confidence.confidence === 'medium') {
215
+ riskScore += 5;
216
+ }
217
+ }
218
+
219
+ return Math.min(100, riskScore);
220
+ }
221
+
222
+ /**
223
+ * Determine verdict based on risk score
224
+ */
225
+ export function getVerdict(riskScore: number): 'approve' | 'warning' | 'reject' {
226
+ if (riskScore >= 70) return 'reject';
227
+ if (riskScore >= 30) return 'warning';
228
+ return 'approve';
229
+ }
230
+
231
+ /**
232
+ * Aggregate review results into a unified response
233
+ */
234
+ export function aggregateReviewResults(
235
+ patternResult: PatternValidationResult | null,
236
+ conflicts: ConflictCheckResult | null,
237
+ confidence: ConfidenceCheckResult | null,
238
+ existingAlternatives: ExistingFunctionResult[] | null,
239
+ testResult: TestCheckResult | null,
240
+ sourcesUsed: string[]
241
+ ): MemoryReviewResponse {
242
+ const riskScore = calculateRiskScore(patternResult, conflicts, testResult, confidence);
243
+
244
+ const response: MemoryReviewResponse = {
245
+ verdict: getVerdict(riskScore),
246
+ risk_score: riskScore,
247
+ sources_used: sourcesUsed,
248
+ };
249
+
250
+ if (patternResult) {
251
+ response.patterns = {
252
+ valid: patternResult.valid,
253
+ score: patternResult.score,
254
+ matched_pattern: patternResult.matchedPattern,
255
+ violations: patternResult.violations,
256
+ };
257
+ }
258
+
259
+ if (conflicts) {
260
+ response.conflicts = {
261
+ has_conflicts: conflicts.hasConflicts,
262
+ conflicts: conflicts.conflicts.map(c => ({
263
+ decision_id: c.decisionId,
264
+ decision_title: c.decisionTitle,
265
+ conflict_description: c.conflictDescription,
266
+ severity: c.severity,
267
+ })),
268
+ };
269
+ }
270
+
271
+ if (existingAlternatives && existingAlternatives.length > 0) {
272
+ response.existing_alternatives = existingAlternatives.map(a => ({
273
+ name: a.name,
274
+ file: a.file,
275
+ line: a.line,
276
+ signature: a.signature,
277
+ similarity: a.similarity,
278
+ }));
279
+ }
280
+
281
+ if (testResult) {
282
+ response.test_impact = {
283
+ safe: testResult.safe,
284
+ coverage_percent: testResult.coveragePercent,
285
+ would_fail: testResult.wouldFail.map(f => ({
286
+ test_name: f.test.name,
287
+ test_file: f.test.file,
288
+ reason: f.reason,
289
+ suggested_fix: f.suggestedFix,
290
+ })),
291
+ suggested_updates: testResult.suggestedTestUpdates.map(u => ({
292
+ file: u.file,
293
+ test_name: u.testName,
294
+ before: u.before,
295
+ after: u.after,
296
+ reason: u.reason,
297
+ })),
298
+ };
299
+ }
300
+
301
+ if (confidence) {
302
+ response.confidence = {
303
+ level: confidence.confidence,
304
+ score: confidence.score,
305
+ reasoning: confidence.reasoning,
306
+ };
307
+ }
308
+
309
+ return response;
310
+ }
311
+
312
+ // ============================================================================
313
+ // Status Result Aggregation
314
+ // ============================================================================
315
+
316
+ /**
317
+ * Build a status response from multiple gathered results
318
+ */
319
+ export function aggregateStatusResults(
320
+ results: Record<string, unknown>,
321
+ sourcesUsed: string[]
322
+ ): MemoryStatusResponse {
323
+ const response: MemoryStatusResponse = {
324
+ sources_used: sourcesUsed,
325
+ };
326
+
327
+ // Copy relevant results into response
328
+ if (results.project) response.project = results.project as MemoryStatusResponse['project'];
329
+ if (results.architecture) response.architecture = results.architecture as MemoryStatusResponse['architecture'];
330
+ if (results.changes) response.changes = results.changes as MemoryStatusResponse['changes'];
331
+ if (results.activity) response.activity = results.activity as MemoryStatusResponse['activity'];
332
+ if (results.changelog) response.changelog = results.changelog as MemoryStatusResponse['changelog'];
333
+ if (results.docs) response.docs = results.docs as MemoryStatusResponse['docs'];
334
+ if (results.health) response.health = results.health as MemoryStatusResponse['health'];
335
+ if (results.patterns) response.patterns = results.patterns as MemoryStatusResponse['patterns'];
336
+ if (results.stats) response.stats = results.stats as MemoryStatusResponse['stats'];
337
+ if (results.critical) response.critical = results.critical as MemoryStatusResponse['critical'];
338
+ if (results.learning) response.learning = results.learning as MemoryStatusResponse['learning'];
339
+ if (results.undocumented) response.undocumented = results.undocumented as MemoryStatusResponse['undocumented'];
340
+
341
+ return response;
342
+ }
343
+
344
+ // ============================================================================
345
+ // Utility Functions
346
+ // ============================================================================
347
+
348
+ /**
349
+ * Truncate a string to a maximum length with ellipsis
350
+ */
351
+ export function truncate(str: string, maxLength: number): string {
352
+ if (str.length <= maxLength) return str;
353
+ return str.slice(0, maxLength - 3) + '...';
354
+ }
355
+
356
+ /**
357
+ * Format a date for display
358
+ */
359
+ export function formatDate(date: Date): string {
360
+ return date.toISOString();
361
+ }
362
+
363
+ /**
364
+ * Calculate time ago string
365
+ */
366
+ export function timeAgo(date: Date): string {
367
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
368
+
369
+ if (seconds < 60) return 'just now';
370
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
371
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
372
+ if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
373
+ return date.toISOString().split('T')[0] || date.toISOString();
374
+ }