{"version":3,"sources":["../../src/validation/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @jobo-ai/scraper-sdk/validation\n *\n * Shared validation logic for job listings and details.\n * Used by the SDK test runner and the production scraper runtime to enforce\n * minimum data-quality requirements before a job is considered valid.\n *\n * Import from '@jobo-ai/scraper-sdk/validation' in your code.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum shape a listing must satisfy for validation.\n * Intentionally loose so both SDK models and runtime types can be passed in.\n */\nexport interface ValidatableJobListing {\n  externalId?: string | null;\n  title?: string | null;\n  applyUrl?: string | null;\n  listingUrl?: string | null;\n  description?: string | null;\n  locations?: Array<{ text?: string | null }> | null;\n}\n\n/**\n * Minimum shape a detail record must satisfy for validation.\n */\nexport interface ValidatableJobDetail extends ValidatableJobListing {\n  /** Some runtime types expose this as a pre-computed number. */\n  descriptionLength?: number;\n}\n\n/**\n * A validation issue describing which required fields are missing.\n */\nexport interface JobValidationIssue {\n  /** The externalId of the job (or \"(unknown)\" if missing). */\n  externalId: string;\n  /** List of field names that failed validation. */\n  missingFields: string[];\n}\n\n/**\n * Result of a full validation pass across a set of listings and/or details.\n */\nexport interface ValidationSummary {\n  /** Whether all items passed validation. */\n  valid: boolean;\n  /** Total number of items validated. */\n  total: number;\n  /** Number of items that passed. */\n  passed: number;\n  /** Number of items that failed. */\n  failed: number;\n  /** Individual issues for each failed item. */\n  issues: JobValidationIssue[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a string is a syntactically valid URL.\n */\nexport function isValidUrl(url: string): boolean {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Listing Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a single listing for the minimum required fields.\n *\n * **Always required:** `externalId`, `applyUrl`, `listingUrl`.\n *\n * When the scraper is a **full** scraper (`isFull = true`), listings carry\n * full detail data, so additionally required: `title`, `description`, and\n * at least one location with non-empty text.\n *\n * @param listing - The listing to validate.\n * @param isFull  - Whether this is from a \"full\" scraper (listing = detail).\n * @returns `null` if the listing is valid, or a {@link JobValidationIssue}.\n */\nexport function validateListing(\n  listing: ValidatableJobListing,\n  isFull: boolean,\n): JobValidationIssue | null {\n  const missing: string[] = [];\n\n  if (!listing.externalId) missing.push(\"externalId\");\n  if (!listing.applyUrl) missing.push(\"applyUrl\");\n  if (!listing.listingUrl) missing.push(\"listingUrl\");\n\n  // Full scrapers provide complete data in listings\n  if (isFull) {\n    if (!listing.title) missing.push(\"title\");\n    if (!listing.description) missing.push(\"description\");\n    const locs = listing.locations;\n    if (!locs || locs.length === 0 || !locs.some((l) => l.text?.trim())) {\n      missing.push(\"locations\");\n    }\n  }\n\n  return missing.length > 0\n    ? { externalId: listing.externalId ?? \"(unknown)\", missingFields: missing }\n    : null;\n}\n\n// ---------------------------------------------------------------------------\n// Detail Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a single detail record for the minimum required fields.\n *\n * Required: `title`, `description`, `applyUrl`, `listingUrl`, and at least\n * one location with non-empty text.\n *\n * @param detail - The detail record to validate.\n * @returns `null` if valid, or a {@link JobValidationIssue}.\n */\nexport function validateDetail(\n  detail: ValidatableJobDetail,\n): JobValidationIssue | null {\n  const missing: string[] = [];\n\n  if (!detail.title) missing.push(\"title\");\n\n  // Accept description text or a non-zero descriptionLength (runtime uses this)\n  const hasDescription =\n    (detail.description && detail.description.length > 0) ||\n    (detail.descriptionLength != null && detail.descriptionLength > 0);\n  if (!hasDescription) missing.push(\"description\");\n\n  if (!detail.applyUrl) missing.push(\"applyUrl\");\n  if (!detail.listingUrl) missing.push(\"listingUrl\");\n\n  const locs = detail.locations;\n  if (!locs || locs.length === 0 || !locs.some((l) => l.text?.trim())) {\n    missing.push(\"locations\");\n  }\n\n  return missing.length > 0\n    ? { externalId: detail.externalId ?? \"(unknown)\", missingFields: missing }\n    : null;\n}\n\n// ---------------------------------------------------------------------------\n// Batch Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an array of listings at once and return a summary.\n *\n * @param listings - The listings to validate.\n * @param isFull   - Whether these are from a \"full\" scraper.\n * @returns A {@link ValidationSummary} with aggregate counts and per-item issues.\n */\nexport function validateListings(\n  listings: ValidatableJobListing[],\n  isFull: boolean,\n): ValidationSummary {\n  const issues: JobValidationIssue[] = [];\n  for (const listing of listings) {\n    const issue = validateListing(listing, isFull);\n    if (issue) issues.push(issue);\n  }\n  return {\n    valid: issues.length === 0,\n    total: listings.length,\n    passed: listings.length - issues.length,\n    failed: issues.length,\n    issues,\n  };\n}\n\n/**\n * Validate an array of detail records at once and return a summary.\n *\n * @param details - The detail records to validate.\n * @returns A {@link ValidationSummary} with aggregate counts and per-item issues.\n */\nexport function validateDetails(\n  details: ValidatableJobDetail[],\n): ValidationSummary {\n  const issues: JobValidationIssue[] = [];\n  for (const detail of details) {\n    const issue = validateDetail(detail);\n    if (issue) issues.push(issue);\n  }\n  return {\n    valid: issues.length === 0,\n    total: details.length,\n    passed: details.length - issues.length,\n    failed: issues.length,\n    issues,\n  };\n}\n"]}