Source code for rath.persistence.manifest

"""Root layout manifest for the ``.openrath/`` data root.

Historically each plane carried its own ``SCHEMA_VERSION`` (config, backend
spec-json, memory meta) with no coordination and no record of the overall
on-disk *layout*. ``manifest.json`` at the data root records the layout version
plus a snapshot of every plane's schema version, so:

- an upgrade can detect an older/newer layout deterministically;
- a newer OpenRath's data root is refused with a clear error rather than being
  silently misread by an older install.

The manifest is intentionally tiny and additive. Writing it is best-effort at
the persistence boundary; :func:`check_manifest` is a no-op when it is absent
(fresh or legacy root), so it never breaks existing installs.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from rath.persistence.atomic import atomic_write_json

__all__ = [
    "LAYOUT_VERSION",
    "MANIFEST_FILENAME",
    "ManifestVersionError",
    "plane_schema_versions",
    "read_manifest",
    "ensure_manifest",
    "check_manifest",
]

#: Bump when the overall on-disk directory layout changes (not when a single
#: plane's schema changes — those are tracked per-plane below).
LAYOUT_VERSION = 1

MANIFEST_FILENAME = "manifest.json"


[docs] class ManifestVersionError(RuntimeError): """Raised when the on-disk layout version is newer than this install."""
def plane_schema_versions() -> dict[str, int]: """Snapshot each plane's current schema version. Imported lazily so this module has no import-time dependency on the config/backend/memory packages (avoids import cycles). """ from rath.backend.persistence.spec_json import SCHEMA_VERSION as BACKEND_V from rath.config.schema import SCHEMA_VERSION as CONFIG_V from rath.memory.adapters.local import META_SCHEMA_VERSION as MEMORY_V return {"config": CONFIG_V, "backend": BACKEND_V, "memory": MEMORY_V} def _manifest_path(root: Path) -> Path: return root / MANIFEST_FILENAME def read_manifest(root: Path) -> dict[str, Any] | None: """Return the parsed manifest, or ``None`` when absent/unreadable.""" path = _manifest_path(root) if not path.is_file(): return None try: data = json.loads(path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return None return data if isinstance(data, dict) else None
[docs] def ensure_manifest(root: Path) -> dict[str, Any]: """Create the manifest if missing; return the effective manifest. Idempotent: an existing current-layout manifest is refreshed with the latest per-plane schema versions but keeps its layout version. """ existing = read_manifest(root) manifest: dict[str, Any] = { "layout_version": LAYOUT_VERSION, "planes": plane_schema_versions(), } if existing == manifest: return existing atomic_write_json(_manifest_path(root), manifest) return manifest
[docs] def check_manifest(root: Path) -> None: """Raise :class:`ManifestVersionError` if the layout is newer than ours. No-op when the manifest is absent (fresh or legacy root) — this must never break an install that predates the manifest. """ data = read_manifest(root) if data is None: return on_disk = data.get("layout_version") if isinstance(on_disk, int) and on_disk > LAYOUT_VERSION: raise ManifestVersionError( f"{_manifest_path(root)} has layout_version={on_disk}, newer than this " f"OpenRath (supports {LAYOUT_VERSION}); upgrade OpenRath to read this " f"data root safely." )