{"record":{"id":"615ad1d2499613cc","repo":"paperclipai/paperclip","slug":"history-date-must-include-a-timezone","errorCode":null,"errorMessage":"History date must include a timezone","messagePattern":"History date must include a timezone","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scripts/evals-hub/build.py","lineNumber":46,"sourceCode":"    if not candidates:\n        raise ValueError(\"Latest campaign is absent from history\")\n    # Report refreshes replace presentation, never the measurement date.\n    return max(candidates, key=lambda c: c.get(\"reportRevision\", {}).get(\"renderedAt\", c[\"generatedAt\"]))\n\n\ndef public_report(url, prefix):\n    parsed = urlsplit(url)\n    if (parsed.scheme != \"https\" or parsed.netloc != urlsplit(ORIGIN).netloc\n            or not parsed.path.startswith(f\"/{prefix}/campaigns/\")\n            or parsed.query or parsed.fragment):\n        raise ValueError(\"Unexpected public report URL\")\n    return html.escape(url, quote=True)\n\n\ndef display_date(value):\n    date = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n    if date.tzinfo is None:\n        raise ValueError(\"History date must include a timezone\")\n    return date.astimezone(timezone.utc).strftime(\"%d %b %Y · %H:%M UTC\")\n\n\ndef summarize(history, kind):\n    prefix, schema = SYSTEMS[kind]\n    campaign = latest(history, schema)\n    totals = campaign[\"totals\"] if kind == \"runner\" else campaign\n    passed, selected = totals[\"passed\"], totals[\"selected\"]\n    if any(type(n) is not int for n in (passed, selected)) or not 0 <= passed <= selected:\n        raise ValueError(\"Invalid campaign counts\")\n    if type(campaign.get(\"complete\")) is not bool:\n        raise ValueError(\"Missing campaign coverage flag\")\n    revision = campaign.get(\"reportRevision\", {})\n    if revision and not isinstance(revision.get(\"sourceGeneratedAt\"), str):\n        raise ValueError(\"Report refresh has no reliable measurement date\")\n    measured = revision.get(\"sourceGeneratedAt\", campaign[\"generatedAt\"])\n    return {\n        \"url\": public_report(campaign[\"publicUrl\"], prefix),","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/evals-hub/build.py#L28-L64","documentation":"display_date() parses an ISO-8601 timestamp with datetime.fromisoformat and requires it to carry timezone information so it can convert to UTC for display. A naive datetime (no offset/Z) would make the rendered date ambiguous, so the script raises ValueError.","triggerScenarios":"Calling display_date (via summarize or the refreshed timestamp) with a string like \"2026-09-18T12:00:00\" — no 'Z' suffix and no UTC offset. Note the code only replaces a trailing 'Z' with '+00:00'; strings with neither Z nor offset stay naive and trigger the error.","commonSituations":"History feeds written by a producer that serialized naive local datetimes; hand-edited fixture dates; dates generated with datetime.now() (naive) instead of datetime.now(timezone.utc).","solutions":["Fix the producer to serialize timezone-aware ISO timestamps (e.g. datetime.now(timezone.utc).isoformat(), which includes +00:00).","If the source is known-UTC, append an offset before parsing: value + \"+00:00\" when no Z/offset is present.","In fixtures, write \"2026-09-18T12:00:00Z\" or \"+00:00\"-suffixed timestamps.","If naive dates must be tolerated, decide a policy (assume UTC) and normalize in display_date."],"exampleFix":"# before\nmeasured = \"2026-09-18T12:00:00\"  # naive\n# after\nmeasured = \"2026-09-18T12:00:00Z\"  # or +00:00 suffix","handlingStrategy":"validation","validationCode":"from datetime import datetime\ndef is_aware_iso(value):\n    d = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n    return d.tzinfo is not None","typeGuard":"def has_timezone(value):\n    try:\n        return datetime.fromisoformat(value.replace(\"Z\", \"+00:00\")).tzinfo is not None\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    values = summarize(history, kind)\nexcept ValueError as e:\n    if str(e) == \"History date must include a timezone\":\n        history = normalize_dates_to_utc(history)  # append +00:00 to naive stamps\n        values = summarize(history, kind)\n    else:\n        raise","preventionTips":["Serialize all feed timestamps with timezone info: datetime.now(timezone.utc).isoformat().","Always suffix UTC stamps with Z or +00:00 in fixtures and hand-edited data.","Validate timestamp fields on the producer side before publishing history.","Never use naive datetime.now() when writing campaign/revision timestamps."],"tags":["datetime","timezone","validation","python"],"backgroundTag":"invalid-date-format","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}