{"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/3ee70a102609e550bd2e68004bf5990a9341c851/shared/provider-catalog/validate_catalog.py#L225-L261","documentation":"Snapshot files catalog/{verified}.yaml are immutable price-dated records. A row's sources set in current.yaml must be a superset of the sources recorded in its snapshot: existing (pricing) citations may be added to but never silently dropped or replaced while verified_at stays put. The check mirrors the Go isSubset check in catalog_test.go and reports both sets so the diff is obvious.","triggerScenarios":"Running validate_catalog after removing or swapping a source URL from a row's sources list while verified_at is unchanged; the snapshot's sources set fails issubset against the current row's sources and the error prints snapshot vs current sorted source lists.","commonSituations":"Cleaning up 'stale' citation URLs that were actually part of the audited snapshot; a sync tool rewrote the sources list in a different order or format; branch merge dropped a citation added at verification time; replacing a primary source link with a 'better' one without re-verifying prices.","solutions":["Restore the missing source(s) listed in the snapshot set to the row's sources in current.yaml","If the source genuinely must be replaced, bump verified_at and mint a new catalog/{date}.yaml snapshot containing the new sources","Re-run the sync tool if it is responsible for mangling the sources list","Re-run validate_catalog to confirm snapshot_sources is now a subset of the current sources"],"exampleFix":"# before (current.yaml)\nsources:\n  - https://new-pricing-page.example.com\n# after (restored snapshot source)\nsources:\n  - https://official-pricing.example.com\n  - https://new-pricing-page.example.com","handlingStrategy":"validation","validationCode":"def sources_preserved(snapshot_row, current_row):\n    snap = set(snapshot_row.get(\"sources\") or [])\n    return snap.issubset(set(current_row[\"sources\"]))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat snapshot sources as append-only while verified_at is unchanged","When replacing a citation, bump verified_at and mint a new snapshot instead of swapping in place","Let the sync tool own the sources list; avoid manual reordering or reformatting","Review sources diffs explicitly in catalog PRs"],"tags":["validation","catalog","immutability","sources"],"backgroundTag":"schema-validation-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}