ncarnate 2.2.0__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.0 → ncarnate-2.2.2}/.github/workflows/publish.yml +8 -7
  3. {ncarnate-2.2.0 → ncarnate-2.2.2}/.gitignore +1 -0
  4. {ncarnate-2.2.0 → ncarnate-2.2.2}/CHANGELOG.md +37 -0
  5. {ncarnate-2.2.0 → ncarnate-2.2.2}/CITATION.cff +3 -3
  6. {ncarnate-2.2.0 → ncarnate-2.2.2}/CODE_OF_CONDUCT.md +1 -1
  7. {ncarnate-2.2.0 → ncarnate-2.2.2}/PKG-INFO +59 -19
  8. {ncarnate-2.2.0 → ncarnate-2.2.2}/README.md +51 -14
  9. ncarnate-2.2.2/brand/README.md +81 -0
  10. ncarnate-2.2.2/brand/build_lockup.py +90 -0
  11. ncarnate-2.2.2/brand/build_raster_lockup.py +149 -0
  12. ncarnate-2.2.2/brand/compose_logo.py +215 -0
  13. ncarnate-2.2.2/brand/globe.py +216 -0
  14. ncarnate-2.2.2/brand/ncarnate-lockup-dark.png +0 -0
  15. ncarnate-2.2.2/brand/ncarnate-lockup-dark.svg +1 -0
  16. ncarnate-2.2.2/brand/ncarnate-lockup.png +0 -0
  17. ncarnate-2.2.2/brand/ncarnate-lockup.svg +1 -0
  18. ncarnate-2.2.2/brand/ncarnate-mark.png +0 -0
  19. ncarnate-2.2.2/brand/ncarnate-mono.svg +1 -0
  20. ncarnate-2.2.2/brand/ncarnate-source.png +0 -0
  21. ncarnate-2.2.2/brand/ncarnate-white.svg +1 -0
  22. ncarnate-2.2.2/brand/ncarnate-wordmark-white.svg +1 -0
  23. ncarnate-2.2.2/brand/ncarnate-wordmark.svg +1 -0
  24. ncarnate-2.2.2/brand/ncarnate.svg +1 -0
  25. {ncarnate-2.2.0 → ncarnate-2.2.2}/conda-recipe/recipe.yaml +5 -2
  26. {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/conf.py +7 -0
  27. {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/fidelity-notes.md +39 -29
  28. ncarnate-2.2.2/docs/index.rst +30 -0
  29. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/__init__.py +28 -0
  30. ncarnate-2.2.2/ncarnate/atttypes.py +257 -0
  31. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/__init__.py +17 -1
  32. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/classify.py +1 -0
  33. ncarnate-2.2.2/ncarnate/audit/codes.py +113 -0
  34. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/inspect.py +17 -6
  35. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/cli.py +6 -2
  36. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/constants.py +1 -1
  37. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/__init__.py +133 -61
  38. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/models.py +18 -1
  39. ncarnate-2.2.2/ncarnate/convert/preflight.py +298 -0
  40. ncarnate-2.2.2/ncarnate/convert/report.py +114 -0
  41. ncarnate-2.2.2/ncarnate/core.py +1198 -0
  42. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/errors.py +36 -0
  43. ncarnate-2.2.2/ncarnate/handoff.py +297 -0
  44. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/hdf4.py +4 -1
  45. ncarnate-2.2.2/ncarnate/hdf4_runtime.py +81 -0
  46. ncarnate-2.2.2/ncarnate/result.py +673 -0
  47. ncarnate-2.2.2/ncarnate/schemas/__init__.py +15 -0
  48. ncarnate-2.2.2/ncarnate/schemas/handoff.schema.json +276 -0
  49. ncarnate-2.2.2/ncarnate/stage.py +114 -0
  50. {ncarnate-2.2.0 → ncarnate-2.2.2}/paper/paper.md +11 -8
  51. {ncarnate-2.2.0 → ncarnate-2.2.2}/pyproject.toml +18 -6
  52. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_codes.py +6 -1
  53. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/conftest.py +3 -1
  54. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_integrity.py +8 -3
  55. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.provenance.json +1 -1
  56. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.hdf +0 -0
  57. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.provenance.json +1 -1
  58. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.hdf +0 -0
  59. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.provenance.json +1 -1
  60. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.hdf +0 -0
  61. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.provenance.json +1 -1
  62. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.provenance.json +1 -1
  63. ncarnate-2.2.2/tests/fixtures/operation_result/amsre_seaice12km.result.json +575 -0
  64. ncarnate-2.2.2/tests/fixtures/operation_result/packed_fill.canonical.json +1 -0
  65. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/trim_hdfeos2.py +2 -2
  66. ncarnate-2.2.2/tests/test_attribute_types.py +210 -0
  67. ncarnate-2.2.2/tests/test_convert_collisions.py +336 -0
  68. ncarnate-2.2.2/tests/test_convert_inplace_collisions.py +237 -0
  69. ncarnate-2.2.2/tests/test_g5_handoff_sufficiency.py +217 -0
  70. ncarnate-2.2.2/tests/test_handoff_gate.py +138 -0
  71. ncarnate-2.2.2/tests/test_handoff_schema.py +148 -0
  72. ncarnate-2.2.2/tests/test_operation_result.py +178 -0
  73. ncarnate-2.2.2/tests/test_optional_hdf4.py +413 -0
  74. ncarnate-2.2.2/tests/test_plan_hash.py +91 -0
  75. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_raw_granules.py +1 -1
  76. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_recompress_netcdf.py +27 -0
  77. ncarnate-2.2.2/tests/test_sdist_contents.py +139 -0
  78. ncarnate-2.2.2/tests/test_stage_api_4b.py +154 -0
  79. ncarnate-2.2.2/tests/test_stage_api_g4.py +128 -0
  80. ncarnate-2.2.2/tools/validate_distributions.py +80 -0
  81. ncarnate-2.2.0/.github/workflows/ci.yml +0 -73
  82. ncarnate-2.2.0/docs/index.rst +0 -9
  83. ncarnate-2.2.0/ncarnate/audit/codes.py +0 -56
  84. ncarnate-2.2.0/ncarnate/convert/report.py +0 -59
  85. ncarnate-2.2.0/ncarnate/core.py +0 -505
  86. {ncarnate-2.2.0 → ncarnate-2.2.2}/.gitattributes +0 -0
  87. {ncarnate-2.2.0 → ncarnate-2.2.2}/.github/FUNDING.yml +0 -0
  88. {ncarnate-2.2.0 → ncarnate-2.2.2}/.readthedocs.yaml +0 -0
  89. {ncarnate-2.2.0 → ncarnate-2.2.2}/.zenodo.json +0 -0
  90. {ncarnate-2.2.0 → ncarnate-2.2.2}/CONTRIBUTING.md +0 -0
  91. {ncarnate-2.2.0 → ncarnate-2.2.2}/LICENSE +0 -0
  92. {ncarnate-2.2.0 → ncarnate-2.2.2}/conda-recipe/README.md +0 -0
  93. {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/api.rst +0 -0
  94. {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/requirements.txt +0 -0
  95. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/__main__.py +0 -0
  96. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/models.py +0 -0
  97. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/report.py +0 -0
  98. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/integrity.py +0 -0
  99. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/reader.py +0 -0
  100. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/discovery.py +0 -0
  101. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/__init__.py +0 -0
  102. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/gctp.py +0 -0
  103. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/grid.py +0 -0
  104. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/structmeta.py +0 -0
  105. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/swath.py +0 -0
  106. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/formats.py +0 -0
  107. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/hashing.py +0 -0
  108. {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/limits.py +0 -0
  109. {ncarnate-2.2.0 → ncarnate-2.2.2}/paper/paper.bib +0 -0
  110. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/__init__.py +0 -0
  111. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/record.schema.json +0 -0
  112. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_agreement.py +0 -0
  113. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_audit_path.py +0 -0
  114. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_checksum.py +0 -0
  115. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_classify.py +0 -0
  116. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_cli_dispatch.py +0 -0
  117. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_contract.py +0 -0
  118. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_emission.py +0 -0
  119. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_error_codes.py +0 -0
  120. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_inspect.py +0 -0
  121. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_models.py +0 -0
  122. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_readonly.py +0 -0
  123. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_scaffold.py +0 -0
  124. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/__init__.py +0 -0
  125. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_agreement.py +0 -0
  126. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_cli_dispatch.py +0 -0
  127. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_cli_integration.py +0 -0
  128. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_convert_manifest.py +0 -0
  129. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_inplace_skip.py +0 -0
  130. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_models.py +0 -0
  131. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_reader.py +0 -0
  132. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_summary_exit.py +0 -0
  133. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/blockers/compound_type.nc +0 -0
  134. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.hdf +0 -0
  135. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.hdf +0 -0
  136. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/endianness.nc +0 -0
  137. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/nested_groups.nc +0 -0
  138. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/packed_fill.nc +0 -0
  139. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/unlimited_dim.nc +0 -0
  140. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/make_blocker_fixtures.py +0 -0
  141. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/make_fixtures.py +0 -0
  142. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_cli.py +0 -0
  143. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_discovery.py +0 -0
  144. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_eos_units.py +0 -0
  145. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_formats.py +0 -0
  146. {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_hdf4_conversion.py +0 -0
  147. {ncarnate-2.2.0 → 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,43 @@ 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
+
21
+ ## [2.2.1] - 2026-07-12
22
+
23
+ A documentation and branding patch. **No functional changes** to conversion,
24
+ recompression, or verification behavior — the fidelity guarantee is unchanged;
25
+ this release only describes it more precisely and dresses up the project.
26
+
27
+ ### Changed
28
+ - Fidelity wording now matches what the verifier enforces. The data-preservation
29
+ guarantee is described as **value-identical** — bit-for-bit for integer, char,
30
+ and packed data; NaN- and signed-zero-insensitive for floating-point/complex — in the
31
+ README, `docs/fidelity-notes.md`, and the HDF4 module docstring, replacing the
32
+ blanket "bit-identical" (which overstated the guarantee for float data, since
33
+ the verifiers compare floats with `numpy.array_equal(equal_nan=True)`).
34
+
35
+ ### Added
36
+ - Brand assets and a theme-aware README logo lockup (light/dark), plus live
37
+ PyPI, conda-forge, and Read the Docs badges.
38
+
39
+ ### Fixed
40
+ - Read the Docs build stays green under current `myst-parser` by suppressing the
41
+ `myst.header` advisory (the strict `fail_on_warning` build would otherwise fail
42
+ on the README-derived landing page).
43
+
7
44
  ## [2.2.0] - 2026-07-11
8
45
 
9
46
  The archive-modernization workflow: a read-only **audit** that plans a
@@ -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.0"
15
- date-released: "2026-07-11"
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,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncarnate
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Convert HDF4/HDF-EOS2 (MODIS, AMSR-E) to CF-annotated netCDF4 and losslessly recompress netCDF/HDF5 files.
5
5
  Project-URL: Homepage, https://github.com/ErickShepherd/ncarnate
6
- Project-URL: Documentation, https://ncarnate.readthedocs.io/en/latest/
6
+ Project-URL: Documentation, https://ncarnate.readthedocs.io/en/stable/
7
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>
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>
10
11
  License-Expression: MIT
11
12
  License-File: LICENSE
12
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
13
- Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Development Status :: 4 - Beta
14
15
  Classifier: Intended Audience :: Science/Research
15
16
  Classifier: Operating System :: MacOS
16
17
  Classifier: Operating System :: Microsoft :: Windows
@@ -29,12 +30,22 @@ Requires-Dist: pyhdf>=0.11.6
29
30
  Requires-Dist: pyproj>=3.6
30
31
  Requires-Dist: tqdm>=4.66
31
32
  Provides-Extra: test
33
+ Requires-Dist: build>=1.0; extra == 'test'
34
+ Requires-Dist: hatchling>=1.27; extra == 'test'
32
35
  Requires-Dist: pytest; extra == 'test'
33
36
  Description-Content-Type: text/markdown
34
37
 
35
- # ncarnate
38
+ <p align="center">
39
+ <picture>
40
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
41
+ <img alt="ncarnate" src="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup.png" width="460">
42
+ </picture>
43
+ </p>
36
44
 
37
45
  [![CI status](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml/badge.svg)](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml)
46
+ [![PyPI version](https://img.shields.io/pypi/v/ncarnate.svg)](https://pypi.org/project/ncarnate/)
47
+ [![conda-forge version](https://img.shields.io/conda/vn/conda-forge/ncarnate.svg)](https://anaconda.org/conda-forge/ncarnate)
48
+ [![Docs](https://readthedocs.org/projects/ncarnate/badge/?version=latest)](https://ncarnate.readthedocs.io/en/latest/)
38
49
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ErickShepherd/ncarnate/blob/main/LICENSE)
39
50
  [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue.svg)](https://pypi.org/project/ncarnate/)
40
51
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21288802-blue)](https://doi.org/10.5281/zenodo.21288802)
@@ -66,7 +77,8 @@ Reach for ncarnate if you are trying to:
66
77
  filter without altering a single stored value.
67
78
  - **Shrink an archive of scientific files** without risking the science: every
68
79
  output is verified value-for-value against its source before it replaces
69
- anything, and stored values round-trip bit-identically.
80
+ anything, and stored values round-trip value-identically (bit-for-bit for
81
+ integer and packed data; NaN- and signed-zero-insensitive for floating point).
70
82
  - **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
71
83
  command.
72
84
 
@@ -74,12 +86,20 @@ Reach for ncarnate if you are trying to:
74
86
 
75
87
  Converting or recompressing a file changes *storage*, never *science data*:
76
88
 
77
- - Every variable's stored values are preserved **bit-identically** — packed
78
- integers stay packed; `scale_factor`/`add_offset`/`_FillValue` are carried
79
- across as declarations, never applied.
80
- - Every dimension (including unlimited-ness), attribute (including its type), and
81
- group survives. HDF-EOS2 `StructMetadata` is preserved verbatim; names netCDF
82
- cannot hold are sanitized with the original recorded in a companion attribute.
89
+ - Every variable's stored values are preserved **value-identically** — bit-for-bit
90
+ for integer and packed data; for floating-point data, distinct NaN
91
+ bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
92
+ `scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
93
+ never applied.
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.
83
103
  - Geolocation reconstruction is strictly **additive**: the original information
84
104
  always rides along, so the conversion never becomes the only copy of the
85
105
  truth. Swath coordinates are attached to variables whose first two axes are
@@ -120,11 +140,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
120
140
  building from sdist requires the system HDF4 library first (Debian/Ubuntu:
121
141
  `apt install libhdf4-dev`).
122
142
 
123
- **Windows via pip:** the netCDF/HDF5 *recompression* path works from PyPI wheels
124
- out of the box, but the HDF4/HDF-EOS2 *conversion* path does **not** — `pyhdf`'s
125
- Windows wheel ships no HDF4 runtime, so `import pyhdf` fails with a DLL-load
126
- error. Use the conda-forge install above for HDF4 on Windows (or **WSL** with the
127
- 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.
128
155
 
129
156
  ## Command line usage
130
157
 
@@ -199,6 +226,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
199
226
  exit code is non-zero **iff** a selected record failed — so a partial failure on
200
227
  a terabyte run surfaces loudly instead of silently mis-converting.
201
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
+
202
242
  ## Library usage
203
243
 
204
244
  ```python
@@ -228,7 +268,7 @@ The AMSR-E daily 12.5 km sea-ice granule this project grew up around:
228
268
  | netCDF4 recompression (`--complevel 9`) | 42.6 MB | 19.9 MB |
229
269
  | HDF-EOS2 → netCDF4 (+ reconstructed lat/lon) | 60.2 MB | 35.5 MB |
230
270
 
231
- Both outputs re-read bit-identically to their sources; the conversion
271
+ Both outputs re-read value-identically to their sources; the conversion
232
272
  additionally carries CF `polar_stereographic` grid mappings and coordinates for
233
273
  both hemispheric grids. The northern grid's reconstructed latitudes/longitudes
234
274
  agree with The HDF Group's independent conversion of the same granule to within
@@ -1,6 +1,14 @@
1
- # ncarnate
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
4
+ <img alt="ncarnate" src="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup.png" width="460">
5
+ </picture>
6
+ </p>
2
7
 
3
8
  [![CI status](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml/badge.svg)](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml)
9
+ [![PyPI version](https://img.shields.io/pypi/v/ncarnate.svg)](https://pypi.org/project/ncarnate/)
10
+ [![conda-forge version](https://img.shields.io/conda/vn/conda-forge/ncarnate.svg)](https://anaconda.org/conda-forge/ncarnate)
11
+ [![Docs](https://readthedocs.org/projects/ncarnate/badge/?version=latest)](https://ncarnate.readthedocs.io/en/latest/)
4
12
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ErickShepherd/ncarnate/blob/main/LICENSE)
5
13
  [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue.svg)](https://pypi.org/project/ncarnate/)
6
14
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21288802-blue)](https://doi.org/10.5281/zenodo.21288802)
@@ -32,7 +40,8 @@ Reach for ncarnate if you are trying to:
32
40
  filter without altering a single stored value.
33
41
  - **Shrink an archive of scientific files** without risking the science: every
34
42
  output is verified value-for-value against its source before it replaces
35
- anything, and stored values round-trip bit-identically.
43
+ 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).
36
45
  - **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
37
46
  command.
38
47
 
@@ -40,12 +49,20 @@ Reach for ncarnate if you are trying to:
40
49
 
41
50
  Converting or recompressing a file changes *storage*, never *science data*:
42
51
 
43
- - Every variable's stored values are preserved **bit-identically** — packed
44
- integers stay packed; `scale_factor`/`add_offset`/`_FillValue` are carried
45
- across as declarations, never applied.
46
- - Every dimension (including unlimited-ness), attribute (including its type), and
47
- group survives. HDF-EOS2 `StructMetadata` is preserved verbatim; names netCDF
48
- cannot hold are sanitized with the original recorded in a companion attribute.
52
+ - Every variable's stored values are preserved **value-identically** — bit-for-bit
53
+ for integer and packed data; for floating-point data, distinct NaN
54
+ bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
55
+ `scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
56
+ never applied.
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.
49
66
  - Geolocation reconstruction is strictly **additive**: the original information
50
67
  always rides along, so the conversion never becomes the only copy of the
51
68
  truth. Swath coordinates are attached to variables whose first two axes are
@@ -86,11 +103,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
86
103
  building from sdist requires the system HDF4 library first (Debian/Ubuntu:
87
104
  `apt install libhdf4-dev`).
88
105
 
89
- **Windows via pip:** the netCDF/HDF5 *recompression* path works from PyPI wheels
90
- out of the box, but the HDF4/HDF-EOS2 *conversion* path does **not** — `pyhdf`'s
91
- Windows wheel ships no HDF4 runtime, so `import pyhdf` fails with a DLL-load
92
- error. Use the conda-forge install above for HDF4 on Windows (or **WSL** with the
93
- 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.
94
118
 
95
119
  ## Command line usage
96
120
 
@@ -165,6 +189,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
165
189
  exit code is non-zero **iff** a selected record failed — so a partial failure on
166
190
  a terabyte run surfaces loudly instead of silently mis-converting.
167
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
+
168
205
  ## Library usage
169
206
 
170
207
  ```python
@@ -194,7 +231,7 @@ The AMSR-E daily 12.5 km sea-ice granule this project grew up around:
194
231
  | netCDF4 recompression (`--complevel 9`) | 42.6 MB | 19.9 MB |
195
232
  | HDF-EOS2 → netCDF4 (+ reconstructed lat/lon) | 60.2 MB | 35.5 MB |
196
233
 
197
- Both outputs re-read bit-identically to their sources; the conversion
234
+ Both outputs re-read value-identically to their sources; the conversion
198
235
  additionally carries CF `polar_stereographic` grid mappings and coordinates for
199
236
  both hemispheric grids. The northern grid's reconstructed latitudes/longitudes
200
237
  agree with The HDF Group's independent conversion of the same granule to within
@@ -0,0 +1,81 @@
1
+ # Brand assets
2
+
3
+ The ncarnate mark: a phoenix rising from an orbited globe — legacy satellite and
4
+ geospatial data reincarnated as modern netCDF4. The Earth is drawn as a
5
+ latitude/longitude graticule (the gridded scientific data); an orbital band
6
+ sweeps around it (a nod to the satellite granules ncarnate reincarnates); and the
7
+ phoenix rises from it, an ember crest at its head — the spark of rebirth.
8
+
9
+ ## Files
10
+
11
+ | File | Use |
12
+ |---|---|
13
+ | `ncarnate.svg` | Primary mark — full colour on the rounded navy tile (app-icon / avatar). |
14
+ | `ncarnate-mono.svg` | Single-ink navy silhouette, transparent — one-colour contexts on light backgrounds. |
15
+ | `ncarnate-white.svg` | Single-ink off-white silhouette, transparent — for dark backgrounds. |
16
+ | `ncarnate-wordmark.svg` / `-white.svg` | The word alone (Sora SemiBold, outlined). |
17
+ | `ncarnate-lockup.png` / `-dark.png` | **Shipped lockup** — source mark tile (raster) + wordmark, horizontal. `-dark` recolours the wordmark off-white for dark backgrounds (the mark tile reads on both themes). |
18
+ | `ncarnate-mark.png` | The source mark tile alone, with transparent rounded corners (favicon / social / reuse). |
19
+ | `ncarnate-lockup.svg` / `-dark.svg` | Fully-vector lockup — **work in progress**, not yet shipped (see below). |
20
+
21
+ The project README header swaps `ncarnate-lockup.png` ⇄ `-dark.png` by
22
+ `prefers-color-scheme` via `<picture>`. The wordmark inside is glyph **outlines**
23
+ (no font needed to render); it is rasterized into the shipped PNG lockup.
24
+
25
+ ### Vector globe: deferred
26
+
27
+ The mark's thin globe **graticule** has not yet re-drawn cleanly as vectors — a
28
+ generated two-tilt globe (`compose_logo.py` + `globe.py`) matches at display scale
29
+ but its acceptance against the source is still open. Rather than block the release on
30
+ it, the **shipped lockup embeds the approved `ncarnate-source.png` mark as raster**
31
+ (`build_raster_lockup.py`), which is crisp at the README's display size. The vector
32
+ mark (`ncarnate.svg`, `ncarnate-lockup.svg`) remains in-repo as work in progress;
33
+ when it is accepted, swap the `<image>` in `build_raster_lockup.py` back to the
34
+ vector paths and the README layout is unchanged. See
35
+ `docs/plans/2026-07-11-logo-fidelity-regen.md`.
36
+
37
+ ## Palette
38
+
39
+ | | Hex | Role |
40
+ |---|---|---|
41
+ | Deep navy | `#152A47` | Field / primary ink |
42
+ | Warm off-white | `#F2EDE1` | Structure (phoenix + graticule) |
43
+ | Ember | `#E8843C` | Crest flame — the spark of rebirth |
44
+
45
+ Wordmark type: **Sora SemiBold** (weight 600), SIL Open Font License 1.1.
46
+
47
+ ## Regenerating
48
+
49
+ `ncarnate-source.png` is the approved raster — a flat, hard-edged emblem made by
50
+ an image model (the pictorial mark), used here as a **placement reference**: the
51
+ phoenix is traced from it, while the globe/orbit geometry (centre, radius, tilt)
52
+ is *measured* from it and then generated as vector primitives, not traced.
53
+
54
+ `compose_logo.py` does the hybrid composition (deps: `vtracer`, `pillow`, `scipy`,
55
+ `numpy`; `globe.py` is pure-stdlib): it splits the raster into field / phoenix /
56
+ ember, masks the globe + orbit out of the structure so only the organic phoenix is
57
+ traced, generates the graticule globe + orbit ring via `globe.py`, and layers them
58
+ (tile → globe → orbit-back → phoenix → orbit-front → ember). The globe geometry
59
+ (`CX, CY, R, TILT, MERIDIANS, PARALLELS`, orbit params) lives in constants at the
60
+ top of `compose_logo.py`; to re-fit it to a new raster, overlay `globe.py --out`
61
+ on the raster and nudge until the limb/graticule line up.
62
+
63
+ ```bash
64
+ # deps: vtracer, fonttools, pillow, scipy, numpy (+ cairosvg or @resvg/resvg-js to rasterise for the eyeball check)
65
+ python3 compose_logo.py ncarnate-source.png ncarnate.svg # mark + -mono + -white
66
+
67
+ # fetch Sora and instance to weight 600:
68
+ # curl -sL -o sora-var.ttf "https://raw.githubusercontent.com/google/fonts/main/ofl/sora/Sora%5Bwght%5D.ttf"
69
+ # python3 -c "from fontTools import ttLib; from fontTools.varLib.instancer import instantiateVariableFont as I; f=ttLib.TTFont('sora-var.ttf'); I(f,{'wght':600},inplace=True); f.save('sora-600.ttf')"
70
+ python3 build_lockup.py --text ncarnate --font sora-600.ttf --mark ncarnate.svg \
71
+ --field '#152A47' --structure '#F2EDE1' --out-prefix ncarnate # wordmark + light/dark VECTOR lockups (WIP)
72
+
73
+ # shipped lockup: raster source mark + wordmark, light/dark PNG (deps: fonttools, pillow, numpy, scipy, cairosvg)
74
+ python3 build_raster_lockup.py --font sora-600.ttf # ncarnate-lockup{,-dark}.png + ncarnate-mark.png
75
+ ```
76
+
77
+ Verify on both GitHub themes before committing — render each lockup on `#ffffff`
78
+ and `#0d1117` and confirm the wordmark is legible on each. The globe is generated,
79
+ so also zoom to a meridian×parallel crossing near the central meridian and confirm
80
+ uniform stroke width through the crossing, true concentric arcs, even meridian
81
+ spacing, and clean round caps.