{"record":{"id":"dddcc900aa8f5370","repo":"github/spec-kit","slug":"corrupt-record-contributed-components-must-be-a","errorCode":null,"errorMessage":"Corrupt record: 'contributed_components' must be a list.","messagePattern":"Corrupt record: 'contributed_components' must be a list\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/models/records.py","lineNumber":65,"sourceCode":"            \"version\": self.version,\n            \"installed_at\": self.installed_at,\n            \"contributed_components\": [\n                _component_to_dict(c) for c in self.contributed_components\n            ],\n        }\n\n    @classmethod\n    def from_dict(cls, data: Any) -> \"InstalledBundleRecord\":\n        if not isinstance(data, dict):\n            raise BundlerError(\"Each installed-bundle record must be a mapping.\")\n        components_raw = data.get(\"contributed_components\")\n        if components_raw is None:\n            components_raw = []\n        elif not isinstance(components_raw, list):\n            # `or []` would coerce a FALSY non-list (0, '', False, {}) to []\n            # before this guard, silently accepting a corrupt record; only an\n            # absent/None value means \"no components\".\n            raise BundlerError(\n                \"Corrupt record: 'contributed_components' must be a list.\"\n            )\n        bundle_id = str(data.get(\"bundle_id\", \"\")).strip()\n        version = str(data.get(\"version\", \"\")).strip()\n        if not bundle_id:\n            raise BundlerError(\n                \"Corrupt records file: an installed-bundle record is missing \"\n                \"its 'bundle_id'.\"\n            )\n        if not version:\n            raise BundlerError(\n                f\"Corrupt records file: record for bundle '{bundle_id}' is \"\n                \"missing its 'version'.\"\n            )\n        return cls(\n            bundle_id=bundle_id,\n            version=version,\n            installed_at=str(data.get(\"installed_at\", \"\")).strip(),","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/models/records.py#L47-L83","documentation":"Raised by InstalledBundleRecord.from_dict when a record's 'contributed_components' key is present but is not a JSON array. The comment documents why: a naive 'or []' would coerce falsy non-lists (0, '', false, {}) to empty and silently accept a corrupt record; only absent/null means 'no components'.","triggerScenarios":"A records-file entry has \"contributed_components\": \"skills\" (string), 0, false, or {} instead of an array. from_dict rejects it before building component tuples.","commonSituations":"Hand-editing the records file to note a component kind as a string; schema drift from older tooling; truncated writes.","solutions":["Set 'contributed_components' to an array of component objects (or [] if none).","Remove the key entirely if there are no contributed components — absence is valid."],"exampleFix":"// before\n\"contributed_components\": \"my-skill\"\n\n// after\n\"contributed_components\": [\n  {\"kind\": \"skill\", \"id\": \"my-skill\", \"source\": \"bundle\"}\n]","handlingStrategy":"validation","validationCode":"def components_key_ok(record: dict) -> bool:\n    c = record.get(\"contributed_components\")\n    return c is None or isinstance(c, list)","typeGuard":"def is_optional_component_list(raw: object) -> bool:\n    return raw is None or isinstance(raw, list)","tryCatchPattern":"try:\n    records = load_records(project_root)\nexcept BundlerError as e:\n    if \"'contributed_components' must be a list\" in str(e):\n        # fix the record or delete it and reinstall\n        ...","preventionTips":["'contributed_components' is an array or absent — never a scalar/bool/object.","Let the bundler write component lists; manual edits corrupt shape."],"tags":["bundler","records","json","validation","corruption"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}