{"record":{"id":"15adc9e955a216d6","repo":"github/spec-kit","slug":"cannot-restore-pack-id-metadata-must-be-a-dic","errorCode":null,"errorMessage":"Cannot restore '{pack_id}': metadata must be a dict","messagePattern":"Cannot restore '(.+?)': metadata must be a dict","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/presets/__init__.py","lineNumber":661,"sourceCode":"        packs[pack_id] = merged\n        self._save()\n\n    def restore(self, pack_id: str, metadata: dict):\n        \"\"\"Restore preset metadata to registry without modifying timestamps.\n\n        Use this method for rollback scenarios where you have a complete backup\n        of the registry entry (including installed_at) and want to restore it\n        exactly as it was.\n\n        Args:\n            pack_id: Preset ID\n            metadata: Complete preset metadata including installed_at\n\n        Raises:\n            ValueError: If metadata is None or not a dict\n        \"\"\"\n        if metadata is None or not isinstance(metadata, dict):\n            raise ValueError(f\"Cannot restore '{pack_id}': metadata must be a dict\")\n        # Ensure presets dict exists (handle corrupted registry)\n        if not isinstance(self.data.get(\"presets\"), dict):\n            self.data[\"presets\"] = {}\n        self.data[\"presets\"][pack_id] = copy.deepcopy(metadata)\n        self._save()\n\n    def get(self, pack_id: str) -> Optional[dict]:\n        \"\"\"Get preset metadata from registry.\n\n        Returns a deep copy to prevent callers from accidentally mutating\n        nested internal registry state without going through the write path.\n\n        Args:\n            pack_id: Preset ID\n\n        Returns:\n            Deep copy of preset metadata, or None if not found or corrupted\n        \"\"\"","sourceCodeStart":643,"sourceCodeEnd":679,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/presets/__init__.py#L643-L679","documentation":"PresetRegistry.restore_backup rejects a metadata argument that is None or not a dict. restore_backup is used for rollback scenarios and expects a complete registry entry (including installed_at) captured earlier via get(); anything else cannot be restored verbatim.","triggerScenarios":"Calling registry.restore_backup(pack_id, metadata) with metadata=None, a JSON-decoded list/string, or passing the whole registry dict instead of a single preset entry.","commonSituations":"Rollback code that captured `registry.get(pack_id)` returning None for an uninstalled preset and later feeds that None back to restore; deserializing a backup with json.load of the wrong node.","solutions":["Pass the exact dict previously returned by registry.get(pack_id) (or a deep copy of it).","Guard before calling: only restore when the backup value is a non-None dict.","If restoring from a JSON backup file, index into the `presets[pack_id]` entry, not the file root."],"exampleFix":"# before\nbackup = registry.get(pack_id)  # may be None\nregistry.restore_backup(pack_id, backup)\n\n# after\nbackup = registry.get(pack_id)\nif isinstance(backup, dict):\n    registry.restore_backup(pack_id, backup)","handlingStrategy":"type-guard","validationCode":"backup = registry.get(pack_id)\nif not isinstance(backup, dict):\n    raise RuntimeError(f\"no valid backup entry for {pack_id}\")\nregistry.restore_backup(pack_id, backup)","typeGuard":"def is_restorable(metadata) -> bool:\n    return isinstance(metadata, dict)","tryCatchPattern":"try:\n    registry.restore_backup(pack_id, metadata)\nexcept ValueError as e:\n    # metadata was None/malformed: re-capture via registry.get() before rollback\n    ...","preventionTips":["Capture backups with registry.get() and validate they are dicts before storing.","When persisting backups as JSON, index the specific presets[pack_id] entry.","Never pass a get() result through unchecked when the preset may be absent."],"tags":["preset","registry","type-validation","rollback"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}