import { z } from "zod"; import { SENSOR_TYPES, TIERS } from "./taxonomy"; /** * Source-registry seed schema (shared by the engine's seed loader, the validator CLI and the * API's bulk-import endpoint). Dependency-free apart from zod; no filesystem access here. */ export const sensorSchema = z.object({ id: z.string().optional(), name: z.string(), url: z.string().url(), type: z.enum(SENSOR_TYPES), connector: z.string().default("http"), tier: z.enum(TIERS).optional(), interval: z.number().int().positive().optional(), weight: z.number().positive().optional(), config: z.record(z.string(), z.unknown()).default({}), }); export type SensorSeed = z.infer; export const productSchema = z.object({ id: z.string().optional(), name: z.string(), type: z.string().default("product"), aliases: z.array(z.string()).default([]) }); export const sourceSchema = z.object({ id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id must be kebab-case"), name: z.string(), domain: z.string(), homepage: z.string().url().optional(), description: z.string().optional(), categories: z.array(z.string()).default([]), tier: z.enum(TIERS).default("B"), weight: z.number().positive().default(1), entity_type: z.string().default("organization"), aliases: z.array(z.string()).default([]), products: z.array(productSchema).default([]), discover: z.object({ rss: z.boolean().optional(), sitemap: z.boolean().optional(), status: z.boolean().optional(), pages: z.boolean().optional() }).default({}), fallback: z.object({ firecrawl: z.boolean().optional(), scrapfly: z.boolean().optional() }).default({}), sensors: z.array(sensorSchema).default([]), notes: z.string().optional(), enabled: z.boolean().default(true), /** false = heuristics only (high-volume feeds such as news wires) */ llm: z.boolean().default(true), /** ISO 3166-1 alpha-2 (upper-case) when the organization is national/regional; "EU" for EU bodies; "INT" for global bodies */ country: z.string().regex(/^[A-Z]{2,3}$/).optional(), /** primary language of the published content (ISO 639-1); defaults to "en" downstream */ language: z.string().regex(/^[a-z]{2}$/).optional(), /** false for media/aggregators reporting about others (news wires, tech press); true for the organization's own channels */ first_party: z.boolean().optional(), /** fragment-only: merge into an already declared source instead of redefining it */ extend: z.boolean().default(false), }); export type SourceSeed = z.infer; /** Partial schema for `extend: true` entries — only `id` is required. */ export const extendSchema = z.object({ id: z.string(), extend: z.literal(true), aliases: z.array(z.string()).default([]), products: z.array(productSchema).default([]), sensors: z.array(sensorSchema).default([]), categories: z.array(z.string()).default([]), fallback: z.object({ firecrawl: z.boolean().optional(), scrapfly: z.boolean().optional() }).optional(), notes: z.string().optional(), country: z.string().regex(/^[A-Z]{2,3}$/).optional(), language: z.string().regex(/^[a-z]{2}$/).optional(), first_party: z.boolean().optional(), }); /** Bulk import document (JSON / YAML already parsed): `{ sources: [...] }`. */ export const importDocumentSchema = z.object({ sources: z.array(z.unknown()).min(1).max(2000) });