{"version":3,"sources":["../../src/runtime/helpers.ts","../../src/testing/assertions.ts","../../src/validation/index.ts","../../src/testing/TestRunner.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @jobo/scraper-sdk/runtime — Common Scraping Helpers\n *\n * Utility functions for common scraping patterns: URL manipulation,\n * data normalization, pagination, and defensive parsing.\n */\n\nimport type {\n  JobLocation,\n  MetadataValue,\n  PaginationCursor,\n} from \"../types/models\";\n\nimport type { UrlFieldExtractor } from \"../types/provider\";\n\n// ---------------------------------------------------------------------------\n// Pagination helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Create an offset-based pagination cursor.\n *\n * @param offset — The offset value for the next page.\n * @returns A {@link PaginationCursor} with `type: \"offset\"`.\n *\n * @example\n * ```ts\n * return { listings, hasMore: true, nextCursor: offsetCursor(50) };\n * ```\n */\nexport function offsetCursor(offset: number): PaginationCursor {\n  return { type: \"offset\", value: String(offset) };\n}\n\n/**\n * Create a page-number pagination cursor.\n *\n * @param page — The page number for the next page (1-indexed).\n * @returns A {@link PaginationCursor} with `type: \"page\"`.\n */\nexport function pageCursor(page: number): PaginationCursor {\n  return { type: \"page\", value: String(page) };\n}\n\n/**\n * Create an opaque cursor-based pagination cursor.\n *\n * @param cursor — The opaque cursor string from the API response.\n * @returns A {@link PaginationCursor} with `type: \"cursor\"`.\n */\nexport function cursorPagination(cursor: string): PaginationCursor {\n  return { type: \"cursor\", value: cursor };\n}\n\n/**\n * Create a URL-based pagination cursor (next page URL).\n *\n * @param url — The full URL of the next page.\n * @returns A {@link PaginationCursor} with `type: \"url\"`.\n */\nexport function urlCursor(url: string): PaginationCursor {\n  return { type: \"url\", value: url };\n}\n\n/**\n * Parse the current page number from a cursor, defaulting to 1.\n *\n * @param cursor — The pagination cursor, or `undefined` for the first page.\n * @returns The page number as an integer.\n */\nexport function currentPage(cursor?: PaginationCursor): number {\n  if (!cursor) return 1;\n  const parsed = parseInt(cursor.value, 10);\n  return isNaN(parsed) ? 1 : parsed;\n}\n\n/**\n * Parse the current offset from a cursor, defaulting to 0.\n *\n * @param cursor — The pagination cursor, or `undefined` for the first page.\n * @returns The offset as an integer.\n */\nexport function currentOffset(cursor?: PaginationCursor): number {\n  if (!cursor) return 0;\n  const parsed = parseInt(cursor.value, 10);\n  return isNaN(parsed) ? 0 : parsed;\n}\n\n// ---------------------------------------------------------------------------\n// Location helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a raw location string into a {@link JobLocation} object.\n *\n * Handles common formats:\n * - `\"San Francisco, CA\"` → `{ text, city, state }`\n * - `\"San Francisco, CA, US\"` → `{ text, city, state, country }`\n * - `\"Remote\"` → `{ text, isRemote: true }`\n * - `\"Remote (US)\"` → `{ text, isRemote: true, country: \"US\" }`\n * - `\"New York, NY 10001\"` → `{ text, city, state, postalCode }`\n *\n * This is a best-effort parser. The platform's location pipeline will\n * further normalize and geocode the result.\n *\n * @param text — Raw location string as displayed on the page.\n * @returns A {@link JobLocation} with as many fields populated as possible.\n */\nexport function parseLocation(text: string): JobLocation {\n  if (!text || !text.trim()) return { text: text || \"\" };\n\n  const trimmed = text.trim();\n  const location: JobLocation = { text: trimmed };\n\n  // Detect remote\n  if (/\\bremote\\b/i.test(trimmed)) {\n    location.isRemote = true;\n    // Extract country from \"Remote (US)\" or \"Remote - US\"\n    const remoteCountry = trimmed.match(\n      /remote\\s*[\\(\\-–—]\\s*([A-Z]{2,})\\s*\\)?/i,\n    );\n    if (remoteCountry) {\n      location.country = remoteCountry[1].toUpperCase();\n    }\n    return location;\n  }\n\n  // Try \"City, State ZIP, Country\" or \"City, State, Country\" or \"City, State\"\n  const parts = trimmed\n    .split(\",\")\n    .map((p) => p.trim())\n    .filter(Boolean);\n\n  if (parts.length >= 1) {\n    location.city = parts[0];\n  }\n\n  if (parts.length >= 2) {\n    // Second part might be \"State ZIP\" or just \"State\" or \"Country\"\n    const stateZip = parts[1].match(/^([A-Z]{2})\\s+(\\d{5}(?:-\\d{4})?)$/);\n    if (stateZip) {\n      location.state = stateZip[1];\n      location.postalCode = stateZip[2];\n    } else if (/^[A-Z]{2}$/.test(parts[1])) {\n      // Could be US state or country code — treat as state if there's a third part\n      if (parts.length >= 3) {\n        location.state = parts[1];\n      } else {\n        location.state = parts[1];\n      }\n    } else {\n      location.state = parts[1];\n    }\n  }\n\n  if (parts.length >= 3) {\n    location.country = parts[parts.length - 1];\n  }\n\n  return location;\n}\n\n/**\n * Parse multiple locations from a delimited string.\n *\n * Handles common multi-location formats:\n * - `\"San Francisco, CA; New York, NY\"` (semicolon-separated)\n * - `\"San Francisco, CA | New York, NY\"` (pipe-separated)\n * - `\"San Francisco, CA / New York, NY\"` (slash-separated)\n *\n * @param text — Raw multi-location string.\n * @param delimiter — Delimiter regex. Defaults to `; | /` with surrounding whitespace.\n * @returns Array of parsed {@link JobLocation} objects.\n */\nexport function parseLocations(\n  text: string,\n  delimiter?: RegExp,\n): JobLocation[] {\n  if (!text || !text.trim()) return [];\n\n  const delim = delimiter ?? /\\s*[;|/]\\s*/;\n  return text\n    .split(delim)\n    .map((s) => s.trim())\n    .filter(Boolean)\n    .map(parseLocation);\n}\n\n// ---------------------------------------------------------------------------\n// Data normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Safely extract a string value from an unknown data source.\n * Returns `undefined` if the value is null, undefined, or not a string.\n *\n * @param value — The value to extract.\n * @returns The string value, or `undefined`.\n */\nexport function asString(value: unknown): string | undefined {\n  if (value === null || value === undefined) return undefined;\n  if (typeof value === \"string\") return value || undefined;\n  if (typeof value === \"number\" || typeof value === \"boolean\")\n    return String(value);\n  return undefined;\n}\n\n/**\n * Safely extract a number value from an unknown data source.\n *\n * @param value — The value to extract.\n * @returns The number value, or `undefined` if not a valid number.\n */\nexport function asNumber(value: unknown): number | undefined {\n  if (value === null || value === undefined) return undefined;\n  if (typeof value === \"number\") return isNaN(value) ? undefined : value;\n  if (typeof value === \"string\") {\n    const parsed = parseFloat(value);\n    return isNaN(parsed) ? undefined : parsed;\n  }\n  return undefined;\n}\n\n/**\n * Safely extract an array of strings from an unknown data source.\n *\n * @param value — The value to extract. Can be a string (split by comma) or an array.\n * @returns Array of non-empty strings.\n */\nexport function asStringArray(value: unknown): string[] {\n  if (!value) return [];\n  if (typeof value === \"string\")\n    return value\n      .split(\",\")\n      .map((s) => s.trim())\n      .filter(Boolean);\n  if (Array.isArray(value)) return value.map((v) => String(v)).filter(Boolean);\n  return [];\n}\n\n/**\n * Build a metadata map from key-value pairs, skipping null/undefined values.\n *\n * @param entries — Key-value pairs to include.\n * @returns A clean metadata map with only defined values.\n *\n * @example\n * ```ts\n * const meta = buildMeta(\n *   [\"employmentType\", job.type],\n *   [\"department\", job.dept],\n *   [\"salary\", undefined], // skipped\n * );\n * // → { employmentType: \"Full-time\", department: \"Engineering\" }\n * ```\n */\nexport function buildMeta(\n  ...entries: [string, MetadataValue | undefined][]\n): Record<string, MetadataValue> {\n  const result: Record<string, MetadataValue> = {};\n  for (const [key, value] of entries) {\n    if (value !== undefined && value !== null && value !== \"\") {\n      result[key] = value;\n    }\n  }\n  return result;\n}\n\n// ---------------------------------------------------------------------------\n// Date helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Attempt to parse a date string into an ISO 8601 string.\n * Returns `undefined` if the input cannot be parsed.\n *\n * Handles common formats:\n * - ISO 8601: `\"2025-01-15T00:00:00Z\"`\n * - US format: `\"01/15/2025\"`, `\"January 15, 2025\"`\n * - Relative: `\"3 days ago\"`, `\"Posted yesterday\"`\n *\n * @param dateStr — Raw date string from the ATS.\n * @returns ISO 8601 date string, or `undefined`.\n */\nexport function parseDate(\n  dateStr: string | null | undefined,\n): string | undefined {\n  if (!dateStr) return undefined;\n\n  const trimmed = dateStr.trim();\n  if (!trimmed) return undefined;\n\n  // Already ISO 8601\n  if (/^\\d{4}-\\d{2}-\\d{2}/.test(trimmed)) {\n    const d = new Date(trimmed);\n    return isNaN(d.getTime()) ? undefined : d.toISOString();\n  }\n\n  // Relative dates: \"X days ago\", \"yesterday\", \"today\"\n  const relativeMatch = trimmed.match(\n    /(\\d+)\\s+(day|hour|minute|week|month)s?\\s+ago/i,\n  );\n  if (relativeMatch) {\n    const amount = parseInt(relativeMatch[1], 10);\n    const unit = relativeMatch[2].toLowerCase();\n    const now = new Date();\n    switch (unit) {\n      case \"minute\":\n        now.setMinutes(now.getMinutes() - amount);\n        break;\n      case \"hour\":\n        now.setHours(now.getHours() - amount);\n        break;\n      case \"day\":\n        now.setDate(now.getDate() - amount);\n        break;\n      case \"week\":\n        now.setDate(now.getDate() - amount * 7);\n        break;\n      case \"month\":\n        now.setMonth(now.getMonth() - amount);\n        break;\n    }\n    return now.toISOString();\n  }\n\n  if (/yesterday/i.test(trimmed)) {\n    const d = new Date();\n    d.setDate(d.getDate() - 1);\n    return d.toISOString();\n  }\n\n  if (/today/i.test(trimmed)) {\n    return new Date().toISOString();\n  }\n\n  // Try native Date parsing as fallback\n  const d = new Date(trimmed);\n  return isNaN(d.getTime()) ? undefined : d.toISOString();\n}\n\n// ---------------------------------------------------------------------------\n// Deduplication\n// ---------------------------------------------------------------------------\n\n/**\n * Deduplicate an array of listings by `externalId`.\n * The first occurrence of each ID is kept.\n *\n * @param listings — Array of listings that may contain duplicates.\n * @returns Deduplicated array.\n */\nexport function deduplicateListings<T extends { externalId: string }>(\n  listings: T[],\n): T[] {\n  const seen = new Set<string>();\n  return listings.filter((listing) => {\n    if (seen.has(listing.externalId)) return false;\n    seen.add(listing.externalId);\n    return true;\n  });\n}\n\n// ---------------------------------------------------------------------------\n// URL helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Match a URL against a glob-style pattern.\n * Supports `*` (single path segment), `**` (any path), and `:param` (named segment).\n *\n * When the pattern does not contain `?`, query strings and fragments are\n * stripped from the URL before matching (mirrors C# UrlPatternMatcher).\n */\nexport function matchesUrlPattern(url: string, pattern: string): boolean {\n  try {\n    const urlToMatch = pattern.includes(\"?\") ? url : url.replace(/[?#].*$/, \"\");\n\n    const regexPattern =\n      \"^\" +\n      escapeRegex(pattern)\n        .replace(/\\\\\\*\\\\\\*/g, \"@@DOUBLESTAR@@\")\n        .replace(/\\\\\\*/g, \"[^/]*\")\n        .replace(/@@DOUBLESTAR@@/g, \".*\")\n        .replace(/\\\\:/g, \"[^/]+\") +\n      \"$\";\n    return new RegExp(regexPattern, \"i\").test(urlToMatch);\n  } catch {\n    return false;\n  }\n}\n\n/** Check if a URL matches any of the given patterns. */\nexport function matchesAnyUrlPattern(url: string, patterns: string[]): boolean {\n  return patterns.some((p) => matchesUrlPattern(url, p));\n}\n\n/**\n * Extract a specific named parameter from a URL using a single glob-style template.\n *\n * When the template does not explicitly handle query parameters (i.e. contains\n * no `?`), query strings and fragments are stripped from the URL before matching.\n * This mirrors the C# `UrlPatternMatcher.NormalizeUrl` behaviour and prevents\n * tracking parameters like `?source=jobright` or `?utm_source=...` from\n * breaking otherwise valid extractions.\n */\nexport function extractNamedParam(\n  url: string,\n  pattern: string,\n  paramName: string,\n): string | undefined {\n  // Strip query string / fragment when the template doesn't handle them,\n  // matching the C# UrlPatternMatcher.NormalizeUrl behaviour.\n  const urlToMatch = pattern.includes(\"?\") ? url : url.replace(/[?#].*$/, \"\");\n\n  const paramNames: string[] = [];\n  const regexStr =\n    \"^\" +\n    pattern\n      .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n      .replace(/\\*\\*/g, \".*\")\n      .replace(/\\*/g, \"[^/]*\")\n      .replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, (_match, name) => {\n        paramNames.push(name);\n        return \"([^/?#&]+)\";\n      }) +\n    \"$\";\n\n  try {\n    const match = new RegExp(regexStr, \"i\").exec(urlToMatch);\n    if (!match) return undefined;\n    const idx = paramNames.indexOf(paramName);\n    if (idx === -1) return undefined;\n    return match[idx + 1];\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a named parameter from a URL by trying multiple glob-style templates.\n * Returns the first successful extraction.\n */\nexport function extractParamFromUrl(\n  url: string,\n  patterns: string[],\n  paramName: string,\n): string | undefined {\n  for (const pattern of patterns) {\n    const result = extractNamedParam(url, pattern, paramName);\n    if (result !== undefined) return result;\n  }\n  return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Structured field extraction (new extractor-based framework)\n// ---------------------------------------------------------------------------\n\n/** Default character class for :paramName captures in urlPattern. */\nconst DEFAULT_SEGMENT_CHARSET = \"a-zA-Z0-9_~%.-\";\n\n/**\n * Normalize extractor(s) to a flat array.\n * Accepts a single extractor, an array of extractors, or undefined.\n */\nexport function toExtractorArray(\n  v: UrlFieldExtractor | UrlFieldExtractor[] | undefined | null,\n): UrlFieldExtractor[] {\n  if (!v) return [];\n  return Array.isArray(v) ? v : [v];\n}\n\n/**\n * Match a Simplify-style path pattern against a URL pathname and return\n * all named captures. Returns undefined if no match.\n *\n * Pattern syntax:\n * - `:paramName` — Named capture (one path segment)\n * - `*` — Wildcard (one unnamed path segment)\n * - `**` — Globstar (zero or more path segments)\n * - `(/*)` — Optional trailing path (zero or more segments at end)\n *\n * @param pathname — The URL pathname to match (e.g. `/acme/jobs/123`).\n * @param pattern — The path pattern (e.g. `/:companyId/jobs/:jobId(/*)`).\n * @param options — Optional extraction options.\n * @param options.segmentCharset — Custom character class for the target param capture.\n * @param options.targetParam — Which named param gets the custom charset (others use default).\n * @returns Map of param names to captured values, or undefined if no match.\n */\nexport function matchPathPattern(\n  pathname: string,\n  pattern: string,\n  options?: { segmentCharset?: string; targetParam?: string },\n): Record<string, string> | undefined {\n  const defaultCaptureGroup = `([${DEFAULT_SEGMENT_CHARSET}]+)`;\n  const targetCharset = options?.segmentCharset || DEFAULT_SEGMENT_CHARSET;\n  const targetCaptureGroup = `([${targetCharset}]+)`;\n  const targetParam = options?.targetParam;\n\n  // Build regex from pattern\n  const paramNames: string[] = [];\n\n  // Handle optional trailing (/*)\n  let corePattern = pattern;\n  let optionalTrailing = false;\n  if (corePattern.endsWith(\"(/*)\") || corePattern.endsWith(\"(/**)\")) {\n    optionalTrailing = true;\n    corePattern = corePattern.replace(/\\(\\/\\*{1,2}\\)$/, \"\");\n  }\n\n  // Also handle trailing /** or /* after the optional suffix is removed\n  let trailingWild = \"\";\n  if (corePattern.endsWith(\"/**\")) {\n    trailingWild = \"/**\";\n    corePattern = corePattern.slice(0, -3);\n  } else if (corePattern.endsWith(\"/*\")) {\n    trailingWild = \"/*\";\n    corePattern = corePattern.slice(0, -2);\n  }\n\n  let regexStr =\n    \"^\" +\n    corePattern\n      // Escape regex special chars except * and :\n      .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n      // Handle ** (globstar) — must be done before * (single wildcard)\n      .replace(/\\*\\*/g, \"@@DOUBLESTAR@@\")\n      // Handle * (single segment wildcard)\n      .replace(/\\*/g, \"[^/]+\")\n      .replace(/@@DOUBLESTAR@@/g, \"(?:.+)?\")\n      // Handle :paramName captures\n      .replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, (_match, name) => {\n        paramNames.push(name);\n        // Apply custom charset only to the target param; others use default.\n        return targetParam && name === targetParam\n          ? targetCaptureGroup\n          : defaultCaptureGroup;\n      });\n\n  // Add trailing wild portion\n  if (trailingWild === \"/**\") {\n    regexStr += \"(?:/.*)?\";\n  } else if (trailingWild === \"/*\") {\n    regexStr += \"(?:/[^/]+)?\";\n  }\n\n  // Add optional trailing\n  if (optionalTrailing) {\n    regexStr += \"(?:/.*)?\";\n  }\n\n  regexStr += \"$\";\n\n  try {\n    const match = new RegExp(regexStr, \"i\").exec(pathname);\n    if (!match) return undefined;\n\n    const result: Record<string, string> = {};\n    for (let i = 0; i < paramNames.length; i++) {\n      if (match[i + 1] !== undefined) {\n        result[paramNames[i]] = match[i + 1];\n      }\n    }\n    return result;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a field value from a URL using a single extractor definition.\n *\n * @param url — The full URL to extract from.\n * @param extractor — The extractor definition.\n * @param defaultParam — Default param name to look for in path captures (e.g. \"companyId\").\n * @returns The extracted value, or undefined if extraction fails.\n */\nexport function applyExtractor(\n  url: string,\n  extractor: UrlFieldExtractor,\n  defaultParam: string,\n): string | undefined {\n  try {\n    const parsed = new URL(url);\n\n    // Mode 1: Query parameter extraction\n    if (extractor.searchParam) {\n      const value = parsed.searchParams.get(extractor.searchParam);\n      return value || undefined;\n    }\n\n    // Mode 2: Hostname segment extraction\n    if (extractor.hostSegment !== undefined) {\n      const segments = parsed.hostname.split(\".\");\n      const value = segments[extractor.hostSegment];\n      return value || undefined;\n    }\n\n    // Mode 3: Path pattern extraction\n    if (extractor.urlPattern) {\n      const paramName = extractor.param || defaultParam;\n      const captures = matchPathPattern(parsed.pathname, extractor.urlPattern, {\n        segmentCharset: extractor.segmentCharset,\n        targetParam: paramName,\n      });\n      if (!captures) return undefined;\n      return captures[paramName] || undefined;\n    }\n\n    return undefined;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a field value from a URL by trying extractors in order.\n * Returns the first successful extraction.\n *\n * @param url — The full URL to extract from.\n * @param extractors — Array of extractor definitions (tried in order).\n * @param defaultParam — Default param name (e.g. \"companyId\" or \"jobId\").\n * @returns The extracted value, or null if no extractor matches.\n */\nexport function extractFieldFromUrl(\n  url: string,\n  extractors: UrlFieldExtractor[],\n  defaultParam: string,\n): string | undefined {\n  for (const extractor of extractors) {\n    const result = applyExtractor(url, extractor, defaultParam);\n    if (result !== undefined) return result;\n  }\n  return undefined;\n}\n\n/**\n * Build a canonical listing URL from a job/detail URL using extractors.\n *\n * For path-based extractors: reconstructs the URL using only the path up to\n * the companyId capture (strips trailing job-specific path).\n *\n * For query/subdomain extractors: returns the origin (scheme + host) as the\n * listing base.\n *\n * @param url — The full URL to derive the listing URL from.\n * @param extractors — The companyId extractors.\n * @returns The canonical listing URL, or null.\n */\nexport function buildListingUrlFromExtractors(\n  url: string,\n  extractors: UrlFieldExtractor[],\n): string | null {\n  try {\n    const parsed = new URL(url);\n    let anyMatched = false;\n\n    for (const extractor of extractors) {\n      // Collect all named captures from the extractor match\n      let captures: Record<string, string> = {};\n\n      if (extractor.urlPattern) {\n        const paramName = extractor.param || \"companyId\";\n        const result = matchPathPattern(parsed.pathname, extractor.urlPattern, {\n          segmentCharset: extractor.segmentCharset,\n          targetParam: paramName,\n        });\n        if (!result) continue;\n        captures = result;\n        if (!captures[paramName]) continue;\n      } else if (extractor.hostSegment !== undefined) {\n        const value = applyExtractor(url, extractor, \"companyId\");\n        if (!value) continue;\n        captures = { companyId: value };\n      } else if (extractor.searchParam) {\n        const value = parsed.searchParams.get(extractor.searchParam);\n        if (!value) continue;\n        captures = { companyId: value };\n      } else {\n        continue;\n      }\n\n      anyMatched = true;\n\n      // Use the explicit template if provided\n      if (extractor.listingUrlTemplate !== undefined) {\n        const filled = extractor.listingUrlTemplate.replace(\n          /:([a-zA-Z][a-zA-Z0-9_]*)/g,\n          (_, name) =>\n            captures[name] !== undefined ? captures[name] : `:${name}`,\n        );\n        return `${parsed.origin}${filled}`;\n      }\n\n      // No template on this extractor — try the next one\n    }\n\n    // Fallback: if anything matched but no template found, return origin\n    return anyMatched ? parsed.origin : null;\n  } catch {\n    return null;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// High-level URL info extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Result of extracting provider info from a URL using manifest patterns.\n * Pure pattern-matching — no HTTP requests or scraping.\n */\nexport interface UrlExtractionResult {\n  /** Whether the URL matches any of the provider's urlPatterns. */\n  urlPatternMatched: boolean;\n  /** Extracted job ID from jobIdExtractor, or null. */\n  jobId: string | null;\n  /** Extracted company ID from companyIdExtractor, or null. */\n  companyId: string | null;\n  /** Canonical listing URL reconstructed from companyIdExtractor, or null. */\n  listingUrl: string | null;\n}\n\n/**\n * Extract provider info (companyId, jobId, listingUrl) from a URL using\n * structured extractors. Pure pattern-matching — no HTTP requests or scraping.\n *\n * @param url — The URL to extract from (e.g. a job listing URL).\n * @param urlPatterns — Glob patterns that identify the provider (from manifest `urlPatterns`).\n * @param companyIdExtractors — Structured extractors for company ID (from manifest `companyIdExtractor`).\n * @param jobIdExtractors — Structured extractors for job ID (from manifest `jobIdExtractor`).\n * @returns Extraction result with matched flag, companyId, jobId, and canonical listingUrl.\n *\n * @example\n * ```ts\n * const result = extractUrlInfo(\n *   \"https://jobs.ashbyhq.com/ramp/abc-def-123\",\n *   [\"https://jobs.ashbyhq.com/**\"],\n *   [{ urlPattern: \"/:companyId(/**)\" }],\n *   [{ urlPattern: \"/:companyId/:jobId(/*)\" }],\n * );\n * // → { urlPatternMatched: true, companyId: \"ramp\", jobId: \"abc-def-123\",\n * //     listingUrl: \"https://jobs.ashbyhq.com/ramp\" }\n * ```\n */\nexport function extractUrlInfo(\n  url: string,\n  urlPatterns: string[],\n  companyIdExtractors: UrlFieldExtractor[],\n  jobIdExtractors: UrlFieldExtractor[],\n  excludeUrlPatterns?: string[],\n): UrlExtractionResult {\n  if (\n    excludeUrlPatterns &&\n    excludeUrlPatterns.length > 0 &&\n    matchesAnyUrlPattern(url, excludeUrlPatterns)\n  ) {\n    return {\n      urlPatternMatched: false,\n      jobId: null,\n      companyId: null,\n      listingUrl: null,\n    };\n  }\n\n  const companyId =\n    extractFieldFromUrl(url, companyIdExtractors, \"companyId\") ?? null;\n  const jobId = extractFieldFromUrl(url, jobIdExtractors, \"jobId\") ?? null;\n  const listingUrl = buildListingUrlFromExtractors(url, companyIdExtractors);\n\n  return {\n    urlPatternMatched: matchesAnyUrlPattern(url, urlPatterns),\n    jobId,\n    companyId,\n    listingUrl,\n  };\n}\n\n/**\n * Extract a path segment from a URL by position (0-indexed from the path root).\n *\n * @param url — The URL to extract from.\n * @param index — The segment index (0 = first path segment).\n * @returns The path segment, or `undefined` if the index is out of bounds.\n *\n * @example\n * ```ts\n * extractPathSegment(\"https://boards.greenhouse.io/acme/jobs/123\", 0);\n * // → \"acme\"\n *\n * extractPathSegment(\"https://boards.greenhouse.io/acme/jobs/123\", 2);\n * // → \"123\"\n * ```\n */\nexport function extractPathSegment(\n  url: string,\n  index: number,\n): string | undefined {\n  try {\n    const segments = new URL(url).pathname.split(\"/\").filter(Boolean);\n    return segments[index];\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Build a URL from a base and path segments, with optional query parameters.\n *\n * @param base — Base URL (e.g. `\"https://api.ats.com\"`).\n * @param path — Path segments to join (e.g. `[\"v1\", \"jobs\"]`).\n * @param params — Optional query parameters.\n * @returns The constructed URL string.\n *\n * @example\n * ```ts\n * buildUrl(\"https://api.ats.com\", [\"v1\", \"jobs\"], { page: \"2\", limit: \"20\" });\n * // → \"https://api.ats.com/v1/jobs?page=2&limit=20\"\n * ```\n */\nexport function buildUrl(\n  base: string,\n  path: string[],\n  params?: Record<string, string | number | boolean | undefined>,\n): string {\n  const cleanBase = base.replace(/\\/+$/, \"\");\n  const cleanPath = path.map((s) => encodeURIComponent(s)).join(\"/\");\n  let url = `${cleanBase}/${cleanPath}`;\n\n  if (params) {\n    const searchParams = new URLSearchParams();\n    for (const [key, value] of Object.entries(params)) {\n      if (value !== undefined && value !== null) {\n        searchParams.set(key, String(value));\n      }\n    }\n    const qs = searchParams.toString();\n    if (qs) url += `?${qs}`;\n  }\n\n  return url;\n}\n\nfunction escapeRegex(str: string): string {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","/**\n * @jobo/scraper-sdk/testing — Typed test case definitions and assertion helpers.\n *\n * tests.json supports five test types:\n *\n * - \"urlPattern\"   — offline check: does the URL match the provider's urlPatterns?\n * - \"validation\"   — calls provider.validate(url), checks isValid + companyId + baseUrl extracted\n * - \"jobId\"        — uses manifest jobIdExtractor definitions to extract jobId + companyId from a job detail URL\n * - \"listingUrl\"   — offline: reconstructs the canonical listing URL from a job detail URL via companyIdExtractor definitions\n * - \"scrape\"       — full live scrape, checks listing/detail counts, field presence, and meta keys\n */\n\nimport {\n  matchesUrlPattern,\n  matchesAnyUrlPattern,\n  extractNamedParam,\n  extractParamFromUrl,\n  extractUrlInfo,\n  toExtractorArray,\n  extractFieldFromUrl,\n  buildListingUrlFromExtractors,\n} from \"../runtime/helpers\";\nimport type { UrlFieldExtractor } from \"../types/provider\";\nexport type { UrlExtractionResult } from \"../runtime/helpers\";\n\nexport {\n  matchesUrlPattern,\n  matchesAnyUrlPattern,\n  extractNamedParam,\n  extractParamFromUrl,\n  extractUrlInfo,\n  toExtractorArray,\n  extractFieldFromUrl,\n  buildListingUrlFromExtractors,\n};\n\n// ---------------------------------------------------------------------------\n// URL Pattern Matching (mirrors backend UrlPatternMatcher)\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Typed Test Cases\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that the URL matches (or doesn't match) the provider's urlPatterns.\n * Offline — no HTTP requests made.\n *\n * @example\n * { \"type\": \"urlPattern\", \"name\": \"Bare domain matches\", \"url\": \"https://acme.applytojob.com\", \"expect\": { \"matches\": true } }\n */\nexport interface UrlPatternTestCase {\n  type: \"urlPattern\";\n  name: string;\n  url: string;\n  expect: {\n    /** Whether the URL should match the provider's urlPatterns. */\n    matches: boolean;\n  };\n}\n\n/**\n * Calls provider.validate(url) and checks the result.\n * Makes no HTTP requests — validate() is synchronous/cheap.\n *\n * @example\n * { \"type\": \"validation\", \"name\": \"Extracts company ID\", \"url\": \"https://acme.applytojob.com\", \"expect\": { \"isValid\": true, \"companyId\": \"acme\" } }\n */\nexport interface ValidationTestCase {\n  type: \"validation\";\n  name: string;\n  url: string;\n  expect: {\n    /** Whether validate() should return isValid: true. */\n    isValid: boolean;\n    /**\n     * Expected companyId extracted from the URL.\n     * Set to `null` to assert that no companyId is extracted.\n     * Omit entirely to skip this assertion.\n     */\n    companyId?: string | null;\n    /** Expected baseUrl returned. */\n    baseUrl?: string;\n  };\n}\n\n/**\n * Calls provider.validate(jobDetailUrl) and checks the jobId extracted.\n * Uses the provider's jobIdExtractor definitions (or validate() result) to extract the job ID.\n * Makes no HTTP requests.\n *\n * @example\n * { \"type\": \"jobId\", \"name\": \"Extracts job ID from detail URL\", \"url\": \"https://boards.greenhouse.io/acme/jobs/12345\", \"expect\": { \"jobId\": \"12345\" } }\n */\nexport interface JobIdTestCase {\n  type: \"jobId\";\n  name: string;\n  /** A job detail URL (e.g. the applyUrl or listingUrl of a specific job). */\n  url: string;\n  expect: {\n    /** Expected jobId extracted from the URL. */\n    jobId: string;\n    /** Expected companyId extracted from the URL (optional). */\n    companyId?: string;\n  };\n}\n\n/**\n * Runs a full live scrape and checks listing/detail counts, field presence, and meta keys.\n * Makes real HTTP requests to the ATS.\n *\n * @example\n * { \"type\": \"scrape\", \"name\": \"Anthropic jobs\", \"url\": \"https://job-boards.greenhouse.io/anthropic\", \"maxPages\": 1, \"maxDetails\": 3, \"expect\": { \"minListings\": 1, \"listingHasFields\": [\"externalId\", \"applyUrl\"], \"detailHasFields\": [\"title\", \"description\"], \"detailMetaHasKeys\": [\"employment_type\", \"department\"] } }\n */\nexport interface ScrapeTestCase {\n  type: \"scrape\";\n  name: string;\n  url: string;\n  /** Max listing pages to scrape. Default: 1. */\n  maxPages?: number;\n  /** Max detail fetches (split scrapers). Default: 3. */\n  maxDetails?: number;\n  expect: {\n    /** Minimum number of listings expected. */\n    minListings?: number;\n    /** Fields that must be present on every listing. */\n    listingHasFields?: string[];\n    /** Fields that must be present on every detail. */\n    detailHasFields?: string[];\n    /**\n     * Meta keys that must be present (non-null, non-empty) on at least one detail.\n     * Checks the `meta` map on each detail record.\n     * @example [\"employment_type\", \"department\", \"is_remote\"]\n     */\n    detailMetaHasKeys?: string[];\n  };\n}\n\n/**\n * Reconstructs the canonical listing URL from a job detail URL using the\n * provider's companyIdExtractor definitions. Offline — no HTTP requests made.\n *\n * @example\n * { \"type\": \"listingUrl\", \"name\": \"Builds listing URL from job URL\", \"url\": \"https://boards.greenhouse.io/acme/jobs/12345\", \"expect\": { \"listingUrl\": \"https://boards.greenhouse.io/acme\" } }\n */\nexport interface ListingUrlTestCase {\n  type: \"listingUrl\";\n  name: string;\n  /** A job detail URL or any sub-page URL. */\n  url: string;\n  expect: {\n    /** The expected canonical listing URL. */\n    listingUrl: string;\n  };\n}\n\n/**\n * A single test case in tests.json. Discriminated by `type`.\n */\nexport type ProviderTestCase =\n  | UrlPatternTestCase\n  | ValidationTestCase\n  | JobIdTestCase\n  | ListingUrlTestCase\n  | ScrapeTestCase;\n\n/**\n * The full tests.json schema.\n */\nexport interface ProviderTestSuite {\n  tests: ProviderTestCase[];\n}\n\n// ---------------------------------------------------------------------------\n// Assertion Results\n// ---------------------------------------------------------------------------\n\nexport interface AssertionResult {\n  /** Assertion name (e.g. \"urlPatternMatches\", \"companyId\", \"jobId\"). */\n  name: string;\n  /** Whether the assertion passed. */\n  passed: boolean;\n  /** Expected value (human-readable). */\n  expected: string;\n  /** Actual value (human-readable). */\n  actual: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-type assertion evaluators\n// ---------------------------------------------------------------------------\n\nexport function evaluateUrlPatternAssertions(\n  tc: UrlPatternTestCase,\n  urlPatterns: string[],\n): AssertionResult[] {\n  const matched = matchesAnyUrlPattern(tc.url, urlPatterns);\n  return [\n    {\n      name: \"urlPatternMatches\",\n      passed: matched === tc.expect.matches,\n      expected: String(tc.expect.matches),\n      actual: String(matched),\n    },\n  ];\n}\n\nexport function evaluateValidationAssertions(\n  tc: ValidationTestCase,\n  actual: { isValid: boolean; companyId?: string; baseUrl?: string },\n): AssertionResult[] {\n  const results: AssertionResult[] = [];\n\n  results.push({\n    name: \"isValid\",\n    passed: actual.isValid === tc.expect.isValid,\n    expected: String(tc.expect.isValid),\n    actual: String(actual.isValid),\n  });\n\n  if (tc.expect.companyId !== undefined) {\n    // null means \"assert nothing was extracted\"; string means \"assert this specific value\"\n    const expectedCompanyId = tc.expect.companyId ?? null;\n    const actualCompanyId = actual.companyId ?? null;\n    results.push({\n      name: \"companyId\",\n      passed: actualCompanyId === expectedCompanyId,\n      expected: expectedCompanyId ?? \"(none)\",\n      actual: actualCompanyId ?? \"(none)\",\n    });\n  }\n\n  if (tc.expect.baseUrl !== undefined) {\n    results.push({\n      name: \"baseUrl\",\n      passed: actual.baseUrl === tc.expect.baseUrl,\n      expected: tc.expect.baseUrl,\n      actual: actual.baseUrl ?? \"(none)\",\n    });\n  }\n\n  return results;\n}\n\nexport function evaluateJobIdAssertions(\n  tc: JobIdTestCase,\n  actual: { jobId?: string; companyId?: string },\n): AssertionResult[] {\n  const results: AssertionResult[] = [];\n\n  results.push({\n    name: \"jobId\",\n    passed: actual.jobId === tc.expect.jobId,\n    expected: tc.expect.jobId,\n    actual: actual.jobId ?? \"(none)\",\n  });\n\n  if (tc.expect.companyId !== undefined) {\n    results.push({\n      name: \"companyId\",\n      passed: actual.companyId === tc.expect.companyId,\n      expected: tc.expect.companyId,\n      actual: actual.companyId ?? \"(none)\",\n    });\n  }\n\n  return results;\n}\n\nexport function evaluateListingUrlAssertions(\n  tc: ListingUrlTestCase,\n  companyIdExtractors: UrlFieldExtractor[],\n): AssertionResult[] {\n  const actual =\n    buildListingUrlFromExtractors(tc.url, companyIdExtractors) ?? \"(none)\";\n  return [\n    {\n      name: \"listingUrl\",\n      passed: actual === tc.expect.listingUrl,\n      expected: tc.expect.listingUrl,\n      actual,\n    },\n  ];\n}\n\nexport function evaluateScrapeAssertions(\n  tc: ScrapeTestCase,\n  actual: {\n    totalListings: number;\n    listings: Array<Record<string, unknown>>;\n    details: Array<Record<string, unknown>>;\n  },\n): AssertionResult[] {\n  const results: AssertionResult[] = [];\n\n  if (tc.expect.minListings !== undefined) {\n    results.push({\n      name: \"minListings\",\n      passed: actual.totalListings >= tc.expect.minListings,\n      expected: `>= ${tc.expect.minListings}`,\n      actual: String(actual.totalListings),\n    });\n  }\n\n  if (tc.expect.listingHasFields && actual.listings.length > 0) {\n    for (const field of tc.expect.listingHasFields) {\n      const missing = actual.listings.filter(\n        (l) => l[field] === undefined || l[field] === null || l[field] === \"\",\n      );\n      results.push({\n        name: `listingHasField:${field}`,\n        passed: missing.length === 0,\n        expected: `all listings have \"${field}\"`,\n        actual:\n          missing.length === 0\n            ? \"all present\"\n            : `${missing.length}/${actual.listings.length} missing`,\n      });\n    }\n  }\n\n  if (tc.expect.detailHasFields && actual.details.length > 0) {\n    for (const field of tc.expect.detailHasFields) {\n      const missing = actual.details.filter(\n        (d) => d[field] === undefined || d[field] === null || d[field] === \"\",\n      );\n      results.push({\n        name: `detailHasField:${field}`,\n        passed: missing.length === 0,\n        expected: `all details have \"${field}\"`,\n        actual:\n          missing.length === 0\n            ? \"all present\"\n            : `${missing.length}/${actual.details.length} missing`,\n      });\n    }\n  }\n\n  if (tc.expect.detailMetaHasKeys && actual.details.length > 0) {\n    for (const key of tc.expect.detailMetaHasKeys) {\n      // Check that at least one detail has this meta key with a non-empty value\n      const presentCount = actual.details.filter((d) => {\n        const meta = d[\"meta\"] as Record<string, unknown> | undefined;\n        if (!meta) return false;\n        const val = meta[key];\n        return val !== undefined && val !== null && val !== \"\";\n      }).length;\n      results.push({\n        name: `detailMetaHasKey:${key}`,\n        passed: presentCount > 0,\n        expected: `at least 1 detail has meta[\"${key}\"]`,\n        actual:\n          presentCount > 0\n            ? `${presentCount}/${actual.details.length} have it`\n            : `0/${actual.details.length} have it`,\n      });\n    }\n  }\n\n  return results;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy shim — kept for backward compat with old single-type test cases\n// ---------------------------------------------------------------------------\n\n/** @deprecated Use typed test cases instead */\nexport interface TestExpectations {\n  urlPatternMatches?: boolean;\n  validationPassed?: boolean;\n  companyId?: string;\n  minListings?: number;\n  listingHasFields?: string[];\n  detailHasFields?: string[];\n}\n\n/** @deprecated Use typed test cases instead */\nexport function evaluateExpectations(\n  expectations: TestExpectations | undefined,\n  actual: {\n    urlPatternMatched: boolean;\n    validationPassed: boolean;\n    companyId: string | undefined;\n    totalListings: number;\n    listings: Array<Record<string, unknown>>;\n    details: Array<Record<string, unknown>>;\n  },\n): AssertionResult[] {\n  if (!expectations) return [];\n  const results: AssertionResult[] = [];\n\n  if (expectations.urlPatternMatches !== undefined) {\n    results.push({\n      name: \"urlPatternMatches\",\n      passed: actual.urlPatternMatched === expectations.urlPatternMatches,\n      expected: String(expectations.urlPatternMatches),\n      actual: String(actual.urlPatternMatched),\n    });\n  }\n  if (expectations.validationPassed !== undefined) {\n    results.push({\n      name: \"validationPassed\",\n      passed: actual.validationPassed === expectations.validationPassed,\n      expected: String(expectations.validationPassed),\n      actual: String(actual.validationPassed),\n    });\n  }\n  if (expectations.companyId !== undefined) {\n    results.push({\n      name: \"companyId\",\n      passed: actual.companyId === expectations.companyId,\n      expected: expectations.companyId,\n      actual: actual.companyId ?? \"(none)\",\n    });\n  }\n  if (expectations.minListings !== undefined) {\n    results.push({\n      name: \"minListings\",\n      passed: actual.totalListings >= expectations.minListings,\n      expected: `>= ${expectations.minListings}`,\n      actual: String(actual.totalListings),\n    });\n  }\n  if (expectations.listingHasFields && actual.listings.length > 0) {\n    for (const field of expectations.listingHasFields) {\n      const missing = actual.listings.filter(\n        (l) => l[field] === undefined || l[field] === null || l[field] === \"\",\n      );\n      results.push({\n        name: `listingHasField:${field}`,\n        passed: missing.length === 0,\n        expected: `all listings have \"${field}\"`,\n        actual:\n          missing.length === 0\n            ? \"all present\"\n            : `${missing.length}/${actual.listings.length} missing`,\n      });\n    }\n  }\n  if (expectations.detailHasFields && actual.details.length > 0) {\n    for (const field of expectations.detailHasFields) {\n      const missing = actual.details.filter(\n        (d) => d[field] === undefined || d[field] === null || d[field] === \"\",\n      );\n      results.push({\n        name: `detailHasField:${field}`,\n        passed: missing.length === 0,\n        expected: `all details have \"${field}\"`,\n        actual:\n          missing.length === 0\n            ? \"all present\"\n            : `${missing.length}/${actual.details.length} missing`,\n      });\n    }\n  }\n  return results;\n}\n","/**\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","/**\n * @jobo/scraper-sdk/testing — Test Runner\n *\n * A lightweight test harness for exercising scraper providers against\n * live ATS pages or mock data. Validates that the provider correctly\n * discovers listings, paginates, and extracts job details.\n *\n * The test runner creates a mock {@link ScraperContext} that uses\n * real HTTP requests (via `node-fetch` or Node's built-in `fetch`)\n * so you can test against live endpoints during development.\n */\n\nimport type {\n  ScraperProvider,\n  ValidationResult,\n  ScraperProviderManifest,\n} from \"../types/provider\";\nimport type {\n  ScraperContext,\n  HttpClient,\n  HttpRequestOptions,\n  HttpResponse,\n  BrowserClient,\n  ScraperLogger,\n} from \"../types/context\";\nimport type {\n  JobListing,\n  JobDetails,\n  ListingsResult,\n  JobDetailsResult,\n  MetadataValue,\n  MetadataMap,\n  JobLocation,\n} from \"../types/models\";\nimport type { ProviderTestSuite, AssertionResult } from \"./assertions\";\nimport {\n  evaluateUrlPatternAssertions,\n  evaluateValidationAssertions,\n  evaluateJobIdAssertions,\n  evaluateListingUrlAssertions,\n  evaluateScrapeAssertions,\n} from \"./assertions\";\nimport {\n  toExtractorArray,\n  extractFieldFromUrl,\n  buildListingUrlFromExtractors,\n} from \"../runtime/helpers\";\nimport {\n  validateListing as sharedValidateListing,\n  validateDetail as sharedValidateDetail,\n  isValidUrl,\n} from \"../validation\";\n\n// ---------------------------------------------------------------------------\n// Test Configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration for a test run.\n */\nexport interface TestConfig {\n  /** The scraper provider instance to test. */\n  provider: ScraperProvider;\n  /** The ATS URL to test against. */\n  url: string;\n  /** Company identifier (if not extractable from the URL). */\n  companyId?: string;\n  /** Maximum number of listing pages to scrape. Default: `2`. */\n  maxPages?: number;\n  /** Maximum number of detail fetches to perform (split scrapers). Default: `3`. */\n  maxDetails?: number;\n  /** Additional config to pass to the scraper context. */\n  config?: Record<string, MetadataValue>;\n  /** Whether to print verbose output. Default: `true`. */\n  verbose?: boolean;\n}\n\n/**\n * Result of a test run.\n */\nexport interface TestResult {\n  /** Whether all test phases passed. */\n  success: boolean;\n  /** Validation result. */\n  validation: ValidationResult;\n  /** Total listings discovered across all pages. */\n  totalListings: number;\n  /** Number of pages scraped. */\n  pagesScraped: number;\n  /** Detail results (split scrapers only). */\n  detailResults: JobDetailsResult[];\n  /** All listings collected. */\n  listings: JobListing[];\n  /** Validation issues found in the scraped data. */\n  issues: TestIssue[];\n  /** Total duration in milliseconds. */\n  duration: number;\n  /** Metadata quality score (0-100). */\n  qualityScore?: QualityScore;\n}\n\n/**\n * Metadata quality assessment for a scraper run.\n */\nexport interface QualityScore {\n  /** Overall score 0-100. */\n  overall: number;\n  /** Score breakdown by category. */\n  breakdown: {\n    /** Do listings have required fields? (0-100) */\n    requiredFields: number;\n    /** Do details have descriptions, locations, titles? (0-100) */\n    detailCompleteness: number;\n    /** How much structured metadata is captured? (0-100) */\n    metadataRichness: number;\n    /** Are URLs valid and not duplicated? (0-100) */\n    dataIntegrity: number;\n  };\n  /** Human-readable summary of quality findings. */\n  findings: string[];\n}\n\n/**\n * A data quality issue found during testing.\n */\nexport interface TestIssue {\n  /** Severity level. */\n  severity: \"error\" | \"warning\" | \"info\";\n  /** Which phase the issue was found in. */\n  phase: \"validation\" | \"listings\" | \"details\";\n  /** Human-readable description. */\n  message: string;\n  /** The listing or detail that triggered the issue, if applicable. */\n  externalId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Test Runner\n// ---------------------------------------------------------------------------\n\n/**\n * Run a comprehensive test of a scraper provider.\n *\n * Phases:\n * 1. **Validation** — Calls `provider.validate(url)` and checks the result.\n * 2. **Initialization** — Calls `provider.initialize(ctx)` if defined.\n * 3. **Listings** — Calls `provider.scrapeListings(ctx)` up to `maxPages` times.\n * 4. **Details** — For split scrapers, calls `provider.scrapeJobDetails(ctx, listing)`\n *    for up to `maxDetails` listings.\n * 5. **Data Quality** — Validates required fields, deduplication, and data integrity.\n *\n * @param config — Test configuration.\n * @returns Test result with all collected data and any issues.\n *\n * @example\n * ```ts\n * import { runTest } from \"@jobo/scraper-sdk/testing\";\n * import provider from \"./src/provider\";\n *\n * const result = await runTest({\n *   provider,\n *   url: \"https://boards.greenhouse.io/acme\",\n *   maxPages: 2,\n *   maxDetails: 5,\n * });\n *\n * if (result.success) {\n *   console.log(`✓ Found ${result.totalListings} listings`);\n * } else {\n *   console.error(\"Issues:\", result.issues);\n * }\n * ```\n */\nexport async function runTest(config: TestConfig): Promise<TestResult> {\n  const startTime = Date.now();\n  const verbose = config.verbose ?? true;\n  const maxPages = config.maxPages ?? 2;\n  const maxDetails = config.maxDetails ?? 3;\n  const issues: TestIssue[] = [];\n  const allListings: JobListing[] = [];\n  const detailResults: JobDetailsResult[] = [];\n\n  const log = (msg: string) => {\n    if (verbose) console.log(msg);\n  };\n\n  // --- Phase 1: Validation ---\n  log(\"\\n━━━ Phase 1: Validation ━━━\");\n  let validation: ValidationResult;\n  try {\n    validation = await config.provider.validate(config.url);\n    log(`  isValid: ${validation.isValid}`);\n    log(`  companyId: ${validation.companyId ?? \"(not extracted)\"}`);\n    log(`  baseUrl: ${validation.baseUrl ?? \"(not set)\"}`);\n  } catch (err) {\n    const message = `validate() threw: ${(err as Error).message}`;\n    log(`  ✗ ${message}`);\n    return {\n      success: false,\n      validation: { isValid: false, reason: message },\n      totalListings: 0,\n      pagesScraped: 0,\n      detailResults: [],\n      listings: [],\n      issues: [{ severity: \"error\", phase: \"validation\", message }],\n      duration: Date.now() - startTime,\n    };\n  }\n\n  if (!validation.isValid) {\n    const message = `URL validation failed: ${validation.reason ?? \"unknown reason\"}`;\n    log(`  ✗ ${message}`);\n    issues.push({ severity: \"error\", phase: \"validation\", message });\n    return {\n      success: false,\n      validation,\n      totalListings: 0,\n      pagesScraped: 0,\n      detailResults: [],\n      listings: [],\n      issues,\n      duration: Date.now() - startTime,\n    };\n  }\n\n  // --- Build mock context ---\n  const baseUrl = validation.baseUrl ?? config.url;\n  const companyId = validation.companyId ?? config.companyId ?? \"\";\n  const ctx = createTestContext(\n    baseUrl,\n    companyId,\n    config.config ?? {},\n    verbose,\n  );\n\n  // --- Phase 2: Initialization ---\n  if (config.provider.initialize) {\n    log(\"\\n━━━ Phase 2: Initialization ━━━\");\n    try {\n      await config.provider.initialize(ctx);\n      log(\"  ✓ initialize() completed\");\n    } catch (err) {\n      const message = `initialize() threw: ${(err as Error).message}`;\n      log(`  ✗ ${message}`);\n      issues.push({ severity: \"error\", phase: \"listings\", message });\n    }\n  }\n\n  // --- Phase 3: Listings ---\n  log(\"\\n━━━ Phase 3: Listings ━━━\");\n  let pagesScraped = 0;\n  let cursor = undefined;\n\n  for (let page = 0; page < maxPages; page++) {\n    try {\n      log(`  Scraping page ${page + 1}...`);\n      const result: ListingsResult = await config.provider.scrapeListings(\n        ctx,\n        cursor,\n      );\n      pagesScraped++;\n\n      log(\n        `    Found ${result.listings.length} listings (hasMore: ${result.hasMore}, total: ${result.totalCount ?? \"unknown\"})`,\n      );\n\n      // Validate each listing using shared validation\n      const isFull = !config.provider.scrapeJobDetails;\n      for (const listing of result.listings) {\n        const listingIssues = validateListingToIssues(listing, isFull);\n        issues.push(...listingIssues);\n      }\n\n      allListings.push(...result.listings);\n\n      if (!result.hasMore || result.listings.length === 0) {\n        log(\"    No more pages.\");\n        break;\n      }\n\n      cursor = result.nextCursor;\n      if (result.hasMore && !cursor) {\n        issues.push({\n          severity: \"error\",\n          phase: \"listings\",\n          message: \"hasMore is true but nextCursor is undefined\",\n        });\n        break;\n      }\n    } catch (err) {\n      const message = `scrapeListings() page ${page + 1} threw: ${(err as Error).message}`;\n      log(`    ✗ ${message}`);\n      issues.push({ severity: \"error\", phase: \"listings\", message });\n      break;\n    }\n  }\n\n  // Check for duplicates\n  const idCounts = new Map<string, number>();\n  for (const listing of allListings) {\n    idCounts.set(\n      listing.externalId,\n      (idCounts.get(listing.externalId) ?? 0) + 1,\n    );\n  }\n  for (const [id, count] of idCounts) {\n    if (count > 1) {\n      issues.push({\n        severity: \"warning\",\n        phase: \"listings\",\n        message: `Duplicate externalId \"${id}\" found ${count} times`,\n        externalId: id,\n      });\n    }\n  }\n\n  log(`\\n  Total: ${allListings.length} listings across ${pagesScraped} pages`);\n\n  // --- Phase 4: Details (split scrapers only) ---\n  if (\n    config.provider.scrapeJobDetails &&\n    allListings.length > 0 &&\n    maxDetails > 0\n  ) {\n    log(\"\\n━━━ Phase 4: Job Details ━━━\");\n    const toFetch = allListings.slice(0, maxDetails);\n\n    for (const listing of toFetch) {\n      try {\n        log(`  Fetching details for ${listing.externalId}...`);\n        const result = await config.provider.scrapeJobDetails(ctx, listing);\n        detailResults.push(result);\n\n        if (result.details) {\n          const detailIssues = validateDetailToIssues(result.details);\n          issues.push(...detailIssues);\n          log(\n            `    ✓ ${result.details.title} (${result.details.locations.length} locations)`,\n          );\n        } else if (result.isExpired) {\n          log(`    ⚠ Expired: ${result.error ?? \"no reason\"}`);\n        } else {\n          log(`    ✗ No details: ${result.error ?? \"unknown\"}`);\n          issues.push({\n            severity: \"warning\",\n            phase: \"details\",\n            message: `No details returned for ${listing.externalId}: ${result.error ?? \"unknown\"}`,\n            externalId: listing.externalId,\n          });\n        }\n      } catch (err) {\n        const message = `scrapeJobDetails() threw for ${listing.externalId}: ${(err as Error).message}`;\n        log(`    ✗ ${message}`);\n        issues.push({\n          severity: \"error\",\n          phase: \"details\",\n          message,\n          externalId: listing.externalId,\n        });\n      }\n    }\n  }\n\n  // --- Quality Score ---\n  const qualityScore = computeQualityScore(allListings, detailResults, issues);\n\n  // --- Summary ---\n  const errorCount = issues.filter((i) => i.severity === \"error\").length;\n  const warnCount = issues.filter((i) => i.severity === \"warning\").length;\n  const success = errorCount === 0;\n\n  log(\"\\n━━━ Summary ━━━\");\n  log(`  Listings: ${allListings.length}`);\n  log(`  Pages: ${pagesScraped}`);\n  log(`  Details: ${detailResults.length}`);\n  log(`  Errors: ${errorCount}`);\n  log(`  Warnings: ${warnCount}`);\n  log(`  Quality Score: ${qualityScore.overall}/100`);\n  log(`    Required Fields: ${qualityScore.breakdown.requiredFields}/100`);\n  log(\n    `    Detail Completeness: ${qualityScore.breakdown.detailCompleteness}/100`,\n  );\n  log(`    Metadata Richness: ${qualityScore.breakdown.metadataRichness}/100`);\n  log(`    Data Integrity: ${qualityScore.breakdown.dataIntegrity}/100`);\n  log(`  Result: ${success ? \"✓ PASS\" : \"✗ FAIL\"}`);\n  log(`  Duration: ${Date.now() - startTime}ms\\n`);\n\n  return {\n    success,\n    validation,\n    totalListings: allListings.length,\n    pagesScraped,\n    detailResults,\n    listings: allListings,\n    issues,\n    duration: Date.now() - startTime,\n    qualityScore,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Data Validators\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a listing into TestIssues using the shared SDK validation module.\n * This ensures test-time and runtime validation rules are identical.\n */\nfunction validateListingToIssues(\n  listing: JobListing,\n  isFull: boolean,\n): TestIssue[] {\n  const issues: TestIssue[] = [];\n  const id = listing.externalId;\n\n  // Core required-field validation (shared with runtime)\n  const issue = sharedValidateListing(listing, isFull);\n  if (issue) {\n    for (const field of issue.missingFields) {\n      issues.push({\n        severity: \"error\",\n        phase: \"listings\",\n        message: `Missing ${field}`,\n        externalId: id,\n      });\n    }\n  }\n\n  // Additional URL-format checks (test-only, informational)\n  if (listing.applyUrl && !isValidUrl(listing.applyUrl)) {\n    issues.push({\n      severity: \"warning\",\n      phase: \"listings\",\n      message: `Invalid applyUrl: ${listing.applyUrl}`,\n      externalId: id,\n    });\n  }\n  if (listing.listingUrl && !isValidUrl(listing.listingUrl)) {\n    issues.push({\n      severity: \"warning\",\n      phase: \"listings\",\n      message: `Invalid listingUrl: ${listing.listingUrl}`,\n      externalId: id,\n    });\n  }\n\n  return issues;\n}\n\n/**\n * Convert a detail record into TestIssues using the shared SDK validation module.\n */\nfunction validateDetailToIssues(details: JobDetails): TestIssue[] {\n  const issues: TestIssue[] = [];\n  const id = details.externalId;\n\n  // Core required-field validation (shared with runtime)\n  const issue = sharedValidateDetail(details);\n  if (issue) {\n    for (const field of issue.missingFields) {\n      issues.push({\n        severity: \"error\",\n        phase: \"details\",\n        message: `Missing ${field}`,\n        externalId: id,\n      });\n    }\n  }\n\n  // Additional quality warnings (test-only)\n  if (!details.locations || details.locations.length === 0) {\n    // Only add if not already flagged by shared validation\n    if (!issue?.missingFields.includes(\"locations\")) {\n      issues.push({\n        severity: \"warning\",\n        phase: \"details\",\n        message: \"No locations\",\n        externalId: id,\n      });\n    }\n  }\n  if (details.description && details.description.length < 50) {\n    issues.push({\n      severity: \"warning\",\n      phase: \"details\",\n      message: `Description suspiciously short (${details.description.length} chars)`,\n      externalId: id,\n    });\n  }\n\n  return issues;\n}\n\n// ---------------------------------------------------------------------------\n// Mock Context Factory\n// ---------------------------------------------------------------------------\n\nfunction createTestContext(\n  baseUrl: string,\n  companyId: string,\n  config: Record<string, MetadataValue>,\n  verbose: boolean,\n): ScraperContext {\n  const logger: ScraperLogger = {\n    debug: (msg, data) => {\n      if (verbose) console.log(`  [DEBUG] ${msg}`, data ?? \"\");\n    },\n    info: (msg, data) => {\n      if (verbose) console.log(`  [INFO]  ${msg}`, data ?? \"\");\n    },\n    warn: (msg, data) => console.warn(`  [WARN]  ${msg}`, data ?? \"\"),\n    error: (msg, data) => console.error(`  [ERROR] ${msg}`, data ?? \"\"),\n  };\n\n  const http: HttpClient = {\n    async request<T = unknown>(\n      options: HttpRequestOptions,\n    ): Promise<HttpResponse<T>> {\n      const url = new URL(options.url);\n      if (options.params) {\n        for (const [key, value] of Object.entries(options.params)) {\n          url.searchParams.set(key, String(value));\n        }\n      }\n\n      const fetchOptions: RequestInit = {\n        method: options.method ?? \"GET\",\n        headers: options.headers ?? {},\n      };\n\n      if (options.body) {\n        if (typeof options.body === \"string\") {\n          fetchOptions.body = options.body;\n        } else {\n          fetchOptions.body = JSON.stringify(options.body);\n          (fetchOptions.headers as Record<string, string>)[\"Content-Type\"] =\n            \"application/json\";\n        }\n      }\n\n      if (verbose) {\n        logger.debug(`HTTP ${fetchOptions.method} ${url.toString()}`);\n      }\n\n      const response = await fetch(url.toString(), fetchOptions);\n\n      const responseHeaders: Record<string, string> = {};\n      response.headers.forEach((value, key) => {\n        responseHeaders[key.toLowerCase()] = value;\n      });\n\n      // Preserve all Set-Cookie headers (forEach overwrites duplicates)\n      const setCookieHeaders = (response.headers as any).getSetCookie?.() as\n        | string[]\n        | undefined;\n      if (setCookieHeaders && setCookieHeaders.length > 0) {\n        responseHeaders[\"set-cookie\"] = setCookieHeaders.join(\", \");\n      }\n\n      let data: T;\n      const responseType = options.responseType ?? \"json\";\n      if (responseType === \"json\") {\n        // Read as text first so non-JSON error responses don't throw\n        const text = await response.text();\n        try {\n          data = JSON.parse(text) as T;\n        } catch {\n          // Return raw text when JSON parsing fails (e.g. 403 \"Forbidden\")\n          data = text as unknown as T;\n        }\n      } else if (responseType === \"binary\") {\n        data = (await response.arrayBuffer()) as T;\n      } else {\n        data = (await response.text()) as T;\n      }\n\n      return {\n        status: response.status,\n        statusText: response.statusText,\n        headers: responseHeaders,\n        data,\n        finalUrl: response.url,\n      };\n    },\n\n    async get<T = unknown>(\n      url: string,\n      options?: Omit<HttpRequestOptions, \"url\" | \"method\">,\n    ): Promise<HttpResponse<T>> {\n      return this.request<T>({ ...options, url, method: \"GET\" });\n    },\n\n    async post<T = unknown>(\n      url: string,\n      options?: Omit<HttpRequestOptions, \"url\" | \"method\">,\n    ): Promise<HttpResponse<T>> {\n      return this.request<T>({ ...options, url, method: \"POST\" });\n    },\n  };\n\n  const browser: BrowserClient = {\n    async renderPage() {\n      throw new Error(\n        \"BrowserClient is not available in the test runner. \" +\n          \"Use ctx.http for API/HTML scrapers, or run integration tests with the full platform.\",\n      );\n    },\n  };\n\n  return { http, browser, log: logger, baseUrl, companyId, config };\n}\n// ---------------------------------------------------------------------------\n// Quality Scoring\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a quality score for the test result.\n * Evaluates: required fields, detail completeness, metadata richness, data integrity.\n */\nexport function computeQualityScore(\n  listings: JobListing[],\n  detailResults: JobDetailsResult[],\n  issues: TestIssue[],\n): QualityScore {\n  const findings: string[] = [];\n\n  // --- Required Fields (25%) ---\n  let requiredFields = 100;\n  if (listings.length === 0) {\n    requiredFields = 0;\n    findings.push(\"No listings found\");\n  } else {\n    let missingId = 0,\n      missingApply = 0,\n      missingListing = 0;\n    for (const l of listings) {\n      if (!l.externalId) missingId++;\n      if (!l.applyUrl) missingApply++;\n      if (!l.listingUrl) missingListing++;\n    }\n    const total = listings.length;\n    const penalty =\n      ((missingId + missingApply + missingListing) / (total * 3)) * 100;\n    requiredFields = Math.max(0, Math.round(100 - penalty));\n    if (missingId > 0)\n      findings.push(`${missingId}/${total} listings missing externalId`);\n    if (missingApply > 0)\n      findings.push(`${missingApply}/${total} listings missing applyUrl`);\n    if (missingListing > 0)\n      findings.push(`${missingListing}/${total} listings missing listingUrl`);\n  }\n\n  // --- Detail Completeness (30%) ---\n  let detailCompleteness = 100;\n  const details = detailResults.filter((d) => d.details).map((d) => d.details!);\n  if (details.length === 0 && detailResults.length > 0) {\n    detailCompleteness = 0;\n    findings.push(\"No details could be fetched\");\n  } else if (details.length > 0) {\n    let missingTitle = 0,\n      missingDesc = 0,\n      missingLoc = 0,\n      shortDesc = 0;\n    for (const d of details) {\n      if (!d.title) missingTitle++;\n      if (!d.description) missingDesc++;\n      if (!d.locations || d.locations.length === 0) missingLoc++;\n      if (d.description && d.description.length < 100) shortDesc++;\n    }\n    const total = details.length;\n    const titlePenalty = (missingTitle / total) * 30;\n    const descPenalty = (missingDesc / total) * 40;\n    const locPenalty = (missingLoc / total) * 20;\n    const shortPenalty = (shortDesc / total) * 10;\n    detailCompleteness = Math.max(\n      0,\n      Math.round(100 - titlePenalty - descPenalty - locPenalty - shortPenalty),\n    );\n    if (missingTitle > 0)\n      findings.push(`${missingTitle}/${total} details missing title`);\n    if (missingDesc > 0)\n      findings.push(`${missingDesc}/${total} details missing description`);\n    if (missingLoc > 0)\n      findings.push(`${missingLoc}/${total} details missing locations`);\n    if (shortDesc > 0)\n      findings.push(\n        `${shortDesc}/${total} details have short descriptions (<100 chars)`,\n      );\n  } else if (detailResults.length === 0) {\n    // Full scraper — check listings for detail fields\n    let hasTitle = 0,\n      hasDesc = 0,\n      hasLoc = 0;\n    for (const l of listings) {\n      if (l.title) hasTitle++;\n      if (l.description) hasDesc++;\n      if (l.locations && l.locations.length > 0) hasLoc++;\n    }\n    const total = listings.length || 1;\n    detailCompleteness = Math.round(\n      ((hasTitle + hasDesc + hasLoc) / (total * 3)) * 100,\n    );\n    if (hasDesc === 0)\n      findings.push(\n        \"No listings have descriptions (expected for full scrapers)\",\n      );\n  }\n\n  // --- Metadata Richness (25%) ---\n  let metadataRichness = 0;\n  const allMeta: Record<string, number> = {};\n  const sources = details.length > 0 ? details : listings;\n  for (const item of sources) {\n    const meta = item.meta;\n    if (meta) {\n      for (const key of Object.keys(meta)) {\n        if (meta[key] != null && meta[key] !== \"\") {\n          allMeta[key] = (allMeta[key] ?? 0) + 1;\n        }\n      }\n    }\n    // Also count first-class fields as richness\n    if (item.companyName)\n      allMeta[\"_companyName\"] = (allMeta[\"_companyName\"] ?? 0) + 1;\n    if (item.postedAt) allMeta[\"_postedAt\"] = (allMeta[\"_postedAt\"] ?? 0) + 1;\n    if (item.closesAt) allMeta[\"_closesAt\"] = (allMeta[\"_closesAt\"] ?? 0) + 1;\n  }\n\n  const uniqueMetaKeys = Object.keys(allMeta).length;\n  // Score based on number of unique metadata fields:\n  // 0 keys = 0, 1-2 = 20, 3-4 = 40, 5-7 = 60, 8-10 = 80, 11+ = 100\n  if (uniqueMetaKeys === 0) {\n    metadataRichness = 0;\n    findings.push(\"No metadata captured at all\");\n  } else if (uniqueMetaKeys <= 2) {\n    metadataRichness = 20;\n    findings.push(`Only ${uniqueMetaKeys} metadata fields captured`);\n  } else if (uniqueMetaKeys <= 4) {\n    metadataRichness = 40;\n    findings.push(`${uniqueMetaKeys} metadata fields captured`);\n  } else if (uniqueMetaKeys <= 7) {\n    metadataRichness = 60;\n    findings.push(`${uniqueMetaKeys} metadata fields captured (good coverage)`);\n  } else if (uniqueMetaKeys <= 10) {\n    metadataRichness = 80;\n    findings.push(\n      `${uniqueMetaKeys} metadata fields captured (very good coverage)`,\n    );\n  } else {\n    metadataRichness = 100;\n    findings.push(\n      `${uniqueMetaKeys} metadata fields captured (excellent coverage)`,\n    );\n  }\n\n  if (uniqueMetaKeys > 0) {\n    findings.push(\n      `Metadata keys: ${Object.keys(allMeta)\n        .filter((k) => !k.startsWith(\"_\"))\n        .join(\", \")}`,\n    );\n  }\n\n  // --- Data Integrity (20%) ---\n  let dataIntegrity = 100;\n  const errorCount = issues.filter((i) => i.severity === \"error\").length;\n  const warnCount = issues.filter((i) => i.severity === \"warning\").length;\n  dataIntegrity = Math.max(\n    0,\n    Math.round(100 - errorCount * 15 - warnCount * 5),\n  );\n  if (errorCount > 0) findings.push(`${errorCount} data integrity errors`);\n  if (warnCount > 0) findings.push(`${warnCount} data integrity warnings`);\n\n  const overall = Math.round(\n    requiredFields * 0.25 +\n      detailCompleteness * 0.3 +\n      metadataRichness * 0.25 +\n      dataIntegrity * 0.2,\n  );\n\n  return {\n    overall,\n    breakdown: {\n      requiredFields,\n      detailCompleteness,\n      metadataRichness,\n      dataIntegrity,\n    },\n    findings,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Result Serialization (truncated descriptions for saved output)\n// ---------------------------------------------------------------------------\n\n/**\n * Serializable test result with truncated descriptions for storage.\n */\nexport interface SavedTestResult {\n  providerId: string;\n  testUrl: string;\n  timestamp: string;\n  success: boolean;\n  duration: number;\n  totalListings: number;\n  pagesScraped: number;\n  qualityScore: QualityScore;\n  issues: TestIssue[];\n  /** All listings with truncated descriptions. */\n  listings: SerializedJobListing[];\n  /** All detail records with truncated descriptions. */\n  details: SerializedJobDetails[];\n}\n\nexport interface SerializedJobListing {\n  externalId: string;\n  applyUrl: string;\n  listingUrl: string;\n  title?: string;\n  description_preview?: string;\n  description_length?: number;\n  locations?: JobLocation[];\n  companyName?: string;\n  postedAt?: string;\n  closesAt?: string;\n  meta: MetadataMap;\n}\n\nexport interface SerializedJobDetails {\n  externalId: string;\n  title: string;\n  description_preview: string;\n  description_length: number;\n  listingUrl: string;\n  applyUrl: string;\n  locations: JobLocation[];\n  companyName?: string;\n  postedAt?: string;\n  closesAt?: string;\n  meta: MetadataMap;\n}\n\n/**\n * Convert a TestResult into a serializable format with truncated descriptions.\n */\nexport function serializeTestResult(\n  providerId: string,\n  testUrl: string,\n  result: TestResult,\n  maxDescriptionLength = 200,\n): SavedTestResult {\n  const details: SerializedJobDetails[] = result.detailResults\n    .filter((d) => d.details)\n    .map((d) => {\n      const det = d.details!;\n      return {\n        externalId: det.externalId,\n        title: det.title,\n        description_preview: det.description\n          ? det.description.substring(0, maxDescriptionLength) +\n            (det.description.length > maxDescriptionLength ? \"...\" : \"\")\n          : \"\",\n        description_length: det.description?.length ?? 0,\n        listingUrl: det.listingUrl,\n        applyUrl: det.applyUrl,\n        locations: det.locations,\n        companyName: det.companyName,\n        postedAt: det.postedAt,\n        closesAt: det.closesAt,\n        meta: det.meta ?? {},\n      };\n    });\n\n  if (details.length === 0) {\n    for (const listing of result.listings) {\n      if (listing.description || listing.meta) {\n        details.push({\n          externalId: listing.externalId,\n          title: listing.title ?? \"\",\n          description_preview: listing.description\n            ? listing.description.substring(0, maxDescriptionLength) +\n              (listing.description.length > maxDescriptionLength ? \"...\" : \"\")\n            : \"\",\n          description_length: listing.description?.length ?? 0,\n          listingUrl: listing.listingUrl,\n          applyUrl: listing.applyUrl,\n          locations: listing.locations ?? [],\n          companyName: listing.companyName,\n          postedAt: listing.postedAt,\n          closesAt: listing.closesAt,\n          meta: listing.meta ?? {},\n        });\n      }\n    }\n  }\n\n  const listings: SerializedJobListing[] = result.listings.map((l) => ({\n    externalId: l.externalId,\n    applyUrl: l.applyUrl,\n    listingUrl: l.listingUrl,\n    title: l.title,\n    description_preview: l.description\n      ? l.description.substring(0, maxDescriptionLength) +\n        (l.description.length > maxDescriptionLength ? \"...\" : \"\")\n      : undefined,\n    description_length: l.description?.length,\n    locations: l.locations,\n    companyName: l.companyName,\n    postedAt: l.postedAt,\n    closesAt: l.closesAt,\n    meta: l.meta ?? {},\n  }));\n\n  return {\n    providerId,\n    testUrl,\n    timestamp: new Date().toISOString(),\n    success: result.success,\n    duration: result.duration,\n    totalListings: result.totalListings,\n    pagesScraped: result.pagesScraped,\n    qualityScore:\n      result.qualityScore ??\n      computeQualityScore(result.listings, result.detailResults, result.issues),\n    issues: result.issues,\n    listings,\n    details,\n  };\n}\n\n/**\n * Generate a markdown report for a single ATS/provider test run.\n */\nexport function generateSingleProviderReport(result: SavedTestResult): string {\n  const lines: string[] = [];\n  const status = result.success ? \"PASS\" : \"FAIL\";\n\n  lines.push(`# Scraper Test Report — ${result.providerId}`);\n  lines.push(\"\");\n  lines.push(`- **Provider:** ${result.providerId}`);\n  lines.push(`- **URL:** ${result.testUrl}`);\n  lines.push(`- **Timestamp:** ${result.timestamp}`);\n  lines.push(`- **Status:** ${status}`);\n  lines.push(`- **Duration:** ${result.duration}ms`);\n  lines.push(`- **Listings:** ${result.totalListings}`);\n  lines.push(`- **Pages:** ${result.pagesScraped}`);\n  lines.push(\"\");\n\n  lines.push(\"## Quality Score\");\n  lines.push(\"\");\n  lines.push(`- **Overall:** ${result.qualityScore.overall}/100`);\n  lines.push(\n    `- **Required Fields:** ${result.qualityScore.breakdown.requiredFields}/100`,\n  );\n  lines.push(\n    `- **Detail Completeness:** ${result.qualityScore.breakdown.detailCompleteness}/100`,\n  );\n  lines.push(\n    `- **Metadata Richness:** ${result.qualityScore.breakdown.metadataRichness}/100`,\n  );\n  lines.push(\n    `- **Data Integrity:** ${result.qualityScore.breakdown.dataIntegrity}/100`,\n  );\n  lines.push(\"\");\n\n  lines.push(\"## Findings\");\n  lines.push(\"\");\n  if (result.qualityScore.findings.length === 0) {\n    lines.push(\"- None\");\n  } else {\n    for (const finding of result.qualityScore.findings) {\n      lines.push(`- ${finding}`);\n    }\n  }\n  lines.push(\"\");\n\n  lines.push(\"## Issues\");\n  lines.push(\"\");\n  if (result.issues.length === 0) {\n    lines.push(\"- None\");\n  } else {\n    for (const issue of result.issues) {\n      const id = issue.externalId ? ` (${issue.externalId})` : \"\";\n      lines.push(\n        `- [${issue.severity.toUpperCase()}] ${issue.phase}: ${issue.message}${id}`,\n      );\n    }\n  }\n  lines.push(\"\");\n\n  lines.push(\"## Metadata Coverage\");\n  lines.push(\"\");\n  const metaKeys = new Set<string>();\n  for (const listing of result.listings) {\n    for (const key of Object.keys(listing.meta ?? {})) {\n      metaKeys.add(key);\n    }\n  }\n  for (const detail of result.details) {\n    for (const key of Object.keys(detail.meta ?? {})) {\n      metaKeys.add(key);\n    }\n  }\n  if (metaKeys.size === 0) {\n    lines.push(\"- No metadata keys captured\");\n  } else {\n    lines.push(`- Captured ${metaKeys.size} unique metadata keys`);\n    lines.push(\"\");\n    for (const key of [...metaKeys].sort()) {\n      lines.push(`- ${key}`);\n    }\n  }\n  lines.push(\"\");\n\n  lines.push(\"## Saved Artifacts\");\n  lines.push(\"\");\n  lines.push(\n    \"- JSON output contains every listing and detail record with metadata.\",\n  );\n  lines.push(\n    \"- Descriptions are truncated in saved output for review readability.\",\n  );\n\n  return lines.join(\"\\n\");\n}\n\n// extractFieldFromUrl and toExtractorArray are imported from ../runtime/helpers\n\n// ---------------------------------------------------------------------------\n// Test Suite Runner (tests.json based)\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration for running a full test suite from tests.json.\n */\nexport interface TestSuiteConfig {\n  /** The scraper provider instance to test. */\n  provider: ScraperProvider;\n  /** The provider manifest (for URL pattern matching). */\n  manifest: ScraperProviderManifest;\n  /** Test cases from tests.json. */\n  testSuite: ProviderTestSuite;\n  /** Whether to print verbose output. Default: true. */\n  verbose?: boolean;\n}\n\n/**\n * Result of a single test case within a suite.\n */\nexport interface TestCaseResult {\n  /** Test case name from tests.json. */\n  name: string;\n  /** URL tested. */\n  url: string;\n  /** Whether all assertions passed and the test run succeeded. */\n  passed: boolean;\n  /** Assertion results from the expect block. */\n  assertions: AssertionResult[];\n  /** The underlying test run result (null if test was assertion-only). */\n  testResult: TestResult | null;\n  /** Duration in milliseconds. */\n  durationMs: number;\n  /** Error message if the test case threw. */\n  error?: string;\n}\n\n/**\n * Result of running a full test suite.\n */\nexport interface TestSuiteResult {\n  /** Provider ID. */\n  providerId: string;\n  /** Whether all test cases passed. */\n  success: boolean;\n  /** Total test cases. */\n  total: number;\n  /** Number of passed test cases. */\n  passed: number;\n  /** Number of failed test cases. */\n  failed: number;\n  /** Per-case results. */\n  cases: TestCaseResult[];\n  /** Total duration in milliseconds. */\n  durationMs: number;\n}\n\n/**\n * Run a full test suite from tests.json against a provider.\n *\n * Dispatches each test case by its `type`:\n * - \"urlPattern\"  — offline URL pattern matching (no HTTP)\n * - \"validation\"  — calls provider.validate(), checks companyId\n * - \"jobId\"       — calls provider.validate() on a job detail URL, checks jobId\n * - \"scrape\"      — full live scrape with listing/detail assertions\n */\nexport async function runTestSuite(\n  config: TestSuiteConfig,\n): Promise<TestSuiteResult> {\n  const startTime = Date.now();\n  const verbose = config.verbose ?? true;\n  const log = (msg: string) => {\n    if (verbose) console.log(msg);\n  };\n  const cases: TestCaseResult[] = [];\n\n  log(`\\n╔══════════════════════════════════════════════════════════════╗`);\n  log(\n    `║  Test Suite: ${config.manifest.providerId} (${config.testSuite.tests.length} test cases)`,\n  );\n  log(`╚══════════════════════════════════════════════════════════════╝`);\n\n  for (let i = 0; i < config.testSuite.tests.length; i++) {\n    const tc = config.testSuite.tests[i];\n    const caseStart = Date.now();\n\n    log(\n      `\\n┌─ [${tc.type}] Test ${i + 1}/${config.testSuite.tests.length}: ${tc.name}`,\n    );\n    log(`│  URL: ${tc.url}`);\n\n    try {\n      let assertions: AssertionResult[] = [];\n      let testResult: TestResult | null = null;\n\n      // ── Dispatch by type ──────────────────────────────────────────────────\n\n      if (tc.type === \"urlPattern\") {\n        // Offline: check URL against manifest urlPatterns\n        assertions = evaluateUrlPatternAssertions(\n          tc,\n          config.manifest.urlPatterns,\n        );\n        log(`│  Pattern match: ${assertions[0].actual}`);\n      } else if (tc.type === \"validation\") {\n        // Call provider.validate() and check companyId / isValid\n        const validation = await config.provider.validate(tc.url);\n        log(\n          `│  isValid: ${validation.isValid}, companyId: ${validation.companyId ?? \"(none)\"}`,\n        );\n        assertions = evaluateValidationAssertions(tc, {\n          isValid: validation.isValid,\n          companyId: validation.companyId,\n          baseUrl: validation.baseUrl,\n        });\n      } else if (tc.type === \"jobId\") {\n        // Extract jobId from URL using manifest extractor definitions\n        const jobIdExtractors = toExtractorArray(\n          config.manifest.jobIdExtractor,\n        );\n        const companyIdExtractors = toExtractorArray(\n          config.manifest.companyIdExtractor,\n        );\n        const jobId = extractFieldFromUrl(tc.url, jobIdExtractors, \"jobId\");\n        const companyId = extractFieldFromUrl(\n          tc.url,\n          companyIdExtractors,\n          \"companyId\",\n        );\n        log(\n          `│  jobId: ${jobId ?? \"(none)\"}, companyId: ${companyId ?? \"(none)\"}`,\n        );\n        assertions = evaluateJobIdAssertions(tc, { jobId, companyId });\n      } else if (tc.type === \"listingUrl\") {\n        // Offline: reconstruct canonical listing URL from companyIdExtractor definitions\n        const companyIdExtractors = toExtractorArray(\n          config.manifest.companyIdExtractor,\n        );\n        const actual = buildListingUrlFromExtractors(\n          tc.url,\n          companyIdExtractors,\n        );\n        log(`│  listingUrl: ${actual ?? \"(none)\"}`);\n        assertions = evaluateListingUrlAssertions(tc, companyIdExtractors);\n      } else if (tc.type === \"scrape\") {\n        // Full live scrape\n        log(`│  Running live scrape...`);\n        // For full scrapers, skip the detail-fetch phase (scrapeJobDetails\n        // is not implemented and would throw). Listings already contain\n        // complete details including meta.\n        const isFullScraper = config.manifest.category === \"full\";\n        testResult = await runTest({\n          provider: config.provider,\n          url: tc.url,\n          maxPages: tc.maxPages ?? 1,\n          maxDetails: isFullScraper ? 0 : (tc.maxDetails ?? 3),\n          verbose: false,\n        });\n        log(\n          `│  Scrape: ${testResult.success ? \"✓ PASS\" : \"✗ FAIL\"} (${testResult.totalListings} listings, ${testResult.duration}ms)`,\n        );\n        // Build detail records for assertion evaluation.\n        // For split scrapers, use the fetched detail results.\n        // For full scrapers, listings already contain full details (including\n        // meta), so fall back to them when no separate detail results exist.\n        const detailRecords = testResult.detailResults\n          .filter((d) => d.details)\n          .map((d) => d.details!) as unknown as Array<Record<string, unknown>>;\n        const effectiveDetails =\n          detailRecords.length > 0\n            ? detailRecords\n            : (testResult.listings as unknown as Array<\n                Record<string, unknown>\n              >);\n        assertions = evaluateScrapeAssertions(tc, {\n          totalListings: testResult.totalListings,\n          listings: testResult.listings as unknown as Array<\n            Record<string, unknown>\n          >,\n          details: effectiveDetails,\n        });\n      }\n\n      // ─────────────────────────────────────────────────────────────────────\n\n      const allAssertionsPassed = assertions.every((a) => a.passed);\n      const casePassed =\n        allAssertionsPassed && (testResult === null || testResult.success);\n\n      for (const a of assertions) {\n        log(\n          `│  ${a.passed ? \"✓\" : \"✗\"} ${a.name}: expected=${a.expected}, actual=${a.actual}`,\n        );\n      }\n      log(\n        `└─ ${casePassed ? \"✓ PASSED\" : \"✗ FAILED\"} (${Date.now() - caseStart}ms)`,\n      );\n\n      cases.push({\n        name: tc.name,\n        url: tc.url,\n        passed: casePassed,\n        assertions,\n        testResult,\n        durationMs: Date.now() - caseStart,\n      });\n    } catch (err) {\n      const error = (err as Error).message;\n      log(`└─ ✗ ERROR: ${error}`);\n      cases.push({\n        name: tc.name,\n        url: tc.url,\n        passed: false,\n        assertions: [],\n        testResult: null,\n        durationMs: Date.now() - caseStart,\n        error,\n      });\n    }\n  }\n\n  const passed = cases.filter((c) => c.passed).length;\n  const failed = cases.filter((c) => !c.passed).length;\n  const success = failed === 0;\n\n  log(`\\n══════════════════════════════════════════════════════════════`);\n  log(`  Suite Result: ${success ? \"✓ ALL PASSED\" : \"✗ FAILURES\"}`);\n  log(`  Passed: ${passed}/${cases.length}  Failed: ${failed}/${cases.length}`);\n  log(`  Duration: ${Date.now() - startTime}ms`);\n  log(`══════════════════════════════════════════════════════════════\\n`);\n\n  return {\n    providerId: config.manifest.providerId,\n    success,\n    total: cases.length,\n    passed,\n    failed,\n    cases,\n    durationMs: Date.now() - startTime,\n  };\n}\n"]}