tlc-claude-code 2.0.1 → 2.2.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/.claude/agents/builder.md +144 -0
- package/.claude/agents/planner.md +143 -0
- package/.claude/agents/reviewer.md +160 -0
- package/.claude/commands/tlc/build.md +4 -0
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/review-plan.md +363 -0
- package/.claude/commands/tlc/review.md +172 -57
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +13 -0
- package/bin/install.js +268 -2
- package/bin/postinstall.js +102 -24
- package/bin/setup-autoupdate.js +206 -0
- package/bin/setup-autoupdate.test.js +124 -0
- package/bin/tlc.js +0 -0
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +4 -2
- package/scripts/project-docs.js +1 -1
- package/server/index.js +228 -2
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/cost-tracker.test.js +49 -12
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +3 -1
- package/server/lib/memory-api.test.js +3 -5
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +69 -4
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +42 -4
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/orchestration/agent-dispatcher.js +114 -0
- package/server/lib/orchestration/agent-dispatcher.test.js +110 -0
- package/server/lib/orchestration/orchestrator.js +130 -0
- package/server/lib/orchestration/orchestrator.test.js +192 -0
- package/server/lib/orchestration/tmux-manager.js +101 -0
- package/server/lib/orchestration/tmux-manager.test.js +109 -0
- package/server/lib/orchestration/worktree-manager.js +132 -0
- package/server/lib/orchestration/worktree-manager.test.js +129 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +37 -2
- package/server/lib/project-scanner.test.js +152 -0
- package/server/lib/remember-command.js +2 -0
- package/server/lib/remember-command.test.js +23 -0
- package/server/lib/review/plan-reviewer.js +260 -0
- package/server/lib/review/plan-reviewer.test.js +269 -0
- package/server/lib/review/review-schemas.js +173 -0
- package/server/lib/review/review-schemas.test.js +152 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +1 -1
- package/server/lib/semantic-recall.test.js +17 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +182 -1
- package/server/lib/workspace-api.test.js +474 -0
- package/server/package-lock.json +737 -0
- package/server/package.json +3 -0
- package/server/setup.sh +271 -271
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
- package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parsePlan,
|
|
4
|
+
validateStructure,
|
|
5
|
+
validateScope,
|
|
6
|
+
validateArchitecture,
|
|
7
|
+
validateCompleteness,
|
|
8
|
+
generateReviewPrompt,
|
|
9
|
+
} from './plan-reviewer.js';
|
|
10
|
+
|
|
11
|
+
const WELL_FORMED_PLAN = `# Phase 42: User Auth — Plan
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
User authentication with JWT tokens.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- [x] Phase 41: Database setup
|
|
20
|
+
|
|
21
|
+
## Tasks
|
|
22
|
+
|
|
23
|
+
### Task 1: Create user schema [ ]
|
|
24
|
+
|
|
25
|
+
**Goal:** Define database schema for users table
|
|
26
|
+
|
|
27
|
+
**Files:**
|
|
28
|
+
- src/modules/user/user.repository.js
|
|
29
|
+
- src/modules/user/user.repository.test.js
|
|
30
|
+
|
|
31
|
+
**Acceptance Criteria:**
|
|
32
|
+
- [ ] Schema has id, email, passwordHash, createdAt
|
|
33
|
+
- [ ] Email is unique
|
|
34
|
+
|
|
35
|
+
**Test Cases:**
|
|
36
|
+
- Schema validates correct user data
|
|
37
|
+
- Schema rejects duplicate emails
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Task 2: Login endpoint [ ]
|
|
42
|
+
|
|
43
|
+
**Goal:** POST /api/auth/login
|
|
44
|
+
|
|
45
|
+
**Files:**
|
|
46
|
+
- src/modules/auth/login.js
|
|
47
|
+
- src/modules/auth/login.test.js
|
|
48
|
+
|
|
49
|
+
**Acceptance Criteria:**
|
|
50
|
+
- [ ] Validates email/password
|
|
51
|
+
- [ ] Returns JWT token
|
|
52
|
+
|
|
53
|
+
**Test Cases:**
|
|
54
|
+
- Valid credentials return token
|
|
55
|
+
- Invalid password returns 401
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
Task 2 depends on Task 1
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const PLAN_MISSING_CRITERIA = `# Phase 43: Something
|
|
63
|
+
|
|
64
|
+
## Tasks
|
|
65
|
+
|
|
66
|
+
### Task 1: Do something [ ]
|
|
67
|
+
|
|
68
|
+
**Goal:** Build the thing
|
|
69
|
+
|
|
70
|
+
**Files:**
|
|
71
|
+
- src/thing.js
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const PLAN_VAGUE_TASK = `# Phase 44: Auth
|
|
75
|
+
|
|
76
|
+
## Prerequisites
|
|
77
|
+
|
|
78
|
+
- [x] Phase 43
|
|
79
|
+
|
|
80
|
+
## Tasks
|
|
81
|
+
|
|
82
|
+
### Task 1: Build auth system [ ]
|
|
83
|
+
|
|
84
|
+
**Goal:** Build the entire authentication system
|
|
85
|
+
|
|
86
|
+
**Files:**
|
|
87
|
+
- src/auth.js
|
|
88
|
+
|
|
89
|
+
**Acceptance Criteria:**
|
|
90
|
+
- [ ] Auth works
|
|
91
|
+
|
|
92
|
+
**Test Cases:**
|
|
93
|
+
- Auth works correctly
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const PLAN_OVERSIZED_FILE = `# Phase 45: Big Module
|
|
97
|
+
|
|
98
|
+
## Prerequisites
|
|
99
|
+
|
|
100
|
+
- [x] Phase 44
|
|
101
|
+
|
|
102
|
+
## Tasks
|
|
103
|
+
|
|
104
|
+
### Task 1: Create mega module [ ]
|
|
105
|
+
|
|
106
|
+
**Goal:** Build a massive module (estimated ~1500 lines)
|
|
107
|
+
|
|
108
|
+
**Files:**
|
|
109
|
+
- src/mega-module.js (estimated: 1500 lines)
|
|
110
|
+
|
|
111
|
+
**Acceptance Criteria:**
|
|
112
|
+
- [ ] Module works
|
|
113
|
+
|
|
114
|
+
**Test Cases:**
|
|
115
|
+
- Module functions correctly
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
describe('plan-reviewer', () => {
|
|
119
|
+
describe('parsePlan', () => {
|
|
120
|
+
it('extracts tasks from well-formed plan', () => {
|
|
121
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
122
|
+
expect(plan.tasks).toHaveLength(2);
|
|
123
|
+
expect(plan.tasks[0].title).toContain('Create user schema');
|
|
124
|
+
expect(plan.tasks[1].title).toContain('Login endpoint');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('extracts acceptance criteria per task', () => {
|
|
128
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
129
|
+
expect(plan.tasks[0].criteria.length).toBeGreaterThanOrEqual(2);
|
|
130
|
+
expect(plan.tasks[0].criteria[0]).toContain('id, email');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('extracts file lists per task', () => {
|
|
134
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
135
|
+
expect(plan.tasks[0].files).toHaveLength(2);
|
|
136
|
+
expect(plan.tasks[0].files[0]).toContain('user.repository.js');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('extracts test cases per task', () => {
|
|
140
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
141
|
+
expect(plan.tasks[0].testCases.length).toBeGreaterThanOrEqual(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('extracts dependencies section', () => {
|
|
145
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
146
|
+
expect(plan.dependencies).toContain('Task 2 depends on Task 1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('handles plan with no tasks', () => {
|
|
150
|
+
const plan = parsePlan('# Empty Plan\n\nNo tasks here.');
|
|
151
|
+
expect(plan.tasks).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('extracts prerequisites', () => {
|
|
155
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
156
|
+
expect(plan.prerequisites.length).toBeGreaterThanOrEqual(1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('validateStructure', () => {
|
|
161
|
+
it('passes for well-formed plan', () => {
|
|
162
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
163
|
+
const issues = validateStructure(plan);
|
|
164
|
+
expect(issues).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('flags missing acceptance criteria', () => {
|
|
168
|
+
const plan = parsePlan(PLAN_MISSING_CRITERIA);
|
|
169
|
+
const issues = validateStructure(plan);
|
|
170
|
+
expect(issues.some(i => i.message.toLowerCase().includes('criteria'))).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('flags missing test cases', () => {
|
|
174
|
+
const plan = parsePlan(PLAN_MISSING_CRITERIA);
|
|
175
|
+
const issues = validateStructure(plan);
|
|
176
|
+
expect(issues.some(i => i.message.toLowerCase().includes('test'))).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('validateScope', () => {
|
|
181
|
+
it('passes for properly scoped tasks', () => {
|
|
182
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
183
|
+
const issues = validateScope(plan);
|
|
184
|
+
expect(issues).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('flags vague task title', () => {
|
|
188
|
+
const plan = parsePlan(PLAN_VAGUE_TASK);
|
|
189
|
+
const issues = validateScope(plan);
|
|
190
|
+
expect(issues.some(i => i.message.toLowerCase().includes('vague') || i.message.toLowerCase().includes('broad'))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('flags task with no files listed', () => {
|
|
194
|
+
const noFiles = `# Phase 50: X
|
|
195
|
+
|
|
196
|
+
## Prerequisites
|
|
197
|
+
|
|
198
|
+
- [x] Phase 49
|
|
199
|
+
|
|
200
|
+
## Tasks
|
|
201
|
+
|
|
202
|
+
### Task 1: Do thing [ ]
|
|
203
|
+
|
|
204
|
+
**Goal:** Do the thing
|
|
205
|
+
|
|
206
|
+
**Acceptance Criteria:**
|
|
207
|
+
- [ ] Thing is done
|
|
208
|
+
|
|
209
|
+
**Test Cases:**
|
|
210
|
+
- Thing works
|
|
211
|
+
`;
|
|
212
|
+
const plan = parsePlan(noFiles);
|
|
213
|
+
const issues = validateScope(plan);
|
|
214
|
+
expect(issues.some(i => i.message.toLowerCase().includes('file'))).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('validateArchitecture', () => {
|
|
219
|
+
it('passes for normal-sized files', () => {
|
|
220
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
221
|
+
const issues = validateArchitecture(plan);
|
|
222
|
+
expect(issues).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('flags planned >1000 line file', () => {
|
|
226
|
+
const plan = parsePlan(PLAN_OVERSIZED_FILE);
|
|
227
|
+
const issues = validateArchitecture(plan);
|
|
228
|
+
expect(issues.some(i => i.message.toLowerCase().includes('1000') || i.message.toLowerCase().includes('large'))).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('validateCompleteness', () => {
|
|
233
|
+
it('passes for complete plan', () => {
|
|
234
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
235
|
+
const issues = validateCompleteness(plan);
|
|
236
|
+
expect(issues).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('flags missing prerequisites', () => {
|
|
240
|
+
const plan = parsePlan(PLAN_MISSING_CRITERIA);
|
|
241
|
+
const issues = validateCompleteness(plan);
|
|
242
|
+
expect(issues.some(i => i.message.toLowerCase().includes('prerequisite'))).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('generateReviewPrompt', () => {
|
|
247
|
+
it('includes project context', () => {
|
|
248
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
249
|
+
const prompt = generateReviewPrompt(plan, { projectName: 'MyApp', techStack: 'Node.js' });
|
|
250
|
+
expect(prompt).toContain('MyApp');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('includes plan content', () => {
|
|
254
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
255
|
+
const prompt = generateReviewPrompt(plan, { projectName: 'MyApp' });
|
|
256
|
+
expect(prompt).toContain('Create user schema');
|
|
257
|
+
expect(prompt).toContain('Login endpoint');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('truncates large plans for external providers', () => {
|
|
261
|
+
const longContent = 'x\n'.repeat(1000);
|
|
262
|
+
const plan = parsePlan(WELL_FORMED_PLAN);
|
|
263
|
+
plan.rawContent = longContent;
|
|
264
|
+
const prompt = generateReviewPrompt(plan, { projectName: 'MyApp' }, { maxLines: 500 });
|
|
265
|
+
const lines = prompt.split('\n').length;
|
|
266
|
+
expect(lines).toBeLessThanOrEqual(600); // 500 + prompt template overhead
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Output Schemas
|
|
3
|
+
* JSON schemas for structured output from Claude and Codex review commands.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {string[]} Valid severity levels in descending order of urgency. */
|
|
7
|
+
export const SEVERITY_LEVELS = ['critical', 'high', 'medium', 'low'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal schema descriptor for code review output.
|
|
11
|
+
* Describes the shape expected from an LLM code review response.
|
|
12
|
+
* @type {object}
|
|
13
|
+
*/
|
|
14
|
+
export const codeReviewSchema = {
|
|
15
|
+
name: 'codeReview',
|
|
16
|
+
required: ['verdict', 'score', 'summary', 'issues'],
|
|
17
|
+
verdicts: ['APPROVED', 'CHANGES_REQUESTED'],
|
|
18
|
+
issueRequired: ['file', 'line', 'severity', 'category', 'message', 'suggestion'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Internal schema descriptor for plan review output.
|
|
23
|
+
* Describes the shape expected from an LLM plan review response.
|
|
24
|
+
* @type {object}
|
|
25
|
+
*/
|
|
26
|
+
export const planReviewSchema = {
|
|
27
|
+
name: 'planReview',
|
|
28
|
+
required: ['verdict', 'structureIssues', 'scopeIssues', 'suggestions'],
|
|
29
|
+
verdicts: ['APPROVED', 'CHANGES_REQUESTED'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validates a review output object against a schema descriptor.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} output - The review output to validate.
|
|
36
|
+
* @param {object} schema - The schema descriptor (codeReviewSchema or planReviewSchema).
|
|
37
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
38
|
+
*/
|
|
39
|
+
export function validateReviewOutput(output, schema) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
|
|
42
|
+
// Check top-level required fields
|
|
43
|
+
for (const field of schema.required) {
|
|
44
|
+
if (!(field in output)) {
|
|
45
|
+
errors.push(`Missing required field: ${field}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate verdict value if present
|
|
50
|
+
if ('verdict' in output && schema.verdicts && !schema.verdicts.includes(output.verdict)) {
|
|
51
|
+
errors.push(`Invalid verdict: "${output.verdict}". Must be one of: ${schema.verdicts.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For code review schema: validate each issue
|
|
55
|
+
if (schema.issueRequired && Array.isArray(output.issues)) {
|
|
56
|
+
output.issues.forEach((issue, idx) => {
|
|
57
|
+
for (const field of schema.issueRequired) {
|
|
58
|
+
if (!(field in issue)) {
|
|
59
|
+
errors.push(`Issue[${idx}] missing required field: ${field}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if ('severity' in issue && !SEVERITY_LEVELS.includes(issue.severity)) {
|
|
63
|
+
errors.push(`Issue[${idx}] has invalid severity: "${issue.severity}". Must be one of: ${SEVERITY_LEVELS.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { valid: errors.length === 0, errors };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parses a freeform markdown review text and extracts structured data.
|
|
73
|
+
*
|
|
74
|
+
* Verdict is detected from lines like `Verdict: APPROVED` or `Verdict: CHANGES_REQUESTED`.
|
|
75
|
+
* Issues are detected from bullet lines like:
|
|
76
|
+
* `- src/auth.js:42 [high] Hardcoded password`
|
|
77
|
+
*
|
|
78
|
+
* @param {string} text - Freeform markdown review text.
|
|
79
|
+
* @returns {{ verdict: string, issues: Array<{file: string, line: number, severity: string, message: string}> }}
|
|
80
|
+
*/
|
|
81
|
+
export function parseMarkdownReview(text) {
|
|
82
|
+
let verdict = 'UNKNOWN';
|
|
83
|
+
const issues = [];
|
|
84
|
+
|
|
85
|
+
// Match "Verdict: APPROVED" or "Verdict: CHANGES_REQUESTED" (case-insensitive label)
|
|
86
|
+
const verdictMatch = text.match(/verdict\s*:\s*(APPROVED|CHANGES_REQUESTED)/i);
|
|
87
|
+
if (verdictMatch) {
|
|
88
|
+
verdict = verdictMatch[1].toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Match bullet lines: - path/file.js:linenum [severity] message
|
|
92
|
+
const issueRegex = /^[-*]\s+([\w./\-]+):(\d+)\s+\[(\w+)\]\s+(.+)$/gm;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = issueRegex.exec(text)) !== null) {
|
|
95
|
+
const [, file, lineStr, severity, message] = match;
|
|
96
|
+
issues.push({
|
|
97
|
+
file,
|
|
98
|
+
line: parseInt(lineStr, 10),
|
|
99
|
+
severity: severity.toLowerCase(),
|
|
100
|
+
message: message.trim(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { verdict, issues };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Exports the internal schema descriptors as proper JSON Schema objects.
|
|
109
|
+
*
|
|
110
|
+
* @returns {{ codeReview: object, planReview: object }}
|
|
111
|
+
*/
|
|
112
|
+
export function exportSchemas() {
|
|
113
|
+
const codeReview = {
|
|
114
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
115
|
+
type: 'object',
|
|
116
|
+
required: ['verdict', 'score', 'summary', 'issues'],
|
|
117
|
+
properties: {
|
|
118
|
+
verdict: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
enum: ['APPROVED', 'CHANGES_REQUESTED'],
|
|
121
|
+
},
|
|
122
|
+
score: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
minimum: 0,
|
|
125
|
+
maximum: 100,
|
|
126
|
+
},
|
|
127
|
+
summary: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
},
|
|
130
|
+
issues: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
required: ['file', 'line', 'severity', 'category', 'message', 'suggestion'],
|
|
135
|
+
properties: {
|
|
136
|
+
file: { type: 'string' },
|
|
137
|
+
line: { type: 'integer' },
|
|
138
|
+
severity: { type: 'string', enum: SEVERITY_LEVELS },
|
|
139
|
+
category: { type: 'string' },
|
|
140
|
+
message: { type: 'string' },
|
|
141
|
+
suggestion: { type: 'string' },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const planReview = {
|
|
149
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
150
|
+
type: 'object',
|
|
151
|
+
required: ['verdict', 'structureIssues', 'scopeIssues', 'suggestions'],
|
|
152
|
+
properties: {
|
|
153
|
+
verdict: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
enum: ['APPROVED', 'CHANGES_REQUESTED'],
|
|
156
|
+
},
|
|
157
|
+
structureIssues: {
|
|
158
|
+
type: 'array',
|
|
159
|
+
items: { type: 'string' },
|
|
160
|
+
},
|
|
161
|
+
scopeIssues: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: { type: 'string' },
|
|
164
|
+
},
|
|
165
|
+
suggestions: {
|
|
166
|
+
type: 'array',
|
|
167
|
+
items: { type: 'string' },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return { codeReview, planReview };
|
|
173
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
codeReviewSchema,
|
|
4
|
+
planReviewSchema,
|
|
5
|
+
validateReviewOutput,
|
|
6
|
+
parseMarkdownReview,
|
|
7
|
+
exportSchemas,
|
|
8
|
+
SEVERITY_LEVELS,
|
|
9
|
+
} from './review-schemas.js';
|
|
10
|
+
|
|
11
|
+
describe('review-schemas', () => {
|
|
12
|
+
describe('codeReviewSchema', () => {
|
|
13
|
+
it('validates correct code review output', () => {
|
|
14
|
+
const output = {
|
|
15
|
+
verdict: 'APPROVED',
|
|
16
|
+
score: 85,
|
|
17
|
+
summary: 'Clean implementation with good test coverage.',
|
|
18
|
+
issues: [],
|
|
19
|
+
};
|
|
20
|
+
const result = validateReviewOutput(output, codeReviewSchema);
|
|
21
|
+
expect(result.valid).toBe(true);
|
|
22
|
+
expect(result.errors).toHaveLength(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('validates output with issues', () => {
|
|
26
|
+
const output = {
|
|
27
|
+
verdict: 'CHANGES_REQUESTED',
|
|
28
|
+
score: 40,
|
|
29
|
+
summary: 'Several issues found.',
|
|
30
|
+
issues: [
|
|
31
|
+
{
|
|
32
|
+
file: 'src/auth.js',
|
|
33
|
+
line: 42,
|
|
34
|
+
severity: 'high',
|
|
35
|
+
category: 'security',
|
|
36
|
+
message: 'Hardcoded password detected',
|
|
37
|
+
suggestion: 'Use environment variable instead',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const result = validateReviewOutput(output, codeReviewSchema);
|
|
42
|
+
expect(result.valid).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('rejects output missing verdict', () => {
|
|
46
|
+
const output = { score: 85, summary: 'Good.', issues: [] };
|
|
47
|
+
const result = validateReviewOutput(output, codeReviewSchema);
|
|
48
|
+
expect(result.valid).toBe(false);
|
|
49
|
+
expect(result.errors.some(e => e.includes('verdict'))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects issue with invalid severity', () => {
|
|
53
|
+
const output = {
|
|
54
|
+
verdict: 'APPROVED',
|
|
55
|
+
score: 85,
|
|
56
|
+
summary: 'Good.',
|
|
57
|
+
issues: [
|
|
58
|
+
{ file: 'a.js', line: 1, severity: 'extreme', category: 'bug', message: 'Bad', suggestion: 'Fix' },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
const result = validateReviewOutput(output, codeReviewSchema);
|
|
62
|
+
expect(result.valid).toBe(false);
|
|
63
|
+
expect(result.errors.some(e => e.includes('severity'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('rejects issue without file field', () => {
|
|
67
|
+
const output = {
|
|
68
|
+
verdict: 'APPROVED',
|
|
69
|
+
score: 85,
|
|
70
|
+
summary: 'Good.',
|
|
71
|
+
issues: [
|
|
72
|
+
{ line: 1, severity: 'low', category: 'style', message: 'Naming', suggestion: 'Rename' },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const result = validateReviewOutput(output, codeReviewSchema);
|
|
76
|
+
expect(result.valid).toBe(false);
|
|
77
|
+
expect(result.errors.some(e => e.includes('file'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('planReviewSchema', () => {
|
|
82
|
+
it('validates correct plan review output', () => {
|
|
83
|
+
const output = {
|
|
84
|
+
verdict: 'APPROVED',
|
|
85
|
+
structureIssues: [],
|
|
86
|
+
scopeIssues: [],
|
|
87
|
+
suggestions: ['Consider adding error handling task'],
|
|
88
|
+
};
|
|
89
|
+
const result = validateReviewOutput(output, planReviewSchema);
|
|
90
|
+
expect(result.valid).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('rejects plan review missing verdict', () => {
|
|
94
|
+
const output = { structureIssues: [], scopeIssues: [], suggestions: [] };
|
|
95
|
+
const result = validateReviewOutput(output, planReviewSchema);
|
|
96
|
+
expect(result.valid).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('SEVERITY_LEVELS', () => {
|
|
101
|
+
it('contains all four levels', () => {
|
|
102
|
+
expect(SEVERITY_LEVELS).toEqual(['critical', 'high', 'medium', 'low']);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('parseMarkdownReview', () => {
|
|
107
|
+
it('extracts APPROVED verdict', () => {
|
|
108
|
+
const text = '## Review\n\nLooks good!\n\nVerdict: APPROVED';
|
|
109
|
+
const result = parseMarkdownReview(text);
|
|
110
|
+
expect(result.verdict).toBe('APPROVED');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('extracts CHANGES_REQUESTED verdict', () => {
|
|
114
|
+
const text = 'Issues found.\n\nVerdict: CHANGES_REQUESTED\n\n- Fix X\n- Fix Y';
|
|
115
|
+
const result = parseMarkdownReview(text);
|
|
116
|
+
expect(result.verdict).toBe('CHANGES_REQUESTED');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('extracts issues from bullet points', () => {
|
|
120
|
+
const text = `Verdict: CHANGES_REQUESTED
|
|
121
|
+
|
|
122
|
+
Issues:
|
|
123
|
+
- src/auth.js:42 [high] Hardcoded password
|
|
124
|
+
- src/db.js:10 [medium] Missing error handling`;
|
|
125
|
+
const result = parseMarkdownReview(text);
|
|
126
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(2);
|
|
127
|
+
expect(result.issues[0].file).toBe('src/auth.js');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns UNKNOWN for ambiguous text', () => {
|
|
131
|
+
const text = 'Some random review text without a clear verdict.';
|
|
132
|
+
const result = parseMarkdownReview(text);
|
|
133
|
+
expect(result.verdict).toBe('UNKNOWN');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('exportSchemas', () => {
|
|
138
|
+
it('produces valid JSON Schema for code review', () => {
|
|
139
|
+
const schemas = exportSchemas();
|
|
140
|
+
expect(schemas.codeReview).toHaveProperty('$schema');
|
|
141
|
+
expect(schemas.codeReview).toHaveProperty('type', 'object');
|
|
142
|
+
expect(schemas.codeReview.required).toContain('verdict');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('produces valid JSON Schema for plan review', () => {
|
|
146
|
+
const schemas = exportSchemas();
|
|
147
|
+
expect(schemas.planReview).toHaveProperty('$schema');
|
|
148
|
+
expect(schemas.planReview).toHaveProperty('type', 'object');
|
|
149
|
+
expect(schemas.planReview.required).toContain('verdict');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -352,8 +352,8 @@ describe('crypto-utils', () => {
|
|
|
352
352
|
const avg2 = times2.reduce((a, b) => a + b) / times2.length;
|
|
353
353
|
const ratio = Math.max(avg1, avg2) / Math.min(avg1, avg2);
|
|
354
354
|
|
|
355
|
-
// Times should be
|
|
356
|
-
expect(ratio).toBeLessThan(
|
|
355
|
+
// Times should be similar — relaxed for slow CI runners
|
|
356
|
+
expect(ratio).toBeLessThan(10);
|
|
357
357
|
});
|
|
358
358
|
});
|
|
359
359
|
|
|
@@ -188,7 +188,7 @@ export function createSemanticRecall({ vectorStore, embeddingClient }) {
|
|
|
188
188
|
types,
|
|
189
189
|
} = options;
|
|
190
190
|
|
|
191
|
-
const rawResults = vectorStore.search(
|
|
191
|
+
const rawResults = vectorStore.search(embedding, { limit: limit * 3 });
|
|
192
192
|
|
|
193
193
|
// Score all raw results and attach the raw data for filtering
|
|
194
194
|
let scored = rawResults.map((raw) => {
|
|
@@ -103,6 +103,23 @@ describe('semantic-recall', () => {
|
|
|
103
103
|
expect(mockVectorStore.search).toHaveBeenCalled();
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
it('calls vectorStore.search with (embedding, {limit}) not ({embedding, limit})', async () => {
|
|
107
|
+
const mockResults = [createMockResult()];
|
|
108
|
+
mockVectorStore.search.mockReturnValue(mockResults);
|
|
109
|
+
|
|
110
|
+
const context = { projectId: 'my-project', workspace: '/ws', branch: 'main', touchedFiles: [] };
|
|
111
|
+
await recall.recall('test query', context, { limit: 5 });
|
|
112
|
+
|
|
113
|
+
// The first argument must be the embedding (Float32Array), not an object
|
|
114
|
+
const firstArg = mockVectorStore.search.mock.calls[0][0];
|
|
115
|
+
expect(firstArg).toBeInstanceOf(Float32Array);
|
|
116
|
+
|
|
117
|
+
// The second argument must be the options object with limit
|
|
118
|
+
const secondArg = mockVectorStore.search.mock.calls[0][1];
|
|
119
|
+
expect(secondArg).toHaveProperty('limit');
|
|
120
|
+
expect(typeof secondArg.limit).toBe('number');
|
|
121
|
+
});
|
|
122
|
+
|
|
106
123
|
it('sorts results by combined score highest first', async () => {
|
|
107
124
|
// mem-1: high similarity but old (low recency)
|
|
108
125
|
// mem-2: moderate similarity but recent (high recency)
|