"""core/store_backend.py — which store is active: `STORE_BACKEND=hf|pg` (X4 / EXIT-2b). import core.store_backend as store # new code store.get('users') # goes to whichever backend is selected ⭐ **C-4 HAPPENED — WAVE 20, 2026-08-05 (owner ruling R1, DEBT D-4).** This header used to say, in bold, that flipping `STORE_BACKEND=pg` did NOT redirect the ~28 callers that say `import core.store as store`, and that "the tempting shortcut — have `core/store.py` itself delegate" would be worse because there was "no dual-read window and no way to compare the two stores' contents first". Both halves of that objection were ANSWERED rather than ignored, which is the only reason the shortcut became the design: * the comparison exists — `ops/seed_pg_from_hf.py --verify` diffs the two stores key-by-key (and value-by-value) and is run BEFORE any environment flips; * the "silently changes backend the moment an env var is set in some shell" risk is why the flip is fail-closed and loud: no `DATABASE_URL` under `pg` RAISES, it never falls back to the file store. A misconfigured process refuses to serve instead of quietly writing to the wrong place — which is the failure mode the original warning actually cared about. So **`core/store.py::handle()` is now the seam**, and it is the one every caller crosses: module-level functions resolve it per call via `_d()`, and `harness.runtime.get_runtime` binds a per-TENANT handle (a dataset repo on `hf`, a `t_` schema on `pg`). THIS module remains the by-name selector for code that wants a specific backend's module rather than the active handle — the migration tooling, and `verify_store_pg`, which asserts `store.backend()` and `name()` agree on every value so the two entry points cannot drift. An unrecognised value is a configuration error and RAISES rather than falling back — a typo'd backend name that quietly served the old store would be discovered by data going missing. """ import os import core.store as _hf _NAMES = ('available', 'get', 'exists', 'put', 'update', 'upload_bytes', 'download_bytes', 'delete_path', 'flush') def name(): """The selected backend name, validated. `hf` unless explicitly told otherwise.""" raw = (os.environ.get('STORE_BACKEND') or 'hf').strip().lower() if raw not in ('hf', 'pg'): raise RuntimeError( f"STORE_BACKEND={raw!r} is not a backend. Use 'hf' (the HF Dataset store, the " f"default) or 'pg' (Postgres, needs DATABASE_URL). Refusing to guess: a typo that " f"silently served the other store is how data ends up in two places.") return raw def active(): """The backend MODULE. Resolved per call, so a test can flip the env var without a reimport.""" if name() == 'pg': import core.store_pg as _pg return _pg return _hf # --- the TEN delegates. Written out rather than generated with setattr so that reading this file # tells you the whole interface, and so a new store operation cannot be added to core/store.py # without something here failing to match (verify_store_pg.py compares the signatures). # # ⭐ That guard did its job in wave 29: `revision()` landed in `core/store.py` and `core/store_pg.py` # and the gate went red HERE, on the one file of the four that the change had missed. A generated # delegate list would have covered it silently and left this file unable to describe itself. def available(): return active().available() def get(name_, fresh=False): return active().get(name_, fresh=fresh) def exists(name_): return active().exists(name_) def put(name_, data): return active().put(name_, data) def update(name_, fn, flush='sync'): return active().update(name_, fn, flush=flush) def upload_bytes(path_in_repo, data, message=None): return active().upload_bytes(path_in_repo, data, message=message) def download_bytes(path_in_repo): return active().download_bytes(path_in_repo) def delete_path(path_in_repo): return active().delete_path(path_in_repo) def flush(name_=None, timeout=30.0): return active().flush(name_, timeout=timeout) def get_projection(name_, drop=()): """A read of `name_` with `drop`'s keys removed from every top-level value (W32-T01 / D-185). The saving is the COPY, not the wire: on tenant #0's `user_tables` a whole read deep-copies 28.6 MB / 81,192 rows, and **99.89% of those bytes are `rows`** that no nav render or permission check reads. Measured warm on that document: 1,823 ms whole vs 2.2 ms projected. ⚠ Both backends implement it, which `verify_store_pg`'s IFACE forces — and this delegate is the THIRD place that has to know, after `core/store.py` and `core/store_pg.py`. The gate found it missing here the first time round; without it the selector falls through to nothing and a `STORE_BACKEND` caller gets `AttributeError` instead of a projection. """ return active().get_projection(name_, drop=drop) def revision(name_): """The bucket's change token (wave 29, C6) — `{'rev', 'updated_at', 'token'}`. Both backends answer, and they answer with the same keys and the same units (`updated_at` is epoch seconds on each). That parity is the reason the change-token endpoint needs no branch on the backend, and it is asserted rather than assumed by `verify_store_pg`'s interface section. """ return active().revision(name_)