neuronlayer 0.1.0 → 1.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/README.md +203 -123
  2. package/dist/index.js +635 -710
  3. package/dist/index.js.map +7 -0
  4. package/package.json +67 -63
  5. package/CONTRIBUTING.md +0 -127
  6. package/esbuild.config.js +0 -26
  7. package/src/cli/commands.ts +0 -382
  8. package/src/core/adr-exporter.ts +0 -253
  9. package/src/core/architecture/architecture-enforcement.ts +0 -228
  10. package/src/core/architecture/duplicate-detector.ts +0 -288
  11. package/src/core/architecture/index.ts +0 -6
  12. package/src/core/architecture/pattern-learner.ts +0 -306
  13. package/src/core/architecture/pattern-library.ts +0 -403
  14. package/src/core/architecture/pattern-validator.ts +0 -324
  15. package/src/core/change-intelligence/bug-correlator.ts +0 -444
  16. package/src/core/change-intelligence/change-intelligence.ts +0 -221
  17. package/src/core/change-intelligence/change-tracker.ts +0 -334
  18. package/src/core/change-intelligence/fix-suggester.ts +0 -340
  19. package/src/core/change-intelligence/index.ts +0 -5
  20. package/src/core/code-verifier.ts +0 -843
  21. package/src/core/confidence/confidence-scorer.ts +0 -251
  22. package/src/core/confidence/conflict-checker.ts +0 -289
  23. package/src/core/confidence/index.ts +0 -5
  24. package/src/core/confidence/source-tracker.ts +0 -263
  25. package/src/core/confidence/warning-detector.ts +0 -241
  26. package/src/core/context-rot/compaction.ts +0 -284
  27. package/src/core/context-rot/context-health.ts +0 -243
  28. package/src/core/context-rot/context-rot-prevention.ts +0 -213
  29. package/src/core/context-rot/critical-context.ts +0 -221
  30. package/src/core/context-rot/drift-detector.ts +0 -255
  31. package/src/core/context-rot/index.ts +0 -7
  32. package/src/core/context.ts +0 -263
  33. package/src/core/decision-extractor.ts +0 -339
  34. package/src/core/decisions.ts +0 -69
  35. package/src/core/deja-vu.ts +0 -421
  36. package/src/core/engine.ts +0 -1455
  37. package/src/core/feature-context.ts +0 -726
  38. package/src/core/ghost-mode.ts +0 -412
  39. package/src/core/learning.ts +0 -485
  40. package/src/core/living-docs/activity-tracker.ts +0 -296
  41. package/src/core/living-docs/architecture-generator.ts +0 -428
  42. package/src/core/living-docs/changelog-generator.ts +0 -348
  43. package/src/core/living-docs/component-generator.ts +0 -230
  44. package/src/core/living-docs/doc-engine.ts +0 -110
  45. package/src/core/living-docs/doc-validator.ts +0 -282
  46. package/src/core/living-docs/index.ts +0 -8
  47. package/src/core/project-manager.ts +0 -297
  48. package/src/core/summarizer.ts +0 -267
  49. package/src/core/test-awareness/change-validator.ts +0 -499
  50. package/src/core/test-awareness/index.ts +0 -5
  51. package/src/index.ts +0 -49
  52. package/src/indexing/ast.ts +0 -563
  53. package/src/indexing/embeddings.ts +0 -85
  54. package/src/indexing/indexer.ts +0 -245
  55. package/src/indexing/watcher.ts +0 -78
  56. package/src/server/gateways/aggregator.ts +0 -374
  57. package/src/server/gateways/index.ts +0 -473
  58. package/src/server/gateways/memory-ghost.ts +0 -343
  59. package/src/server/gateways/memory-query.ts +0 -452
  60. package/src/server/gateways/memory-record.ts +0 -346
  61. package/src/server/gateways/memory-review.ts +0 -410
  62. package/src/server/gateways/memory-status.ts +0 -517
  63. package/src/server/gateways/memory-verify.ts +0 -392
  64. package/src/server/gateways/router.ts +0 -434
  65. package/src/server/gateways/types.ts +0 -610
  66. package/src/server/mcp.ts +0 -154
  67. package/src/server/resources.ts +0 -85
  68. package/src/server/tools.ts +0 -2261
  69. package/src/storage/database.ts +0 -262
  70. package/src/storage/tier1.ts +0 -135
  71. package/src/storage/tier2.ts +0 -764
  72. package/src/storage/tier3.ts +0 -123
  73. package/src/types/documentation.ts +0 -619
  74. package/src/types/index.ts +0 -222
  75. package/src/utils/config.ts +0 -193
  76. package/src/utils/files.ts +0 -117
  77. package/src/utils/time.ts +0 -37
  78. package/src/utils/tokens.ts +0 -52
