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; 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 => { 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)); };