import { z } from "zod"; /** Controlled vocabularies (keep in sync with docs/data-model.md). */ export const PRODUCT_TYPES = ["application", "library", "framework", "api-service", "platform", "cli-tool", "language", "database", "model", "data-service", "extension", "specification", "service"] as const; export const PRODUCT_STATUSES = ["active", "maintenance", "deprecated", "discontinued", "renamed", "unknown"] as const; export const SOURCE_MODELS = ["open-source", "proprietary", "source-available", "mixed"] as const; export const PRICING_MODELS = ["free", "freemium", "paid", "enterprise", "usage-based", "open-source", "academic", "unknown"] as const; export const OPERATING_SYSTEMS = ["macos", "windows", "linux", "ios", "android", "web", "any"] as const; export const EXECUTION_MODES = ["cloud", "self-hosted", "local"] as const; export const INTERFACES = ["web", "desktop", "mobile", "cli", "api", "sdk", "library", "ide-extension", "browser-extension", "mcp-server"] as const; export const DEPLOYMENT_MODELS = ["saas", "on-premises", "hybrid", "desktop", "embedded", "managed-cloud"] as const; export const SOURCE_TYPES = ["official_site", "official_docs", "repository", "release_notes", "pricing_page", "policy_page", "license_page", "package_registry", "academic_paper", "third_party_directory", "news", "other"] as const; export const CONFIDENCE = ["high", "medium", "low"] as const; export const DEPTHS = ["deep", "standard"] as const; export const EDITORIAL_STATUSES = ["draft", "reviewed", "published", "deprecated"] as const; export const VISIBILITIES = ["public", "private"] as const; export const USE_STATES = ["active", "testing", "candidate", "archived", "rejected"] as const; const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "slug must be kebab-case"); const url = z.string().url().refine((u) => /^https?:\/\//.test(u), "http(s) only"); const date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD"); const shortList = (max = 12) => z.array(z.string().min(1).max(160)).max(max); export const sourceSchema = z.object({ url, type: z.enum(SOURCE_TYPES), title: z.string().max(200).optional(), /** Which record fields this source supports (e.g. ["license_spdx","pricing_model"]). */ supports: z.array(z.string()).default([]), checked_at: date, note: z.string().max(400).optional(), }); export const toolRecordSchema = z.object({ slug, name: z.string().min(1).max(120), aliases: z.array(z.string().max(120)).default([]), vendor: z.object({ name: z.string().min(1).max(120), slug: slug.optional(), website: url.optional() }), homepage_url: url, docs_url: url.optional(), repo_url: url.optional(), pricing_url: url.optional(), license_url: url.optional(), privacy_url: url.optional(), security_url: url.optional(), logo_url: url.optional(), logo_attribution: z.string().max(200).optional(), product_type: z.enum(PRODUCT_TYPES), product_status: z.enum(PRODUCT_STATUSES), summary: z.string().min(40).max(220), /** Original editorial description in Markdown. deep: 100–300 words; standard: ≥ 40 words. */ description: z.string().min(150), capabilities: shortList(10).min(2), workflows: shortList(8).default([]), advantages: shortList(8).default([]), limitations: shortList(8).default([]), prerequisites: shortList(8).default([]), audiences: shortList(8).default([]), alternatives: z.array(slug).max(10).default([]), comparison_notes: z.string().max(1200).optional(), categories: z.array(z.object({ slug, primary: z.boolean().default(false), label: z.string().max(80).optional() })).min(1).max(5), tags: z.array(slug).max(20).default([]), use_cases: z.array(slug).max(12).default([]), operating_systems: z.array(z.enum(OPERATING_SYSTEMS)).default([]), execution_modes: z.array(z.enum(EXECUTION_MODES)).min(1), interfaces: z.array(z.enum(INTERFACES)).min(1), deployment_models: z.array(z.enum(DEPLOYMENT_MODELS)).default([]), integrations: shortList(20).default([]), data_formats: shortList(20).default([]), languages: shortList(20).default([]), source_model: z.enum(SOURCE_MODELS), license_spdx: z.string().max(60).optional(), pricing_model: z.array(z.enum(PRICING_MODELS)).min(1), pricing_checked_at: date.optional(), privacy_posture: z.string().max(600).optional(), data_residency: z.string().max(300).optional(), self_hostable: z.boolean().nullable().default(null), sources: z.array(sourceSchema).min(1), confidence: z.enum(CONFIDENCE), uncertainty_note: z.string().max(600).optional(), discovery_method: z.string().max(80).default("editorial-research"), depth: z.enum(DEPTHS).default("standard"), editorial_status: z.enum(EDITORIAL_STATUSES).default("reviewed"), visibility: z.enum(VISIBILITIES).default("public"), featured: z.boolean().default(false), author: z.string().max(80).default("editorial"), reviewed_at: date.optional(), reviewed_by: z.string().max(80).optional(), }); export type ToolRecord = z.infer; export type ToolRecordInput = z.input; export const categorySchema: z.ZodType<{ slug: string; name: string; description: string; children?: any[] }> = z.object({ slug, name: z.string(), description: z.string(), children: z.array(z.lazy(() => categorySchema)).optional(), }); export const collectionSchema = z.object({ slug, title: z.string().min(3).max(120), description: z.string().min(40).max(1200), published: z.boolean().default(true), items: z.array(z.object({ tool: slug, blurb: z.string().max(400).optional() })).min(2), }); export const ownerRelevanceSchema = z.object({ tool: slug, use_state: z.enum(USE_STATES), rating: z.number().int().min(1).max(5).optional(), notes: z.string().max(4000).optional(), cluster_compat: z.enum(["yes", "partial", "no", "unknown"]).default("unknown"), priority: z.enum(["high", "medium", "low"]).default("medium"), projects: z.array(slug).default([]), subscription_status: z.string().max(120).optional(), custom_tags: z.array(slug).default([]), review_at: date.optional(), }); export const WORD = (s: string) => s.trim().split(/\s+/).filter(Boolean).length;