{"record":{"id":"3abf45d121cd57f9","repo":"JuliusBrussee/caveman","slug":"label-verified-at-must-be-an-rfc3339-timestamp","errorCode":null,"errorMessage":"{label}: verified_at must be an RFC3339 timestamp","messagePattern":"(.+?): verified_at must be an RFC3339 timestamp","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":110,"sourceCode":"    except (OSError, yaml.YAMLError) as error:\n        raise CatalogError(f\"{path}: invalid YAML: {error}\") from error\n    if not isinstance(document, list) or not document:\n        raise CatalogError(f\"{path}: catalog must be a non-empty list\")\n    if not all(isinstance(row, dict) for row in document):\n        raise CatalogError(f\"{path}: every catalog row must be an object\")\n    return document\n\n\ndef parsed_time(value: Any, label: str) -> datetime:\n    if isinstance(value, datetime):\n        parsed = value\n    elif isinstance(value, str):\n        try:\n            parsed = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n        except ValueError as error:\n            raise CatalogError(f\"{label}: verified_at must be RFC3339\") from error\n    else:\n        raise CatalogError(f\"{label}: verified_at must be an RFC3339 timestamp\")\n    if parsed.tzinfo is None:\n        raise CatalogError(f\"{label}: verified_at must include a timezone\")\n    return parsed.astimezone(timezone.utc)\n\n\ndef validate_row(row: dict[str, Any], label: str, now: datetime) -> tuple[str, str, str]:\n    unknown = set(row) - TOP_LEVEL_KEYS\n    missing = REQUIRED_KEYS - set(row)\n    if unknown:\n        raise CatalogError(f\"{label}: unknown field(s): {', '.join(sorted(unknown))}\")\n    if missing:\n        raise CatalogError(f\"{label}: missing field(s): {', '.join(sorted(missing))}\")\n\n    identity: list[str] = []\n    for field in (\"provider\", \"model\", \"region\"):\n        value = row[field]\n        if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):\n            raise CatalogError(f\"{label}: {field} must be a safe non-empty identifier\")","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/provider-catalog/validate_catalog.py#L92-L128","documentation":"verified_at is neither a YAML-parsed datetime nor a string — it is some other type (int, float, list, bool, etc.). parsed_time only accepts datetime objects or RFC3339 strings, so any other type is rejected outright. This keeps price provenance a real timestamp rather than a number that could be confused with a version.","triggerScenarios":"A row sets verified_at to a number (e.g. `verified_at: 20260705`), a boolean, a list, or a YAML date written in a form PyYAML parses as non-datetime non-string. Called from validate_row/validate_catalog via parsed_time.","commonSituations":"Writing a date as digits without quotes so YAML makes it an int; a script emitting an epoch/unix timestamp; accidentally quoting the wrong field so a list ends up in verified_at.","solutions":["Change verified_at to an RFC3339 string like `\"2026-07-05T00:00:00Z\"` (quote it to prevent YAML type coercion)","If you intended an epoch timestamp, convert it to an ISO-8601 UTC string first","Check for accidental YAML structure — ensure verified_at maps to a single scalar, not a nested list/mapping"],"exampleFix":"# before\nverified_at: 20260705\n\n# after\nverified_at: \"2026-07-05T00:00:00Z\"","handlingStrategy":"type-guard","validationCode":"def verified_at_type_ok(row: dict) -> bool:\n    v = row.get(\"verified_at\")\n    return isinstance(v, (str, datetime))","typeGuard":"def is_timestamp_like(value: object) -> bool:\n    return isinstance(value, str) or isinstance(value, datetime)","tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"RFC3339 timestamp\" in str(e):\n        coerce_to_iso_string(str(e))","preventionTips":["Quote date values in YAML (`verified_at: \"2026-07-05T00:00:00Z\"`) so YAML cannot coerce them to int","Never use epoch integers for verified_at","Check emitted types in sync scripts before writing YAML"],"tags":["type-mismatch","yaml","datetime","catalog"],"backgroundTag":"type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}