tlc-claude-code 1.2.26 → 1.2.28
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/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { injectMemoryContext, extractMemorySection, MEMORY_SECTION_MARKERS } from './claude-injector.js';
|
|
6
|
+
|
|
7
|
+
describe('claude-injector', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let claudeMdPath;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-injector-test-'));
|
|
13
|
+
claudeMdPath = path.join(testDir, 'CLAUDE.md');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('injectMemoryContext', () => {
|
|
21
|
+
it('creates CLAUDE.md if missing', async () => {
|
|
22
|
+
await injectMemoryContext(testDir, '## Active Memory\n\nTest content');
|
|
23
|
+
|
|
24
|
+
expect(fs.existsSync(claudeMdPath)).toBe(true);
|
|
25
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
26
|
+
expect(content).toContain('Active Memory');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('appends memory section to existing CLAUDE.md', async () => {
|
|
30
|
+
fs.writeFileSync(claudeMdPath, '# Project\n\nExisting content\n');
|
|
31
|
+
|
|
32
|
+
await injectMemoryContext(testDir, '## Preferences\n\n- style: functional');
|
|
33
|
+
|
|
34
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
35
|
+
expect(content).toContain('Existing content');
|
|
36
|
+
expect(content).toContain('Preferences');
|
|
37
|
+
expect(content).toContain('functional');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('replaces existing memory section without duplication', async () => {
|
|
41
|
+
const initial = `# Project
|
|
42
|
+
|
|
43
|
+
${MEMORY_SECTION_MARKERS.START}
|
|
44
|
+
## Old Memory
|
|
45
|
+
|
|
46
|
+
Old content
|
|
47
|
+
${MEMORY_SECTION_MARKERS.END}
|
|
48
|
+
|
|
49
|
+
## Other Section
|
|
50
|
+
`;
|
|
51
|
+
fs.writeFileSync(claudeMdPath, initial);
|
|
52
|
+
|
|
53
|
+
await injectMemoryContext(testDir, '## New Memory\n\nNew content');
|
|
54
|
+
|
|
55
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
56
|
+
expect(content).toContain('New Memory');
|
|
57
|
+
expect(content).toContain('New content');
|
|
58
|
+
expect(content).not.toContain('Old Memory');
|
|
59
|
+
expect(content).not.toContain('Old content');
|
|
60
|
+
expect(content).toContain('Other Section');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves content before and after memory section', async () => {
|
|
64
|
+
const initial = `# Project Title
|
|
65
|
+
|
|
66
|
+
Introduction paragraph.
|
|
67
|
+
|
|
68
|
+
${MEMORY_SECTION_MARKERS.START}
|
|
69
|
+
## Memory
|
|
70
|
+
Content
|
|
71
|
+
${MEMORY_SECTION_MARKERS.END}
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
- /help
|
|
76
|
+
`;
|
|
77
|
+
fs.writeFileSync(claudeMdPath, initial);
|
|
78
|
+
|
|
79
|
+
await injectMemoryContext(testDir, '## Updated Memory\n\nUpdated');
|
|
80
|
+
|
|
81
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
82
|
+
expect(content).toContain('Project Title');
|
|
83
|
+
expect(content).toContain('Introduction paragraph');
|
|
84
|
+
expect(content).toContain('Commands');
|
|
85
|
+
expect(content).toContain('/help');
|
|
86
|
+
expect(content).toContain('Updated Memory');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles empty memory context gracefully', async () => {
|
|
90
|
+
fs.writeFileSync(claudeMdPath, '# Project\n\nContent\n');
|
|
91
|
+
|
|
92
|
+
await injectMemoryContext(testDir, '');
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
95
|
+
expect(content).toContain('Project');
|
|
96
|
+
// Should still have markers even if empty
|
|
97
|
+
expect(content).toContain(MEMORY_SECTION_MARKERS.START);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('adds markers around injected content', async () => {
|
|
101
|
+
await injectMemoryContext(testDir, '## Memory\n\nContent');
|
|
102
|
+
|
|
103
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
104
|
+
expect(content).toContain(MEMORY_SECTION_MARKERS.START);
|
|
105
|
+
expect(content).toContain(MEMORY_SECTION_MARKERS.END);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles CLAUDE.md with only whitespace', async () => {
|
|
109
|
+
fs.writeFileSync(claudeMdPath, ' \n\n \n');
|
|
110
|
+
|
|
111
|
+
await injectMemoryContext(testDir, '## Memory\n\nContent');
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
114
|
+
expect(content).toContain('Memory');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('extractMemorySection', () => {
|
|
119
|
+
it('extracts content between markers', () => {
|
|
120
|
+
const content = `Before
|
|
121
|
+
${MEMORY_SECTION_MARKERS.START}
|
|
122
|
+
## Memory
|
|
123
|
+
Content here
|
|
124
|
+
${MEMORY_SECTION_MARKERS.END}
|
|
125
|
+
After`;
|
|
126
|
+
|
|
127
|
+
const extracted = extractMemorySection(content);
|
|
128
|
+
|
|
129
|
+
expect(extracted).toContain('Memory');
|
|
130
|
+
expect(extracted).toContain('Content here');
|
|
131
|
+
expect(extracted).not.toContain('Before');
|
|
132
|
+
expect(extracted).not.toContain('After');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns null if no memory section', () => {
|
|
136
|
+
const content = '# Just a normal file\n\nNo memory here';
|
|
137
|
+
|
|
138
|
+
const extracted = extractMemorySection(content);
|
|
139
|
+
|
|
140
|
+
expect(extracted).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('returns null for malformed markers', () => {
|
|
144
|
+
const content = `${MEMORY_SECTION_MARKERS.START}
|
|
145
|
+
No end marker`;
|
|
146
|
+
|
|
147
|
+
const extracted = extractMemorySection(content);
|
|
148
|
+
|
|
149
|
+
expect(extracted).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('handles empty section between markers', () => {
|
|
153
|
+
const content = `${MEMORY_SECTION_MARKERS.START}
|
|
154
|
+
${MEMORY_SECTION_MARKERS.END}`;
|
|
155
|
+
|
|
156
|
+
const extracted = extractMemorySection(content);
|
|
157
|
+
|
|
158
|
+
expect(extracted).toBe('');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consensus Engine - Aggregate reviews from multiple models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class ConsensusEngine {
|
|
6
|
+
constructor(adapters, config = {}) {
|
|
7
|
+
this.adapters = adapters;
|
|
8
|
+
this.config = {
|
|
9
|
+
consensusType: 'majority',
|
|
10
|
+
requireMinimum: 2,
|
|
11
|
+
budgetAware: true,
|
|
12
|
+
...config,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run review across all adapters
|
|
18
|
+
* @param {string} code - Code to review
|
|
19
|
+
* @param {Object} context - Review context
|
|
20
|
+
* @returns {Promise<Object>} Aggregated review result
|
|
21
|
+
*/
|
|
22
|
+
async review(code, context = {}) {
|
|
23
|
+
// Filter adapters by budget if configured
|
|
24
|
+
const availableAdapters = this.config.budgetAware
|
|
25
|
+
? this.adapters.filter(a => a.canAfford())
|
|
26
|
+
: this.adapters;
|
|
27
|
+
|
|
28
|
+
// Run all reviews in parallel
|
|
29
|
+
const results = await Promise.allSettled(
|
|
30
|
+
availableAdapters.map(async a => {
|
|
31
|
+
const result = await a.review(code, context);
|
|
32
|
+
return result;
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const reviews = [];
|
|
37
|
+
const warnings = [];
|
|
38
|
+
|
|
39
|
+
results.forEach((result, i) => {
|
|
40
|
+
if (result.status === 'fulfilled') {
|
|
41
|
+
reviews.push(result.value);
|
|
42
|
+
} else {
|
|
43
|
+
warnings.push(`${availableAdapters[i].name} failed: ${result.reason.message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Check minimum requirement
|
|
48
|
+
if (reviews.length < this.config.requireMinimum) {
|
|
49
|
+
throw new Error(`Insufficient reviews: got ${reviews.length}, need ${this.config.requireMinimum}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Calculate consensus
|
|
53
|
+
const consensus = ConsensusEngine.calculateConsensus(reviews, this.config.consensusType);
|
|
54
|
+
const costs = ConsensusEngine.summarizeCosts(reviews);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
reviews,
|
|
58
|
+
warnings,
|
|
59
|
+
consensus,
|
|
60
|
+
costs,
|
|
61
|
+
consensusType: reviews.length === 1 ? 'single-model' : this.config.consensusType,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calculate consensus from reviews
|
|
67
|
+
* @param {Array} reviews - Array of review results
|
|
68
|
+
* @param {string} type - Consensus type ('majority' or 'unanimous')
|
|
69
|
+
* @returns {Object} Consensus result
|
|
70
|
+
*/
|
|
71
|
+
static calculateConsensus(reviews, type = 'majority') {
|
|
72
|
+
const issueMap = new Map();
|
|
73
|
+
|
|
74
|
+
// Collect all issues
|
|
75
|
+
for (const review of reviews) {
|
|
76
|
+
for (const issue of (review.issues || [])) {
|
|
77
|
+
const key = issue.id || ConsensusEngine.hashIssue(issue);
|
|
78
|
+
if (!issueMap.has(key)) {
|
|
79
|
+
issueMap.set(key, { ...issue, votes: 0, voters: [] });
|
|
80
|
+
}
|
|
81
|
+
const entry = issueMap.get(key);
|
|
82
|
+
entry.votes++;
|
|
83
|
+
entry.voters.push(review.model);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Filter by consensus type
|
|
88
|
+
const threshold = type === 'unanimous' ? reviews.length : 1;
|
|
89
|
+
|
|
90
|
+
const consensusIssues = Array.from(issueMap.values())
|
|
91
|
+
.filter(issue => issue.votes >= threshold)
|
|
92
|
+
.map(issue => ({
|
|
93
|
+
...issue,
|
|
94
|
+
confidence: issue.votes / reviews.length,
|
|
95
|
+
}))
|
|
96
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
issues: consensusIssues,
|
|
100
|
+
totalReviews: reviews.length,
|
|
101
|
+
consensusThreshold: threshold,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create hash for issue deduplication
|
|
107
|
+
* @param {Object} issue - Issue object
|
|
108
|
+
* @returns {string} Hash
|
|
109
|
+
*/
|
|
110
|
+
static hashIssue(issue) {
|
|
111
|
+
return `${issue.line || ''}-${issue.message || ''}-${issue.severity || ''}`.toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Summarize costs across reviews
|
|
116
|
+
* @param {Array} reviews - Array of review results
|
|
117
|
+
* @returns {Object} Cost summary
|
|
118
|
+
*/
|
|
119
|
+
static summarizeCosts(reviews) {
|
|
120
|
+
const byModel = {};
|
|
121
|
+
let total = 0;
|
|
122
|
+
|
|
123
|
+
for (const review of reviews) {
|
|
124
|
+
const cost = review.cost || 0;
|
|
125
|
+
byModel[review.model] = cost;
|
|
126
|
+
total += cost;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { byModel, total };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
ConsensusEngine,
|
|
135
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ConsensusEngine } from './consensus-engine.js';
|
|
3
|
+
|
|
4
|
+
// Mock adapters
|
|
5
|
+
const createMockAdapter = (name, issues = []) => ({
|
|
6
|
+
name,
|
|
7
|
+
canAfford: vi.fn(() => true),
|
|
8
|
+
review: vi.fn(() => Promise.resolve({
|
|
9
|
+
issues,
|
|
10
|
+
suggestions: [],
|
|
11
|
+
score: 80,
|
|
12
|
+
model: name,
|
|
13
|
+
tokensUsed: 100,
|
|
14
|
+
cost: 0.01,
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('ConsensusEngine', () => {
|
|
19
|
+
describe('review', () => {
|
|
20
|
+
it('aggregates reviews from multiple models', async () => {
|
|
21
|
+
const adapters = [
|
|
22
|
+
createMockAdapter('claude'),
|
|
23
|
+
createMockAdapter('openai'),
|
|
24
|
+
createMockAdapter('deepseek'),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const engine = new ConsensusEngine(adapters);
|
|
28
|
+
const result = await engine.review('const x = 1;');
|
|
29
|
+
|
|
30
|
+
expect(result.reviews).toHaveLength(3);
|
|
31
|
+
expect(result.reviews.map(r => r.model)).toEqual(['claude', 'openai', 'deepseek']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('runs reviews in parallel', async () => {
|
|
35
|
+
const delay = ms => new Promise(r => setTimeout(r, ms));
|
|
36
|
+
const adapters = [
|
|
37
|
+
{ name: 'a', canAfford: () => true, review: async () => { await delay(50); return { issues: [], suggestions: [], score: 80, model: 'a', tokensUsed: 0, cost: 0 }; } },
|
|
38
|
+
{ name: 'b', canAfford: () => true, review: async () => { await delay(50); return { issues: [], suggestions: [], score: 80, model: 'b', tokensUsed: 0, cost: 0 }; } },
|
|
39
|
+
{ name: 'c', canAfford: () => true, review: async () => { await delay(50); return { issues: [], suggestions: [], score: 80, model: 'c', tokensUsed: 0, cost: 0 }; } },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const engine = new ConsensusEngine(adapters);
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
await engine.review('code');
|
|
45
|
+
const elapsed = Date.now() - start;
|
|
46
|
+
|
|
47
|
+
// Should take ~50ms (parallel), not ~150ms (sequential)
|
|
48
|
+
expect(elapsed).toBeLessThan(120);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles model failures gracefully', async () => {
|
|
52
|
+
const adapters = [
|
|
53
|
+
createMockAdapter('claude'),
|
|
54
|
+
{
|
|
55
|
+
name: 'failing',
|
|
56
|
+
canAfford: () => true,
|
|
57
|
+
review: () => Promise.reject(new Error('API error')),
|
|
58
|
+
},
|
|
59
|
+
createMockAdapter('deepseek'),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const engine = new ConsensusEngine(adapters);
|
|
63
|
+
const result = await engine.review('code');
|
|
64
|
+
|
|
65
|
+
expect(result.reviews).toHaveLength(2);
|
|
66
|
+
expect(result.warnings).toContain('failing failed: API error');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('skips models over budget', async () => {
|
|
70
|
+
const adapters = [
|
|
71
|
+
createMockAdapter('claude'),
|
|
72
|
+
{ ...createMockAdapter('openai'), canAfford: () => false },
|
|
73
|
+
createMockAdapter('deepseek'),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const engine = new ConsensusEngine(adapters, { budgetAware: true });
|
|
77
|
+
const result = await engine.review('code');
|
|
78
|
+
|
|
79
|
+
expect(result.reviews).toHaveLength(2);
|
|
80
|
+
expect(result.reviews.map(r => r.model)).toEqual(['claude', 'deepseek']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws if insufficient reviews', async () => {
|
|
84
|
+
const adapters = [
|
|
85
|
+
{ ...createMockAdapter('claude'), review: () => Promise.reject(new Error('fail')) },
|
|
86
|
+
{ ...createMockAdapter('openai'), review: () => Promise.reject(new Error('fail')) },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const engine = new ConsensusEngine(adapters, { requireMinimum: 2 });
|
|
90
|
+
|
|
91
|
+
await expect(engine.review('code')).rejects.toThrow('Insufficient reviews');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('calculateConsensus', () => {
|
|
96
|
+
it('calculates majority consensus', () => {
|
|
97
|
+
const reviews = [
|
|
98
|
+
{ model: 'claude', issues: [{ id: 'A', severity: 'high' }] },
|
|
99
|
+
{ model: 'openai', issues: [{ id: 'A', severity: 'high' }, { id: 'B', severity: 'low' }] },
|
|
100
|
+
{ model: 'deepseek', issues: [{ id: 'A', severity: 'medium' }] },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const consensus = ConsensusEngine.calculateConsensus(reviews, 'majority');
|
|
104
|
+
|
|
105
|
+
// Issue A flagged by all 3 - confidence 1.0
|
|
106
|
+
const issueA = consensus.issues.find(i => i.id === 'A');
|
|
107
|
+
expect(issueA.confidence).toBe(1.0);
|
|
108
|
+
|
|
109
|
+
// Issue B flagged by 1 of 3 - confidence 0.33
|
|
110
|
+
const issueB = consensus.issues.find(i => i.id === 'B');
|
|
111
|
+
expect(issueB.confidence).toBeCloseTo(0.33, 1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('uses unanimous consensus when configured', () => {
|
|
115
|
+
const reviews = [
|
|
116
|
+
{ model: 'claude', issues: [{ id: 'A' }] },
|
|
117
|
+
{ model: 'openai', issues: [{ id: 'A' }, { id: 'B' }] },
|
|
118
|
+
{ model: 'deepseek', issues: [{ id: 'A' }] },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const consensus = ConsensusEngine.calculateConsensus(reviews, 'unanimous');
|
|
122
|
+
|
|
123
|
+
// Only issue A is unanimous
|
|
124
|
+
expect(consensus.issues).toHaveLength(1);
|
|
125
|
+
expect(consensus.issues[0].id).toBe('A');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('calculates total cost', () => {
|
|
129
|
+
const reviews = [
|
|
130
|
+
{ model: 'claude', cost: 0 },
|
|
131
|
+
{ model: 'openai', cost: 0.12 },
|
|
132
|
+
{ model: 'deepseek', cost: 0.02 },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const result = ConsensusEngine.summarizeCosts(reviews);
|
|
136
|
+
|
|
137
|
+
expect(result.total).toBeCloseTo(0.14, 2);
|
|
138
|
+
expect(result.byModel.openai).toBe(0.12);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('single model fallback', () => {
|
|
143
|
+
it('returns single-model consensus type', async () => {
|
|
144
|
+
const adapters = [createMockAdapter('claude')];
|
|
145
|
+
|
|
146
|
+
const engine = new ConsensusEngine(adapters, { requireMinimum: 1 });
|
|
147
|
+
const result = await engine.review('code');
|
|
148
|
+
|
|
149
|
+
expect(result.consensusType).toBe('single-model');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder - Build session context from memory
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { loadTeamDecisions, loadTeamGotchas, loadPersonalPreferences, loadRecentSessions } = require('./memory-reader.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimate token count from text (rough: ~4 chars per token)
|
|
9
|
+
* @param {string} text - Text to estimate
|
|
10
|
+
* @returns {number} Estimated token count
|
|
11
|
+
*/
|
|
12
|
+
function estimateTokens(text) {
|
|
13
|
+
if (!text) return 0;
|
|
14
|
+
return Math.ceil(text.length / 4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build session context from all memory sources
|
|
19
|
+
* @param {string} projectRoot - Project root directory
|
|
20
|
+
* @param {Object} options - Options
|
|
21
|
+
* @param {number} options.maxTokens - Maximum tokens for context
|
|
22
|
+
* @returns {Promise<string>} Formatted context for CLAUDE.md
|
|
23
|
+
*/
|
|
24
|
+
async function buildSessionContext(projectRoot, options = {}) {
|
|
25
|
+
const { maxTokens = 2000 } = options;
|
|
26
|
+
|
|
27
|
+
const sections = [];
|
|
28
|
+
|
|
29
|
+
// Load all memory
|
|
30
|
+
const [decisions, gotchas, preferences, sessions] = await Promise.all([
|
|
31
|
+
loadTeamDecisions(projectRoot),
|
|
32
|
+
loadTeamGotchas(projectRoot),
|
|
33
|
+
loadPersonalPreferences(projectRoot),
|
|
34
|
+
loadRecentSessions(projectRoot, 10),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Check if any memory exists
|
|
38
|
+
const hasMemory = decisions.length > 0 ||
|
|
39
|
+
gotchas.length > 0 ||
|
|
40
|
+
Object.keys(preferences).length > 0 ||
|
|
41
|
+
sessions.length > 0;
|
|
42
|
+
|
|
43
|
+
if (!hasMemory) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build preferences section
|
|
48
|
+
if (Object.keys(preferences).length > 0) {
|
|
49
|
+
const prefsLines = ['## Preferences', ''];
|
|
50
|
+
for (const [key, value] of Object.entries(preferences)) {
|
|
51
|
+
if (typeof value === 'object') {
|
|
52
|
+
prefsLines.push(`- **${key}:** ${JSON.stringify(value)}`);
|
|
53
|
+
} else {
|
|
54
|
+
prefsLines.push(`- **${key}:** ${value}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
sections.push({ content: prefsLines.join('\n'), priority: 1 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build decisions section (most recent first)
|
|
61
|
+
if (decisions.length > 0) {
|
|
62
|
+
const sortedDecisions = [...decisions].reverse();
|
|
63
|
+
const decisionLines = ['## Recent Decisions', ''];
|
|
64
|
+
for (const d of sortedDecisions.slice(0, 5)) {
|
|
65
|
+
decisionLines.push(`- **${d.title}**: ${d.reasoning?.substring(0, 100) || ''}`);
|
|
66
|
+
}
|
|
67
|
+
sections.push({ content: decisionLines.join('\n'), priority: 2 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build gotchas section
|
|
71
|
+
if (gotchas.length > 0) {
|
|
72
|
+
const gotchaLines = ['## Gotchas', ''];
|
|
73
|
+
for (const g of gotchas.slice(0, 5)) {
|
|
74
|
+
gotchaLines.push(`- **${g.title}**: ${g.issue?.substring(0, 100) || ''}`);
|
|
75
|
+
}
|
|
76
|
+
sections.push({ content: gotchaLines.join('\n'), priority: 3 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build recent activity section
|
|
80
|
+
if (sessions.length > 0) {
|
|
81
|
+
const activityLines = ['## Recent Activity', ''];
|
|
82
|
+
for (const s of sessions.slice(-5)) {
|
|
83
|
+
if (s.content) {
|
|
84
|
+
activityLines.push(`- ${s.type}: ${s.content}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (activityLines.length > 2) {
|
|
88
|
+
sections.push({ content: activityLines.join('\n'), priority: 4 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sort by priority and build final context
|
|
93
|
+
sections.sort((a, b) => a.priority - b.priority);
|
|
94
|
+
|
|
95
|
+
let context = '';
|
|
96
|
+
let currentTokens = 0;
|
|
97
|
+
|
|
98
|
+
for (const section of sections) {
|
|
99
|
+
const sectionTokens = estimateTokens(section.content);
|
|
100
|
+
if (currentTokens + sectionTokens <= maxTokens) {
|
|
101
|
+
context += section.content + '\n\n';
|
|
102
|
+
currentTokens += sectionTokens;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return context.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
buildSessionContext,
|
|
111
|
+
estimateTokens,
|
|
112
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { buildSessionContext, estimateTokens } from './context-builder.js';
|
|
6
|
+
import { initMemoryStructure } from './memory-storage.js';
|
|
7
|
+
import { writeTeamDecision, writeTeamGotcha, writePersonalPreference, appendSessionLog } from './memory-writer.js';
|
|
8
|
+
|
|
9
|
+
describe('context-builder', () => {
|
|
10
|
+
let testDir;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-context-test-'));
|
|
14
|
+
await initMemoryStructure(testDir);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('buildSessionContext', () => {
|
|
22
|
+
it('returns empty context when no memory exists', async () => {
|
|
23
|
+
const context = await buildSessionContext(testDir);
|
|
24
|
+
expect(context).toBe('');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('includes personal preferences', async () => {
|
|
28
|
+
await writePersonalPreference(testDir, 'codeStyle', 'functional');
|
|
29
|
+
await writePersonalPreference(testDir, 'exports', 'named');
|
|
30
|
+
|
|
31
|
+
const context = await buildSessionContext(testDir);
|
|
32
|
+
|
|
33
|
+
expect(context).toContain('functional');
|
|
34
|
+
expect(context).toContain('named');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('includes team decisions', async () => {
|
|
38
|
+
await writeTeamDecision(testDir, {
|
|
39
|
+
title: 'Use Postgres',
|
|
40
|
+
reasoning: 'JSONB support',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const context = await buildSessionContext(testDir);
|
|
44
|
+
|
|
45
|
+
expect(context).toContain('Postgres');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('includes team gotchas', async () => {
|
|
49
|
+
await writeTeamGotcha(testDir, {
|
|
50
|
+
title: 'Auth Warmup',
|
|
51
|
+
issue: 'Service needs 2 seconds',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const context = await buildSessionContext(testDir);
|
|
55
|
+
|
|
56
|
+
expect(context).toContain('Auth Warmup');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('includes recent session activity', async () => {
|
|
60
|
+
await appendSessionLog(testDir, { type: 'decision', content: 'chose REST API' });
|
|
61
|
+
|
|
62
|
+
const context = await buildSessionContext(testDir);
|
|
63
|
+
|
|
64
|
+
expect(context).toContain('REST API');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('limits context to token budget', async () => {
|
|
68
|
+
// Create lots of decisions
|
|
69
|
+
for (let i = 0; i < 50; i++) {
|
|
70
|
+
await writeTeamDecision(testDir, {
|
|
71
|
+
title: `Decision ${i}`,
|
|
72
|
+
reasoning: 'A'.repeat(200), // Long reasoning
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const context = await buildSessionContext(testDir, { maxTokens: 500 });
|
|
77
|
+
const tokens = estimateTokens(context);
|
|
78
|
+
|
|
79
|
+
expect(tokens).toBeLessThanOrEqual(500);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('formats context as markdown', async () => {
|
|
83
|
+
await writeTeamDecision(testDir, { title: 'Use Postgres', reasoning: 'JSONB' });
|
|
84
|
+
await writePersonalPreference(testDir, 'style', 'functional');
|
|
85
|
+
|
|
86
|
+
const context = await buildSessionContext(testDir);
|
|
87
|
+
|
|
88
|
+
expect(context).toContain('## ');
|
|
89
|
+
expect(context).toContain('- ');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('prioritizes recent items', async () => {
|
|
93
|
+
await writeTeamDecision(testDir, { title: 'Old Decision', reasoning: 'old' });
|
|
94
|
+
// Simulate time passing
|
|
95
|
+
await new Promise(r => setTimeout(r, 10));
|
|
96
|
+
await writeTeamDecision(testDir, { title: 'New Decision', reasoning: 'new' });
|
|
97
|
+
|
|
98
|
+
const context = await buildSessionContext(testDir, { maxTokens: 100 });
|
|
99
|
+
|
|
100
|
+
// New decision should appear if space is limited
|
|
101
|
+
expect(context).toContain('New Decision');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('estimateTokens', () => {
|
|
106
|
+
it('estimates tokens from text', () => {
|
|
107
|
+
const text = 'Hello world this is a test';
|
|
108
|
+
const tokens = estimateTokens(text);
|
|
109
|
+
|
|
110
|
+
// Rough estimate: ~1 token per 4 chars
|
|
111
|
+
expect(tokens).toBeGreaterThan(0);
|
|
112
|
+
expect(tokens).toBeLessThan(text.length);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns 0 for empty text', () => {
|
|
116
|
+
expect(estimateTokens('')).toBe(0);
|
|
117
|
+
expect(estimateTokens(null)).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|