{"record":{"id":"503f15e25cbc7969","repo":"JuliusBrussee/caveman","slug":"label-verified-at-must-include-a-timezone","errorCode":null,"errorMessage":"{label}: verified_at must include a timezone","messagePattern":"(.+?): verified_at must include a timezone","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":112,"sourceCode":"    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\")\n        identity.append(value)\n    if row[\"currency\"] != \"USD\":","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/provider-catalog/validate_catalog.py#L94-L130","documentation":"verified_at parsed as a datetime/string but carries no timezone (naive datetime). The validator requires an explicit timezone so every row's date converts deterministically to UTC — provenance dates must be unambiguous across timezones. Thrown from parsed_time after successful parsing.","triggerScenarios":"verified_at is a naive ISO string like `2026-07-05T00:00:00` (no Z or offset), or a quoted YAML date `\"2026-07-05\"`-style value parsed without tz info, or a YAML unquoted timestamp lacking an offset in a form PyYAML parses as naive.","commonSituations":"Writing `verified_at: 2026-07-05T12:00:00` and assuming UTC; local tooling generating naive isoformat() output (`datetime.now().isoformat()` instead of `datetime.now(timezone.utc).isoformat()`).","solutions":["Append `Z` (or `+00:00`) to the timestamp: `2026-07-05T00:00:00Z`","If the value is a local time, convert to UTC first, then add the offset","Generate dates with timezone-aware code: `datetime.now(timezone.utc).isoformat()`"],"exampleFix":"# before\nverified_at: \"2026-07-05T00:00:00\"\n\n# after\nverified_at: \"2026-07-05T00:00:00Z\"","handlingStrategy":"validation","validationCode":"from datetime import datetime\n\ndef has_timezone(value: str) -> bool:\n    try:\n        return datetime.fromisoformat(value.replace(\"Z\", \"+00:00\")).tzinfo is not None\n    except ValueError:\n        return False","typeGuard":"def is_aware_timestamp(value: object) -> bool:\n    if isinstance(value, datetime):\n        return value.tzinfo is not None\n    return isinstance(value, str) and has_timezone(value)","tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"must include a timezone\" in str(e):\n        append_utc_suffix(str(e))","preventionTips":["Always end timestamps with `Z` or an explicit `+00:00` offset","Use timezone-aware constructors: datetime.now(timezone.utc), never datetime.now()","Treat naive ISO strings as invalid in code review"],"tags":["datetime","timezone","yaml","catalog"],"backgroundTag":"invalid-date-format","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"}