JuliusBrussee/caveman · error · CatalogError
{label}: dropped or replaced a source from its immutable sna
Error message
{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'])}) What it means
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.
Source
Thrown at shared/provider-catalog/validate_catalog.py:243
verified,
load_yaml(CATALOG_DIR / f"{verified}.yaml"),
)
identity = pricing_identity(row)
matched = next(
(snap_row for snap_row in snapshot if pricing_identity(snap_row) == identity),
None,
)
if matched is None:
raise CatalogError(
f"{label}: pricing changed without a new verified_at snapshot version (checked catalog/{verified}.yaml)"
)
# sources is excluded from pricing_identity so a capability citation can
# be added without minting a new price-dated snapshot, but a PRICING
# citation must never be silently swapped or dropped while verified_at
# stays put. Mirrors the Go isSubset check in catalog_test.go.
snapshot_sources = set(matched.get("sources") or [])
if not snapshot_sources.issubset(set(row["sources"])):
raise CatalogError(
f"{label}: dropped or replaced a source from its immutable snapshot "
f"catalog/{verified}.yaml without a new verified_at "
f"(snapshot had {sorted(snapshot_sources)}, current has {sorted(row['sources'])})"
)
def main() -> int:
try:
validate_catalog()
except CatalogError as error:
print(f"provider catalog invalid: {error}", file=sys.stderr)
return 1
print("provider catalog valid: current rows are fresh, sourced, and snapshot-backed")
return 0
if __name__ == "__main__":
raise SystemExit(main())View on GitHub (pinned to 27d5a3981a)
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.
Example fix
# before — snapshot had https://openai.com/pricing, current replaced it sources: ["https://platform.openai.com/docs/pricing"] # after — superset keeps the pinned citation sources: ["https://openai.com/pricing", "https://platform.openai.com/docs/pricing"]
Defensive patterns
Strategy: validation
Validate before calling
def sources_cover_snapshot(current_sources: list[str], snapshot_sources: list[str]) -> bool:
return set(snapshot_sources).issubset(set(current_sources)) Try / catch
try:
validate_catalog()
except CatalogError as e:
if "dropped or replaced a source" in str(e):
# re-add the pinned URLs from the snapshot's set, or re-verify + mint snapshot
restore_or_reattest(e)
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- {label}: verified_at is in the future
- {label}: verified_at is older than 120 days
- {label}: unreviewed sync proposal marker on line(s) {', '.jo
- {label}: pricing changed without a new verified_at snapshot
- %s %s/%s: at least one source is required
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/ee5c5ee67dd9bf10.
Report an issue: GitHub.