@@ -1,263 +0,0 @@
1
- import { dirname, relative, basename } from 'path';
2
- import { Tier1Storage } from '../storage/tier1.js';
3
- import { Tier2Storage } from '../storage/tier2.js';
4
- import { Tier3Storage } from '../storage/tier3.js';
5
- import { EmbeddingGenerator } from '../indexing/embeddings.js';
6
- import { TokenBudget, estimateTokens } from '../utils/tokens.js';
7
- import type { FeatureContextManager } from './feature-context.js';
8
- import type { AssembledContext, AssemblyOptions, SearchResult, Decision, ContextParts, HotContext } from '../types/index.js';
9
-
10
- export class ContextAssembler {
11
- private tier1: Tier1Storage;
12
- private tier2: Tier2Storage;
13
- private tier3: Tier3Storage;
14
- private embeddingGenerator: EmbeddingGenerator;
15
- private featureContextManager: FeatureContextManager | null = null;
16
-
17
- constructor(
18
- tier1: Tier1Storage,
19
- tier2: Tier2Storage,
20
- tier3: Tier3Storage,
21
- embeddingGenerator: EmbeddingGenerator
22
- ) {
23
- this.tier1 = tier1;
24
- this.tier2 = tier2;
25
- this.tier3 = tier3;
26
- this.embeddingGenerator = embeddingGenerator;
27
- }
28
-
29
- // Set feature context manager (injected after construction to avoid circular deps)
30
- setFeatureContextManager(manager: FeatureContextManager): void {
31
- this.featureContextManager = manager;
32
- }
33
-
34
- async assemble(query: string, options: AssemblyOptions = {}): Promise<AssembledContext> {
35
- const budget = new TokenBudget(options.maxTokens || 6000);
36
- const queryEmbedding = await this.embeddingGenerator.embed(query);
37
-
38
- // Step 0: HOT CONTEXT (HIGHEST PRIORITY) - Active Feature Context
39
- let hotContext: HotContext | null = null;
40
- if (this.featureContextManager) {
41
- hotContext = this.featureContextManager.getHotContext();
42
- if (hotContext.files.length > 0 || hotContext.changes.length > 0) {
43
- const hotText = this.formatHotContext(hotContext);
44
- if (budget.canFit(hotText)) {
45
- budget.allocate(hotText, 'hot-context');
46
- }
47
- }
48
- }
49
-
50
- // Step 1: Load and format Tier 1 (working context)
51
- const working = this.tier1.getContext();
52
- const workingText = this.formatTier1(working);
53
-
54
- if (workingText && budget.canFit(workingText)) {
55
- budget.allocate(workingText, 'tier1');
56
- }
57
-
58
- // Step 2: Semantic search in Tier 2
59
- const searchResults = this.tier2.search(queryEmbedding, 20);
60
- const rankedResults = this.rankResults(searchResults, options.currentFile);
61
-
62
- const tier2Content: SearchResult[] = [];
63
- for (const result of rankedResults) {
64
- const formatted = this.formatSearchResult(result);
65
- if (budget.canFit(formatted)) {
66
- budget.allocate(formatted, 'tier2');
67
- tier2Content.push(result);
68
- } else {
69
- break;
70
- }
71
- }
72
-
73
- // Step 3: Query Tier 3 if budget remains
74
- const tier3Content: string[] = [];
75
- if (budget.remaining() > 200) {
76
- const archives = this.tier3.searchRelevant(query, 3);
77
- for (const archive of archives) {
78
- if (archive.summary && budget.canFit(archive.summary)) {
79
- budget.allocate(archive.summary, 'tier3');
80
- tier3Content.push(archive.summary);
81
- }
82
- }
83
- }
84
-
85
- // Step 4: Get relevant decisions
86
- let decisions: Decision[] = [];
87
- try {
88
- decisions = this.tier2.searchDecisions(queryEmbedding, 5);
89
- } catch {
90
- decisions = this.tier2.getRecentDecisions(5);
91
- }
92
-
93
- const decisionsText = this.formatDecisions(decisions);
94
- if (decisionsText && budget.canFit(decisionsText)) {
95
- budget.allocate(decisionsText, 'decisions');
96
- }
97
-
98
- // Step 5: Assemble final context
99
- const context = this.formatFinalContext({
100
- working: { activeFile: working.activeFile },
101
- relevant: tier2Content,
102
- archive: tier3Content,
103
- decisions
104
- }, hotContext);
105
-
106
- return {
107
- context,
108
- sources: tier2Content.map(r => r.file),
109
- tokenCount: budget.used(),
110
- decisions
111
- };
112
- }
113
-
114
- private formatHotContext(hot: HotContext): string {
115
- let output = `## Active Feature Context\n\n`;
116
-
117
- if (hot.summary) {
118
- output += `**${hot.summary}**\n\n`;
119
- }
120
-
121
- if (hot.files.length > 0) {
122
- output += `### Files You're Working On\n`;
123
- for (const f of hot.files.slice(0, 8)) {
124
- output += `- \`${f.path}\` (touched ${f.touchCount}x)\n`;
125
- }
126
- output += `\n`;
127
- }
128
-
129
- if (hot.changes.length > 0) {
130
- output += `### Recent Changes\n`;
131
- for (const c of hot.changes.slice(0, 5)) {
132
- const fileName = basename(c.file);
133
- output += `- \`${fileName}\`: ${c.diff}\n`;
134
- }
135
- output += `\n`;
136
- }
137
-
138
- if (hot.queries.length > 0) {
139
- output += `### Recent Questions\n`;
140
- for (const q of hot.queries.slice(0, 3)) {
141
- output += `- "${q.query}"\n`;
142
- }
143
- output += `\n`;
144
- }
145
-
146
- return output;
147
- }
148
-
149
- private formatTier1(context: { activeFile: { path: string; content: string; language: string } | null }): string {
150
- if (!context.activeFile) {
151
- return '';
152
- }
153
-
154
- return `### Currently Active File
155
- File: ${context.activeFile.path}
156
- \`\`\`${context.activeFile.language}
157
- ${context.activeFile.content}
158
- \`\`\`
159
- `;
160
- }
161
-
162
- private formatSearchResult(result: SearchResult): string {
163
- const score = result.score !== undefined ? result.score : result.similarity;
164
- return `#### ${result.file} (relevance: ${(score * 100).toFixed(0)}%)
165
- \`\`\`
166
- ${result.preview}
167
- \`\`\`
168
- `;
169
- }
170
-
171
- private formatDecisions(decisions: Decision[]): string {
172
- if (decisions.length === 0) {
173
- return '';
174
- }
175
-
176
- return decisions.map(d => {
177
- const date = d.createdAt.toLocaleDateString();
178
- return `- **${d.title}** (${date})
179
- ${d.description}`;
180
- }).join('\n\n');
181
- }
182
-
183
- private rankResults(results: SearchResult[], currentFile?: string): SearchResult[] {
184
- const filesViewed = this.tier1.getFilesViewed();
185
-
186
- return results
187
- .map(r => {
188
- let score = r.similarity;
189
-
190
- // Boost: Same directory as current file
191
- if (currentFile && dirname(r.file) === dirname(currentFile)) {
192
- score *= 1.5;
193
- }
194
-
195
- // Boost: Recently modified (within 24 hours)
196
- const hoursSinceModified = (Date.now() - r.lastModified) / 3600000;
197
- if (hoursSinceModified < 24) {
198
- score *= 1 + (0.3 * (24 - hoursSinceModified) / 24);
199
- }
200
-
201
- // Boost: Recently viewed in session
202
- if (filesViewed.includes(r.file)) {
203
- score *= 1.3;
204
- }
205
-
206
- return { ...r, score };
207
- })
208
- .sort((a, b) => (b.score || 0) - (a.score || 0));
209
- }
210
-
211
- private formatFinalContext(parts: ContextParts, hotContext?: HotContext | null): string {
212
- const sections: string[] = [];
213
-
214
- // Hot context first (highest priority)
215
- if (hotContext && (hotContext.files.length > 0 || hotContext.changes.length > 0)) {
216
- sections.push(this.formatHotContext(hotContext));
217
- }
218
-
219
- sections.push('## Codebase Context\n');
220
-
221
- // Working file
222
- if (parts.working.activeFile) {
223
- sections.push(`### Working File
224
- File: ${parts.working.activeFile.path}
225
- \`\`\`${parts.working.activeFile.language}
226
- ${parts.working.activeFile.content}
227
- \`\`\`
228
- `);
229
- }
230
-
231
- // Relevant code
232
- if (parts.relevant.length > 0) {
233
- sections.push('### Relevant Code\n');
234
- for (const r of parts.relevant) {
235
- const score = r.score !== undefined ? r.score : r.similarity;
236
- sections.push(`#### ${r.file} (relevance: ${(score * 100).toFixed(0)}%)
237
- \`\`\`
238
- ${r.preview}
239
- \`\`\`
240
- `);
241
- }
242
- }
243
-
244
- // Decisions
245
- if (parts.decisions.length > 0) {
246
- sections.push('### Architecture Decisions\n');
247
- for (const d of parts.decisions) {
248
- const date = d.createdAt.toLocaleDateString();
249
- sections.push(`- **${d.title}** (${date})
250
- ${d.description}
251
- `);
252
- }
253
- }
254
-
255
- // Archive
256
- if (parts.archive.length > 0) {
257
- sections.push('### Historical Context\n');
258
- sections.push(parts.archive.join('\n\n'));
259
- }
260
-
261
- return sections.join('\n').trim();
262
- }
263
- }
@@ -1,339 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import { readFileSync, existsSync } from 'fs';
3
- import { join } from 'path';
4
- import { glob } from 'glob';
5
- import type { Decision } from '../types/index.js';
6
- import { randomUUID } from 'crypto';
7
-
8
- interface ExtractedDecision {
9
- title: string;
10
- description: string;
11
- source: 'git' | 'comment' | 'adr';
12
- file?: string;
13
- line?: number;
14
- tags: string[];
15
- }
16
-
17
- // Patterns for detecting decisions in commit messages
18
- const COMMIT_PATTERNS = [
19
- // Conventional commits with architectural significance
20
- /^(?:feat|refactor|perf|build|ci)\(([^)]+)\):\s*(.+)/i,
21
- // Explicit decision markers
22
- /^(?:DECISION|ARCHITECTURE|ADR):\s*(.+)/i,
23
- // "Use X instead of Y" pattern
24
- /use\s+(\w+)\s+(?:instead of|over|rather than)\s+(\w+)/i,
25
- // "Switch to X" pattern
26
- /switch(?:ed|ing)?\s+to\s+(\w+)/i,
27
- // "Implement X pattern"
28
- /implement(?:ed|ing)?\s+(\w+)\s+pattern/i,
29
- ];
30
-
31
- // Patterns for detecting decisions in code comments
32
- const COMMENT_PATTERNS = [
33
- // Explicit decision markers
34
- { pattern: /(?:\/\/|#|\/\*)\s*DECISION:\s*(.+?)(?:\*\/)?$/gm, tag: 'decision' },
35
- { pattern: /(?:\/\/|#|\/\*)\s*ARCHITECTURE:\s*(.+?)(?:\*\/)?$/gm, tag: 'architecture' },
36
- { pattern: /(?:\/\/|#|\/\*)\s*ADR:\s*(.+?)(?:\*\/)?$/gm, tag: 'adr' },
37
- { pattern: /(?:\/\/|#|\/\*)\s*WHY:\s*(.+?)(?:\*\/)?$/gm, tag: 'rationale' },
38
- { pattern: /(?:\/\/|#|\/\*)\s*NOTE:\s*(.+?)(?:\*\/)?$/gm, tag: 'note' },
39
- { pattern: /(?:\/\/|#|\/\*)\s*IMPORTANT:\s*(.+?)(?:\*\/)?$/gm, tag: 'important' },
40
- ];
41
-
42
- // Tags to assign based on content
43
- const TAG_KEYWORDS: Record<string, string[]> = {
44
- database: ['database', 'db', 'sql', 'postgres', 'mysql', 'mongo', 'redis', 'sqlite', 'orm', 'prisma'],
45
- authentication: ['auth', 'login', 'jwt', 'oauth', 'session', 'password', 'token'],
46
- api: ['api', 'rest', 'graphql', 'endpoint', 'route', 'http'],
47
- performance: ['performance', 'cache', 'optimize', 'speed', 'fast', 'slow', 'memory'],
48
- security: ['security', 'encrypt', 'hash', 'ssl', 'https', 'csrf', 'xss', 'injection'],
49
- testing: ['test', 'jest', 'vitest', 'mocha', 'cypress', 'e2e', 'unit'],
50
- infrastructure: ['docker', 'kubernetes', 'aws', 'gcp', 'azure', 'deploy', 'ci', 'cd'],
51
- frontend: ['react', 'vue', 'angular', 'svelte', 'css', 'ui', 'component'],
52
- backend: ['server', 'node', 'express', 'fastify', 'middleware'],
53
- };
54
-
55
- export class DecisionExtractor {
56
- private projectPath: string;
57
- private isGitRepo: boolean;
58
-
59
- constructor(projectPath: string) {
60
- this.projectPath = projectPath;
61
- this.isGitRepo = existsSync(join(projectPath, '.git'));
62
- }
63
-
64
- async extractAll(): Promise<ExtractedDecision[]> {
65
- const decisions: ExtractedDecision[] = [];
66
-
67
- // Extract from git commits
68
- if (this.isGitRepo) {
69
- try {
70
- const gitDecisions = await this.extractFromGitCommits();
71
- decisions.push(...gitDecisions);
72
- } catch (error) {
73
- console.error('Error extracting from git:', error);
74
- }
75
- }
76
-
77
- // Extract from code comments
78
- try {
79
- const commentDecisions = await this.extractFromComments();
80
- decisions.push(...commentDecisions);
81
- } catch (error) {
82
- console.error('Error extracting from comments:', error);
83
- }
84
-
85
- // Extract from ADR files
86
- try {
87
- const adrDecisions = await this.extractFromADRFiles();
88
- decisions.push(...adrDecisions);
89
- } catch (error) {
90
- console.error('Error extracting from ADR files:', error);
91
- }
92
-
93
- return decisions;
94
- }
95
-
96
- private async extractFromGitCommits(): Promise<ExtractedDecision[]> {
97
- const decisions: ExtractedDecision[] = [];
98
-
99
- try {
100
- // Get recent commits with architectural significance
101
- const output = execSync(
102
- 'git log --oneline -100 --format="%H|%s|%b"',
103
- { cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 }
104
- );
105
-
106
- const commits = output.split('\n').filter(Boolean);
107
-
108
- for (const commit of commits) {
109
- const [hash, subject, ...bodyParts] = commit.split('|');
110
- const body = bodyParts.join('|');
111
- const fullMessage = `${subject}\n${body}`.trim();
112
-
113
- // Check for decision patterns
114
- for (const pattern of COMMIT_PATTERNS) {
115
- const match = fullMessage.match(pattern);
116
- if (match) {
117
- const title = this.extractTitle(subject || '');
118
- const description = body || subject || '';
119
-
120
- if (title && this.isArchitecturallySignificant(fullMessage)) {
121
- decisions.push({
122
- title,
123
- description: description.slice(0, 500),
124
- source: 'git',
125
- tags: this.extractTags(fullMessage)
126
- });
127
- break;
128
- }
129
- }
130
- }
131
- }
132
- } catch (error) {
133
- // Git command failed, probably not a git repo or no commits
134
- }
135
-
136
- return decisions.slice(0, 20); // Limit to 20 most recent
137
- }
138
-
139
- private async extractFromComments(): Promise<ExtractedDecision[]> {
140
- const decisions: ExtractedDecision[] = [];
141
-
142
- // Find all code files
143
- const patterns = ['**/*.ts', '**/*.js', '**/*.py', '**/*.go', '**/*.java', '**/*.rs'];
144
- const ignorePatterns = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'];
145
-
146
- for (const pattern of patterns) {
147
- try {
148
- const files = await glob(pattern, {
149
- cwd: this.projectPath,
150
- ignore: ignorePatterns,
151
- absolute: true,
152
- nodir: true
153
- });
154
-
155
- for (const file of files.slice(0, 100)) { // Limit files to scan
156
- try {
157
- const content = readFileSync(file, 'utf-8');
158
- const lines = content.split('\n');
159
-
160
- for (let i = 0; i < lines.length; i++) {
161
- const line = lines[i] || '';
162
-
163
- for (const { pattern, tag } of COMMENT_PATTERNS) {
164
- pattern.lastIndex = 0; // Reset regex state
165
- const match = pattern.exec(line);
166
- if (match && match[1]) {
167
- const relativePath = file.replace(this.projectPath, '').replace(/^[/\\]/, '');
168
-
169
- decisions.push({
170
- title: this.extractTitle(match[1]),
171
- description: match[1],
172
- source: 'comment',
173
- file: relativePath,
174
- line: i + 1,
175
- tags: [tag, ...this.extractTags(match[1])]
176
- });
177
- }
178
- }
179
- }
180
- } catch {
181
- // Skip files that can't be read
182
- }
183
- }
184
- } catch {
185
- // Glob failed
186
- }
187
- }
188
-
189
- return decisions;
190
- }
191
-
192
- private async extractFromADRFiles(): Promise<ExtractedDecision[]> {
193
- const decisions: ExtractedDecision[] = [];
194
-
195
- // Look for ADR files in common locations
196
- const adrPatterns = [
197
- '**/docs/decisions/*.md',
198
- '**/docs/adr/*.md',
199
- '**/adr/*.md',
200
- '**/decisions/*.md'
201
- ];
202
-
203
- for (const pattern of adrPatterns) {
204
- try {
205
- const files = await glob(pattern, {
206
- cwd: this.projectPath,
207
- ignore: ['**/node_modules/**'],
208
- absolute: true,
209
- nodir: true
210
- });
211
-
212
- for (const file of files) {
213
- try {
214
- const content = readFileSync(file, 'utf-8');
215
- const title = this.extractADRTitle(content);
216
- const status = this.extractADRStatus(content);
217
-
218
- if (title && status !== 'superseded' && status !== 'deprecated') {
219
- const relativePath = file.replace(this.projectPath, '').replace(/^[/\\]/, '');
220
-
221
- decisions.push({
222
- title,
223
- description: this.extractADRDescription(content),
224
- source: 'adr',
225
- file: relativePath,
226
- tags: ['adr', ...this.extractTags(content)]
227
- });
228
- }
229
- } catch {
230
- // Skip files that can't be read
231
- }
232
- }
233
- } catch {
234
- // Glob failed
235
- }
236
- }
237
-
238
- return decisions;
239
- }
240
-
241
- private extractTitle(text: string): string {
242
- // Clean up the text to make a good title
243
- let title = text
244
- .replace(/^(?:feat|fix|refactor|perf|build|ci|docs|style|test|chore)\([^)]*\):\s*/i, '')
245
- .replace(/^(?:DECISION|ARCHITECTURE|ADR|WHY|NOTE|IMPORTANT):\s*/i, '')
246
- .trim();
247
-
248
- // Capitalize first letter
249
- if (title.length > 0) {
250
- title = title.charAt(0).toUpperCase() + title.slice(1);
251
- }
252
-
253
- // Truncate if too long
254
- if (title.length > 100) {
255
- title = title.slice(0, 97) + '...';
256
- }
257
-
258
- return title;
259
- }
260
-
261
- private extractTags(text: string): string[] {
262
- const tags: string[] = [];
263
- const lowerText = text.toLowerCase();
264
-
265
- for (const [tag, keywords] of Object.entries(TAG_KEYWORDS)) {
266
- for (const keyword of keywords) {
267
- if (lowerText.includes(keyword)) {
268
- tags.push(tag);
269
- break;
270
- }
271
- }
272
- }
273
-
274
- return [...new Set(tags)]; // Deduplicate
275
- }
276
-
277
- private isArchitecturallySignificant(text: string): boolean {
278
- const significantKeywords = [
279
- 'architecture', 'design', 'pattern', 'implement', 'refactor',
280
- 'migrate', 'switch', 'replace', 'instead', 'because', 'why',
281
- 'decision', 'chose', 'choose', 'use', 'adopt', 'introduce'
282
- ];
283
-
284
- const lowerText = text.toLowerCase();
285
- return significantKeywords.some(keyword => lowerText.includes(keyword));
286
- }
287
-
288
- private extractADRTitle(content: string): string {
289
- // Look for # Title or title: in YAML frontmatter
290
- const titleMatch = content.match(/^#\s+(.+)$/m) ||
291
- content.match(/^title:\s*["']?([^"'\n]+)["']?$/m);
292
-
293
- return titleMatch ? titleMatch[1]!.trim() : '';
294
- }
295
-
296
- private extractADRStatus(content: string): string {
297
- const statusMatch = content.match(/^(?:##\s*)?status:\s*["']?(\w+)["']?$/im);
298
- return statusMatch ? statusMatch[1]!.toLowerCase() : 'accepted';
299
- }
300
-
301
- private extractADRDescription(content: string): string {
302
- // Try to find context or decision section
303
- const contextMatch = content.match(/##\s*Context\s*\n([\s\S]*?)(?=##|$)/i);
304
- const decisionMatch = content.match(/##\s*Decision\s*\n([\s\S]*?)(?=##|$)/i);
305
-
306
- let description = '';
307
- if (contextMatch) {
308
- description += contextMatch[1]!.trim();
309
- }
310
- if (decisionMatch) {
311
- description += (description ? '\n\n' : '') + decisionMatch[1]!.trim();
312
- }
313
-
314
- if (!description) {
315
- // Fallback: use first paragraph after title
316
- const lines = content.split('\n');
317
- const startIndex = lines.findIndex(l => l.startsWith('#'));
318
- if (startIndex >= 0) {
319
- const remaining = lines.slice(startIndex + 1).join('\n').trim();
320
- const firstPara = remaining.split(/\n\n/)[0];
321
- description = firstPara || '';
322
- }
323
- }
324
-
325
- return description.slice(0, 1000);
326
- }
327
-
328
- // Convert extracted decisions to proper Decision objects
329
- toDecisions(extracted: ExtractedDecision[]): Decision[] {
330
- return extracted.map(e => ({
331
- id: randomUUID(),
332
- title: e.title,
333
- description: `${e.description}\n\n[Source: ${e.source}${e.file ? `, ${e.file}` : ''}${e.line ? `:${e.line}` : ''}]`,
334
- files: e.file ? [e.file] : [],
335
- tags: e.tags,
336
- createdAt: new Date()
337
- }));
338
- }
339
- }
@@ -1,69 +0,0 @@
1
- import { randomUUID } from 'crypto';
2
- import { Tier1Storage } from '../storage/tier1.js';
3
- import { Tier2Storage } from '../storage/tier2.js';
4
- import { EmbeddingGenerator } from '../indexing/embeddings.js';
5
- import type { Decision } from '../types/index.js';
6
-
7
- export class DecisionTracker {
8
- private tier1: Tier1Storage;
9
- private tier2: Tier2Storage;
10
- private embeddingGenerator: EmbeddingGenerator;
11
-
12
- constructor(
13
- tier1: Tier1Storage,
14
- tier2: Tier2Storage,
15
- embeddingGenerator: EmbeddingGenerator
16
- ) {
17
- this.tier1 = tier1;
18
- this.tier2 = tier2;
19
- this.embeddingGenerator = embeddingGenerator;
20
- }
21
-
22
- async recordDecision(
23
- title: string,
24
- description: string,
25
- files: string[] = [],
26
- tags: string[] = []
27
- ): Promise<Decision> {
28
- const decision: Decision = {
29
- id: randomUUID(),
30
- title,
31
- description,
32
- files,
33
- tags,
34
- createdAt: new Date()
35
- };
36
-
37
- // Store in Tier 1 for immediate access
38
- this.tier1.addDecision(decision);
39
-
40
- // Generate embedding for semantic search
41
- const textToEmbed = `${title}\n${description}\n${tags.join(' ')}`;
42
- const embedding = await this.embeddingGenerator.embed(textToEmbed);
43
-
44
- // Store in Tier 2 for persistence
45
- this.tier2.upsertDecision(decision, embedding);
46
-
47
- return decision;
48
- }
49
-
50
- getRecentDecisions(limit: number = 10): Decision[] {
51
- // First try Tier 1 (faster)
52
- const tier1Decisions = this.tier1.getRecentDecisions(limit);
53
- if (tier1Decisions.length >= limit) {
54
- return tier1Decisions;
55
- }
56
-
57
- // Fall back to Tier 2 for more
58
- return this.tier2.getRecentDecisions(limit);
59
- }
60
-
61
- getDecision(id: string): Decision | null {
62
- return this.tier2.getDecision(id);
63
- }
64
-
65
- async searchDecisions(query: string, limit: number = 5): Promise<Decision[]> {
66
- const queryEmbedding = await this.embeddingGenerator.embed(query);
67
- return this.tier2.searchDecisions(queryEmbedding, limit);
68
- }
69
- }