ncarnate 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. ncarnate-2.2.2/.github/workflows/ci.yml +157 -0
  2. {ncarnate-2.2.1 → ncarnate-2.2.2}/.github/workflows/publish.yml +8 -7
  3. {ncarnate-2.2.1 → ncarnate-2.2.2}/.gitignore +1 -0
  4. {ncarnate-2.2.1 → ncarnate-2.2.2}/CHANGELOG.md +14 -0
  5. {ncarnate-2.2.1 → ncarnate-2.2.2}/CITATION.cff +3 -3
  6. {ncarnate-2.2.1 → ncarnate-2.2.2}/CODE_OF_CONDUCT.md +1 -1
  7. ncarnate-2.2.1/README.md → ncarnate-2.2.2/PKG-INFO +73 -10
  8. ncarnate-2.2.1/PKG-INFO → ncarnate-2.2.2/README.md +36 -44
  9. {ncarnate-2.2.1 → ncarnate-2.2.2}/conda-recipe/recipe.yaml +5 -2
  10. {ncarnate-2.2.1 → ncarnate-2.2.2}/docs/fidelity-notes.md +35 -28
  11. ncarnate-2.2.2/docs/index.rst +30 -0
  12. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/__init__.py +28 -0
  13. ncarnate-2.2.2/ncarnate/atttypes.py +257 -0
  14. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/audit/__init__.py +17 -1
  15. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/audit/classify.py +1 -0
  16. ncarnate-2.2.2/ncarnate/audit/codes.py +113 -0
  17. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/audit/inspect.py +17 -6
  18. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/cli.py +6 -2
  19. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/constants.py +1 -1
  20. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/convert/__init__.py +133 -61
  21. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/convert/models.py +18 -1
  22. ncarnate-2.2.2/ncarnate/convert/preflight.py +298 -0
  23. ncarnate-2.2.2/ncarnate/convert/report.py +114 -0
  24. ncarnate-2.2.2/ncarnate/core.py +1198 -0
  25. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/errors.py +36 -0
  26. ncarnate-2.2.2/ncarnate/handoff.py +297 -0
  27. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/hdf4.py +2 -1
  28. ncarnate-2.2.2/ncarnate/hdf4_runtime.py +81 -0
  29. ncarnate-2.2.2/ncarnate/result.py +673 -0
  30. ncarnate-2.2.2/ncarnate/schemas/__init__.py +15 -0
  31. ncarnate-2.2.2/ncarnate/schemas/handoff.schema.json +276 -0
  32. ncarnate-2.2.2/ncarnate/stage.py +114 -0
  33. {ncarnate-2.2.1 → ncarnate-2.2.2}/paper/paper.md +11 -8
  34. {ncarnate-2.2.1 → ncarnate-2.2.2}/pyproject.toml +18 -6
  35. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_codes.py +6 -1
  36. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_integrity.py +8 -3
  37. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.provenance.json +1 -1
  38. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.hdf +0 -0
  39. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.provenance.json +1 -1
  40. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.hdf +0 -0
  41. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.provenance.json +1 -1
  42. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.hdf +0 -0
  43. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.provenance.json +1 -1
  44. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.provenance.json +1 -1
  45. ncarnate-2.2.2/tests/fixtures/operation_result/amsre_seaice12km.result.json +575 -0
  46. ncarnate-2.2.2/tests/fixtures/operation_result/packed_fill.canonical.json +1 -0
  47. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/trim_hdfeos2.py +2 -2
  48. ncarnate-2.2.2/tests/test_attribute_types.py +210 -0
  49. ncarnate-2.2.2/tests/test_convert_collisions.py +336 -0
  50. ncarnate-2.2.2/tests/test_convert_inplace_collisions.py +237 -0
  51. ncarnate-2.2.2/tests/test_g5_handoff_sufficiency.py +217 -0
  52. ncarnate-2.2.2/tests/test_handoff_gate.py +138 -0
  53. ncarnate-2.2.2/tests/test_handoff_schema.py +148 -0
  54. ncarnate-2.2.2/tests/test_operation_result.py +178 -0
  55. ncarnate-2.2.2/tests/test_optional_hdf4.py +413 -0
  56. ncarnate-2.2.2/tests/test_plan_hash.py +91 -0
  57. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_raw_granules.py +1 -1
  58. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_recompress_netcdf.py +27 -0
  59. ncarnate-2.2.2/tests/test_sdist_contents.py +139 -0
  60. ncarnate-2.2.2/tests/test_stage_api_4b.py +154 -0
  61. ncarnate-2.2.2/tests/test_stage_api_g4.py +128 -0
  62. ncarnate-2.2.2/tools/validate_distributions.py +80 -0
  63. ncarnate-2.2.1/.github/workflows/ci.yml +0 -73
  64. ncarnate-2.2.1/docs/index.rst +0 -9
  65. ncarnate-2.2.1/ncarnate/audit/codes.py +0 -56
  66. ncarnate-2.2.1/ncarnate/convert/report.py +0 -59
  67. ncarnate-2.2.1/ncarnate/core.py +0 -505
  68. {ncarnate-2.2.1 → ncarnate-2.2.2}/.gitattributes +0 -0
  69. {ncarnate-2.2.1 → ncarnate-2.2.2}/.github/FUNDING.yml +0 -0
  70. {ncarnate-2.2.1 → ncarnate-2.2.2}/.readthedocs.yaml +0 -0
  71. {ncarnate-2.2.1 → ncarnate-2.2.2}/.zenodo.json +0 -0
  72. {ncarnate-2.2.1 → ncarnate-2.2.2}/CONTRIBUTING.md +0 -0
  73. {ncarnate-2.2.1 → ncarnate-2.2.2}/LICENSE +0 -0
  74. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/README.md +0 -0
  75. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/build_lockup.py +0 -0
  76. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/build_raster_lockup.py +0 -0
  77. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/compose_logo.py +0 -0
  78. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/globe.py +0 -0
  79. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-lockup-dark.png +0 -0
  80. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-lockup-dark.svg +0 -0
  81. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-lockup.png +0 -0
  82. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-lockup.svg +0 -0
  83. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-mark.png +0 -0
  84. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-mono.svg +0 -0
  85. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-source.png +0 -0
  86. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-white.svg +0 -0
  87. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-wordmark-white.svg +0 -0
  88. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate-wordmark.svg +0 -0
  89. {ncarnate-2.2.1 → ncarnate-2.2.2}/brand/ncarnate.svg +0 -0
  90. {ncarnate-2.2.1 → ncarnate-2.2.2}/conda-recipe/README.md +0 -0
  91. {ncarnate-2.2.1 → ncarnate-2.2.2}/docs/api.rst +0 -0
  92. {ncarnate-2.2.1 → ncarnate-2.2.2}/docs/conf.py +0 -0
  93. {ncarnate-2.2.1 → ncarnate-2.2.2}/docs/requirements.txt +0 -0
  94. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/__main__.py +0 -0
  95. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/audit/models.py +0 -0
  96. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/audit/report.py +0 -0
  97. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/convert/integrity.py +0 -0
  98. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/convert/reader.py +0 -0
  99. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/discovery.py +0 -0
  100. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/eos/__init__.py +0 -0
  101. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/eos/gctp.py +0 -0
  102. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/eos/grid.py +0 -0
  103. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/eos/structmeta.py +0 -0
  104. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/eos/swath.py +0 -0
  105. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/formats.py +0 -0
  106. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/hashing.py +0 -0
  107. {ncarnate-2.2.1 → ncarnate-2.2.2}/ncarnate/limits.py +0 -0
  108. {ncarnate-2.2.1 → ncarnate-2.2.2}/paper/paper.bib +0 -0
  109. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/__init__.py +0 -0
  110. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/record.schema.json +0 -0
  111. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_agreement.py +0 -0
  112. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_audit_path.py +0 -0
  113. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_checksum.py +0 -0
  114. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_classify.py +0 -0
  115. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_cli_dispatch.py +0 -0
  116. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_contract.py +0 -0
  117. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_emission.py +0 -0
  118. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_error_codes.py +0 -0
  119. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_inspect.py +0 -0
  120. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_models.py +0 -0
  121. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_readonly.py +0 -0
  122. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/audit/test_scaffold.py +0 -0
  123. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/conftest.py +0 -0
  124. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/__init__.py +0 -0
  125. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_agreement.py +0 -0
  126. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_cli_dispatch.py +0 -0
  127. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_cli_integration.py +0 -0
  128. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_convert_manifest.py +0 -0
  129. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_inplace_skip.py +0 -0
  130. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_models.py +0 -0
  131. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_reader.py +0 -0
  132. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/convert/test_summary_exit.py +0 -0
  133. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/blockers/compound_type.nc +0 -0
  134. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.hdf +0 -0
  135. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.hdf +0 -0
  136. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/endianness.nc +0 -0
  137. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/nested_groups.nc +0 -0
  138. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/packed_fill.nc +0 -0
  139. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/unlimited_dim.nc +0 -0
  140. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/make_blocker_fixtures.py +0 -0
  141. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/fixtures/make_fixtures.py +0 -0
  142. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_cli.py +0 -0
  143. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_discovery.py +0 -0
  144. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_eos_units.py +0 -0
  145. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_formats.py +0 -0
  146. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_hdf4_conversion.py +0 -0
  147. {ncarnate-2.2.1 → ncarnate-2.2.2}/tests/test_limits.py +0 -0
