import { z } from 'zod'; import { getRouter } from '../router.js'; import type { CostContext, ModelProvider } from '../types.js'; export interface MatchCandidate { title: string; attributes?: Record; identifiers?: Record; grade?: { grader?: string | null; grade?: string | null } | null; source?: string; } export const MatchVerdictSchema = z.object({ sameAsset: z.boolean().describe('True if both describe the same canonical collectible (ignoring grade/condition)'), sameVariant: z.boolean().describe('True if grade/condition/edition also match'), confidence: z.number().min(0).max(1), blockingDifferences: z.array(z.string()).describe('Concrete attribute differences that prevent a match (set, number, edition, language, year…)'), rationale: z.string(), }); export type MatchVerdict = z.infer; /** * LLM verification for entity resolution (§112) — the last step after deterministic identifiers, * canonical keys and fuzzy matching have produced a candidate pair. */ export async function verifyEntityMatch(a: MatchCandidate, b: MatchCandidate, opts: { provider?: ModelProvider; cost?: CostContext } = {}) { const provider = opts.provider ?? getRouter(); const res = await provider.extract('resolve', { schema: MatchVerdictSchema, system: 'You verify whether two collectible records refer to the same canonical asset. Be strict: different set, card number, edition (1st Edition vs Unlimited), language, year, reference number, colorway or size are different assets. Grade and condition differences make different variants of the same asset.', prompt: 'Compare record A and record B.', input: `Record A:\n${JSON.stringify(a, null, 2)}\n\nRecord B:\n${JSON.stringify(b, null, 2)}`, maxTokens: 800, effort: 'low', cost: { endpoint: 'verify_match', ...(opts.cost ?? {}) }, }); return res; }