Skip to content

feat(client): manifest cache invalidation + immutable-tag policy #258

@gerchowl

Description

@gerchowl

Problem

The Python client (`clients/python/src/mat_vis_client/client.py:539-559`) caches `release-manifest.json` on disk under `<cache_dir>//.manifest.json` and never revalidates. Once present, the cached manifest is served forever. JS/Rust/shell follow the same per-tag-cache pattern.

The implicit contract is "release tags are immutable" — but the v0.6.0 cut against `gerchowl/mat-vis@v2026.04.2` mutated the tag multiple times mid-release:

A user who pinned `MatVisClient(tag="v2026.04.2")` early has a stale 3-source / 2-tier manifest on disk and won't see the additions until they manually wipe `~/.cache/mat_vis_client/v2026.04.2/`.

`check_updates` (line 565+) only polls the GH releases API for "is there a newer tag" with a 24h TTL — it has no signal for "the SAME tag mutated."

Proposal

Two independent layers; both worth landing:

1. Policy: immutable tags

Codify in CHANGELOG + docs that once published, a release tag must not be mutated. New data ⇒ new CalVer (the v2026.05.1 branching pattern we discussed for KTX2 in #214 phase-7 chat). The release-cut PR for v0.6.0 should ship with this stated.

Acceptance:

  • CHANGELOG entry under v0.6.0 "Policy" / "Notes" subsection naming the immutability contract.
  • README quickstart pinning example mentions "tags are immutable".
  • (Maybe) a CI hook that errors if a workflow_dispatch targets an existing tag with a non-zero `limit` — defense in depth, but possibly overkill until we have a few releases under the new policy.

2. Mechanism: ETag-aware cache (defense in depth)

`HfApi` returns ETags on every `resolve` GET. Wire `MatVisClient.manifest` to:

  1. Send `If-None-Match: ` on every property access in a fresh client lifecycle (not per-call within one process — `self._manifest` in-memory cache stays).
  2. On 304 → serve cached value.
  3. On 200 → overwrite cache + ETag.

Cost: one conditional GET per client lifecycle. ~50 ms with cache hit, free thereafter via in-memory cache.

Acceptance:

  • First call in fresh process performs `HEAD`-style ETag probe (or conditional GET).
  • Cache file format gains an ETag sidecar (e.g. `.manifest.json.etag` or embed via JSON wrapper).
  • Test: bake a manifest, instantiate client, mutate manifest on "server" (mock), instantiate fresh client → sees the new manifest.
  • Mirror in JS/Rust/shell once the Python contract is set.

Severity

Not a v2026.04.2 ship blocker — that tag's mutations are now stable and a one-shot `rm -rf` is the operator workaround. But shipping v0.6.0 client packages without fixing this means the broken contract becomes a public API for the lifetime of the major. File for v0.6.x patch follow-up; should land before the next CalVer release that mutates a tag (v2026.05.1 KTX2 add, v2026.05.x 2k add per #257, etc.).

Out of scope

  • Cache-busting via query string (`?cb=`) — that defeats HF's CDN.
  • Removing the cache entirely — would burn unnecessary bandwidth on repeat fetches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions