{"record":{"id":"ab7cb461c8ac8357","repo":"JuliusBrussee/caveman","slug":"label-verified-at-is-older-than-120-days","errorCode":null,"errorMessage":"{label}: verified_at is older than 120 days","messagePattern":"(.+?): verified_at is older than 120 days","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":166,"sourceCode":"            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:\n        raise CatalogError(\n            f\"{label}: unreviewed sync proposal marker on line(s) \"","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/provider-catalog/validate_catalog.py#L148-L184","documentation":"Raised by validate_row() when a row's verified_at is more than 120 days before the validation time. The catalog treats price data as perishable: every row must have been re-checked against the vendor's pricing page within the last 120 days, because prices feed cost accounting and signed receipts (catalogVersion()). This is a staleness gate, not a format check — the timestamp parses fine but has aged out.","triggerScenarios":"validate_catalog() runs (CI gate, pre-commit, or direct python validate_catalog.py) and any row in current.yaml carries verified_at older than now - timedelta(days=120). Typical producers: nobody re-verifying prices for a quarter, an old row surviving model additions, or a reverted re-attest that restored an earlier date (as documented for the 2026-07-30 snapshot removal).","commonSituations":"A repo that has been quiet for months resumes work and CI fails the catalog gate; a long-lived branch merged after its verification window lapsed; teams that only add new models and never refresh existing rows' price checks.","solutions":["Re-check the offending row's pricing against its cited provider pricing page, then bump verified_at to the check date AND add/copy the row into the matching catalog/YYYY-MM-DD.yaml snapshot (a new verified_at requires a snapshot that pins it).","If prices are unchanged, this is still a re-attest: bump verified_at and mint the dated snapshot — you are attesting the numbers still match the vendor page.","Run validate_catalog.py after editing to confirm the whole pipeline (future check, 120-day check, snapshot pin, sources subset) passes."],"exampleFix":"# before: verified_at older than 120 days\nverified_at: \"2026-03-01T00:00:00Z\"\n\n# after: re-checked prices on 2026-08-15, snapshot copied to catalog/2026-08-15.yaml\nverified_at: \"2026-08-15T00:00:00Z\"","handlingStrategy":"validation","validationCode":"from datetime import datetime, timedelta, timezone\n\ndef verified_at_is_fresh(verified_at: str, max_age_days: int = 120) -> bool:\n    ts = datetime.fromisoformat(verified_at.replace(\"Z\", \"+00:00\")).astimezone(timezone.utc)\n    return datetime.now(timezone.utc) - ts <= timedelta(days=max_age_days)","typeGuard":null,"tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"older than 120 days\" in str(e):\n        # schedule a price re-verification pass; bump verified_at + mint snapshot\n        log_stale_rows(e)\n    raise","preventionTips":["Put a recurring (e.g. monthly) calendar task to re-verify catalog prices so rows never approach 120 days.","Treat the 120-day gate as a hard CI deadline; do not rename dates to pass it.","When bumping verified_at, always create the matching catalog/<date>.yaml snapshot in the same change."],"tags":["validation","catalog","staleness","provenance"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}