github/spec-kit · error · ValueError

Cannot restore '{pack_id}': metadata must be a dict

Error message

Cannot restore '{pack_id}': metadata must be a dict

What it means

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.

Source

Thrown at src/specify_cli/presets/__init__.py:661

        packs[pack_id] = merged
        self._save()

    def restore(self, pack_id: str, metadata: dict):
        """Restore preset metadata to registry without modifying timestamps.

        Use this method for rollback scenarios where you have a complete backup
        of the registry entry (including installed_at) and want to restore it
        exactly as it was.

        Args:
            pack_id: Preset ID
            metadata: Complete preset metadata including installed_at

        Raises:
            ValueError: If metadata is None or not a dict
        """
        if metadata is None or not isinstance(metadata, dict):
            raise ValueError(f"Cannot restore '{pack_id}': metadata must be a dict")
        # Ensure presets dict exists (handle corrupted registry)
        if not isinstance(self.data.get("presets"), dict):
            self.data["presets"] = {}
        self.data["presets"][pack_id] = copy.deepcopy(metadata)
        self._save()

    def get(self, pack_id: str) -> Optional[dict]:
        """Get preset metadata from registry.

        Returns a deep copy to prevent callers from accidentally mutating
        nested internal registry state without going through the write path.

        Args:
            pack_id: Preset ID

        Returns:
            Deep copy of preset metadata, or None if not found or corrupted
        """

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Pass the exact dict previously returned by registry.get(pack_id) (or a deep copy of it).
  2. Guard before calling: only restore when the backup value is a non-None dict.
  3. If restoring from a JSON backup file, index into the `presets[pack_id]` entry, not the file root.

Example fix

# before
backup = registry.get(pack_id)  # may be None
registry.restore_backup(pack_id, backup)

# after
backup = registry.get(pack_id)
if isinstance(backup, dict):
    registry.restore_backup(pack_id, backup)
Defensive patterns

Strategy: type-guard

Validate before calling

backup = registry.get(pack_id)
if not isinstance(backup, dict):
    raise RuntimeError(f"no valid backup entry for {pack_id}")
registry.restore_backup(pack_id, backup)

Type guard

def is_restorable(metadata) -> bool:
    return isinstance(metadata, dict)

Try / catch

try:
    registry.restore_backup(pack_id, metadata)
except ValueError as e:
    # metadata was None/malformed: re-capture via registry.get() before rollback
    ...

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/15adc9e955a216d6. Report an issue: GitHub.