{"record":{"id":"cb1a36ec74072bed","repo":"JuliusBrussee/caveman","slug":"label-unknown-field-s-join-sorted-unkno","errorCode":null,"errorMessage":"{label}: unknown field(s): {', '.join(sorted(unknown))}","messagePattern":"(.+?): unknown field\\(s\\): (.+?)","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":120,"sourceCode":"    if isinstance(value, datetime):\n        parsed = value\n    elif isinstance(value, str):\n        try:\n            parsed = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n        except ValueError as error:\n            raise CatalogError(f\"{label}: verified_at must be RFC3339\") from error\n    else:\n        raise CatalogError(f\"{label}: verified_at must be an RFC3339 timestamp\")\n    if parsed.tzinfo is None:\n        raise CatalogError(f\"{label}: verified_at must include a timezone\")\n    return parsed.astimezone(timezone.utc)\n\n\ndef validate_row(row: dict[str, Any], label: str, now: datetime) -> tuple[str, str, str]:\n    unknown = set(row) - TOP_LEVEL_KEYS\n    missing = REQUIRED_KEYS - set(row)\n    if unknown:\n        raise CatalogError(f\"{label}: unknown field(s): {', '.join(sorted(unknown))}\")\n    if missing:\n        raise CatalogError(f\"{label}: missing field(s): {', '.join(sorted(missing))}\")\n\n    identity: list[str] = []\n    for field in (\"provider\", \"model\", \"region\"):\n        value = row[field]\n        if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):\n            raise CatalogError(f\"{label}: {field} must be a safe non-empty identifier\")\n        identity.append(value)\n    if row[\"currency\"] != \"USD\":\n        raise CatalogError(f\"{label}: currency must be USD\")\n\n    pricing = row[\"pricing\"]\n    if not isinstance(pricing, dict):\n        raise CatalogError(f\"{label}: pricing must be an object\")\n    pricing_unknown = set(pricing) - PRICING_KEYS\n    if pricing_unknown:\n        raise CatalogError(f\"{label}: unknown pricing field(s): {', '.join(sorted(pricing_unknown))}\")","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/provider-catalog/validate_catalog.py#L102-L138","documentation":"A catalog row contains a top-level field not in the allowed schema (REQUIRED_KEYS plus the optional capabilities_verified_at). The validator is strict-allowlist: unknown fields are typos, drifted schema, or unvetted data, so they fail the build rather than being silently ignored. Thrown from validate_row.","triggerScenarios":"validate_row sees `set(row) - TOP_LEVEL_KEYS` non-empty, e.g. a row with `verifiedAt` (camelCase), `verification_date`, or a newly invented key like `notes` placed at row level instead of inside capabilities.","commonSituations":"Renaming a field without updating the schema; camelCase vs snake_case mismatch from a JS-authored script; adding free-form commentary at row level instead of as a YAML comment; schema additions made in one file but not the other.","solutions":["Remove or rename the listed unknown field(s) to the schema's snake_case names","If the data belongs in the row, move it into the allowed `capabilities` mapping or a YAML comment","If a new top-level field is genuinely needed, add it to TOP_LEVEL_KEYS/REQUIRED_KEYS in validate_catalog.py and to schemas/provider-catalog.schema.json in the same change","Check the row against an existing valid row in current.yaml for exact field names"],"exampleFix":"# before\n- provider: openai\n  verifiedAt: \"2026-07-05T00:00:00Z\"\n\n# after\n- provider: openai\n  verified_at: \"2026-07-05T00:00:00Z\"","handlingStrategy":"validation","validationCode":"TOP_LEVEL_KEYS = {\"provider\",\"model\",\"region\",\"currency\",\"pricing\",\"capabilities\",\"sources\",\"verified_at\",\"capabilities_verified_at\"}\n\ndef no_unknown_fields(row: dict) -> bool:\n    return not (set(row) - TOP_LEVEL_KEYS)","typeGuard":"def has_known_keys_only(row: dict) -> bool:\n    return set(row).issubset(TOP_LEVEL_KEYS)","tryCatchPattern":"try:\n    validate_catalog()\nexcept CatalogError as e:\n    if \"unknown field(s)\" in str(e):\n        rename_or_remove_fields(str(e))","preventionTips":["Validate rows against schemas/provider-catalog.schema.json in CI before the Python validator runs","Use snake_case field names matching existing rows","Keep free-form notes as YAML comments, not fields"],"tags":["schema-validation","yaml","catalog"],"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"}