@@ -0,0 +1,157 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ # The pip-based jobs (test, test-windows-degraded, build) install no system
10
+ # HDF libraries on purpose: the netCDF4 wheels bundle libhdf5/libnetcdf, and
11
+ # the pyhdf >= 0.11.6 manylinux wheels bundle libhdf4 (verified against PyPI
12
+ # 2026-07-08 for cp310-cp313). Platforms without a pyhdf wheel (e.g.
13
+ # linux-aarch64) need a system libhdf4 for the sdist build instead. The
14
+ # one deliberate exception is test-conda-forge, which provisions the whole
15
+ # runtime stack (incl. a system-HDF4-linked pyhdf) from conda-forge to prove
16
+ # the *supported* install channel delivers full HDF4 capability (KD-L7, G2).
17
+
18
+ jobs:
19
+ lint:
20
+ name: ruff
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
24
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
25
+ with:
26
+ python-version: "3.12"
27
+ - name: Install ruff
28
+ # Minor-pinned so a future ruff release can't red a green branch with
29
+ # no code change (new lint rules land in version bumps). Bump
30
+ # deliberately; keep in step with the version developers run locally.
31
+ run: python -m pip install "ruff==0.15.*"
32
+ - name: Run ruff
33
+ run: ruff check .
34
+
35
+ test:
36
+ name: pytest (${{ matrix.os }} py${{ matrix.python-version }})
37
+ runs-on: ${{ matrix.os }}
38
+ strategy:
39
+ fail-fast: false
40
+ matrix:
41
+ os: [ubuntu-latest]
42
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
43
+ # Smoke the self-contained-wheel install story (pyhdf + netCDF4 +
44
+ # pyproj, no system HDF libs) on macOS too. Windows is deliberately
45
+ # excluded from the *full* matrix: pyhdf's Windows wheel ships no
46
+ # HDF4 runtime, so the HDF4 fixtures can't run there. Windows-pip
47
+ # instead gets the dedicated degraded-capability job below
48
+ # (KD-L4/KD-L7), which pins exactly what must keep working — and
49
+ # exactly how the HDF4 path must refuse — without a usable pyhdf.
50
+ include:
51
+ - os: macos-latest
52
+ python-version: "3.12"
53
+ steps:
54
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
55
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
56
+ with:
57
+ python-version: ${{ matrix.python-version }}
58
+ - name: Install package with test extras
59
+ run: |
60
+ python -m pip install --upgrade pip
61
+ python -m pip install -e ".[test]"
62
+ - name: Run tests (offline; raw-granule marks self-skip)
63
+ run: python -m pytest -q
64
+
65
+ test-windows-degraded:
66
+ name: pytest degraded-capability (windows pip)
67
+ runs-on: windows-latest
68
+ steps:
69
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
70
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
71
+ with:
72
+ python-version: "3.12"
73
+ - name: Install package with test extras (pip wheels only)
74
+ # pyhdf's Windows wheel *installs* but cannot *import* (it ships no
75
+ # HDF4 runtime) — which is precisely the degraded environment this
76
+ # job exists to pin (KD-L4/KD-L7, gate G2).
77
+ run: |
78
+ python -m pip install --upgrade pip
79
+ python -m pip install -e ".[test]"
80
+ - name: Degraded-capability contract (tests/test_optional_hdf4.py)
81
+ # The purpose-built matrix: import / --help / --version / detection /
82
+ # netCDF recompression / audits all work without a usable pyhdf; an
83
+ # HDF4 attempt refuses with the stable HDF4_RUNTIME_UNAVAILABLE
84
+ # message before output creation, never an import traceback.
85
+ run: python -m pytest tests/test_optional_hdf4.py -q
86
+
87
+ test-conda-forge:
88
+ name: pytest full-capability (conda-forge)
89
+ runs-on: ubuntu-latest
90
+ # conda-forge is the *supported* HDF4 install channel (the refusal and
91
+ # the README point Windows/aarch64 users at `conda install -c
92
+ # conda-forge ncarnate`). This lane proves that channel actually
93
+ # delivers the full HDF4/HDF-EOS2 capability — a promise the pip matrix
94
+ # cannot make, since it relies on the bundled-libhdf4 pyhdf wheels
95
+ # rather than the conda-forge pyhdf built against a system HDF4
96
+ # (KD-L7 / priority step 2.5; gate G2 cannot close without it).
97
+ defaults:
98
+ run:
99
+ shell: bash -el {0}
100
+ steps:
101
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
102
+ - name: Provision a conda-forge full-capability environment
103
+ uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 # v3.0.0
104
+ with:
105
+ environment-name: ncarnate-cf
106
+ # The runtime stack comes wholly from conda-forge — pyhdf here is
107
+ # built against a real HDF4 library, unlike the pip wheel's
108
+ # bundled runtime. Pin Python; float the rest to what the channel
109
+ # currently ships (the point is the channel's promise, not a
110
+ # frozen resolution).
111
+ condarc: |
112
+ channels:
113
+ - conda-forge
114
+ channel_priority: strict
115
+ create-args: >-
116
+ python=3.12
117
+ pyhdf
118
+ netCDF4
119
+ numpy
120
+ pyproj
121
+ tqdm
122
+ pytest
123
+ - name: Install ncarnate from source without replacing native deps
124
+ # --no-deps: install only ncarnate; the native stack (pyhdf,
125
+ # netCDF4, numpy, pyproj, tqdm) stays the conda-forge build. Build
126
+ # isolation still fetches the hatchling backend into a throwaway
127
+ # env, which never touches the runtime dependencies above.
128
+ run: python -m pip install -e . --no-deps
129
+ - name: Prove the conda-forge HDF4 runtime imports
130
+ run: |
131
+ python -c "from ncarnate.hdf4_runtime import require_hdf4_runtime; require_hdf4_runtime(); print('conda-forge HDF4 runtime OK')"
132
+ - name: Full suite — HDF4 import, conversion, audit, manifest on real fixtures
133
+ # The committed HDF4/HDF-EOS2 fixtures drive real conversions here
134
+ # (unlike the pip matrix's bundled-wheel path), exercising the
135
+ # supported channel end to end. Raw-granule tests self-skip offline.
136
+ run: python -m pytest -q
137
+
138
+ build:
139
+ name: build + twine check
140
+ runs-on: ubuntu-latest
141
+ steps:
142
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
143
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
144
+ with:
145
+ python-version: "3.12"
146
+ - name: Install build tooling
147
+ run: python -m pip install build twine
148
+ - name: Build sdist + wheel
149
+ run: python -m build
150
+ - name: Check distributions
151
+ run: twine check dist/*
152
+ - name: Validate distributions
153
+ run: >-
154
+ python tools/validate_distributions.py
155
+ --allowed-email dev@erickshepherd.com
156
+ --allowed-email ca@trufo.ai
157
+ dist/*
@@ -31,8 +31,8 @@ jobs:
31
31
  name: Build sdist + wheel
32
32
  runs-on: ubuntu-latest
33
33
  steps:
34
- - uses: actions/checkout@v4
35
- - uses: actions/setup-python@v5
34
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
35
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
36
36
  with:
37
37
  python-version: "3.12"
38
38
  - name: Build
@@ -40,7 +40,8 @@ jobs:
40
40
  python -m pip install --upgrade build twine
41
41
  python -m build
42
42
  python -m twine check dist/*
43
- - uses: actions/upload-artifact@v4
43
+ python tools/validate_distributions.py --allowed-email dev@erickshepherd.com --allowed-email ca@trufo.ai dist/*
44
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
44
45
  with:
45
46
  name: dist
46
47
  path: dist/
@@ -54,11 +55,11 @@ jobs:
54
55
  permissions:
55
56
  id-token: write
56
57
  steps:
57
- - uses: actions/download-artifact@v4
58
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
58
59
  with:
59
60
  name: dist
60
61
  path: dist/
61
- - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
62
+ - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
62
63
  with:
63
64
  repository-url: https://test.pypi.org/legacy/
64
65
  skip-existing: true # re-dispatching a dry run for an existing version won't fail
@@ -72,8 +73,8 @@ jobs:
72
73
  permissions:
73
74
  id-token: write
74
75
  steps:
75
- - uses: actions/download-artifact@v4
76
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
76
77
  with:
77
78
  name: dist
78
79
  path: dist/
79
- - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
80
+ - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
@@ -15,6 +15,7 @@ venv/
15
15
 
16
16
  # Local scratch.
17
17
  tmp/
18
+ brand/_work/
18
19
 
19
20
  # Audit orchestration sidecars (ephemeral analysis).
20
21
  docs/audits/*.run.log
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.2.2] - 2026-07-21
8
+
9
+ Maintenance release with packaging and CI updates. Conversion, recompression,
10
+ audit, and verification behavior are unchanged.
11
+
12
+ ### Changed
13
+ - Standardized package metadata and normalized bundled test fixtures.
14
+ - Added automated distribution-policy validation.
15
+ - Pinned GitHub Actions to reviewed immutable commits and current Node 24 action
16
+ releases.
17
+ - Kept the bundled conda recipe on the last published PyPI artifact; its version
18
+ and source hash will be advanced together in a post-publication update after
19
+ the definitive 2.2.2 sdist exists.
20
+
7
21
  ## [2.2.1] - 2026-07-12
8
22
 
9
23
  A documentation and branding patch. **No functional changes** to conversion,
@@ -9,10 +9,10 @@ type: software
9
9
  authors:
10
10
  - family-names: "Shepherd"
11
11
  given-names: "Erick Edward"
12
- email: "Contact@ErickShepherd.com"
12
+ email: "dev@erickshepherd.com"
13
13
  orcid: "https://orcid.org/0000-0002-4750-6100"
14
- version: "2.2.1"
15
- date-released: "2026-07-12"
14
+ version: "2.2.2"
15
+ date-released: "2026-07-21"
16
16
  doi: "10.5281/zenodo.21288802"
17
17
  license: MIT
18
18
  repository-code: "https://github.com/ErickShepherd/ncarnate"
@@ -57,7 +57,7 @@ an individual is officially representing the community in public spaces.
57
57
 
58
58
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
59
59
  reported to the community leaders responsible for enforcement at
60
- **Contact@ErickShepherd.com**. All complaints will be reviewed and investigated
60
+ **dev@erickshepherd.com**. All complaints will be reviewed and investigated
61
61
  promptly and fairly.
62
62
 
63
63
  All community leaders are obligated to respect the privacy and security of the
@@ -1,3 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: ncarnate
3
+ Version: 2.2.2
4
+ Summary: Convert HDF4/HDF-EOS2 (MODIS, AMSR-E) to CF-annotated netCDF4 and losslessly recompress netCDF/HDF5 files.
5
+ Project-URL: Homepage, https://github.com/ErickShepherd/ncarnate
6
+ Project-URL: Documentation, https://ncarnate.readthedocs.io/en/stable/
7
+ Project-URL: Source, https://github.com/ErickShepherd/ncarnate
8
+ Project-URL: Changelog, https://github.com/ErickShepherd/ncarnate/releases
9
+ Project-URL: Issues, https://github.com/ErickShepherd/ncarnate/issues
10
+ Author-email: Erick Shepherd <dev@erickshepherd.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: AMSR-E,CF conventions,GIS,HDF-EOS,HDF-EOS2,HDF4,HDF5,MODIS,atmospheric science,compression,geolocation,grid,netCDF,netCDF4,recompression,remote sensing,swath,xarray
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
25
+ Classifier: Topic :: Scientific/Engineering :: GIS
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: netcdf4>=1.6
28
+ Requires-Dist: numpy>=1.26
29
+ Requires-Dist: pyhdf>=0.11.6
30
+ Requires-Dist: pyproj>=3.6
31
+ Requires-Dist: tqdm>=4.66
32
+ Provides-Extra: test
33
+ Requires-Dist: build>=1.0; extra == 'test'
34
+ Requires-Dist: hatchling>=1.27; extra == 'test'
35
+ Requires-Dist: pytest; extra == 'test'
36
+ Description-Content-Type: text/markdown
37
+
1
38
  <p align="center">
2
39
  <picture>
3
40
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
@@ -41,7 +78,7 @@ Reach for ncarnate if you are trying to:
41
78
  - **Shrink an archive of scientific files** without risking the science: every
42
79
  output is verified value-for-value against its source before it replaces
43
80
  anything, and stored values round-trip value-identically (bit-for-bit for
44
- integer and packed data; NaN- and signed-zero-insensitive for floating-point/complex).
81
+ integer and packed data; NaN- and signed-zero-insensitive for floating point).
45
82
  - **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
46
83
  command.
47
84
 
@@ -50,13 +87,19 @@ Reach for ncarnate if you are trying to:
50
87
  Converting or recompressing a file changes *storage*, never *science data*:
51
88
 
52
89
  - Every variable's stored values are preserved **value-identically** — bit-for-bit
53
- for integer and packed data; for floating-point and complex data, distinct NaN
90
+ for integer and packed data; for floating-point data, distinct NaN
54
91
  bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
55
92
  `scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
56
93
  never applied.
57
- - Every dimension (including unlimited-ness), attribute (including its type), and
58
- group survives. HDF-EOS2 `StructMetadata` is preserved verbatim; names netCDF
59
- cannot hold are sanitized with the original recorded in a companion attribute.
94
+ - Every dimension (including unlimited-ness), attribute (including its exact
95
+ storage type — an `NC_STRING` scalar stays `NC_STRING`, verified via netCDF-C
96
+ type inquiry), and group survives. HDF-EOS2 `StructMetadata` is preserved
97
+ verbatim; names netCDF cannot hold are sanitized with the original recorded in
98
+ a companion attribute.
99
+ - **Complex-valued variables (`complex64`/`complex128`) are excluded** from the
100
+ fidelity guarantee: netCDF stores them as compound types, which ncarnate
101
+ **refuses loudly** with the stable `UNSUPPORTED_TYPE` error rather than
102
+ guessing at a lossy copy. Complex support is a later, evidence-backed feature.
60
103
  - Geolocation reconstruction is strictly **additive**: the original information
61
104
  always rides along, so the conversion never becomes the only copy of the
62
105
  truth. Swath coordinates are attached to variables whose first two axes are
@@ -97,11 +140,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
97
140
  building from sdist requires the system HDF4 library first (Debian/Ubuntu:
98
141
  `apt install libhdf4-dev`).
99
142
 
100
- **Windows via pip:** the netCDF/HDF5 *recompression* path works from PyPI wheels
101
- out of the box, but the HDF4/HDF-EOS2 *conversion* path does **not** — `pyhdf`'s
102
- Windows wheel ships no HDF4 runtime, so `import pyhdf` fails with a DLL-load
103
- error. Use the conda-forge install above for HDF4 on Windows (or **WSL** with the
104
- pip instructions).
143
+ **Windows via pip:** PyPI wheels give you the full netCDF/HDF5 surface —
144
+ `import ncarnate`, the CLI (`--help`/`--version`), format detection, audits,
145
+ manifest runs, and verified recompression — but **not** HDF4/HDF-EOS2
146
+ *conversion*: `pyhdf`'s Windows wheel ships no HDF4 runtime. An HDF4 attempt
147
+ is refused cleanly **before any output is created** with the stable
148
+ `[HDF4_RUNTIME_UNAVAILABLE]` message naming the detected cause, the
149
+ capabilities that still work, and the fix — never an unexplained import
150
+ traceback. An audit of an archive containing HDF4 files still completes,
151
+ recording those files as `unsupported` with the same code. For HDF4 on
152
+ Windows use the conda-forge install above (or **WSL** with the pip
153
+ instructions); a dedicated CI job pins this degraded-capability contract on
154
+ every change.
105
155
 
106
156
  ## Command line usage
107
157
 
@@ -176,6 +226,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
176
226
  exit code is non-zero **iff** a selected record failed — so a partial failure on
177
227
  a terabyte run surfaces loudly instead of silently mis-converting.
178
228
 
229
+ **Destination collision preflight.** Before any directory or output file is
230
+ created, every selected record's destination is computed up front — from the
231
+ source's *detected bytes*, never the manifest's declared format — and any
232
+ collision refuses the **entire run** with exit code 2 and a stable
233
+ `[DESTINATION_COLLISION]` message on stderr listing every involved source and
234
+ the contested destination. No last-writer-wins, no auto-rename, no partial
235
+ proceed, and nothing is written. Refused collisions include: two records
236
+ landing on one output path (e.g. an `a.hdf` → `a.nc` conversion next to a real
237
+ `a.nc` sibling), case-fold-equivalent names (one file on NTFS/APFS), duplicate
238
+ records for one source, an output tree overlapping a source tree (symlinks
239
+ resolved), and a pre-existing destination unless you pass `--skip-existing` to
240
+ resume.
241
+
179
242
  ## Library usage
180
243
 
181
244
  ```python
@@ -1,37 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: ncarnate
3
- Version: 2.2.1
4
- Summary: Convert HDF4/HDF-EOS2 (MODIS, AMSR-E) to CF-annotated netCDF4 and losslessly recompress netCDF/HDF5 files.
5
- Project-URL: Homepage, https://github.com/ErickShepherd/ncarnate
6
- Project-URL: Documentation, https://ncarnate.readthedocs.io/en/latest/
7
- Project-URL: Source, https://github.com/ErickShepherd/ncarnate
8
- Project-URL: Bug Tracker, https://github.com/ErickShepherd/ncarnate/issues
9
- Author-email: Erick Shepherd <Contact@ErickShepherd.com>
10
- License-Expression: MIT
11
- License-File: LICENSE
12
- Keywords: AMSR-E,CF conventions,GIS,HDF-EOS,HDF-EOS2,HDF4,HDF5,MODIS,atmospheric science,compression,geolocation,grid,netCDF,netCDF4,recompression,remote sensing,swath,xarray
13
- Classifier: Development Status :: 5 - Production/Stable
14
- Classifier: Intended Audience :: Science/Research
15
- Classifier: Operating System :: MacOS
16
- Classifier: Operating System :: Microsoft :: Windows
17
- Classifier: Operating System :: POSIX :: Linux
18
- Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.10
20
- Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.13
23
- Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
24
- Classifier: Topic :: Scientific/Engineering :: GIS
25
- Requires-Python: >=3.10
26
- Requires-Dist: netcdf4>=1.6
27
- Requires-Dist: numpy>=1.26
28
- Requires-Dist: pyhdf>=0.11.6
29
- Requires-Dist: pyproj>=3.6
30
- Requires-Dist: tqdm>=4.66
31
- Provides-Extra: test
32
- Requires-Dist: pytest; extra == 'test'
33
- Description-Content-Type: text/markdown
34
-
35
1
  <p align="center">
36
2
  <picture>
37
3
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
@@ -75,7 +41,7 @@ Reach for ncarnate if you are trying to:
75
41
  - **Shrink an archive of scientific files** without risking the science: every
76
42
  output is verified value-for-value against its source before it replaces
77
43
  anything, and stored values round-trip value-identically (bit-for-bit for
78
- integer and packed data; NaN- and signed-zero-insensitive for floating-point/complex).
44
+ integer and packed data; NaN- and signed-zero-insensitive for floating point).
79
45
  - **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
80
46
  command.
81
47
 
@@ -84,13 +50,19 @@ Reach for ncarnate if you are trying to:
84
50
  Converting or recompressing a file changes *storage*, never *science data*:
85
51
 
86
52
  - Every variable's stored values are preserved **value-identically** — bit-for-bit
87
- for integer and packed data; for floating-point and complex data, distinct NaN
53
+ for integer and packed data; for floating-point data, distinct NaN
88
54
  bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
89
55
  `scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
90
56
  never applied.
91
- - Every dimension (including unlimited-ness), attribute (including its type), and
92
- group survives. HDF-EOS2 `StructMetadata` is preserved verbatim; names netCDF
93
- cannot hold are sanitized with the original recorded in a companion attribute.
57
+ - Every dimension (including unlimited-ness), attribute (including its exact
58
+ storage type — an `NC_STRING` scalar stays `NC_STRING`, verified via netCDF-C
59
+ type inquiry), and group survives. HDF-EOS2 `StructMetadata` is preserved
60
+ verbatim; names netCDF cannot hold are sanitized with the original recorded in
61
+ a companion attribute.
62
+ - **Complex-valued variables (`complex64`/`complex128`) are excluded** from the
63
+ fidelity guarantee: netCDF stores them as compound types, which ncarnate
64
+ **refuses loudly** with the stable `UNSUPPORTED_TYPE` error rather than
65
+ guessing at a lossy copy. Complex support is a later, evidence-backed feature.
94
66
  - Geolocation reconstruction is strictly **additive**: the original information
95
67
  always rides along, so the conversion never becomes the only copy of the
96
68
  truth. Swath coordinates are attached to variables whose first two axes are
@@ -131,11 +103,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
131
103
  building from sdist requires the system HDF4 library first (Debian/Ubuntu:
132
104
  `apt install libhdf4-dev`).
133
105
 
134
- **Windows via pip:** the netCDF/HDF5 *recompression* path works from PyPI wheels
135
- out of the box, but the HDF4/HDF-EOS2 *conversion* path does **not** — `pyhdf`'s
136
- Windows wheel ships no HDF4 runtime, so `import pyhdf` fails with a DLL-load
137
- error. Use the conda-forge install above for HDF4 on Windows (or **WSL** with the
138
- pip instructions).
106
+ **Windows via pip:** PyPI wheels give you the full netCDF/HDF5 surface —
107
+ `import ncarnate`, the CLI (`--help`/`--version`), format detection, audits,
108
+ manifest runs, and verified recompression — but **not** HDF4/HDF-EOS2
109
+ *conversion*: `pyhdf`'s Windows wheel ships no HDF4 runtime. An HDF4 attempt
110
+ is refused cleanly **before any output is created** with the stable
111
+ `[HDF4_RUNTIME_UNAVAILABLE]` message naming the detected cause, the
112
+ capabilities that still work, and the fix — never an unexplained import
113
+ traceback. An audit of an archive containing HDF4 files still completes,
114
+ recording those files as `unsupported` with the same code. For HDF4 on
115
+ Windows use the conda-forge install above (or **WSL** with the pip
116
+ instructions); a dedicated CI job pins this degraded-capability contract on
117
+ every change.
139
118
 
140
119
  ## Command line usage
141
120
 
@@ -210,6 +189,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
210
189
  exit code is non-zero **iff** a selected record failed — so a partial failure on
211
190
  a terabyte run surfaces loudly instead of silently mis-converting.
212
191
 
192
+ **Destination collision preflight.** Before any directory or output file is
193
+ created, every selected record's destination is computed up front — from the
194
+ source's *detected bytes*, never the manifest's declared format — and any
195
+ collision refuses the **entire run** with exit code 2 and a stable
196
+ `[DESTINATION_COLLISION]` message on stderr listing every involved source and
197
+ the contested destination. No last-writer-wins, no auto-rename, no partial
198
+ proceed, and nothing is written. Refused collisions include: two records
199
+ landing on one output path (e.g. an `a.hdf` → `a.nc` conversion next to a real
200
+ `a.nc` sibling), case-fold-equivalent names (one file on NTFS/APFS), duplicate
201
+ records for one source, an output tree overlapping a source tree (symlinks
202
+ resolved), and a pre-existing destination unless you pass `--skip-existing` to
203
+ resume.
204
+
213
205
  ## Library usage
214
206
 
215
207
  ```python
@@ -1,7 +1,10 @@
1
1
  schema_version: 1
2
2
 
3
3
  context:
4
- version: "2.2.0"
4
+ # Keep this on the last published PyPI artifact. Updating the recipe to a
5
+ # new release requires that release's definitive sdist SHA-256 and therefore
6
+ # happens only after the new sdist exists on PyPI.
7
+ version: "2.2.1"
5
8
 
6
9
  package:
7
10
  name: ncarnate
@@ -9,7 +12,7 @@ package:
9
12
 
10
13
  source:
11
14
  url: https://pypi.org/packages/source/n/ncarnate/ncarnate-${{ version }}.tar.gz
12
- sha256: f1e3c9f7de98d11ee91e93db1d6e251ec82071762baabe133eb3bd0b44b5aeb5
15
+ sha256: 213760512d0272a97148728463c41dc49c2142dd70b95fc11e8ce3df9603d98e
13
16
 
14
17
  build:
15
18
  number: 0
@@ -12,7 +12,8 @@ changes *storage* (compression level, shuffle, chunk/endian layout, container fo
12
12
  ### netCDF/HDF5 → netCDF4 (recompression)
13
13
 
14
14
  Guaranteed preserved when read raw (`set_auto_maskandscale(False)`), value-identical —
15
- bit-for-bit for integer/packed data, NaN- and signed-zero-insensitive for floating-point/complex:
15
+ bit-for-bit for integer/packed data, NaN- and signed-zero-insensitive for floating point
16
+ (complex is excluded — see the guarantee boundary):
16
17
 
17
18
  - every variable's stored values (packed integers stay packed — no mask-and-scale
18
19
  round-trip, which silently re-quantizes);
@@ -30,7 +31,8 @@ replaced until the new file has been written and re-opened successfully.
30
31
 
31
32
  - every SDS's values value-identical — bit-for-bit for integer and char8 data (char8 SDS
32
33
  map to netCDF `NC_CHAR` and round-trip byte-for-byte), NaN- and signed-zero-insensitive
33
- for floating-point/complex; dimensions (with their HDF4 names) and attributes preserved; fill/scale
34
+ for floating point (HDF4's DFNT type system has no complex type, so complex never
35
+ arises on this path); dimensions (with their HDF4 names) and attributes preserved; fill/scale
34
36
  carried as declarations;
35
37
  - `StructMetadata.0` (and other EOS metadata attributes) preserved **verbatim** as
36
38
  attributes of an `HDFEOS_INFORMATION` group — reconstruction never becomes the only copy
@@ -64,11 +66,35 @@ edges). Interpolated variables carry a `comment` attribute saying they are inter
64
66
  edge pixels outside the geolocation envelope are linearly extrapolated; geolocation fill
65
67
  propagates, never interpolated across.
66
68
 
69
+ ### Manifest runs: destination collision preflight (no-data-loss gate)
70
+
71
+ Fidelity is meaningless if two outputs silently overwrite each other, so
72
+ manifest-driven runs (`convert --manifest`) add a **whole-run destination
73
+ preflight** before any directory or output is created: every selected record's
74
+ destination is computed up front from its *detected bytes* (never the
75
+ manifest's declared format — the manifest is untrusted input), and any
76
+ collision refuses the **entire run** with the stable `DESTINATION_COLLISION`
77
+ code (exit code 2, rendered on stderr) listing every involved source and the
78
+ contested destination. No last-writer-wins, no auto-rename, no partial
79
+ proceed, zero output-tree mutation. Refused classes: duplicate or
80
+ case-fold-equivalent destinations (one file on NTFS/APFS), an `.hdf` → `.nc`
81
+ conversion colliding with a real `.nc` sibling, duplicate records for one
82
+ source (symlink-aliased included), source-tree/output-tree overlap (symlinks
83
+ resolved), a destination aliasing a selected source, and a pre-existing
84
+ destination without `--skip-existing`. Pinned end-to-end — including the
85
+ zero-mutation guarantee at the process boundary — by
86
+ `tests/test_convert_collisions.py`.
87
+
67
88
  ### Guarantee boundary
68
89
 
69
90
  - Compound, VLen, enum, and opaque netCDF4 types are **out of scope for v2**: ncarnate
70
- raises a clear unsupported-type error rather than guessing (none of the target-domain
71
- files surveyed use them; revisit on demand).
91
+ raises a clear unsupported-type error (stable code `UNSUPPORTED_TYPE`) rather than
92
+ guessing (none of the target-domain files surveyed use them; revisit on demand).
93
+ - **Complex-valued variables (`complex64`/`complex128`) are excluded from the fidelity
94
+ guarantee** (owner decision, 2026-07-16): netCDF stores recognized complex encodings
95
+ as compound types (e.g. `_PFNC_DOUBLE_COMPLEX_TYPE`), so they are refused by the
96
+ compound rule above — loudly, with `UNSUPPORTED_TYPE`, before any output is written.
97
+ Complex support is a later, evidence-backed feature, not a silent partial copy.
72
98
  - Already-lossy packing is preserved as-is, never "un-quantized".
73
99
  - HDF4 inputs are read via the pyhdf SD API: SDS datasets and attributes. Vdata/Vgroup
74
100
  payloads beyond SD are out of scope for v2.
@@ -77,9 +103,10 @@ propagates, never interpolated across.
77
103
 
78
104
  For every committed fixture: convert/recompress → re-open both files raw → assert
79
105
  (a) value arrays equal on raw reads (`numpy.array_equal`, with `equal_nan` set for
80
- float/complex — bit-for-bit for integer/packed data, NaN-/±0-insensitive for floats), (b) dimension names,
81
- sizes, and unlimited flags equal, (c) attribute sets equal (fill/scale compared as
82
- declarations), (d) group tree equal, and (e) output ≤ input size at `complevel≥7` for the
106
+ floats — bit-for-bit for integer/packed data, NaN-/±0-insensitive for floats), (b) dimension names,
107
+ sizes, and unlimited flags equal, (c) attribute sets equal — values *and* netCDF storage
108
+ types (`NC_STRING` vs `NC_CHAR`, via netCDF-C `nc_inq_atttype`; `tests/test_attribute_types.py`)
109
+ — (d) group tree equal, and (e) output ≤ input size at `complevel≥7` for the
83
110
  compressible fixtures. The packed-integer + `_FillValue` fixture exists precisely because
84
111
  v1 failed both (a crash and a silent re-quantization).
85
112
 
@@ -102,7 +129,7 @@ exactly (written with each source attribute's true HDF4 type code via `attr.info
102
129
  inferred from type-erased Python values — e.g. an INT16 `_FillValue` stays INT16).
103
130
 
104
131
  HDF-EOS2 fixtures are trimmed from real granules kept outside the repo
105
- (`~/ncarnate-data/granules/` + `PROVENANCE.md` there); each fixture has a
132
+ (`the local source-granule archive/` + `PROVENANCE.md` there); each fixture has a
106
133
  `.provenance.json` sidecar (source granule, SHA-256, trim parameters). Trim rules: the
107
134
  AMSR-E grid fixture keeps `StructMetadata.0` **verbatim** (grid dims unchanged; metadata
108
135
  lists DataFields not carried over — readers must tolerate that, as with subsetted granules
@@ -115,23 +142,3 @@ values *and* HDF4 type codes preserved) passed 2026-07-08.
115
142
 
116
143
  Raw-granule cross-checks (the 29–60 MB originals) stay in a local, non-CI test mark.
117
144
 
118
- ## History-rewrite record (D1 + D2, executed 2026-07-08)
119
-
120
- Executed in one `git filter-repo` pass (per
121
- [the plan](plans/2026-07-08-repo-improvement-plan.md) §2 D1/D2), before any v2 code:
122
-
123
- - **Stripped from all history:** `netcdf_recompressor/AMSR_E_L3_SeaIce12km_V15_20070101.hdf`
124
- (62.5 MB), `netcdf4_compressor/AMSR_E_L3_SeaIce12km_V15_20070101.hdf` (62.1 MB),
125
- `netcdf4_compressor/2007_MYD_CALIPSO_ice_cloud_collocation_discrepancies_test.nc` (9.5 MB).
126
- - **Author/committer rewrite:** the owner's private Gmail → `24425940+ErickShepherd@users.noreply.github.com`
127
- (mailmap in the same pass); local `user.email` set to the noreply address.
128
- - **Pack size:** `size-pack` **30.20 MiB → 61.70 KiB** (`git count-objects -vH`, after
129
- `git gc --prune=now`); `.git` = 264 KB on disk. No `.hdf`/`.nc` objects remain in
130
- `git rev-list --all --objects`.
131
- - **Content integrity:** the rewritten tip's tree hash equals the pre-rewrite tip's tree
132
- (`12761123d5d26d62e2025abe8f5dfcf3e360ef3c`) — file content is untouched; only history
133
- metadata changed.
134
- - **Backup:** full pre-rewrite bundle at
135
- `~/ncarnate-data/backup/netcdf_recompressor-pre-rewrite-20260708.bundle`
136
- (`git bundle verify` clean). All commit SHAs changed, as planned and victimless
137
- (repo never public). **Pushing the rewritten history remains owner-gated.**
@@ -0,0 +1,30 @@
1
+ .. Theme-aware logo for Read the Docs. The README's <picture> block keys off the OS
2
+ ``prefers-color-scheme``, which does NOT track furo's independent light/dark toggle
3
+ (toggling furo to light while the OS is in dark mode otherwise shows the dark,
4
+ cream-wordmark logo on a white page). Furo's ``.only-light`` / ``.only-dark`` classes
5
+ follow furo's ACTUAL theme, so render the logo with those here and skip the README's
6
+ own <picture> in the include below (``:start-after: </p>`` drops the logo block, whose
7
+ closing ``</p>`` is unique in the README).
8
+
9
+ .. image:: https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup.png
10
+ :class: only-light
11
+ :width: 460
12
+ :align: center
13
+ :alt: ncarnate
14
+
15
+ .. image:: https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png
16
+ :class: only-dark
17
+ :width: 460
18
+ :align: center
19
+ :alt: ncarnate
20
+
21
+ .. include:: ../README.md
22
+ :parser: myst_parser.sphinx_
23
+ :start-after: </p>
24
+
25
+ .. toctree::
26
+ :hidden:
27
+ :maxdepth: 2
28
+
29
+ Overview <self>
30
+ API reference <api>