tlc-claude-code 2.1.0 → 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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Plan Reviewer
3
+ * Validates .planning/phases/{N}-PLAN.md files for structure, scope, and completeness.
4
+ */
5
+
6
+ /**
7
+ * Parse a TLC plan markdown file into a structured object.
8
+ *
9
+ * @param {string} content - Raw markdown content of the plan file.
10
+ * @returns {{ tasks: Array, prerequisites: string[], dependencies: string, rawContent: string }}
11
+ */
12
+ export function parsePlan(content) {
13
+ const rawContent = content;
14
+ const tasks = [];
15
+ const prerequisites = [];
16
+ let dependencies = '';
17
+
18
+ // --- Prerequisites section (also accept "Context" with "Depends on" lines) ---
19
+ const prereqMatch = content.match(/^##\s+(?:Prerequisites|Context)\s*\n([\s\S]*?)(?=^##\s|\Z)/m);
20
+ if (prereqMatch) {
21
+ const block = prereqMatch[1];
22
+ const lines = block.split('\n');
23
+ for (const line of lines) {
24
+ const trimmed = line.trim();
25
+ if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
26
+ const item = trimmed.replace(/^[-*]\s*/, '').trim();
27
+ if (item) prerequisites.push(item);
28
+ }
29
+ }
30
+ }
31
+
32
+ // --- Dependencies section ---
33
+ const depsMatch = content.match(/^##\s+Dependencies\s*\n([\s\S]*?)(?=^##\s|$)/m);
34
+ if (depsMatch) {
35
+ dependencies = depsMatch[1].trim();
36
+ }
37
+
38
+ // --- Tasks section ---
39
+ // Split on ### Task N: headings
40
+ const taskHeadingRegex = /^###\s+Task\s+\d+:\s+(.+?)(?:\s+\[.*?\])?\s*$/m;
41
+ // Use a global split to get all task blocks
42
+ const taskBlocks = [];
43
+ const taskHeadingGlobal = /^###\s+Task\s+[\d.]+:\s+(.*?)(?:\s+\[.*?\])?\s*$/gm;
44
+ let match;
45
+ const headingPositions = [];
46
+
47
+ while ((match = taskHeadingGlobal.exec(content)) !== null) {
48
+ headingPositions.push({ index: match.index, title: match[1].trim(), end: match.index + match[0].length });
49
+ }
50
+
51
+ for (let i = 0; i < headingPositions.length; i++) {
52
+ const start = headingPositions[i].end;
53
+ const end = i + 1 < headingPositions.length ? headingPositions[i + 1].index : content.length;
54
+ taskBlocks.push({ title: headingPositions[i].title, body: content.slice(start, end) });
55
+ }
56
+
57
+ for (const { title, body } of taskBlocks) {
58
+ // Goal
59
+ const goalMatch = body.match(/^\*\*Goal:\*\*\s*(.+)$/m);
60
+ const goal = goalMatch ? goalMatch[1].trim() : '';
61
+
62
+ // Helper: extract a named section from the task body.
63
+ // Stops at the next **SectionName:** heading or end of body.
64
+ const extractSection = (sectionName) => {
65
+ const pattern = new RegExp(`\\*\\*${sectionName}:\\*\\*\\s*\\n([\\s\\S]*?)(?=\\*\\*[A-Za-z]|$)`);
66
+ const m = body.match(pattern);
67
+ return m ? m[1] : null;
68
+ };
69
+
70
+ // Files
71
+ const files = [];
72
+ const filesBlock = extractSection('Files');
73
+ if (filesBlock) {
74
+ for (const line of filesBlock.split('\n')) {
75
+ const trimmed = line.trim();
76
+ if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
77
+ const file = trimmed.replace(/^[-*]\s*/, '').trim();
78
+ if (file) files.push(file);
79
+ }
80
+ }
81
+ }
82
+
83
+ // Acceptance Criteria
84
+ const criteria = [];
85
+ const criteriaBlock = extractSection('Acceptance Criteria');
86
+ if (criteriaBlock) {
87
+ for (const line of criteriaBlock.split('\n')) {
88
+ const trimmed = line.trim();
89
+ // Match "- [ ] text" or "- [x] text" checkboxes
90
+ const cbMatch = trimmed.match(/^[-*]\s+\[[ xX]\]\s+(.+)$/);
91
+ if (cbMatch) {
92
+ criteria.push(cbMatch[1].trim());
93
+ }
94
+ }
95
+ }
96
+
97
+ // Test Cases
98
+ const testCases = [];
99
+ const testCasesBlock = extractSection('Test Cases');
100
+ if (testCasesBlock) {
101
+ for (const line of testCasesBlock.split('\n')) {
102
+ const trimmed = line.trim();
103
+ if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
104
+ const tc = trimmed.replace(/^[-*]\s*/, '').trim();
105
+ if (tc) testCases.push(tc);
106
+ }
107
+ }
108
+ }
109
+
110
+ tasks.push({ title, goal, files, criteria, testCases });
111
+ }
112
+
113
+ return { tasks, prerequisites, dependencies, rawContent };
114
+ }
115
+
116
+ /**
117
+ * Validate the structural completeness of a parsed plan.
118
+ * Flags tasks missing acceptance criteria or test cases.
119
+ *
120
+ * @param {{ tasks: Array }} plan
121
+ * @returns {Array<{ task: string, message: string }>}
122
+ */
123
+ export function validateStructure(plan) {
124
+ const issues = [];
125
+ for (const task of plan.tasks) {
126
+ if (!task.criteria || task.criteria.length === 0) {
127
+ issues.push({ task: task.title, message: `Task "${task.title}" is missing acceptance criteria` });
128
+ }
129
+ if (!task.testCases || task.testCases.length === 0) {
130
+ issues.push({ task: task.title, message: `Task "${task.title}" is missing test cases` });
131
+ }
132
+ }
133
+ return issues;
134
+ }
135
+
136
+ // Words that indicate a vague/broad task title
137
+ const VAGUE_WORDS = ['system', 'entire', 'all', 'everything', 'complete', 'whole', 'full'];
138
+
139
+ /**
140
+ * Validate the scope of tasks in a parsed plan.
141
+ * Flags vague task titles and tasks with no files listed.
142
+ *
143
+ * @param {{ tasks: Array }} plan
144
+ * @returns {Array<{ task: string, message: string }>}
145
+ */
146
+ export function validateScope(plan) {
147
+ const issues = [];
148
+ for (const task of plan.tasks) {
149
+ // Check for vague/overly-broad titles
150
+ const titleLower = task.title.toLowerCase();
151
+ const vagueFound = VAGUE_WORDS.some(w => titleLower.includes(w));
152
+ if (vagueFound) {
153
+ issues.push({
154
+ task: task.title,
155
+ message: `Task "${task.title}" title is too vague or broad — consider narrowing the scope`,
156
+ });
157
+ }
158
+
159
+ // Check for missing files
160
+ if (!task.files || task.files.length === 0) {
161
+ issues.push({
162
+ task: task.title,
163
+ message: `Task "${task.title}" has no files listed`,
164
+ });
165
+ }
166
+ }
167
+ return issues;
168
+ }
169
+
170
+ /**
171
+ * Validate the architecture of a parsed plan.
172
+ * Flags files with line-count estimates exceeding 1000 lines.
173
+ *
174
+ * @param {{ tasks: Array }} plan
175
+ * @returns {Array<{ task: string, message: string }>}
176
+ */
177
+ export function validateArchitecture(plan) {
178
+ const issues = [];
179
+ // Match patterns like "(estimated: 1500 lines)" or "(estimated ~1500 lines)"
180
+ const overSizedPattern = /estimated[^)]*?(\d{4,})\s*lines/i;
181
+
182
+ for (const task of plan.tasks) {
183
+ for (const file of task.files) {
184
+ const match = file.match(overSizedPattern);
185
+ if (match) {
186
+ const lineCount = parseInt(match[1], 10);
187
+ if (lineCount > 1000) {
188
+ issues.push({
189
+ task: task.title,
190
+ message: `Task "${task.title}" plans a file larger than 1000 lines (${lineCount} lines estimated): ${file}`,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ // Also check task goal for large estimates
196
+ if (task.goal) {
197
+ const goalMatch = task.goal.match(overSizedPattern);
198
+ if (goalMatch) {
199
+ const lineCount = parseInt(goalMatch[1], 10);
200
+ if (lineCount > 1000) {
201
+ issues.push({
202
+ task: task.title,
203
+ message: `Task "${task.title}" goal describes a file larger than 1000 lines (${lineCount} lines estimated)`,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return issues;
210
+ }
211
+
212
+ /**
213
+ * Validate the completeness of a parsed plan.
214
+ * Flags plans with no prerequisites section.
215
+ *
216
+ * @param {{ prerequisites: string[] }} plan
217
+ * @returns {Array<{ task: string, message: string }>}
218
+ */
219
+ export function validateCompleteness(plan) {
220
+ const issues = [];
221
+ if (!plan.prerequisites || plan.prerequisites.length === 0) {
222
+ issues.push({
223
+ task: null,
224
+ message: 'Plan is missing a prerequisites section — add a ## Prerequisites block listing prior phases',
225
+ });
226
+ }
227
+ return issues;
228
+ }
229
+
230
+ /**
231
+ * Generate a review prompt string for LLM-based plan review.
232
+ *
233
+ * @param {{ tasks: Array, rawContent: string }} plan
234
+ * @param {{ projectName?: string, techStack?: string }} context
235
+ * @param {{ maxLines?: number }} [options]
236
+ * @returns {string}
237
+ */
238
+ export function generateReviewPrompt(plan, context = {}, options = {}) {
239
+ const projectName = context.projectName || 'Unknown Project';
240
+ const techStack = context.techStack ? `\nTech stack: ${context.techStack}` : '';
241
+
242
+ const taskList = plan.tasks.map((t, i) => ` ${i + 1}. ${t.title}`).join('\n');
243
+
244
+ let planContent = plan.rawContent || '';
245
+ if (options.maxLines && typeof options.maxLines === 'number') {
246
+ const lines = planContent.split('\n');
247
+ if (lines.length > options.maxLines) {
248
+ planContent = lines.slice(0, options.maxLines).join('\n') + '\n[... truncated ...]';
249
+ }
250
+ }
251
+
252
+ return `You are reviewing a TLC phase plan for the project: ${projectName}.${techStack}
253
+
254
+ Tasks in this plan:
255
+ ${taskList}
256
+
257
+ Please review the following plan for structure, scope, architecture, and completeness:
258
+
259
+ ${planContent}`;
260
+ }
@@ -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
+ }