{"record":{"id":"e0cbce18b1182e2e","repo":"JuliusBrussee/caveman","slug":"label-pricing-changed-without-a-new-verified-at","errorCode":null,"errorMessage":"{label}: pricing changed without a new verified_at snapshot version (checked catalog/{verified}.yaml)","messagePattern":"(.+?): pricing changed without a new verified_at snapshot version \\(checked catalog/(.+?)\\.yaml\\)","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":234,"sourceCode":"    for index, row in enumerate(current):\n        label = f\"current.yaml row {index + 1}\"\n        identity = validate_row(row, label, check_time)\n        if identity in seen:\n            raise CatalogError(f\"{label}: duplicate identity {'/'.join(identity)}\")\n        seen.add(identity)\n\n        verified = parsed_time(row[\"verified_at\"], label).date().isoformat()\n        snapshot = snapshots.setdefault(\n            verified,\n            load_yaml(CATALOG_DIR / f\"{verified}.yaml\"),\n        )\n        identity = pricing_identity(row)\n        matched = next(\n            (snap_row for snap_row in snapshot if pricing_identity(snap_row) == identity),\n            None,\n        )\n        if matched is None:\n            raise CatalogError(\n                f\"{label}: pricing changed without a new verified_at snapshot version (checked catalog/{verified}.yaml)\"\n            )\n        # sources is excluded from pricing_identity so a capability citation can\n        # be added without minting a new price-dated snapshot, but a PRICING\n        # citation must never be silently swapped or dropped while verified_at\n        # stays put. Mirrors the Go isSubset check in catalog_test.go.\n        snapshot_sources = set(matched.get(\"sources\") or [])\n        if not snapshot_sources.issubset(set(row[\"sources\"])):\n            raise CatalogError(\n                f\"{label}: dropped or replaced a source from its immutable snapshot \"\n                f\"catalog/{verified}.yaml without a new verified_at \"\n                f\"(snapshot had {sorted(snapshot_sources)}, current has {sorted(row['sources'])})\"\n            )\n\n\ndef main() -> int:\n    try:\n        validate_catalog()","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/provider-catalog/validate_catalog.py#L216-L252","documentation":"Raised by validate_catalog() when a current.yaml row's pricing identity (provider, model, region, currency, pricing, verified_at, plus price-affecting capabilities) has no byte-semantic match in the immutable dated snapshot catalog/<verified_at>.yaml. The snapshot is the tamper-evidence for the price an earlier verified_at already attested — if pricing changed while verified_at stayed put, the change ships under an attestation nobody made, so validation refuses it.","triggerScenarios":"Editing any pricing field (or a PriceAffectingCapabilities key like regional_processing_multiplier, inference_geo_us_multiplier, region_agnostic_pricing) without bumping verified_at; bumping verified_at but forgetting to create/update catalog/<new-date>.yaml; fixing a typo in an existing price in place; or reusing an old verified_at date for a changed price.","commonSituations":"A quick 'just fix the number' edit to current.yaml; a new model added to current.yaml but not copied into a dated snapshot; snapshot files pruned by an over-eager cleanup that deleted a referenced date.","solutions":["If the price genuinely changed: bump verified_at to the day you re-checked it, and create (or extend) catalog/<that-date>.yaml containing the row with its new pricing — the snapshot must contain a row whose pricing identity matches exactly.","If you bumped verified_at already, verify catalog/<verified_at>.yaml exists and contains the row (load_yaml is called on it lazily; a missing file errors as invalid YAML/missing file).","If the edit was accidental (typo), revert the pricing fields to match the snapshot instead of minting a new one.","Remember capabilities edits that are NOT price-affecting (tools/vision/json_mode/context_window_tokens) do not require a snapshot — only pricing and the three price-affecting capability keys do."],"exampleFix":"# before — price edited in place, verified_at unchanged (no snapshot match)\n- provider: openai\n  model: gpt-4o\n  region: us\n  pricing: {input_per_million: 2.75}   # was 2.50 in catalog/2026-06-01.yaml\n  verified_at: \"2026-06-01T00:00:00Z\"\n\n# after — re-verified, new date + new snapshot catalog/2026-08-15.yaml with the same row\n- provider: openai\n  model: gpt-4o\n  region: us\n  pricing: {input_per_million: 2.75}\n  verified_at: \"2026-08-15T00:00:00Z\"","handlingStrategy":"validation","validationCode":"PRICING_IDENTITY_KEYS = (\"provider\", \"model\", \"region\", \"currency\", \"pricing\", \"verified_at\")\nPRICE_AFFECTING_CAPS = (\"regional_processing_multiplier\", \"inference_geo_us_multiplier\", \"region_agnostic_pricing\")\n\ndef snapshot_pins_row(current_row: dict, snapshot_rows: list[dict]) -> bool:\n    def ident(r):\n        caps = {k: r.get(\"capabilities\", {}).get(k) for k in PRICE_AFFECTING_CAPS}\n        return tuple(r.get(k) for k in PRICING_IDENTITY_KEYS), tuple(sorted(caps.items()))\n    return any(ident(s) == ident(current_row) for s in snapshot_rows)","typeGuard":null,"tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"without a new verified_at snapshot\" in str(e):\n        # bump verified_at to today and copy the row into catalog/<today>.yaml\n        mint_snapshot_for_changed_rows()\n    raise","preventionTips":["Pair every pricing (or price-affecting capability) edit with a verified_at bump and a dated snapshot in the same commit.","Remember the three price-affecting capability keys count as pricing even though they live under capabilities.","Run go test ./public/shared/platform/catalog as the same gate — it enforces the identical pin."],"tags":["validation","catalog","provenance","snapshot","data-integrity"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}