diff --git a/.github/scripts/bench_metrics.py b/.github/scripts/bench_metrics.py index c35b92f4a..30da2eebb 100644 --- a/.github/scripts/bench_metrics.py +++ b/.github/scripts/bench_metrics.py @@ -1,10 +1,11 @@ """Pure metric derivations for the Lambda benchmark CI (issue #110). -Kept import-light and side-effect-free so the workflow CLIs (``run_benchmark``, -``update_series``, ``plot_series``) and the unit tests can all call in. The live -Lambda dispatch lives in ``run_benchmark.py``; everything here is arithmetic over -the run summary ``zagg.runner.agg`` already returns plus the pinned target -metadata, so it runs with no AWS/network access. +Kept import-light and free of AWS/network access at import, so the workflow CLIs +(``run_benchmark``, ``update_series``, ``plot_series``) and the unit tests can all +call in. The one import-time read is the committed ``targets.json`` manifest, a +repo fixture the shard-map recipe section at the bottom needs. The live Lambda +dispatch lives in ``run_benchmark.py``; everything here is arithmetic over the run +summary ``zagg.runner.agg`` already returns plus the pinned target metadata. The benchmark dispatches exactly ONE shard, so the summary's per-fan-out worker stats collapse to that single worker: ``worker_max_s`` is the shard's runtime and @@ -13,8 +14,10 @@ from __future__ import annotations +import json import math import re +from pathlib import Path # Cost model and grid types come straight from the package so the benchmark can # never drift from what production actually bills/uses (arm64, 4 GB -- issue #110/#193). @@ -510,3 +513,146 @@ def latest_markdown(records: list[dict]) -> str: lines += _table_block(records) lines += ["", "Machine-readable companion: `metrics.json` (same directory)."] return "\n".join(lines) + + +# --- pinned shard-map recipe (issues #110 / #148 / #444) --------------------- +# +# THE rebuild recipe and THE pin rule for the committed benchmark shard maps, +# shared by the drift guard (``tests/test_benchmark_shardmap.py``, the accident +# detector) and the deliberate re-pin driver +# (``tools/repin_benchmark_shardmaps.py``). Both CALL these rather than +# restating them, so the guard and its counterpart cannot build or pin +# differently -- and neither a ``tools/`` script nor CI depends on a test +# module for core logic. The heavy zagg imports stay inside the functions; +# importing this module stays light. + +REPO = Path(__file__).resolve().parents[2] +BENCH = REPO / "tests" / "data" / "benchmark" + +#: The pinned-target manifest, as committed. Loaded once at import; the re-pin +#: driver deliberately re-reads the file instead when it needs the on-disk NOW. +MANIFEST = json.loads((BENCH / "targets.json").read_text()) + + +def resolve_aoi_temporal_cmr(sm_meta: dict) -> tuple[dict, dict, dict]: + """Resolve a shard map's ``aoi``/``temporal``/``cmr`` (issue #121). + + A per-entry override wins; an absent key falls back to the top-level manifest + default. Existing single-AOI (NEON) shard maps carry no override, so they + resolve byte-identically to the top-level ``aoi``/``temporal``/``cmr``. + """ + return ( + sm_meta.get("aoi", MANIFEST["aoi"]), + sm_meta.get("temporal", MANIFEST["temporal"]), + sm_meta.get("cmr", MANIFEST["cmr"]), + ) + + +def _containing_shard(parent_grid, shard_key: int) -> int: + """The parent-grid shard containing a finer shard (via its center point). + + HEALPix cells nest, so a finer cell's center maps unambiguously into its + containing coarser cell; routing through ``assign``/``shards_of`` keeps this + on the same mortie machinery the shard maps themselves are built with. + """ + import numpy as np + from mortie import mort2geo + + lat, lon = mort2geo(np.array([shard_key], dtype=np.uint64)) + leaf = parent_grid.assign(np.atleast_1d(lat), np.atleast_1d(lon)) + return int(parent_grid.shards_of(leaf)[0]) + + +def _config_for_shardmap(sm_key: str) -> Path: + """Any target's config that uses this shard map (config sets the grid). + + Searches the committed matrix first, then ``provisional_targets`` (issue + #130 block) — the 88S stress shard maps (issue #148) are referenced only by + provisional targets, and the drift check still needs their grid config. + """ + provisional = { + k: v for k, v in MANIFEST.get("provisional_targets", {}).items() if k != "_comment" + } + for target in list(MANIFEST["targets"].values()) + list(provisional.values()): + if target["shardmap"] == sm_key: + return BENCH / target["config"] + raise AssertionError(f"no target references shardmap '{sm_key}'") + + +def rebuild_shardmap(sm_key: str, sm_meta: dict): + """Rebuild one shard map from its ``targets.json`` entry — THE recipe. + + Entry config → grid; the entry's resolved AOI/temporal/CMR (a per-entry + override, issue #121, over the top-level manifest default — NEON entries + carry none); the committed map's ``metadata.backend``; and either the + committed ``catalog_parquet`` snapshot when the entry carries one or a live + CMR fetch when it does not. + + Build-once catalog (issue #148): an entry carrying ``catalog_parquet`` + rebuilds from the committed stac-geoparquet snapshot instead of re-fetching + CMR — the rebuild is then deterministic and offline, per the catalog design + (fetch once, save the parquet, reuse). Regenerate the snapshot only to + deliberately re-pin. + + Shared by the drift guard and the issue #444 re-pin driver rather than + restated in either: the guard and its deliberate counterpart must build the + same way, and a second copy of this recipe is exactly what would let them + drift apart. + """ + from zagg.catalog import load_polygon, polygon_to_bbox + from zagg.catalog.shardmap import ShardMap + from zagg.catalog.sources import Catalog, CMRSource, Query + from zagg.config import load_config + from zagg.grids import from_config + + backend = json.loads((BENCH / sm_meta["path"]).read_text())["metadata"]["backend"] + grid = from_config(load_config(str(_config_for_shardmap(sm_key)))) + aoi, temporal, cmr = resolve_aoi_temporal_cmr(sm_meta) + # aoi.file is relative to the manifest dir, like the config/shardmap paths. + parts = load_polygon(str(BENCH / aoi["file"])) + if sm_meta.get("catalog_parquet"): + catalog = Catalog.from_geoparquet(str(BENCH / sm_meta["catalog_parquet"])) + else: + catalog = CMRSource().fetch( + Query( + cmr["short_name"], + cmr["version"], + temporal["start"], + temporal["end"], + region=polygon_to_bbox(parts), + provider=cmr["provider"], + ) + ) + return ShardMap.build(catalog, grid, region=parts, backend=backend, footprint=cmr["footprint"]) + + +def select_pin(rebuilt, sm_meta: dict, parent_key: int | None = None) -> tuple[int, int]: + """The ``(shard_key, n_granules)`` pin for a rebuilt map — THE pin rule. + + A nested pin (issue #148: the 88S o10 stress shard is the densest o10 shard + INSIDE the pinned o9 stress shard, so one o9 extraction pass covers both + orders) is selected over the shards nested in the parent's pin, never the + global densest — otherwise a correct rebuild would read as drift. + + ``parent_key`` lets the issue #444 driver extract against a parent pin it + has just rewritten on disk; the guard passes none and reads the manifest as + loaded at import. Shared with that driver for the same reason as + :func:`rebuild_shardmap`. + """ + from zagg.config import load_config + from zagg.grids import from_config + + shard_keys, granules = rebuilt.shard_keys, rebuilt.granules + nested_in = sm_meta.get("nested_in") + if nested_in: + if parent_key is None: + parent_key = int(MANIFEST["shardmaps"][nested_in]["shard_key"]) + parent_grid = from_config(load_config(str(_config_for_shardmap(nested_in)))) + keep = [ + i + for i, k in enumerate(shard_keys) + if int(_containing_shard(parent_grid, int(k))) == parent_key + ] + shard_keys = [shard_keys[i] for i in keep] + granules = [granules[i] for i in keep] + return select_densest_shard({"shard_keys": shard_keys, "granules": granules}) diff --git a/src/zagg/configs/sentinel2_l2a.yaml b/src/zagg/configs/sentinel2_l2a.yaml index 7a371c22e..e7e6402b4 100644 --- a/src/zagg/configs/sentinel2_l2a.yaml +++ b/src/zagg/configs/sentinel2_l2a.yaml @@ -38,6 +38,7 @@ output: # int64 microseconds cannot state and a toc word can. Output-defining: a # store born on toc is a different product from a legacy-axis one. time_encoding: toc + pyramid: false # overview family off: raster leaves are column-less (issue #399, issue #459) grid: type: healpix parent_order: 11 diff --git a/tests/data/benchmark/README.md b/tests/data/benchmark/README.md index 58a54b3bb..fe6b1a6ed 100644 --- a/tests/data/benchmark/README.md +++ b/tests/data/benchmark/README.md @@ -179,6 +179,22 @@ places the panel. print(bench_metrics.select_densest_shard(sm)) # -> (shard_key, n_granules) ``` + > **Re-pinning a map that already exists** is one command: + > `tools/repin_benchmark_shardmaps.py` (issue #444) rebuilds through the + > shared recipe in `.github/scripts/bench_metrics.py` — `rebuild_shardmap` + > and `select_pin`, which the drift check and this driver both call — + > selects the pin (nested rule included), prunes the ring maps, and writes + > the map plus its `targets.json` + > `shard_key`/`n_granules`. It re-pins **deliberately** — the drift check + > stays the accident detector — so run it only when a convention or grammar + > change makes the committed words wrong. `--check` rebuilds and reports the + > differences without writing. The entry's `note` is prose: restate it by + > hand in the same commit. + > + > ```bash + > uv run python tools/repin_benchmark_shardmaps.py --check healpix_o9_88s + > ``` + > **The committed maps span two granule-record schemas.** The five > `sm_healpix_*.json` maps were last rebuilt after issue #246, so their > granule records carry `time_start`/`time_end` (and their `metadata` a diff --git a/tests/data/benchmark/configs/s2_neon_o9.yaml b/tests/data/benchmark/configs/s2_neon_o9.yaml index 871c89549..73337d8d1 100644 --- a/tests/data/benchmark/configs/s2_neon_o9.yaml +++ b/tests/data/benchmark/configs/s2_neon_o9.yaml @@ -6,9 +6,11 @@ # the shipped src/zagg/configs/sentinel2_l2a.yaml except ``parent_order: 9`` -- # the o9 dispatch shard matching the point-pipeline legs (~12.8 km shards, 4 # over the SERC box, 4^10 order-19 cells each; the same override espg's -# operational S2 SERC run pins) -- and the harness-local ``pyramid: false`` -# opt-out below. Both are packaging: the semantic hash (D19) of this config -# equals the shipped one, so the leg measures the shipped product. +# operational S2 SERC run pins). That override is the whole divergence budget, +# and it is packaging: the semantic hash (D19) of this config equals the +# shipped one, so the leg measures the shipped product. ``pyramid: false`` +# below is *not* a divergence -- the shipped config declares the overview +# family off too (issue #459, issue #399). # # Layout notes: ``store_layout: hive`` -- the production default for HEALPix # raster since issues #247/#253 (issue #237 promoted, ratified by @espg on @@ -32,7 +34,7 @@ output: # Hive: the promoted production default for HEALPix raster (issue #237, # ratified issue #272). One leaf zarr object per array per dispatch shard. store_layout: hive - pyramid: false # overview sweep opted out pending Phase E fleet sizing (issue #201) + pyramid: false # overview family off, as shipped (issue #399, issue #459) # Time axis as mortie toc words (spec §8, issue #443) -- output-defining, so # it tracks the shipped config (issue #451): a store born on toc is a # different product from a legacy-axis one. diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 81e02f506..c6e45f72c 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -1058,13 +1058,11 @@ def test_targets_manifest_consistent(): def test_every_live_shardmap_resolves_to_a_config(): # The drift test parametrizes over manifest["shardmaps"], and the consistency # test over manifest["targets"]; both rely on every LIVE shardmap resolving to - # a referencing config (the drift test's _config_for_shardmap lookup). This + # a referencing config (the shared _config_for_shardmap lookup). This # guards that wiring -- the invariant that made o9 drop-in coverage automatic # the moment its entry landed, and that keeps any future order covered too. - import test_benchmark_shardmap as drift - - for sm_key in drift.MANIFEST["shardmaps"]: - cfg = drift._config_for_shardmap(sm_key) # raises if no target references it + for sm_key in bench_metrics.MANIFEST["shardmaps"]: + cfg = bench_metrics._config_for_shardmap(sm_key) # raises if no target references it assert cfg.exists() @@ -1254,17 +1252,13 @@ def test_scalar_config_is_genuinely_scalar(): def test_override_resolution_falls_back_to_defaults(): # A shard map with no override inherits the top-level aoi/temporal/cmr # *by identity* -- existing NEON entries resolve byte-identically to today. - import test_benchmark_shardmap as drift - - aoi, temporal, cmr = drift.resolve_aoi_temporal_cmr({"path": "x", "shard_key": 0}) - assert aoi is drift.MANIFEST["aoi"] - assert temporal is drift.MANIFEST["temporal"] - assert cmr is drift.MANIFEST["cmr"] + aoi, temporal, cmr = bench_metrics.resolve_aoi_temporal_cmr({"path": "x", "shard_key": 0}) + assert aoi is bench_metrics.MANIFEST["aoi"] + assert temporal is bench_metrics.MANIFEST["temporal"] + assert cmr is bench_metrics.MANIFEST["cmr"] def test_override_resolution_uses_overrides(): - import test_benchmark_shardmap as drift - sm_meta = { "path": "x", "shard_key": 0, @@ -1272,7 +1266,7 @@ def test_override_resolution_uses_overrides(): "temporal": {"start": "2019-01-01", "end": "2020-01-01"}, "cmr": {"short_name": "ATL03", "version": "007", "provider": "P", "footprint": "swath"}, } - aoi, temporal, cmr = drift.resolve_aoi_temporal_cmr(sm_meta) + aoi, temporal, cmr = bench_metrics.resolve_aoi_temporal_cmr(sm_meta) assert aoi == sm_meta["aoi"] assert temporal == sm_meta["temporal"] assert cmr == sm_meta["cmr"] @@ -1280,13 +1274,11 @@ def test_override_resolution_uses_overrides(): def test_override_resolution_partial_override(): # aoi overridden, temporal/cmr omitted -> override wins, rest falls back. - import test_benchmark_shardmap as drift - sm_meta = {"path": "x", "shard_key": 0, "aoi": {"file": "f.geojson", "name": "n"}} - aoi, temporal, cmr = drift.resolve_aoi_temporal_cmr(sm_meta) + aoi, temporal, cmr = bench_metrics.resolve_aoi_temporal_cmr(sm_meta) assert aoi == sm_meta["aoi"] - assert temporal is drift.MANIFEST["temporal"] - assert cmr is drift.MANIFEST["cmr"] + assert temporal is bench_metrics.MANIFEST["temporal"] + assert cmr is bench_metrics.MANIFEST["cmr"] def test_antarctic_88s_aoi_fixture_loads_near_turning_latitude(): @@ -2150,8 +2142,6 @@ def test_88s_nested_pin_invariant(): # o9 extraction pass covers both orders, issue #148). This runs offline in # milliseconds — the gated weekly drift job must not be the only guard on # the nesting or on the nested_in reference itself. - import test_benchmark_shardmap as drift - from zagg.config import load_config from zagg.grids import from_config @@ -2162,9 +2152,9 @@ def test_88s_nested_pin_invariant(): parent_meta = manifest["shardmaps"].get(sm_meta["nested_in"]) assert parent_meta is not None, f"{sm_key}: nested_in references a missing entry" parent_grid = from_config( - load_config(str(drift._config_for_shardmap(sm_meta["nested_in"]))) + load_config(str(bench_metrics._config_for_shardmap(sm_meta["nested_in"]))) ) - containing = drift._containing_shard(parent_grid, int(sm_meta["shard_key"])) + containing = bench_metrics._containing_shard(parent_grid, int(sm_meta["shard_key"])) assert containing == int(parent_meta["shard_key"]), ( f"{sm_key}: pinned shard {sm_meta['shard_key']} is not inside its " f"nested_in parent {parent_meta['shard_key']} (got {containing})" diff --git a/tests/test_benchmark_shardmap.py b/tests/test_benchmark_shardmap.py index 9c2a4f143..b8e69bc7c 100644 --- a/tests/test_benchmark_shardmap.py +++ b/tests/test_benchmark_shardmap.py @@ -35,138 +35,298 @@ import json import os import sys +from dataclasses import replace from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[1] -BENCH = REPO / "tests" / "data" / "benchmark" sys.path.insert(0, str(REPO / ".github" / "scripts")) -import bench_metrics # noqa: E402 +# The rebuild recipe and the pin rule live in ``bench_metrics`` (shared with the +# issue #444 re-pin driver, ``tools/repin_benchmark_shardmaps.py`` -- neither a +# ``tools/`` script nor CI should depend on a test module for core logic); this +# guard CALLS them, so the accident detector and the deliberate counterpart +# cannot build or pin differently. +from bench_metrics import ( # noqa: E402 + BENCH, + MANIFEST, + _config_for_shardmap, + rebuild_shardmap, + select_pin, +) -MANIFEST = json.loads((BENCH / "targets.json").read_text()) +#: The network gate is the drift check's own, not the module's (it was +#: module-level while the drift check was the only test here): the issue #444 +#: re-pin-driver tests below rebuild from the committed catalogs and need no CMR. +needs_cmr = pytest.mark.skipif( + os.environ.get("ZAGG_BENCHMARK_DRIFT") != "1", + reason="set ZAGG_BENCHMARK_DRIFT=1 to run the CMR shard-map drift check", +) -def resolve_aoi_temporal_cmr(sm_meta: dict) -> tuple[dict, dict, dict]: - """Resolve a shard map's ``aoi``/``temporal``/``cmr`` (issue #121). +@pytest.mark.slow +@needs_cmr +@pytest.mark.parametrize("sm_key", list(MANIFEST["shardmaps"])) +def test_pinned_shardmap_no_drift(sm_key): + sm_meta = MANIFEST["shardmaps"][sm_key] + committed = json.loads((BENCH / sm_meta["path"]).read_text()) + if committed["metadata"]["backend"] == "spherely": + pytest.importorskip("spherely") - A per-entry override wins; an absent key falls back to the top-level manifest - default. Existing single-AOI (NEON) shard maps carry no override, so they - resolve byte-identically to the top-level ``aoi``/``temporal``/``cmr``. - """ - return ( - sm_meta.get("aoi", MANIFEST["aoi"]), - sm_meta.get("temporal", MANIFEST["temporal"]), - sm_meta.get("cmr", MANIFEST["cmr"]), + key, n = select_pin(rebuild_shardmap(sm_key, sm_meta), sm_meta) + pinned_n = sm_meta["n_granules"] + # Tie-tolerant: the densest *count* is the stable quantity; an equally-dense + # reselection (different key, same count) is fine -- a count drift is not. + assert abs(n - pinned_n) <= 1, ( + f"{sm_key}: densest granule count drifted {pinned_n} -> {n} " + f"(rebuilt densest shard {key}). Re-pin the shard map + targets.json." ) -pytestmark = [ - pytest.mark.slow, - pytest.mark.skipif( - os.environ.get("ZAGG_BENCHMARK_DRIFT") != "1", - reason="set ZAGG_BENCHMARK_DRIFT=1 to run the CMR shard-map drift check", - ), -] +# -- the deliberate re-pin driver (issue #444) -------------------------------- +# +# ``tools/repin_benchmark_shardmaps.py`` is the counterpart of the drift check +# above: the guard detects an accidental move, the driver makes a deliberate +# one. Both CALL ``bench_metrics.rebuild_shardmap`` and +# ``bench_metrics.select_pin`` (the shared recipe the drift check runs +# through), so the rebuild and the pin rule are already covered by the drift +# check; these tests pin the parts they do not reach -- the pruning, the pin +# write-back, the re-pin ordering, and the claim the driver exists to support: +# that it reproduces the PR #441 artifacts from the committed catalogs. + +OFFLINE_PINS = [k for k, v in MANIFEST["shardmaps"].items() if v.get("catalog_parquet")] + +#: The metadata keys the byte comparison below excuses, pinned HERE rather than +#: imported from the driver. The driver's ``EXCUSED_META`` has a second job -- +#: labelling ``--check`` output -- so someone quieting a noisy check by +#: appending a key to it would silently widen this acceptance test's blind spot, +#: with nothing failing. That is precisely the relaxation issue #444's +#: "byte-identically" exists to prevent, so widening it has to be a deliberate +#: edit here too. +EXCUSED_META = ("build_wall_s", "mortie_order") + + +# ``tools/`` is not an installed package, so it goes on the path ONCE here -- +# matching the ``bench_metrics`` pattern above -- rather than on every +# ``_driver()`` call, which stacked one identical entry per test. +sys.path.insert(0, str(REPO / "tools")) + +def _driver(): + """The re-pin driver, imported from ``tools/`` (not an installed module). -def _containing_shard(parent_grid, shard_key: int) -> int: - """The parent-grid shard containing a finer shard (via its center point). + The import stays lazy so a broken driver fails the driver tests, not this + whole module's collection (the drift check above does not need it). + """ + import repin_benchmark_shardmaps + + return repin_benchmark_shardmaps + + +def _without_volatile(text: str, volatile) -> str: + """A written map's text minus the metadata lines a rebuild legitimately moves. + + Line-oriented on purpose: what issue #444 asks for is "byte-identically", + which a parsed comparison would soften to structural equality. The cost is + a silent dependency on ``ShardMap.to_json`` pretty-printing one metadata key + per line -- were it ever to emit compact JSON, the whole map would become a + single line carrying ``"build_wall_s"``, both sides would filter down to + ``""``, and the acceptance test would pass while comparing NOTHING. - HEALPix cells nest, so a finer cell's center maps unambiguously into its - containing coarser cell; routing through ``assign``/``shards_of`` keeps this - on the same mortie machinery the shard maps themselves are built with. + So the filter checks its own work: exactly one line per excused key, no + more (a key string surfacing inside a granule record) and no less. """ - import numpy as np - from mortie import mort2geo + lines = text.splitlines(keepends=True) + kept = [line for line in lines if not any(f'"{k}"' in line for k in volatile)] + assert len(lines) - len(kept) == len(volatile), ( + f"expected one line per excused metadata key, dropped {len(lines) - len(kept)} of " + f"{len(lines)} -- has ShardMap.to_json stopped pretty-printing?" + ) + return "".join(kept) - lat, lon = mort2geo(np.array([shard_key], dtype=np.uint64)) - leaf = parent_grid.assign(np.atleast_1d(lat), np.atleast_1d(lon)) - return int(parent_grid.shards_of(leaf)[0]) +@pytest.mark.slow +@pytest.mark.parametrize("sm_key", OFFLINE_PINS) +def test_offline_pin_reproduces_committed_map(sm_key, tmp_path): + """The driver reproduces the PR #441 artifacts from the committed catalogs. -def _config_for_shardmap(sm_key: str) -> Path: - """Any target's config that uses this shard map (config sets the grid). + The acceptance test issue #444 asks for, and the reason it can only cover + the ``catalog_parquet`` (88S ring) entries: the NEON trio rebuilds from CMR + by design -- an ATL03 footprint quad blankets the whole NEON box, so a local + full-catalog snapshot over-includes (``tests/data/benchmark/README.md``). - Searches the committed matrix first, then ``provisional_targets`` (issue - #130 block) — the 88S stress shard maps (issue #148) are referenced only by - provisional targets, and the drift check still needs their grid config. + Byte-for-byte over the whole written manifest -- every granule record, + ``shard_keys``, ``grid_signature``, and the metadata the build derives -- + except the two keys a faithful rebuild still moves, which are asserted + separately below. """ - provisional = { - k: v for k, v in MANIFEST.get("provisional_targets", {}).items() if k != "_comment" - } - for target in list(MANIFEST["targets"].values()) + list(provisional.values()): - if target["shardmap"] == sm_key: - return BENCH / target["config"] - raise AssertionError(f"no target references shardmap '{sm_key}'") + driver = _driver() + mapped, key, n = driver.repin(sm_key) + sm_meta = MANIFEST["shardmaps"][sm_key] + assert (key, n) == (sm_meta["shard_key"], sm_meta["n_granules"]) + # The driver may not widen this test's exemption on its own (see EXCUSED_META). + assert tuple(driver.EXCUSED_META) == EXCUSED_META, ( + "the driver's excused-metadata set moved -- restate it here deliberately" + ) + + written = tmp_path / "rebuilt.json" + mapped.to_json(str(written)) + assert _without_volatile(written.read_text(), EXCUSED_META) == _without_volatile( + (BENCH / sm_meta["path"]).read_text(), EXCUSED_META + ) -@pytest.mark.parametrize("sm_key", list(MANIFEST["shardmaps"])) -def test_pinned_shardmap_no_drift(sm_key): - from zagg.catalog import load_polygon, polygon_to_bbox - from zagg.catalog.shardmap import ShardMap - from zagg.catalog.sources import Catalog, CMRSource, Query from zagg.config import load_config from zagg.grids import from_config - sm_meta = MANIFEST["shardmaps"][sm_key] - committed = json.loads((BENCH / sm_meta["path"]).read_text()) - backend = committed["metadata"]["backend"] - if backend == "spherely": - pytest.importorskip("spherely") + # The one live divergence, and why it is not a pin move: PR #447 made the + # unpinned HEALPix ``swath`` cover order the SHARD order, where the + # committed maps recorded the chunk order they were built at. The + # assignment is unchanged -- which is what the byte comparison above just + # showed, over the same catalog. + grid = from_config(load_config(str(_config_for_shardmap(sm_key)))) + assert mapped.metadata["mortie_order"] == grid.parent_order + # ...and the OTHER side, which nothing else constrains. ``mortie_order`` is + # deterministic on both sides, so its exemption is a stale-fixture excuse + # with an expiry, not a standing licence: pinning the committed value makes + # this test fail at the next deliberate re-pin, when ``STALE_META`` must be + # emptied rather than left to mask a genuine regression. + assert json.loads((BENCH / sm_meta["path"]).read_text())["metadata"]["mortie_order"] == 13 + assert driver.STALE_META == ("mortie_order",) + - cfg = load_config(str(_config_for_shardmap(sm_key))) - grid = from_config(cfg) - # Resolve this shard map's AOI/temporal/CMR: a per-entry override (issue #121) - # falls back to the top-level manifest default. NEON entries have no override. - aoi, temporal, cmr = resolve_aoi_temporal_cmr(sm_meta) - # aoi.file is relative to the manifest dir, like the config/shardmap paths. - parts = load_polygon(str(BENCH / aoi["file"])) - - if sm_meta.get("catalog_parquet"): - # Build-once catalog (issue #148): an entry carrying ``catalog_parquet`` - # rebuilds from the committed stac-geoparquet snapshot instead of - # re-fetching CMR -- the drift check then guards the shardmap build + - # pin deterministically and offline, per the catalog design (fetch - # once, save the parquet, reuse). Regenerate the snapshot only to - # deliberately re-pin. - catalog = Catalog.from_geoparquet(str(BENCH / sm_meta["catalog_parquet"])) - else: - query = Query( - cmr["short_name"], - cmr["version"], - temporal["start"], - temporal["end"], - region=polygon_to_bbox(parts), - provider=cmr["provider"], - ) - catalog = CMRSource().fetch(query) - rebuilt = ShardMap.build( - catalog, grid, region=parts, backend=backend, footprint=cmr["footprint"] +def test_repin_updates_only_the_pin_literals_in_targets(): + # The write-back is surgical because targets.json is hand-formatted (compact + # inline ``worker`` objects survive a re-pin); the entry's prose ``note`` is + # the re-pinner's to restate, not the driver's to rewrite. + driver = _driver() + text = (BENCH / "targets.json").read_text() + out = driver.update_targets(text, "healpix_o9", 4242, 7) + + entry = json.loads(out)["shardmaps"]["healpix_o9"] + assert (entry["shard_key"], entry["n_granules"]) == (4242, 7) + assert entry["note"] == MANIFEST["shardmaps"]["healpix_o9"]["note"] + changed = [(a, b) for a, b in zip(text.splitlines(), out.splitlines(), strict=True) if a != b] + assert len(changed) == 2, changed + + +def test_repin_prune_slices_the_aoi_mask_with_the_shard_keys(): + """The prune is the one place the driver hand-rebuilds a ``ShardMap``. + + ``aoi_mask`` is parallel to ``shard_keys`` (issue #101), so it has to be + sliced with them. Latent today -- no committed benchmark map carries one, + the strict-AOI arm building its mask at dispatch instead + (``run_benchmark._shardmap_with_mask``, issue #202) -- but a field dropped + here would be written out as a maskless map without a word. + """ + from zagg.catalog.shardmap import ShardMap + + driver = _driver() + rebuilt = ShardMap( + {"grid": "healpix"}, + [10, 11], + [[{"granule": "a"}], [{"granule": "b"}]], + {"total_shards": 2}, + aoi_mask=[[1, 2], [3, 4]], ) + pruned = driver.prune_to_pin(rebuilt, 11, "pruned to the pinned shard") - # A nested pin (issue #148: the 88S o10 stress shard is the densest o10 - # shard INSIDE the pinned o9 stress shard, so one o9 extraction pass covers - # both orders) is compared against the same nested quantity, not the global - # densest — otherwise a correct rebuild would read as drift. - shard_keys, granules = rebuilt.shard_keys, rebuilt.granules - nested_in = sm_meta.get("nested_in") - if nested_in: - parent_key = int(MANIFEST["shardmaps"][nested_in]["shard_key"]) - parent_grid = from_config(load_config(str(_config_for_shardmap(nested_in)))) - keep = [ - i - for i, k in enumerate(shard_keys) - if int(_containing_shard(parent_grid, int(k))) == parent_key - ] - shard_keys = [shard_keys[i] for i in keep] - granules = [granules[i] for i in keep] - - key, n = bench_metrics.select_densest_shard({"shard_keys": shard_keys, "granules": granules}) - pinned_n = sm_meta["n_granules"] - # Tie-tolerant: the densest *count* is the stable quantity; an equally-dense - # reselection (different key, same count) is fine -- a count drift is not. - assert abs(n - pinned_n) <= 1, ( - f"{sm_key}: densest granule count drifted {pinned_n} -> {n} " - f"(rebuilt densest shard {key}). Re-pin the shard map + targets.json." + assert (pruned.shard_keys, pruned.granules) == ([11], [[{"granule": "b"}]]) + assert pruned.aoi_mask == [[3, 4]] + # metadata stays the FULL build's, plus the carried note + assert pruned.metadata == {"total_shards": 2, "pruned": "pruned to the pinned shard"} + assert driver.prune_to_pin(replace(rebuilt, aoi_mask=None), 10, "note").aoi_mask is None + + +def test_repin_targets_write_back_is_anchored_on_the_key(): + """The entry is located by KEY, not by any occurrence of its name. + + An entry name also appears as a ``"nested_in"`` VALUE (``healpix_o10_88s`` + names ``healpix_o9_88s`` that way today). Today's manifest writes the parent + first, so an unanchored search happens to land right; it stops doing so the + moment a child precedes its parent, which the nested-pin design invites (an + o11 nested in an o10). Here the child comes first and carries an inner + object, so the unanchored form splices that object instead — silently, since + it too has the two literals to restate. + """ + driver = _driver() + text = json.dumps( + { + "shardmaps": { + "child": { + "nested_in": "parent", + "provisional": {"shard_key": 1, "n_granules": 2}, + "shard_key": 3, + "n_granules": 4, + }, + "parent": {"shard_key": 5, "n_granules": 6}, + } + }, + indent=2, ) + maps = json.loads(driver.update_targets(text, "parent", 4242, 7))["shardmaps"] + + assert maps["parent"] == {"shard_key": 4242, "n_granules": 7} + assert maps["child"] == json.loads(text)["shardmaps"]["child"] + + +def test_repin_orders_by_nesting_depth(): + """Depth, not a parent/child boolean -- a grandchild must follow its parent.""" + driver = _driver() + known = {"a": {}, "b": {"nested_in": "a"}, "c": {"nested_in": "b"}} + + assert [driver.nesting_depth(known, k) for k in ("a", "b", "c")] == [0, 1, 2] + assert sorted("cab", key=lambda k: driver.nesting_depth(known, k)) == ["a", "b", "c"] + with pytest.raises(ValueError, match="cycle"): + driver.nesting_depth({"x": {"nested_in": "y"}, "y": {"nested_in": "x"}}, "x") + + +def test_repin_writes_parents_before_children(monkeypatch, tmp_path): + """``main()``'s write path, and the ordering claim it rests on. + + A ``nested_in`` child extracts against its parent's pin as it stands on + disk (issue #148), so a child re-pinned FIRST would be extracted against + the parent's stale shard and commit a wrong fixture -- one the drift + guard's +/-1 count tolerance need not catch. ``repin`` is stubbed, so this + pins the ordering and the write-back without two shard-map builds. + """ + driver = _driver() + bench = tmp_path / "benchmark" + (bench / "shardmaps").mkdir(parents=True) + (bench / "targets.json").write_text((BENCH / "targets.json").read_text()) + monkeypatch.setattr(driver, "BENCH", bench) + monkeypatch.setattr(driver, "TARGETS", bench / "targets.json") + + class _Stub: + metadata: dict = {} + + def to_json(self, path): + Path(path).write_text("{}") + + seen = [] + + def fake_repin(sm_key): + # record the parent pin VISIBLE ON DISK as this entry is re-pinned + seen.append((sm_key, int(driver.entry("healpix_o9_88s")["shard_key"]))) + return _Stub(), 4242 if sm_key == "healpix_o9_88s" else 99, 7 + + monkeypatch.setattr(driver, "repin", fake_repin) + monkeypatch.setattr(driver, "differences", lambda sm_key, mapped: []) + + assert driver.main(["healpix_o10_88s", "healpix_o9_88s"]) == 0 + + assert [k for k, _ in seen] == ["healpix_o9_88s", "healpix_o10_88s"] + # the child was extracted against the parent's NEW pin, not the run's start state + assert seen[1][1] == 4242 + maps = json.loads((bench / "targets.json").read_text())["shardmaps"] + assert (maps["healpix_o9_88s"]["shard_key"], maps["healpix_o9_88s"]["n_granules"]) == (4242, 7) + assert (maps["healpix_o10_88s"]["shard_key"], maps["healpix_o10_88s"]["n_granules"]) == (99, 7) + # ...and the prose the driver refuses to write is still the committed prose + assert maps["healpix_o10_88s"]["note"] == MANIFEST["shardmaps"]["healpix_o10_88s"]["note"] + + +def test_repin_refuses_an_unknown_shardmap(capsys): + driver = _driver() + with pytest.raises(SystemExit): + driver.main(["--check", "healpix_o42"]) + assert "unknown shard map(s) ['healpix_o42']" in capsys.readouterr().err diff --git a/tests/test_raster_benchmark.py b/tests/test_raster_benchmark.py index a0f5430a7..e4ac4e5a9 100644 --- a/tests/test_raster_benchmark.py +++ b/tests/test_raster_benchmark.py @@ -47,8 +47,10 @@ def test_s2_neon_o9_tracks_the_shipped_sentinel2_config(): # The leg's config claims to be the shipped src/zagg/configs/sentinel2_l2a.yaml # with the o9 dispatch shard (issue #451); hold it to that mechanically so a # knob added to one and not the other (as time_encoding was, issue #443) - # cannot drift silently. Divergence budget: parent_order and the harness's - # pyramid opt-out -- both packaging, so the semantic hash (D19) matches. + # cannot drift silently. Divergence budget: parent_order -- packaging, so + # the semantic hash (D19) matches. (The pyramid opt-out left the budget when + # issue #459 restated it on the shipped config too: both sides declare + # ``pyramid: false`` and the dict comparison below now covers it.) from zagg.config import get_store_layout, load_config from zagg.semantics import semantic_hash @@ -63,7 +65,7 @@ def test_s2_neon_o9_tracks_the_shipped_sentinel2_config(): def normalized(cfg): # store is a run-local output path (the harness overrides it); grid # indexing_scheme is descriptive-only (config.py rejects any other value). - out = {k: v for k, v in cfg.output.items() if k not in ("store", "grid", "pyramid")} + out = {k: v for k, v in cfg.output.items() if k not in ("store", "grid")} out["store_layout"] = get_store_layout(cfg) out["grid"] = {k: v for k, v in cfg.output["grid"].items() if k != "indexing_scheme"} return out @@ -72,13 +74,10 @@ def normalized(cfg): assert b["grid"].pop("parent_order") == 9 assert s["grid"].pop("parent_order") == 11 assert b == s - # pyramid is dropped from the dict comparison above, so pin both sides of it - # explicitly: the shipped config growing an overview declaration (the issue - # #382 grammar) would otherwise turn the family on for the product while the - # leg keeps measuring it off, at an unchanged semantic hash -- output.pyramid - # is not in semantic_core, so the D19 assertion cannot see it either. + # ``pyramid`` rides the b == s comparison (semantic_hash cannot see it -- + # output.pyramid is not in semantic_core), so also pin the VALUE the pair + # agrees on: both configs declare the overview family off (issue #459). assert bench.output["pyramid"] is False - assert "pyramid" not in shipped.output def test_pinned_s2_catalog_carries_raster_entries(): diff --git a/tests/test_raster_runner.py b/tests/test_raster_runner.py index 1ee8f135d..ed770429a 100644 --- a/tests/test_raster_runner.py +++ b/tests/test_raster_runner.py @@ -966,6 +966,19 @@ def test_sentinel2_l2a_config_loads_and_validates(self): assert cfg.data_source["bands"]["scl"]["dtype"] == "uint8" assert cfg.output["grid"]["child_order"] == 19 + def test_sentinel2_l2a_config_declares_the_overview_family_off(self): + # Issue #459: an ABSENT output.pyramid resolves to the every-2-orders + # default schedule (``get_pyramid`` -> ``{}``), so a raster run + # dispatches an overview family that generates nothing -- raster leaves + # are column-less by construction (issue #399 option (b), unimplemented). + # The shipped template declares the opt-out rather than inheriting the + # default, and ``None`` is the "family OFF" resolution. + from zagg.config import get_pyramid + + cfg = default_config("sentinel2_l2a") + assert cfg.output["pyramid"] is False + assert get_pyramid(cfg) is None + class TestRasterHiveLocalBackend: """Local raster hive runs (issue #247 phase 3): manifest, leaves, coverage.""" diff --git a/tools/repin_benchmark_shardmaps.py b/tools/repin_benchmark_shardmaps.py new file mode 100644 index 000000000..d68491a54 --- /dev/null +++ b/tools/repin_benchmark_shardmaps.py @@ -0,0 +1,267 @@ +"""DELIBERATELY re-pin the benchmark shard-map fixtures (issue #444). + +The pins under ``tests/data/benchmark/`` — the committed +``shardmaps/sm_*.json`` maps plus their ``targets.json`` ``shard_key`` / +``n_granules`` entries — move only on purpose. A run of this script IS that +purpose: a convention or grammar change made the old words wrong (the authalic +latitude flip, issue #438 / PR #441, is the case it was written from) and the +pins are being restated against it. Nothing here detects drift. The accident +detector is and stays ``tests/test_benchmark_shardmap.py:: +test_pinned_shardmap_no_drift``, which rebuilds the same maps and fails loudly +when a pin moves on its own; this script is that guard's deliberate +counterpart, and both CALL the same rebuild and pin functions — +``bench_metrics.rebuild_shardmap`` and ``bench_metrics.select_pin``, in +``.github/scripts/bench_metrics.py`` beside the ``select_densest_shard`` rule +they end in — rather than restating them, so the two cannot build or pin +differently. A second copy of the recipe is precisely what let the uncommitted +scratch driver drift off the guard. + +One run, per shard-map entry named on the command line: + +1. rebuilds the map with ``bench_metrics.rebuild_shardmap`` — the entry's + config for the grid, its resolved AOI/temporal/CMR (per-entry override over + the top-level manifest default), the committed map's ``metadata.backend``, + and either the committed ``catalog_parquet`` snapshot when the entry carries + one (offline) or a live CMR fetch when it does not; +2. selects the pin with ``bench_metrics.select_pin`` — for a ``nested_in`` entry over + the finer shards inside the pinned parent shard only, never the global + densest, and against the parent's pin as it stands on disk NOW; +3. prunes the written map to the pinned shard when the committed map is pruned + (``metadata.pruned`` — the 88S ring maps, whose full form is hundreds of MB + of JSON), carrying that note over verbatim: it is editorial prose, not a + derived quantity; +4. writes the map and updates the entry's ``shard_key`` / ``n_granules`` in + ``targets.json``, leaving every other byte of that hand-formatted file + alone. The entry's ``note`` is prose and is NOT rewritten — restate it in + the same commit. + +``--check`` stops after (3) and reports how the rebuild differs from the +committed bytes instead of writing anything. +``tests/test_benchmark_shardmap.py::test_offline_pin_reproduces_committed_map`` +is that path as an acceptance test over the two offline entries. The NEON trio +cannot be checked offline: an ATL03 footprint quad blankets the whole NEON box, +so a local full-catalog snapshot over-includes and inflates the pins +(``tests/data/benchmark/README.md``, "Reproducing / re-pinning the NEON maps") +— they need the live fetch, which is why only the ring entries carry +``catalog_parquet``. + +Run from a zagg checkout:: + + uv run python tools/repin_benchmark_shardmaps.py --check healpix_o9_88s + uv run python tools/repin_benchmark_shardmaps.py healpix_o9_88s healpix_o10_88s + +Every ``targets.json`` shard map is HEALPix today, so every re-pin runs on the +mortie backend. If a rectilinear map is ever added to the manifest, re-pinning +it will need the non-PyPI exact-S2 ``spherely`` fork installed (README): the +rebuild takes the committed map's own ``metadata.backend``, so ``ShardMap.build`` +raises rather than quietly falling back to mortie. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +BENCH = REPO / "tests" / "data" / "benchmark" +TARGETS = BENCH / "targets.json" +# ``bench_metrics`` owns the rebuild recipe and the pin rule (the drift guard +# calls the same two functions); CALLING them rather than restating them is +# what keeps this driver on the guard's rails. +sys.path.insert(0, str(REPO / ".github" / "scripts")) + +import bench_metrics # noqa: E402 + +from zagg.catalog.shardmap import ShardMap # noqa: E402 + +#: Genuinely volatile: the build's own wall clock, different every run. +#: Legitimately excused from the comparison forever. +VOLATILE_META = ("build_wall_s",) + +#: NOT volatile — deterministic on both sides, and simply STALE in the +#: committed bytes: PR #447 made the unpinned HEALPix cover order the SHARD +#: order, where the committed maps recorded the chunk order they were built at +#: (13 -> the grid's 9/10). The assignment it produces is unchanged. +#: +#: This exemption is meant to EXPIRE. The next deliberate re-pin — a run of +#: this script — writes the current value and makes both sides agree, at which +#: point the exemption stops excusing a known delta and starts masking a real +#: regression. ``test_offline_pin_reproduces_committed_map`` pins the stale +#: committed value so it fails loudly then and this tuple gets emptied. +STALE_META = ("mortie_order",) + +#: How ``--check`` labels a metadata key that moved without the pin moving. +EXCUSED_META = { + **{k: " (volatile)" for k in VOLATILE_META}, + **{k: " (stale committed value, drop at the next re-pin)" for k in STALE_META}, +} + + +def entry(sm_key: str) -> dict: + """One shard-map entry, read from ``targets.json`` as it stands NOW. + + Deliberately not ``bench_metrics.MANIFEST`` (loaded once at import): re-pinning a + parent and its ``nested_in`` child in one run has to extract the child + against the parent's freshly written pin, not the one this process started + with. The top-level ``aoi``/``temporal``/``cmr`` defaults the guard + resolves against are never rewritten here, so those stay the guard's. + """ + return json.loads(TARGETS.read_text())["shardmaps"][sm_key] + + +def committed(sm_key: str) -> dict: + """The committed shard map an entry points at, as JSON.""" + return json.loads((BENCH / entry(sm_key)["path"]).read_text()) + + +def prune_to_pin(rebuilt: ShardMap, key: int, note: str) -> ShardMap: + """The rebuild reduced to its pinned shard, carrying ``metadata.pruned``. + + The 88S ring maps are committed pruned — their full form is hundreds of MB + of JSON. ``metadata`` stays the FULL build's (``total_shards`` / + ``total_pairs`` count the ring, not the surviving shard), matching the + committed maps, and ``note`` is carried over verbatim: it is editorial + prose, not a derived quantity. + + ``aoi_mask`` is sliced with ``shard_keys`` rather than dropped: it is + documented as parallel to them (``ShardMap``), so positional construction + would silently write a maskless map, and slicing it wrong would be worse + still. No committed benchmark map carries one today (the strict-AOI arm + builds its mask at dispatch), so this is the latent case, not a live one. + """ + i = [j for j, k in enumerate(rebuilt.shard_keys) if int(k) == key][0] + return ShardMap( + rebuilt.grid_signature, + [rebuilt.shard_keys[i]], + [rebuilt.granules[i]], + {**rebuilt.metadata, "pruned": note}, + aoi_mask=None if rebuilt.aoi_mask is None else [rebuilt.aoi_mask[i]], + ) + + +def repin(sm_key: str) -> tuple[ShardMap, int, int]: + """``(map to write, shard_key, n_granules)`` for one entry. + + The rebuild and the pin rule are shared with the guard — + ``bench_metrics.rebuild_shardmap`` and ``bench_metrics.select_pin`` — + called, not restated, so this driver cannot + build or pin differently from the accident detector. Only the parent-pin + source differs: a ``nested_in`` child extracts against the parent's pin as + it stands on disk NOW, which is the parent's FRESH pin when both are + re-pinned in one run. + + The written map is the rebuild pruned to the pinned shard when the + committed map is pruned, and the whole rebuild otherwise. + """ + sm_meta = entry(sm_key) + rebuilt = bench_metrics.rebuild_shardmap(sm_key, sm_meta) + nested_in = sm_meta.get("nested_in") + parent_key = int(entry(nested_in)["shard_key"]) if nested_in else None + key, n = bench_metrics.select_pin(rebuilt, sm_meta, parent_key) + note = committed(sm_key)["metadata"].get("pruned") + if note is None: + return rebuilt, key, n + return prune_to_pin(rebuilt, key, note), key, n + + +def differences(sm_key: str, mapped: ShardMap) -> list[str]: + """How a rebuilt map differs from the committed one, key by key.""" + old_map = committed(sm_key) + out = [] + for key in ("grid_signature", "shard_keys", "granules"): + if getattr(mapped, key) != old_map[key]: + out.append(f"{key} differs") + old, new = old_map["metadata"], mapped.metadata + for key in sorted(set(old) | set(new)): + if old.get(key) != new.get(key): + why = EXCUSED_META.get(key, "") + out.append(f"metadata.{key}: {old.get(key)!r} -> {new.get(key)!r}{why}") + return out + + +def update_targets(text: str, sm_key: str, key: int, n: int) -> str: + """Restate one shard-map entry's pin literals in ``targets.json`` text. + + Surgical rather than a load/dump round trip: the manifest is hand-formatted + (compact inline ``worker`` objects), so re-serializing would churn lines + this re-pin does not touch. The entry's prose ``note`` is left alone. + + The entry is located on ``"":`` WITH the colon, which only a JSON key + can be followed by: a bare ``""`` also matches a ``"nested_in"`` + *value* (``healpix_o10_88s`` names ``healpix_o9_88s`` that way), so the + unanchored form would rewrite the wrong entry whenever a child happened to + be written before its parent. + """ + decoder = json.JSONDecoder() + maps_at = text.index("{", text.index('"shardmaps"')) + _, maps_end = decoder.raw_decode(text, maps_at) + entry_at = text.index("{", text.index(f'"{sm_key}":', maps_at, maps_end)) + _, entry_end = decoder.raw_decode(text, entry_at) + entry = text[entry_at:entry_end] + for field, value in (("shard_key", key), ("n_granules", n)): + entry, hits = re.subn(rf'("{field}":\s*)\d+', rf"\g<1>{value}", entry, count=1) + if hits != 1: + raise ValueError(f"targets.json entry {sm_key!r} has no {field} literal to restate") + return text[:entry_at] + entry + text[entry_end:] + + +def nesting_depth(known: dict, sm_key: str) -> int: + """How many ``nested_in`` hops separate an entry from an unnested ancestor. + + Re-pin order keys on this depth rather than on a parent/child boolean: a + grandchild (an o11 nested in an o10 nested in an o9 — the nested-pin design + of issue #148 does not forbid it) sorts EQUAL to its own parent under a + boolean, leaving whatever order the command line happened to give, which + can be the wrong one. + """ + seen: list[str] = [] + while known[sm_key].get("nested_in"): + seen.append(sm_key) + sm_key = known[sm_key]["nested_in"] + if sm_key in seen: + raise ValueError(f"targets.json nested_in cycle at {sm_key!r}") + return len(seen) + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "shardmaps", + nargs="+", + help="shard-map entry names from targets.json (e.g. healpix_o9_88s). " + "Re-pin only what you mean to commit — each map is a deliberate move.", + ) + parser.add_argument( + "--check", + action="store_true", + help="rebuild and report differences against the committed bytes; write nothing.", + ) + args = parser.parse_args(argv) + known = json.loads(TARGETS.read_text())["shardmaps"] + unknown = sorted(set(args.shardmaps) - set(known)) + if unknown: + parser.error(f"unknown shard map(s) {unknown} (known: {sorted(known)})") + # A nested entry extracts against its parent's pin as it stands on disk, so + # re-pin shallowest-first even when the command line names them the other + # way round -- a child re-pinned before its parent would extract against + # the STALE parent shard and commit a wrong fixture. + for sm_key in sorted(args.shardmaps, key=lambda k: nesting_depth(known, k)): + mapped, key, n = repin(sm_key) + print(f"{sm_key}: pin {key} at {n} granules") + for line in differences(sm_key, mapped) or ["identical to the committed map"]: + print(f" {line}") + if args.check: + continue + path = entry(sm_key)["path"] + mapped.to_json(str(BENCH / path)) + TARGETS.write_text(update_targets(TARGETS.read_text(), sm_key, key, n)) + print(f" wrote {path} + its targets.json pin") + print(" restate the entry's note by hand — this driver does not write prose") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())