import { z } from 'zod'; /** Connector manifest (CLAUDE.md §9). Seeds the `sources` table; drives the public /source pages. */ export const ConnectorManifest = z.object({ id: z.string().regex(/^[a-z0-9-]+$/), name: z.string(), organization: z.string(), category: z.enum(['terminology', 'genes', 'genomics', 'variants', 'epidemiology', 'trials', 'literature', 'drugs', 'regulatory', 'evidence', 'pathways', 'funding', 'guidelines']), tier: z.number().int().min(0).max(11), description: z.string(), homepage: z.string().url(), docsUrl: z.string().url().optional(), termsUrl: z.string().url().optional(), access: z.object({ type: z.enum(['api', 'bulk', 'rss', 'ftp', 'graphql', 'rest', 'scrape', 'manual']), auth: z.enum(['none', 'api_key', 'oauth', 'account', 'controlled']), baseUrl: z.string().url().optional(), }), license: z.string(), licenseStatus: z.enum(['review', 'approved', 'restricted', 'blocked']), commercialUse: z.enum(['allowed', 'restricted', 'prohibited', 'unknown']), redistribution: z.enum(['allowed', 'attribution', 'restricted', 'prohibited', 'unknown']), attribution: z.string().optional(), termsReviewedAt: z.string().optional(), // YYYY-MM-DD, human review date termsNotes: z.string().optional(), updateFrequency: z.string(), expectedLatency: z.string().optional(), supportsIncrementalSync: z.boolean(), entities: z.array(z.string()), metrics: z.array(z.string()).default([]), rateLimits: z.object({ requestsPerSecond: z.number().positive().optional(), requestsPerMinute: z.number().positive().optional(), maxConcurrency: z.number().int().positive().default(1), notes: z.string().optional(), }), retryPolicy: z .object({ maxRetries: z.number().int().min(0).default(4), baseDelayMs: z.number().int().min(0).default(500), maxDelayMs: z.number().int().min(0).default(30_000), }) .default({ maxRetries: 4, baseDelayMs: 500, maxDelayMs: 30_000 }), rawRetention: z.enum(['full', 'summary', 'none']).default('full'), schemaVersion: z.string().default('1'), documentationVerifiedAt: z.string(), // YYYY-MM-DD, the date official docs were checked (CLAUDE.md §224-225) owner: z.string().default('cancerindex'), status: z.enum(['planned', 'active', 'paused', 'awaiting_credentials', 'review', 'retired']), schedule: z.string().optional(), // cron expression for the worker scheduler anomalyGuard: z .object({ /** Refuse to mark records source_missing when the fetched count drops below this ratio of the previous run. */ minRatioOfPrevious: z.number().min(0).max(1).default(0.5), }) .default({ minRatioOfPrevious: 0.5 }), /** * Mid-run cursor checkpoint (CLAUDE.md §90): `connector_cursors.cursor` is persisted after this many * upserted records (or every 60 s) whenever `ctx.cursor` changed, so a killed run resumes from the * last completed page instead of from the start of the run. See docs/connectors/README.md. */ checkpointEvery: z.number().int().positive().default(2000), }); export type ConnectorManifest = z.infer; export function defineManifest(m: z.input): ConnectorManifest { return ConnectorManifest.parse(m); }