161 lines
4.6 KiB
TypeScript
161 lines
4.6 KiB
TypeScript
import { ConvexError } from 'convex/values';
|
|
import OpenAI from 'openai';
|
|
|
|
type ReviewRisk = 'low' | 'medium' | 'high';
|
|
type ReviewAction = 'sync' | 'open_review_pr' | 'manual_review' | 'do_not_sync';
|
|
|
|
export type AiCompatibilityReview = {
|
|
summary: string;
|
|
risk: ReviewRisk;
|
|
compatible: boolean;
|
|
requiresHumanReview: boolean;
|
|
recommendedAction: ReviewAction;
|
|
potentialConflicts: string[];
|
|
importantFiles: string[];
|
|
reasoningSummary: string;
|
|
};
|
|
|
|
export type ReviewInput = {
|
|
spoonName: string;
|
|
upstreamFullName: string;
|
|
forkFullName: string;
|
|
status: string;
|
|
upstreamAheadBy: number;
|
|
forkAheadBy: number;
|
|
upstreamCommits: {
|
|
sha: string;
|
|
message: string;
|
|
authorName?: string;
|
|
committedAt?: number;
|
|
}[];
|
|
forkCommits: {
|
|
sha: string;
|
|
message: string;
|
|
authorName?: string;
|
|
committedAt?: number;
|
|
}[];
|
|
importantFilePatterns?: string[];
|
|
ignoredFilePatterns?: string[];
|
|
};
|
|
|
|
export type ReasoningEffort =
|
|
| 'none'
|
|
| 'minimal'
|
|
| 'low'
|
|
| 'medium'
|
|
| 'high'
|
|
| 'xhigh';
|
|
|
|
export type OpenAiReviewSettings = {
|
|
apiKey: string;
|
|
model: string;
|
|
reasoningEffort: ReasoningEffort;
|
|
};
|
|
|
|
const reviewSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
summary: { type: 'string' },
|
|
risk: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
compatible: { type: 'boolean' },
|
|
requiresHumanReview: { type: 'boolean' },
|
|
recommendedAction: {
|
|
type: 'string',
|
|
enum: ['sync', 'open_review_pr', 'manual_review', 'do_not_sync'],
|
|
},
|
|
potentialConflicts: { type: 'array', items: { type: 'string' } },
|
|
importantFiles: { type: 'array', items: { type: 'string' } },
|
|
reasoningSummary: { type: 'string' },
|
|
},
|
|
required: [
|
|
'summary',
|
|
'risk',
|
|
'compatible',
|
|
'requiresHumanReview',
|
|
'recommendedAction',
|
|
'potentialConflicts',
|
|
'importantFiles',
|
|
'reasoningSummary',
|
|
],
|
|
} as const;
|
|
|
|
const isReviewRisk = (value: unknown): value is ReviewRisk =>
|
|
value === 'low' || value === 'medium' || value === 'high';
|
|
|
|
const isReviewAction = (value: unknown): value is ReviewAction =>
|
|
value === 'sync' ||
|
|
value === 'open_review_pr' ||
|
|
value === 'manual_review' ||
|
|
value === 'do_not_sync';
|
|
|
|
const validateReview = (value: unknown): AiCompatibilityReview => {
|
|
if (!value || typeof value !== 'object') {
|
|
throw new ConvexError('OpenAI returned an invalid review payload.');
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
if (
|
|
typeof record.summary !== 'string' ||
|
|
!isReviewRisk(record.risk) ||
|
|
typeof record.compatible !== 'boolean' ||
|
|
typeof record.requiresHumanReview !== 'boolean' ||
|
|
!isReviewAction(record.recommendedAction) ||
|
|
!Array.isArray(record.potentialConflicts) ||
|
|
!Array.isArray(record.importantFiles) ||
|
|
typeof record.reasoningSummary !== 'string'
|
|
) {
|
|
throw new ConvexError('OpenAI review did not match the expected schema.');
|
|
}
|
|
return {
|
|
summary: record.summary,
|
|
risk: record.risk,
|
|
compatible: record.compatible,
|
|
requiresHumanReview: record.requiresHumanReview,
|
|
recommendedAction: record.recommendedAction,
|
|
potentialConflicts: record.potentialConflicts.filter(
|
|
(item): item is string => typeof item === 'string',
|
|
),
|
|
importantFiles: record.importantFiles.filter(
|
|
(item): item is string => typeof item === 'string',
|
|
),
|
|
reasoningSummary: record.reasoningSummary,
|
|
};
|
|
};
|
|
|
|
export const reviewUpstreamCompatibility = async (
|
|
input: ReviewInput,
|
|
settings: OpenAiReviewSettings,
|
|
): Promise<AiCompatibilityReview> => {
|
|
const response = await new OpenAI({
|
|
apiKey: settings.apiKey,
|
|
}).responses.create({
|
|
model: settings.model,
|
|
store: false,
|
|
reasoning: {
|
|
effort: settings.reasoningEffort,
|
|
},
|
|
input: [
|
|
{
|
|
role: 'system',
|
|
content:
|
|
'You are reviewing whether upstream changes can be safely brought into a maintained fork. You do not execute code. You do not claim tests passed. Treat fork-only commits as user customizations that must be preserved. If changed files overlap with fork-only changes, increase risk. If patch context is incomplete, say so and require human review. Prefer conservative recommendations. Return only the required structured output.',
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: JSON.stringify(input, null, 2),
|
|
},
|
|
],
|
|
text: {
|
|
format: {
|
|
type: 'json_schema',
|
|
name: 'spoon_upstream_compatibility_review',
|
|
strict: true,
|
|
schema: reviewSchema,
|
|
},
|
|
},
|
|
});
|
|
const raw = response.output_text;
|
|
if (!raw) throw new ConvexError('OpenAI returned an empty review.');
|
|
return validateReview(JSON.parse(raw));
|
|
};
|