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,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
|
+
}
|