{"record":{"id":"cbc98e8a56a966cd","repo":"JuliusBrussee/caveman","slug":"label-verified-at-is-in-the-future","errorCode":null,"errorMessage":"{label}: verified_at is in the future","messagePattern":"(.+?): verified_at is in the future","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":164,"sourceCode":"            continue\n        if isinstance(value, bool) or not isinstance(value, (int, float)):\n            raise CatalogError(f\"{label}: pricing.{field} must be numeric or null\")\n        if not math.isfinite(value) or value < 0:\n            raise CatalogError(f\"{label}: pricing.{field} must be finite and nonnegative\")\n        if field == \"batch_discount_fraction\" and value > 1:\n            raise CatalogError(f\"{label}: pricing.batch_discount_fraction must be <= 1\")\n\n    if not isinstance(row[\"capabilities\"], dict):\n        raise CatalogError(f\"{label}: capabilities must be an object\")\n    sources = row[\"sources\"]\n    if not isinstance(sources, list) or not sources:\n        raise CatalogError(f\"{label}: sources must be a non-empty list\")\n    if any(not isinstance(source, str) or not source.startswith(\"https://\") for source in sources):\n        raise CatalogError(f\"{label}: every source must be HTTPS\")\n\n    verified_at = parsed_time(row[\"verified_at\"], label)\n    if verified_at > now:\n        raise CatalogError(f\"{label}: verified_at is in the future\")\n    if verified_at < now - timedelta(days=120):\n        raise CatalogError(f\"{label}: verified_at is older than 120 days\")\n    return identity[0], identity[1], identity[2]\n\n\ndef check_review_markers(text: str, label: str) -> None:\n    \"\"\"Refuse a catalog that still carries an unreviewed sync proposal.\n\n    YAML comments are invisible to the parsed rows, so this reads the raw file:\n    a rubber-stamped merge must not be able to advance price provenance while\n    the line saying \"nobody has confirmed this yet\" is still in the file.\n    \"\"\"\n    offending = [\n        index + 1\n        for index, line in enumerate(text.splitlines())\n        if line.lstrip().startswith(REVIEW_MARKER)\n    ]\n    if offending:","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/provider-catalog/validate_catalog.py#L146-L182","documentation":"Raised by validate_row() in the provider-catalog validator when a row's verified_at timestamp is strictly later than the validation time (now, default datetime.now(timezone.utc)). verified_at is price provenance — the sole input to catalogVersion() embedded in signed receipts — so a future date would attest a price check that has not happened yet. The comparison happens after parsed_time() normalizes the value to UTC, so both naive-skew and timezone-skew land here.","triggerScenarios":"Running validate_catalog.py (or validate_catalog(now=...)) when any row in catalog/current.yaml has verified_at later than the check time: a sync script stamping the proposal date instead of the check date, a hand-edit copying tomorrow's date, a test passing a fixed `now` earlier than the fixture's verified_at, or a machine clock behind the author's clock (timestamp written on a fast clock, validated on a slow one).","commonSituations":"Running the catalog validator in CI or on a fresh checkout right after an automated catalog sync proposed changes; timezone confusion where a local-time timestamp without offset would fail earlier ('must include a timezone') but a +00:00 future date reaches this check; fixtures in tests using hardcoded dates that drift into the future relative to a frozen `now`.","solutions":["Set verified_at to the actual UTC date you checked the vendor pricing page (RFC3339 with timezone, e.g. 2026-08-15T00:00:00Z); it must be <= now.","If the timestamp came from scripts/catalog_sync_modelsdev.py, re-check the rows against their cited sources and correct the date before merging.","If this fires in a test, pass a `now` that is at or after the fixture's verified_at (validate_catalog(now=...)).","If the machine clock is wrong (VM, CI runner drift), fix the clock via NTP and re-run; do not bump the date to silence it."],"exampleFix":"# current.yaml (before)\nverified_at: \"2026-09-01T00:00:00Z\"  # dated in the future\n\n# after\nverified_at: \"2026-08-15T00:00:00Z\"  # the day prices were actually checked","handlingStrategy":"validation","validationCode":"from datetime import datetime, timezone\n\ndef verified_at_is_not_future(verified_at: str) -> bool:\n    try:\n        ts = datetime.fromisoformat(verified_at.replace(\"Z\", \"+00:00\"))\n    except ValueError:\n        return False\n    return ts.tzinfo is not None and ts.astimezone(timezone.utc) <= datetime.now(timezone.utc)","typeGuard":null,"tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    # message names the row label; fix the row's verified_at, do not catch-and-continue\n    raise SystemExit(f\"catalog invalid: {e}\")","preventionTips":["Write verified_at as the UTC date you actually checked the vendor page, never a planned/launch date.","Run validate_catalog.py in CI and pre-commit so future-dated rows fail before merge.","Keep test fixtures' verified_at parametric (computed from now) rather than hardcoded."],"tags":["validation","catalog","provenance","time"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}