{"record":{"id":"ee5c5ee67dd9bf10","repo":"JuliusBrussee/caveman","slug":"label-dropped-or-replaced-a-source-from-its-imm","errorCode":null,"errorMessage":"{label}: dropped or replaced a source from its immutable snapshot catalog/{verified}.yaml without a new verified_at (snapshot had {sorted(snapshot_sources)}, current has {sorted(row['sources'])})","messagePattern":"(.+?): dropped or replaced a source from its immutable snapshot catalog/(.+?)\\.yaml without a new verified_at \\(snapshot had (.+?), current has (.+?)\\)","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":243,"sourceCode":"            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()\n    except CatalogError as error:\n        print(f\"provider catalog invalid: {error}\", file=sys.stderr)\n        return 1\n    print(\"provider catalog valid: current rows are fresh, sourced, and snapshot-backed\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/provider-catalog/validate_catalog.py#L225-L261","documentation":"Raised by validate_catalog() when the matched snapshot row's sources are not a subset of the current row's sources. sources is deliberately excluded from pricing_identity so a capability citation can be added without minting a price-dated snapshot — but a PRICING source must never be silently swapped or dropped while verified_at stays put, because the citation is how the attested price gets audited. Mirrors the Go isSubset check in catalog_test.go.","triggerScenarios":"Removing or replacing a URL in a row's `sources` list without bumping verified_at and minting a new snapshot — e.g. swapping a dead pricing-page URL for a new one, deduplicating sources, or a sync script rewriting the list. The message shows both sorted sets (snapshot had X, current has Y) so the dropped/replaced entry is visible.","commonSituations":"Link-rot cleanup PRs that 'fix broken URLs' across the catalog; tools that normalize or sort source lists and drop entries; a re-verification that replaces rather than supplements the old citation.","solutions":["If a pricing source must change, ADD the new URL alongside the old one (superset is allowed) rather than replacing it.","If the old source truly must go (dead link, wrong page), treat it as a price re-check: confirm the price against the new source, bump verified_at, and mint catalog/<new-date>.yaml.","Restore the dropped URL(s) listed in the message's 'snapshot had' set if the removal was unintentional."],"exampleFix":"# before — snapshot had https://openai.com/pricing, current replaced it\nsources: [\"https://platform.openai.com/docs/pricing\"]\n\n# after — superset keeps the pinned citation\nsources: [\"https://openai.com/pricing\", \"https://platform.openai.com/docs/pricing\"]","handlingStrategy":"validation","validationCode":"def sources_cover_snapshot(current_sources: list[str], snapshot_sources: list[str]) -> bool:\n    return set(snapshot_sources).issubset(set(current_sources))","typeGuard":null,"tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"dropped or replaced a source\" in str(e):\n        # re-add the pinned URLs from the snapshot's set, or re-verify + mint snapshot\n        restore_or_reattest(e)\n    raise","preventionTips":["Only ever ADD source URLs; never remove or replace one without a fresh verified_at.","Keep link-rot cleanup PRs away from catalog `sources` unless paired with re-attestation.","Diff current.yaml against its named snapshot before committing source-list edits."],"tags":["validation","catalog","provenance","sources","audit"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}