specvector 0.1.9 → 0.3.1
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/package.json +2 -4
- package/src/agent/loop.ts +10 -6
- package/src/context/linear.ts +77 -65
- package/src/llm/index.ts +2 -2
- package/src/llm/provider.ts +67 -0
- package/src/review/engine.ts +27 -4
- package/src/review/formatter.ts +38 -1
- package/src/types/review.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specvector",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Context-aware AI code review using Model Context Protocol (MCP)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -44,9 +44,7 @@
|
|
|
44
44
|
"engines": {
|
|
45
45
|
"bun": ">=1.0.0"
|
|
46
46
|
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"@larryhudson/linear-mcp-server": "0.1.4"
|
|
49
|
-
},
|
|
47
|
+
"dependencies": {},
|
|
50
48
|
"devDependencies": {
|
|
51
49
|
"@types/bun": "latest"
|
|
52
50
|
},
|
package/src/agent/loop.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { ok, err } from "../types/result";
|
|
18
18
|
import type { Message, ToolCall } from "../types/llm";
|
|
19
19
|
import type { LLMProvider, LLMError } from "../llm/provider";
|
|
20
|
+
import { withRetry } from "../llm/provider";
|
|
20
21
|
import type {
|
|
21
22
|
Tool,
|
|
22
23
|
AgentConfig,
|
|
@@ -70,12 +71,15 @@ export class AgentLoop {
|
|
|
70
71
|
return err(AgentErrors.timeout());
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
// Get LLM response
|
|
74
|
-
const llmResult = await
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// Get LLM response with retry on transient errors
|
|
75
|
+
const llmResult = await withRetry(
|
|
76
|
+
() => this.provider.chat(
|
|
77
|
+
messages,
|
|
78
|
+
{
|
|
79
|
+
tools: Array.from(this.tools.values()).map(toolToLLMTool),
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
{ maxRetries: 1, delayMs: 2000 }
|
|
79
83
|
);
|
|
80
84
|
|
|
81
85
|
if (!llmResult.ok) {
|
package/src/context/linear.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linear Context Provider - Fetches ticket context for reviews.
|
|
3
3
|
*
|
|
4
|
-
* Extracts ticket ID from branch name/PR and fetches details via
|
|
4
|
+
* Extracts ticket ID from branch name/PR and fetches details via MCP.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createMCPClient } from "../mcp";
|
|
7
8
|
import type { Result } from "../types/result";
|
|
8
9
|
import { ok, err } from "../types/result";
|
|
9
10
|
|
|
@@ -12,12 +13,12 @@ import { ok, err } from "../types/result";
|
|
|
12
13
|
// ============================================================================
|
|
13
14
|
|
|
14
15
|
export interface LinearTicket {
|
|
15
|
-
|
|
16
|
-
title
|
|
17
|
-
description
|
|
18
|
-
state
|
|
19
|
-
priority
|
|
20
|
-
labels
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
state: string;
|
|
20
|
+
priority: number;
|
|
21
|
+
labels: string[];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface LinearContextError {
|
|
@@ -72,14 +73,21 @@ export function extractTicketId(
|
|
|
72
73
|
// Linear Context Fetching
|
|
73
74
|
// ============================================================================
|
|
74
75
|
|
|
76
|
+
/** Extended ticket data from Linear MCP */
|
|
77
|
+
export interface LinearTicketData {
|
|
78
|
+
context: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
url?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
/**
|
|
76
84
|
* Fetch Linear ticket context for a review.
|
|
77
85
|
*
|
|
78
|
-
*
|
|
86
|
+
* Returns formatted context string and extracted metadata.
|
|
79
87
|
*/
|
|
80
88
|
export async function fetchLinearContext(
|
|
81
89
|
ticketId: string
|
|
82
|
-
): Promise<Result<
|
|
90
|
+
): Promise<Result<LinearTicketData, LinearContextError>> {
|
|
83
91
|
// Check for API token
|
|
84
92
|
const apiToken = process.env.LINEAR_API_TOKEN;
|
|
85
93
|
if (!apiToken) {
|
|
@@ -89,85 +97,83 @@ export async function fetchLinearContext(
|
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
state { name }
|
|
108
|
-
priority
|
|
109
|
-
labels { nodes { name } }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
`,
|
|
113
|
-
variables: { id: ticketId },
|
|
114
|
-
}),
|
|
100
|
+
// Create MCP client
|
|
101
|
+
const clientResult = await createMCPClient({
|
|
102
|
+
name: "linear",
|
|
103
|
+
command: "npx",
|
|
104
|
+
args: ["linear-mcp-server"],
|
|
105
|
+
env: {
|
|
106
|
+
LINEAR_API_KEY: apiToken,
|
|
107
|
+
},
|
|
108
|
+
timeout: 30000,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!clientResult.ok) {
|
|
112
|
+
return err({
|
|
113
|
+
code: "CONNECTION_FAILED",
|
|
114
|
+
message: clientResult.error.message,
|
|
115
115
|
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const client = clientResult.value;
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
try {
|
|
121
|
+
// Fetch issue details using Linear MCP
|
|
122
|
+
const ticketResult = await client.callTool("get_issue", { issue_id: ticketId });
|
|
123
|
+
|
|
124
|
+
if (!ticketResult.ok) {
|
|
118
125
|
return err({
|
|
119
126
|
code: "FETCH_FAILED",
|
|
120
|
-
message:
|
|
127
|
+
message: ticketResult.error.message,
|
|
121
128
|
});
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
131
|
+
// Parse the response
|
|
132
|
+
const content = ticketResult.value.content;
|
|
133
|
+
const textContent = content.find(c => c.type === "text");
|
|
125
134
|
|
|
126
|
-
if (
|
|
135
|
+
if (!textContent?.text) {
|
|
127
136
|
return err({
|
|
128
137
|
code: "FETCH_FAILED",
|
|
129
|
-
message:
|
|
138
|
+
message: "No ticket data returned",
|
|
130
139
|
});
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const ticketText = textContent.text;
|
|
143
|
+
|
|
144
|
+
// Extract URL from MCP response if present (Linear API includes url field)
|
|
145
|
+
// Pattern matches: "url": "https://linear.app/..." or URL: https://linear.app/...
|
|
146
|
+
const urlMatch = ticketText.match(/"?url"?:\s*"?(https:\/\/linear\.app\/[^"\s]+)"?/i);
|
|
147
|
+
const extractedUrl = urlMatch?.[1];
|
|
148
|
+
|
|
149
|
+
// Extract title from MCP response if present
|
|
150
|
+
const titleMatch = ticketText.match(/"?title"?:\s*"([^"]+)"/i);
|
|
151
|
+
const extractedTitle = titleMatch?.[1];
|
|
140
152
|
|
|
141
153
|
// Format the context for the review prompt
|
|
142
|
-
const contextBlock = formatLinearContext(ticketId,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
154
|
+
const contextBlock = formatLinearContext(ticketId, ticketText);
|
|
155
|
+
|
|
156
|
+
return ok({
|
|
157
|
+
context: contextBlock,
|
|
158
|
+
title: extractedTitle,
|
|
159
|
+
url: extractedUrl,
|
|
149
160
|
});
|
|
161
|
+
|
|
162
|
+
} finally {
|
|
163
|
+
await client.close();
|
|
150
164
|
}
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
|
-
* Format Linear
|
|
168
|
+
* Format Linear ticket data as context for review prompt.
|
|
155
169
|
*/
|
|
156
|
-
function formatLinearContext(ticketId: string,
|
|
157
|
-
const labels = issue.labels?.nodes?.map(l => l.name).filter(Boolean).join(", ") || "none";
|
|
158
|
-
const priority = issue.priority ?? 0;
|
|
159
|
-
const priorityLabels = ["", "Urgent", "High", "Medium", "Low", "No Priority"];
|
|
160
|
-
|
|
170
|
+
function formatLinearContext(ticketId: string, ticketData: string): string {
|
|
161
171
|
return `
|
|
162
172
|
## Linear Ticket Context: ${ticketId}
|
|
163
173
|
|
|
164
|
-
|
|
165
|
-
**Status:** ${issue.state?.name ?? "Unknown"}
|
|
166
|
-
**Priority:** ${priorityLabels[priority] ?? "Unknown"}
|
|
167
|
-
**Labels:** ${labels}
|
|
174
|
+
The following is the context from the linked Linear ticket. Use this to understand the business requirements and acceptance criteria for this PR.
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
${issue.description ?? "No description provided."}
|
|
176
|
+
${ticketData}
|
|
171
177
|
|
|
172
178
|
---
|
|
173
179
|
When reviewing, check that the PR changes align with the ticket requirements above.
|
|
@@ -184,7 +190,7 @@ export async function getLinearContextForReview(
|
|
|
184
190
|
branchName?: string,
|
|
185
191
|
prTitle?: string,
|
|
186
192
|
prBody?: string
|
|
187
|
-
): Promise<{ context: string | null; ticketId: string | null; warning?: string }> {
|
|
193
|
+
): Promise<{ context: string | null; ticketId: string | null; ticketTitle?: string; ticketUrl?: string; warning?: string }> {
|
|
188
194
|
// Extract ticket ID
|
|
189
195
|
const ticketId = extractTicketId(branchName, prTitle, prBody);
|
|
190
196
|
|
|
@@ -212,8 +218,14 @@ export async function getLinearContextForReview(
|
|
|
212
218
|
};
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
const { context, title, url } = result.value;
|
|
222
|
+
|
|
215
223
|
return {
|
|
216
|
-
context
|
|
224
|
+
context,
|
|
217
225
|
ticketId,
|
|
226
|
+
// Use extracted title from MCP if available, fallback to generic
|
|
227
|
+
ticketTitle: title ?? `Linear Ticket ${ticketId}`,
|
|
228
|
+
// Use extracted URL from MCP if available (no fallback - better to have no link than broken link)
|
|
229
|
+
ticketUrl: url,
|
|
218
230
|
};
|
|
219
231
|
}
|
package/src/llm/index.ts
CHANGED
|
@@ -33,8 +33,8 @@ export type {
|
|
|
33
33
|
} from "../types/llm";
|
|
34
34
|
|
|
35
35
|
// Re-export provider interface and errors
|
|
36
|
-
export type { LLMProvider, LLMError, LLMErrorCode } from "./provider";
|
|
37
|
-
export { LLMErrors, isRetryableError, createLLMError } from "./provider";
|
|
36
|
+
export type { LLMProvider, LLMError, LLMErrorCode, RetryConfig } from "./provider";
|
|
37
|
+
export { LLMErrors, isRetryableError, createLLMError, withRetry } from "./provider";
|
|
38
38
|
|
|
39
39
|
// Re-export factory
|
|
40
40
|
export {
|
package/src/llm/provider.ts
CHANGED
|
@@ -131,3 +131,70 @@ export const LLMErrors = {
|
|
|
131
131
|
retryable: false,
|
|
132
132
|
}),
|
|
133
133
|
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retry configuration for LLM calls.
|
|
137
|
+
*/
|
|
138
|
+
export interface RetryConfig {
|
|
139
|
+
/** Maximum number of retries (default: 1) */
|
|
140
|
+
maxRetries?: number;
|
|
141
|
+
/** Delay between retries in ms (default: 2000) */
|
|
142
|
+
delayMs?: number;
|
|
143
|
+
/** Whether to log retry attempts (default: true) */
|
|
144
|
+
logRetries?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wrap an async function with retry logic for transient errors.
|
|
149
|
+
*
|
|
150
|
+
* @param fn - Async function returning Result<T, LLMError>
|
|
151
|
+
* @param config - Retry configuration
|
|
152
|
+
* @returns Result with success value or final error
|
|
153
|
+
*/
|
|
154
|
+
export async function withRetry<T>(
|
|
155
|
+
fn: () => Promise<Result<T, LLMError>>,
|
|
156
|
+
config: RetryConfig = {}
|
|
157
|
+
): Promise<Result<T, LLMError>> {
|
|
158
|
+
const maxRetries = config.maxRetries ?? 1;
|
|
159
|
+
const delayMs = config.delayMs ?? 2000;
|
|
160
|
+
const logRetries = config.logRetries ?? true;
|
|
161
|
+
|
|
162
|
+
let lastError: LLMError | undefined;
|
|
163
|
+
|
|
164
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
165
|
+
const result = await fn();
|
|
166
|
+
|
|
167
|
+
if (result.ok) {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
lastError = result.error;
|
|
172
|
+
|
|
173
|
+
// Check if we should retry
|
|
174
|
+
if (attempt < maxRetries && isRetryableError(result.error)) {
|
|
175
|
+
const retryDelay = result.error.retryAfterMs ?? delayMs;
|
|
176
|
+
|
|
177
|
+
if (logRetries) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`⚠️ LLM call failed (${result.error.code}), retrying in ${retryDelay}ms... (${attempt + 1}/${maxRetries})`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await sleep(retryDelay);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Non-retryable error or max retries reached
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Should not reach here, but return last error if we do
|
|
192
|
+
return { ok: false, error: lastError! };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sleep for a given number of milliseconds.
|
|
197
|
+
*/
|
|
198
|
+
function sleep(ms: number): Promise<void> {
|
|
199
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
200
|
+
}
|
package/src/review/engine.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createListDirTool } from "../agent/tools/list-dir";
|
|
|
16
16
|
import { createOutlineTool } from "../agent/tools/outline";
|
|
17
17
|
import { createFindSymbolTool } from "../agent/tools/find-symbol";
|
|
18
18
|
import { calculateStats, determineRecommendation } from "../types/review";
|
|
19
|
-
import type { ReviewResult, ReviewFinding, Severity } from "../types/review";
|
|
19
|
+
import type { ReviewResult, ReviewFinding, Severity, ContextSource } from "../types/review";
|
|
20
20
|
import type { Result } from "../types/result";
|
|
21
21
|
import { ok, err } from "../types/result";
|
|
22
22
|
import { loadConfig, getStrictnessModifier } from "../config";
|
|
@@ -100,6 +100,9 @@ export async function runReview(
|
|
|
100
100
|
const strictnessGuidance = getStrictnessModifier(strictness);
|
|
101
101
|
let systemPrompt = REVIEW_SYSTEM_PROMPT + `\n\n## Strictness Setting: ${strictness.toUpperCase()}\n${strictnessGuidance}`;
|
|
102
102
|
|
|
103
|
+
// Track context sources for citation
|
|
104
|
+
const contextSources: ContextSource[] = [];
|
|
105
|
+
|
|
103
106
|
// Fetch Linear context if available
|
|
104
107
|
const linearResult = await getLinearContextForReview(
|
|
105
108
|
config.branchName,
|
|
@@ -110,6 +113,16 @@ export async function runReview(
|
|
|
110
113
|
if (linearResult.context) {
|
|
111
114
|
systemPrompt = linearResult.context + "\n\n" + systemPrompt;
|
|
112
115
|
console.log(`📎 Loaded Linear context for ticket: ${linearResult.ticketId}`);
|
|
116
|
+
|
|
117
|
+
// Track Linear as a context source (only if we have a valid ticket ID)
|
|
118
|
+
if (linearResult.ticketId) {
|
|
119
|
+
contextSources.push({
|
|
120
|
+
type: "linear",
|
|
121
|
+
id: linearResult.ticketId,
|
|
122
|
+
title: linearResult.ticketTitle,
|
|
123
|
+
url: linearResult.ticketUrl,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
113
126
|
} else if (linearResult.warning) {
|
|
114
127
|
console.warn(`⚠️ ${linearResult.warning}`);
|
|
115
128
|
}
|
|
@@ -119,12 +132,21 @@ export async function runReview(
|
|
|
119
132
|
if (adrResult) {
|
|
120
133
|
systemPrompt = adrResult.formatted + "\n\n" + systemPrompt;
|
|
121
134
|
console.log(`📚 Loaded ${adrResult.context.count} ADR files from ${adrResult.context.path}`);
|
|
135
|
+
|
|
136
|
+
// Track each ADR file as a context source
|
|
137
|
+
for (const adrFile of adrResult.context.files) {
|
|
138
|
+
contextSources.push({
|
|
139
|
+
type: "adr",
|
|
140
|
+
id: adrFile.name,
|
|
141
|
+
title: adrFile.name.replace(".md", ""),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
// Create agent
|
|
125
147
|
const agent = createAgentLoop(providerResult.value, tools, {
|
|
126
148
|
maxIterations: config.maxIterations || fileConfig.maxIterations || 15,
|
|
127
|
-
timeoutMs:
|
|
149
|
+
timeoutMs: 15 * 60 * 1000, // 15 minutes for larger PRs with slower models
|
|
128
150
|
systemPrompt,
|
|
129
151
|
});
|
|
130
152
|
|
|
@@ -142,7 +164,7 @@ export async function runReview(
|
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
// Parse the response into structured findings
|
|
145
|
-
const reviewResult = parseReviewResponse(agentResult.value, diffSummary);
|
|
167
|
+
const reviewResult = parseReviewResponse(agentResult.value, diffSummary, contextSources);
|
|
146
168
|
|
|
147
169
|
return ok(reviewResult);
|
|
148
170
|
}
|
|
@@ -214,7 +236,7 @@ ${diff.length > 15000 ? "\n(diff truncated, use tools to read full files if need
|
|
|
214
236
|
/**
|
|
215
237
|
* Parse the agent's response into structured findings.
|
|
216
238
|
*/
|
|
217
|
-
function parseReviewResponse(response: string, diffSummary: string): ReviewResult {
|
|
239
|
+
function parseReviewResponse(response: string, diffSummary: string, contextSources: ContextSource[] = []): ReviewResult {
|
|
218
240
|
const findings: ReviewFinding[] = [];
|
|
219
241
|
|
|
220
242
|
// Extract summary
|
|
@@ -270,5 +292,6 @@ function parseReviewResponse(response: string, diffSummary: string): ReviewResul
|
|
|
270
292
|
recommendation,
|
|
271
293
|
stats,
|
|
272
294
|
filesReviewed: parseInt(filesReviewed, 10) || 0,
|
|
295
|
+
contextSources: contextSources.length > 0 ? contextSources : undefined,
|
|
273
296
|
};
|
|
274
297
|
}
|
package/src/review/formatter.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Converts ReviewResult into markdown for PR comments.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ReviewFinding, ReviewResult, Severity } from "../types/review";
|
|
6
|
+
import type { ReviewFinding, ReviewResult, Severity, ContextSource } from "../types/review";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Severity emoji indicators.
|
|
@@ -60,6 +60,14 @@ export function formatReviewComment(result: ReviewResult): string {
|
|
|
60
60
|
lines.push(result.summary);
|
|
61
61
|
lines.push("");
|
|
62
62
|
|
|
63
|
+
// Context Used section (if any context was used)
|
|
64
|
+
if (result.contextSources && result.contextSources.length > 0) {
|
|
65
|
+
lines.push("### Context Used");
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push(...formatContextSources(result.contextSources));
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
// Findings by severity (only include sections with findings)
|
|
64
72
|
if (result.stats.critical > 0) {
|
|
65
73
|
lines.push("---");
|
|
@@ -134,6 +142,35 @@ function formatFindings(findings: ReviewFinding[], severity: Severity): string[]
|
|
|
134
142
|
return lines;
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Format context sources for the Context Used section.
|
|
147
|
+
* Renders Linear tickets with clickable links and ADR files as a list.
|
|
148
|
+
*/
|
|
149
|
+
function formatContextSources(sources: ContextSource[]): string[] {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
|
|
152
|
+
// Group sources by type
|
|
153
|
+
const linearSources = sources.filter(s => s.type === "linear");
|
|
154
|
+
const adrSources = sources.filter(s => s.type === "adr");
|
|
155
|
+
|
|
156
|
+
// Format Linear tickets
|
|
157
|
+
for (const source of linearSources) {
|
|
158
|
+
if (source.url) {
|
|
159
|
+
lines.push(`- **Linear ticket:** [${source.id}](${source.url})`);
|
|
160
|
+
} else {
|
|
161
|
+
lines.push(`- **Linear ticket:** ${source.id}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Format ADR files
|
|
166
|
+
if (adrSources.length > 0) {
|
|
167
|
+
const adrList = adrSources.map(s => `\`${s.id}\``).join(", ");
|
|
168
|
+
lines.push(`- **ADRs referenced:** ${adrList}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
|
|
137
174
|
/**
|
|
138
175
|
* Format recommendation as human-readable text.
|
|
139
176
|
*/
|
package/src/types/review.ts
CHANGED
|
@@ -32,6 +32,25 @@ export interface ReviewFinding {
|
|
|
32
32
|
*/
|
|
33
33
|
export type Recommendation = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Type of external context source used in the review.
|
|
37
|
+
*/
|
|
38
|
+
export type ContextSourceType = "linear" | "adr";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* External context source that informed the review.
|
|
42
|
+
*/
|
|
43
|
+
export interface ContextSource {
|
|
44
|
+
/** Type of context source */
|
|
45
|
+
type: ContextSourceType;
|
|
46
|
+
/** Identifier (ticket ID or file path) */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Display title */
|
|
49
|
+
title?: string;
|
|
50
|
+
/** URL for clickable link (Linear tickets) */
|
|
51
|
+
url?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* Statistics about the review findings.
|
|
37
56
|
*/
|
|
@@ -57,6 +76,8 @@ export interface ReviewResult {
|
|
|
57
76
|
stats: ReviewStats;
|
|
58
77
|
/** Files that were reviewed */
|
|
59
78
|
filesReviewed: number;
|
|
79
|
+
/** External context sources used in the review */
|
|
80
|
+
contextSources?: ContextSource[];
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
/**
|