gpconf 0.3.0__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 (90) hide show
  1. gpconf-0.3.0/LICENSE +30 -0
  2. gpconf-0.3.0/PKG-INFO +506 -0
  3. gpconf-0.3.0/README.md +488 -0
  4. gpconf-0.3.0/gpconf/__init__.py +6 -0
  5. gpconf-0.3.0/gpconf/__main__.py +203 -0
  6. gpconf-0.3.0/gpconf/adapters/__init__.py +2 -0
  7. gpconf-0.3.0/gpconf/adapters/naive.py +142 -0
  8. gpconf-0.3.0/gpconf/adapters/node_preflight.mjs +29 -0
  9. gpconf-0.3.0/gpconf/adapters/pyephem_adapter.py +55 -0
  10. gpconf-0.3.0/gpconf/adapters/reference.py +75 -0
  11. gpconf-0.3.0/gpconf/adapters/satellitejs.mjs +77 -0
  12. gpconf-0.3.0/gpconf/adapters/sgp4_adapter.py +78 -0
  13. gpconf-0.3.0/gpconf/adapters/tlejs.mjs +49 -0
  14. gpconf-0.3.0/gpconf/adapters/tlejs_vectors.mjs +34 -0
  15. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-A-100000-saramago-first.provenance.json +34 -0
  16. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-A-100000-saramago-first.tle +3 -0
  17. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-A-last-30-days-snapshot.provenance.json +289 -0
  18. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-A-last-30-days-snapshot.tle +768 -0
  19. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-T-270449-analyst-first.provenance.json +34 -0
  20. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-T-270449-analyst-first.tle +3 -0
  21. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-T-analyst-27xxxx-snapshot.provenance.json +379 -0
  22. gpconf-0.3.0/gpconf/corpus/derived/alpha5-tle/alpha5-T-analyst-27xxxx-snapshot.tle +1038 -0
  23. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v01-baseline-reserialised.kvn +27 -0
  24. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v01-baseline-reserialised.provenance.json +19 -0
  25. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v02-day-of-year-epoch-Z.kvn +27 -0
  26. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v02-day-of-year-epoch-Z.provenance.json +19 -0
  27. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v03-units-brackets-leading-zeros.kvn +27 -0
  28. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v03-units-brackets-leading-zeros.provenance.json +21 -0
  29. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v04-comments-blank-lines-whitespace-LF.kvn +32 -0
  30. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v04-comments-blank-lines-whitespace-LF.provenance.json +24 -0
  31. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v05-omm-3.0-header-optional-keywords-omitted.kvn +23 -0
  32. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v05-omm-3.0-header-optional-keywords-omitted.provenance.json +21 -0
  33. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v06-signed-integers-lowercase-exponent.kvn +27 -0
  34. gpconf-0.3.0/gpconf/corpus/derived/kvn-variants/v06-signed-integers-lowercase-exponent.provenance.json +20 -0
  35. gpconf-0.3.0/gpconf/corpus/fixtures/alpha5-encoding-vectors/case.md +56 -0
  36. gpconf-0.3.0/gpconf/corpus/fixtures/alpha5-encoding-vectors/expected.json +677 -0
  37. gpconf-0.3.0/gpconf/corpus/fixtures/alpha5-tle-derived/case.md +58 -0
  38. gpconf-0.3.0/gpconf/corpus/fixtures/alpha5-tle-derived/expected.json +41753 -0
  39. gpconf-0.3.0/gpconf/corpus/fixtures/analyst-objects/case.md +91 -0
  40. gpconf-0.3.0/gpconf/corpus/fixtures/analyst-objects/expected.json +42959 -0
  41. gpconf-0.3.0/gpconf/corpus/fixtures/baseline-iss-five-formats/case.md +67 -0
  42. gpconf-0.3.0/gpconf/corpus/fixtures/baseline-iss-five-formats/expected.json +618 -0
  43. gpconf-0.3.0/gpconf/corpus/fixtures/bstar-and-derivative-forms/case.md +65 -0
  44. gpconf-0.3.0/gpconf/corpus/fixtures/bstar-and-derivative-forms/expected.json +7225 -0
  45. gpconf-0.3.0/gpconf/corpus/fixtures/csv-json-omitted-mandatory-fields/case.md +67 -0
  46. gpconf-0.3.0/gpconf/corpus/fixtures/csv-json-omitted-mandatory-fields/expected.json +887 -0
  47. gpconf-0.3.0/gpconf/corpus/fixtures/epoch-year-19xx/case.md +77 -0
  48. gpconf-0.3.0/gpconf/corpus/fixtures/epoch-year-19xx/expected.json +643 -0
  49. gpconf-0.3.0/gpconf/corpus/fixtures/kvn-syntax-variants/case.md +52 -0
  50. gpconf-0.3.0/gpconf/corpus/fixtures/kvn-syntax-variants/expected.json +811 -0
  51. gpconf-0.3.0/gpconf/corpus/fixtures/mean-motion-derivative-convention/case.md +69 -0
  52. gpconf-0.3.0/gpconf/corpus/fixtures/mean-motion-derivative-convention/expected.json +3523 -0
  53. gpconf-0.3.0/gpconf/corpus/fixtures/nine-digit-supgp-launch-nominals/case.md +69 -0
  54. gpconf-0.3.0/gpconf/corpus/fixtures/nine-digit-supgp-launch-nominals/expected.json +402 -0
  55. gpconf-0.3.0/gpconf/corpus/fixtures/omm-xml-schema/case.md +59 -0
  56. gpconf-0.3.0/gpconf/corpus/fixtures/omm-xml-schema/expected.json +435 -0
  57. gpconf-0.3.0/gpconf/corpus/fixtures/satcat-70000-cutoff/case.md +57 -0
  58. gpconf-0.3.0/gpconf/corpus/fixtures/satcat-70000-cutoff/expected.json +327 -0
  59. gpconf-0.3.0/gpconf/corpus/fixtures/six-digit-omm-saramago/case.md +76 -0
  60. gpconf-0.3.0/gpconf/corpus/fixtures/six-digit-omm-saramago/expected.json +832 -0
  61. gpconf-0.3.0/gpconf/corpus/fixtures/supgp-celestrak-classification-c/case.md +67 -0
  62. gpconf-0.3.0/gpconf/corpus/fixtures/supgp-celestrak-classification-c/expected.json +347 -0
  63. gpconf-0.3.0/gpconf/corpus/fixtures/tle-omits-six-digit-objects/case.md +56 -0
  64. gpconf-0.3.0/gpconf/corpus/fixtures/tle-omits-six-digit-objects/expected.json +9105 -0
  65. gpconf-0.3.0/gpconf/corpus/fixtures/tle-vs-omm-precision-loss/case.md +85 -0
  66. gpconf-0.3.0/gpconf/corpus/fixtures/tle-vs-omm-precision-loss/expected.json +9581 -0
  67. gpconf-0.3.0/gpconf/corpus/fixtures/tle-writer-alpha5/case.md +86 -0
  68. gpconf-0.3.0/gpconf/corpus/fixtures/tle-writer-alpha5/expected.json +27062 -0
  69. gpconf-0.3.0/gpconf/corpus/manifest.json +2423 -0
  70. gpconf-0.3.0/gpconf/corpus/tools/fetchlist.json +478 -0
  71. gpconf-0.3.0/gpconf/corpus/vectors/alpha5.json +400 -0
  72. gpconf-0.3.0/gpconf/corpus/vectors/ccsds-epoch-strings.json +75 -0
  73. gpconf-0.3.0/gpconf/corpus/vectors/norad-cat-id-text.json +65 -0
  74. gpconf-0.3.0/gpconf/corpus/vectors/two-digit-epoch-year.json +54 -0
  75. gpconf-0.3.0/gpconf/fetch.py +443 -0
  76. gpconf-0.3.0/gpconf/gates.py +162 -0
  77. gpconf-0.3.0/gpconf/locate.py +111 -0
  78. gpconf-0.3.0/gpconf/presets.py +212 -0
  79. gpconf-0.3.0/gpconf/reference.py +380 -0
  80. gpconf-0.3.0/gpconf/runner.py +1304 -0
  81. gpconf-0.3.0/gpconf/tle.py +201 -0
  82. gpconf-0.3.0/gpconf/writer.py +363 -0
  83. gpconf-0.3.0/gpconf.egg-info/PKG-INFO +506 -0
  84. gpconf-0.3.0/gpconf.egg-info/SOURCES.txt +88 -0
  85. gpconf-0.3.0/gpconf.egg-info/dependency_links.txt +1 -0
  86. gpconf-0.3.0/gpconf.egg-info/entry_points.txt +2 -0
  87. gpconf-0.3.0/gpconf.egg-info/requires.txt +6 -0
  88. gpconf-0.3.0/gpconf.egg-info/top_level.txt +1 -0
  89. gpconf-0.3.0/pyproject.toml +38 -0
  90. gpconf-0.3.0/setup.cfg +4 -0
gpconf-0.3.0/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NEOGY LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Scope note. This licence covers the corpus's code, documentation, specification
26
+ vectors, derived files and expected values. It does not cover, and the
27
+ repository does not contain, raw data served by CelesTrak or any other provider;
28
+ tools/fetch.py retrieves such data to your machine under the provider's own
29
+ terms (https://celestrak.org/usage-policy.php). The XML schemas under schemas/
30
+ are CCSDS/SANA publications redistributed unmodified; see schemas/SOURCE.md.
gpconf-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,506 @@
1
+ Metadata-Version: 2.4
2
+ Name: gpconf
3
+ Version: 0.3.0
4
+ Summary: Runner for the GP/OMM conformance corpus (Alpha-5, 6- and 9-digit catalog numbers, OMM formats)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://gpconf.neogy.dev
7
+ Project-URL: Repository, https://github.com/hneogy/gp-omm-conformance
8
+ Project-URL: Issues, https://github.com/hneogy/gp-omm-conformance/issues
9
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.22867654
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Provides-Extra: sgp4
14
+ Requires-Dist: sgp4; extra == "sgp4"
15
+ Provides-Extra: pyephem
16
+ Requires-Dist: ephem; extra == "pyephem"
17
+ Dynamic: license-file
18
+
19
+ # gp-omm-conformance
20
+
21
+ A conformance corpus for orbital-data parsers crossing the five-digit catalog-number boundary:
22
+ Alpha-5 TLEs, six- and nine-digit `NORAD_CAT_ID`s, and the CCSDS Orbit Mean-Elements Message
23
+ (OMM) in CSV, JSON, XML and KVN, as actually served by CelesTrak.
24
+
25
+ It answers one question for a developer: **does my parser survive the migration?** You point it
26
+ at your parser; it tells you, case by case, what breaks and why, with every expected value
27
+ traceable to a provider response whose URL, retrieval time and SHA-256 are recorded. One case
28
+ (`tle-writer-alpha5`) asks the same of code that *writes* TLEs: Alpha-5 in the catalog field,
29
+ valid lines, and a refusal for the numbers the format cannot carry.
30
+
31
+ Status: version `0.3.0`, a minor release: the runner and the corpus install with pip as `gpconf`, with
32
+ presets that test a library without an adapter and a GitHub Action, and the adapter protocol gains a
33
+ refusal channel, an additive change; the seventeen cases stay, no frozen expected value changed (the
34
+ writer case gained one input), and the naive and python-sgp4 adapters still fail 14 and 7 of them. The
35
+ independent audit (`AUDIT.md`) covered v0.1.0; neither the writer-side case of v0.2.0, the fixes of
36
+ v0.2.1 nor the packaging and protocol changes of v0.3.0 have been separately audited. Maintainer:
37
+ Honorius Neogy (NEOGY LLC).
38
+
39
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22867654.svg)](https://doi.org/10.5281/zenodo.22867654) See `DECISIONS.md` for the full decision log and `MANIFEST.md` for every case,
40
+ source and known gap.
41
+
42
+ ## The migration in one page
43
+
44
+ **What changed.** On 2026-07-11 the US Space Force catalog assigned number 100000 (to the
45
+ Portuguese CubeSat SARAMAGO) after exhausting the five-digit range, which ends at 69999 (CelesTrak).
46
+ Of the 70000-99999 block, primary sources describe only 80000-89999, the analyst range (Space-Track);
47
+ what the rest of the block is used for is reported by secondary sources only. Every object catalogued
48
+ since has a six-digit number. The fixed-width
49
+ TLE format has room for five characters, so two coexisting answers exist:
50
+
51
+ - **Alpha-5** (US Space Force stopgap; Space-Track serves it, CelesTrak does not): the first digit is replaced
52
+ by a letter, A=10 ... Z=33 with I and O skipped, so 100000 becomes `A0000` and the ceiling is
53
+ 339999. Only TLE/3LE lines carry it.
54
+ - **OMM** (CCSDS 502.0-B-3, served by CelesTrak since May 2020 and by Space-Track's GP class): `NORAD_CAT_ID` is
55
+ an integer of up to nine digits; four-digit years; no fixed widths.
56
+
57
+ **What CelesTrak does.** It emits no Alpha-5 at all. A TLE/3LE/2LE request returns only the
58
+ objects below 100000; if none qualify (today, any group of recent launches) the response is
59
+ HTTP 404 with the body `No GP data found`. The OMM formats return everything. CelesTrak's
60
+ default format has been CSV since 2026-05-09.
61
+
62
+ **What breaks** (each item is a corpus case; see `docs/FAILURES.md` for the evidence):
63
+
64
+ 1. `int()` on TLE columns 3-7, or any five-character assumption about the catalog number.
65
+ 2. Treating an empty TLE file or a 404 as "no new data" and silently losing every object
66
+ launched after 2026-07-11.
67
+ 3. Requiring `OBJECT_ID`: analyst objects have an empty one in every format, and Python's
68
+ ElementTree returns `None` for the empty element (other XML libraries may do the same).
69
+ 4. Requiring the constant metadata keywords: CelesTrak CSV/JSON omit `CENTER_NAME`,
70
+ `REF_FRAME`, `TIME_SYSTEM`, `MEAN_ELEMENT_THEORY` and the whole header.
71
+ 5. One epoch format: CCSDS allows day-of-year, no fraction and a trailing `Z`.
72
+ 6. Assuming `CLASSIFICATION_TYPE` is `U`, that `ELEMENT_SET_NO` is never 0, or that a CSV has
73
+ no columns you do not know (`RMS`, `DATA_SOURCE` in supplemental data).
74
+ 7. Comparing a TLE to its OMM digit for digit: the TLE truncates eccentricity to 7 digits and
75
+ rounds the BSTAR and second-derivative mantissa to 5, so OMM to TLE to OMM does not round trip.
76
+ 8. Reading `MEAN_MOTION_DOT` as the true derivative: the OMM carries the TLE field as printed
77
+ (the ndot/2 convention).
78
+ 9. Storing the catalog number as an Alpha-5 string internally: python-sgp4 cannot load any
79
+ nine-digit record, and such records are live (launch nominals of the 18th Space Defense
80
+ Squadron, 18 SDS, in CelesTrak SupGP).
81
+ 10. Writing a TLE with the catalog number through an integer format (`%05d`, `f"{n:05d}"`):
82
+ above 99999 that yields six digits, a 70-character line and a checksum over shifted columns.
83
+ The field must be Alpha-5 from 100000, and a number above 339999 cannot be written as a TLE
84
+ at all: the correct output is a refusal, and the record belongs in an OMM format.
85
+
86
+ **What to change.** Parse the catalog number as an integer from any source; decode Alpha-5
87
+ strictly (reject I, O, lowercase) if you read Space-Track TLEs; treat empty `OBJECT_ID` and
88
+ `OBJECT_NAME` as legitimate; default the constant metadata; accept all CCSDS epoch forms;
89
+ tolerate unknown keys and non-`U` classifications; compare TLE and OMM under the precision
90
+ rules; treat a TLE 404 as "unrepresentable in this format", not as an outage; and prefer the
91
+ OMM formats, which are what both providers say the future is. When writing TLEs, encode the
92
+ catalog number as Alpha-5 above 99999 and refuse numbers above 339999 (or below 0).
93
+
94
+ ## What is in the corpus, and what is not
95
+
96
+ | in the repository | not in the repository |
97
+ |---|---|
98
+ | `fixtures/<case>/expected.json`: frozen expected values and structural checks for 17 cases, all numbers as decimal strings | raw CelesTrak responses (`fixtures/<case>/raw/`): rebuilt on your machine by `tools/fetch.py` or `gpconf fetch` under CelesTrak's own usage policy |
99
+ | `fixtures/<case>/case.md`: what the case tests, how to read a failure, coverage gaps stated per case | Space-Track data, in any form (a `.gitignore` guard refuses such paths) |
100
+ | `derived/`: Alpha-5 TLE lines rendered from real CelesTrak OMM records (letters A and T only), six CCSDS-legal KVN variants, each with a provenance sidecar | invented element sets: none, anywhere |
101
+ | `vectors/`: specification vectors (Alpha-5 table, catalog-id text forms, two-digit-year pivot, CCSDS epoch strings) | |
102
+ | `manifest.json` / `MANIFEST.md`: every source with URL, retrieval time, SHA-256, tier; every check and ambiguity by id | SupGP-derived snapshot values: the element values of CelesTrak supplemental records are withheld from the public release; those cases keep their structural checks and run against your own fetch |
103
+ | `schemas/`: SANA NDM/XML schema sets 2.0.0 and 4.0.0, unmodified | |
104
+ | `gpconf/`: the runner (standard library, Python 3.9+) and the adapters and harnesses behind its presets | |
105
+ | `harnesses/`: recipes for the five hand-run libraries that cannot be presets (libsgp4, Gpredict, SatDump, astroz, gods-eye-view): each harness, its pinned commit and build commands; best-effort, not in the pip package, and tied to the projects' internals (D-155) | SatDump's link stand-ins: the recipe lists the symbols to define instead (D-152) |
106
+ | `docs/`: the adapter guide (`docs/ADAPTERS.md`), research notes with verbatim sources, cross-check, breakage catalogue, upstream bug-report drafts; `AUDIT.md`: the independent audit and its resolutions | |
107
+
108
+ The pip package, `gpconf`, carries the runner and the corpus's own files: every case's `expected.json` and
109
+ `case.md`, `derived/`, `vectors/`, `manifest.json` and the fetch list. It carries no provider data; the schemas,
110
+ the other documents, the build tools, the test suite and the recipes stay in the repository.
111
+
112
+ Why no raw files: CelesTrak's site states no redistribution terms for its data
113
+ (`docs/RESEARCH.md` §11), and silence is not permission. Rebuilding locally means the
114
+ provider's terms apply to you directly, fixtures cannot rot unnoticed, and SHA-256 hashes tell
115
+ you whether you are looking at the exact bytes we tested.
116
+
117
+ ## Quick start
118
+
119
+ From v0.3.0 the runner installs from PyPI with the corpus's own files; the provider data is fetched once,
120
+ into a per-user cache:
121
+
122
+ ```bash
123
+ pip install gpconf
124
+ gpconf run --preset reference # before any fetch: the 4 cases whose files ship in the package run; 13 say they need provider data
125
+ gpconf fetch # once: ~60 requests, ~12.6 MB, 2 s apart, cached, never repeated
126
+ gpconf run --preset reference # then all seventeen, as in a clone
127
+ ```
128
+
129
+ Or from a clone, as before:
130
+
131
+ ```bash
132
+ git clone https://github.com/hneogy/gp-omm-conformance.git && cd gp-omm-conformance
133
+ python3 tools/fetch.py # once: ~60 requests, ~12.6 MB, 2 s apart, cached, never repeated
134
+ python3 -m gpconf run --preset reference # the control: 16 cases pass; the SATCAT case is a data check reported not-exercised for every parser, and the nine-digit case says not-exercised outside a launch window
135
+ python3 -m gpconf run --preset naive # the parser most projects have: a demonstration of failure, not a parser to use
136
+ ```
137
+
138
+ Installed, the command is `gpconf`; in a clone it is `python3 -m gpconf`; the two take the same arguments.
139
+ A preset runs a shipped adapter with nothing written. `python3 -m gpconf presets` lists them:
140
+ `reference` and `naive` (standard library only), `sgp4` (needs python-sgp4: `pip install sgp4`, or
141
+ `pip install "gpconf[sgp4]"`), `pyephem` (needs PyEphem: `pip install ephem`, or
142
+ `pip install "gpconf[pyephem]"`), and three that need Node.js and the library
143
+ installed where you run the command: `satellite.js`, `tle.js`, which reads the epoch from the raw
144
+ year and day fields, and `tle.js-api`, which reads it through `getEpochTimestamp()`. A library
145
+ preset records the version it was tested against and the report prints the version it found beside
146
+ it, since a count is a result against one version. A Node preset's harness runs from the working
147
+ directory, so the library resolves as it would for a script in your project; `--module PATH` names
148
+ the library's entry file instead, for a checkout that cannot import itself by name. A preset whose
149
+ library is missing is refused before any case runs, with exit status 2 and a message saying that
150
+ nothing ran and that this says nothing about the library (D-153, D-154). The adapters are in
151
+ `gpconf/adapters/`; the older names `tests.adapters.reference:Parser` and the like still work (D-151).
152
+
153
+ In a GitHub Actions job, from v0.3.0, three lines run a preset against your library:
154
+
155
+ ```yaml
156
+ - uses: hneogy/gp-omm-conformance@v0.3.0
157
+ with:
158
+ preset: sgp4
159
+ ```
160
+
161
+ Install your library first, in the job's Python (the `python` input names the interpreter) or, for a Node
162
+ preset, in the job's working folder. The Action runs the corpus from this repository at the tag you name,
163
+ so a corpus release changes nothing in your CI until you move the tag. It runs offline and fetches nothing:
164
+ only the cases that ship with the corpus run, and the rest are reported as needing provider data. It is
165
+ report-only: failed cases do not fail the job, and the job summary carries the case table, the gate line and
166
+ the reminder that a count is a result against that version on that date, not a verdict on the project. A
167
+ preset whose library will not import fails the job as a setup error, exit status 2, saying that nothing ran.
168
+ There is no badge (D-153, D-158).
169
+
170
+ Then write an adapter for your own parser: a Python class the runner imports (`--adapter module:Class`), or, for a
171
+ parser in any other language, a command that reads a file on standard input and prints JSON (`--cmd`, with
172
+ `--vectors-cmd` and `--write-cmd` for the hooks). [`docs/ADAPTERS.md`](https://github.com/hneogy/gp-omm-conformance/blob/v0.3.0/docs/ADAPTERS.md) is the guide.
173
+
174
+ If the writer cannot be wrapped at all (an interactive program such as strf's `rffit`, which
175
+ satno2tle drives), check the file it wrote instead. The format checks need nothing else; the
176
+ round trip needs the source records the lines were written from:
177
+
178
+ ```bash
179
+ python3 -m gpconf check-tle output.tle # lines, checksums, catalog field, column layout
180
+ python3 -m gpconf check-tle output.tle --against records.csv # plus the round trip, per record
181
+ ```
182
+
183
+ Name lines, `#` comment lines (rffit's trailer) and LF or CRLF endings are accepted; a six-digit
184
+ number in the catalog columns is reported with the Alpha-5 form it should have had, and a number the
185
+ format cannot carry with the statement that a refusal was the correct output. Exit code 1 on any
186
+ failure or when no record is found.
187
+
188
+ `python3 -m gpconf list` prints the cases and their tags; `--case ID` and `--tag TAG` select.
189
+
190
+ ### Fetching responsibly
191
+
192
+ `tools/fetch.py` follows CelesTrak's published usage policy
193
+ (https://celestrak.org/usage-policy.php): each URL once, files cached with metadata, no
194
+ redirects followed, no retries, 2 s between requests, and it stops on the first unexpected
195
+ response. The largest file is the 9.4 MB legacy SATCAT; leave it out with
196
+ `--skip-case satcat-70000-cutoff` if you do not need the 70000-cutoff case. Do not run the fetch
197
+ more than once per two hours; the tool never re-fetches a file it already has (with or without its
198
+ metadata) unless you pass `--force` and the recorded `retrieved_at` is at least two hours old, and it
199
+ never requests a URL twice in one run (a re-capture entry is requested only when its original is on
200
+ disk and two hours old). After each run it compares every file with the SHA-256 recorded in
201
+ `manifest.json`, prints a `DRIFT` line for any stable-tier source whose bytes differ and exits 2
202
+ (`--check-drift` does the comparison without fetching). If you receive an HTTP 403 read the
203
+ body: CelesTrak explains why.
204
+
205
+ The same fetch runs as `python3 -m gpconf fetch`, with the same options. In a clone it writes the
206
+ provider files under `fixtures/<case>/raw/` as always. `--data DIR`, or the `GPCONF_DATA` environment
207
+ variable, puts them in another folder, and `gpconf run` then reads them from the same place. A copy of
208
+ the runner outside a clone reads its corpus from the package and keeps the provider files in a per-user
209
+ cache folder, one per corpus version: `~/.cache/gpconf/<version>` (or under `$XDG_CACHE_HOME`) on
210
+ Linux, `~/Library/Caches/gpconf/<version>` on macOS, `%LOCALAPPDATA%\gpconf\Cache\<version>` on
211
+ Windows. The run prints that folder above the case table whenever it is not the corpus's own (D-150).
212
+ When a new corpus version's folder is filled, a stable-tier file whose bytes hash to the new version's
213
+ recorded SHA-256 is copied from an earlier version's folder, with its metadata, instead of requested
214
+ again; the fetch prints `reused from <version>` for it, its metadata records `reused_from`, and the run
215
+ report says how many files were reused. Live files are always requested (D-157).
216
+
217
+ ### Two tiers: snapshot and live
218
+
219
+ Each source in the manifest is `stable` or `live`.
220
+
221
+ - **stable** (CelesTrak `gp-first.php` first-ever records, derived files, vectors): if your
222
+ fetched bytes hash to the recorded SHA-256, the frozen, human-verified expected values are
223
+ applied exactly.
224
+ - **live** (rolling groups, current objects, supplemental data, SATCAT records): CelesTrak
225
+ updates every two hours, so your bytes will differ from the tested snapshot. The runner then
226
+ applies the case's structural checks (cross-format agreement, count relations, presence rules,
227
+ 404 semantics) and compares values against the corpus's own reference reader applied to your
228
+ bytes, and it says so in the report. The snapshot values remain in `expected.json` as a dated
229
+ reference.
230
+ - **mixed** (the pairs, facts and XML-schema cases, which list files fetched for other cases): the
231
+ case uses the file structurally and applies no frozen values of its own; whether the file may
232
+ drift is judged by the tier the fetching case records (D-114). Vector and derived files ship with
233
+ the repository and are never fetched; their source rows say so instead of an HTTP status.
234
+
235
+ ## Writing an adapter
236
+
237
+ An adapter hands the corpus's files to your parser and returns what it read, in the corpus's field names: a Python
238
+ class with a `parse(raw, fmt)` method, or a command that reads a file on standard input and prints a JSON array.
239
+ Optional hooks answer the vector case and the writer case, and a refusal channel reports a record the library
240
+ rejected, with the library's reason. [`docs/ADAPTERS.md`](https://github.com/hneogy/gp-omm-conformance/blob/v0.3.0/docs/ADAPTERS.md) is the guide: both protocols, the refusal
241
+ channel, how returned records are counted, the hooks, running and reading a report, and what an adapter must not do,
242
+ with worked examples copied from the shipped adapters. The sections below describe how any parser's results are
243
+ judged and reported.
244
+
245
+ ### Tolerances, and how they are reported
246
+
247
+ The oracle values are exact decimal strings. For the parser under test the runner allows:
248
+
249
+ | comparison | tolerance | why |
250
+ |---|---|---|
251
+ | a value you return as a binary `float` | 1e-12 relative: abs(got − want) / max(abs(got), abs(want)); an exact zero on either side must be matched by an exact zero | most decimal element values have no exact double; 1e-12 is far below any physical meaning and above the noise of unit conversions (observed 1e-16 in `docs/CROSSCHECK.md`) |
252
+ | an epoch you return | 2 microseconds | python-sgp4's Julian-date epoch differed from the exact value by up to 1 microsecond in the cross-check (`docs/CROSSCHECK.md`) |
253
+ | TLE epoch against the OMM epoch of the same record | 432 microseconds | half of the TLE's 1e-8 day resolution; the provider itself rounds here |
254
+
255
+ Everything else is exact. A check that passed only because of a tolerance is reported as
256
+ `pass-tolerance`, separately from `pass` (exact), with the per-field count, mean signed
257
+ difference and maximum, so a systematic sub-tolerance bias is visible instead of hidden. The
258
+ reference adapter must be exact; the test suite enforces that.
259
+
260
+ A provider file that is not on disk is reported as `not-fetched`, never as `skip`: the corpus does not
261
+ ship provider data (see "Fetching responsibly"), and `skip` is kept for a parser that has no reader for a
262
+ format or no hook for a check. A case with none of its provider files reports `not-fetched`; the count line
263
+ says how many cases need fetched data, the `n/f` column counts the missing files per case, and the runner
264
+ prints the command that fetches them. A case that ran on some of its files keeps the result of those
265
+ files and is named below the count line. The three re-capture files are an example: each is the same
266
+ endpoint requested a second time when the corpus was built, and the fetch requests it only with
267
+ `--include-recaptures`, so a normal fetch leaves those three cases without it (D-150 corrects D-148's
268
+ wording here, which said a later run would bring it). A file that ships with the
269
+ corpus (under `derived/` or `vectors/`) and is missing stops the run with exit status 2, since the copy
270
+ is incomplete and no status would be true of the parser (D-148).
271
+
272
+ A source file the corpus's own reader reads zero records from, where the manifest records some, fails
273
+ the case with a `source-readable` item that says what the file looked like (size, first bytes, and a guess
274
+ such as an HTML error page, an empty file, a BOM prefix or a TLE cut off after line 1), and the file's other
275
+ checks are not run; a 16-byte CelesTrak 404 body is a legitimate zero-record file and stays `empty-404`.
276
+ A stable-tier source whose bytes no longer hash to the recorded SHA-256 is reported, not silently treated
277
+ as live: the case gets a failing `stable-source-drift` item naming the recorded and actual hashes, the JSON
278
+ report gets a `drift` field, and the file's values item says the frozen expected values were not applied
279
+ and that the parser was compared against the reference reader instead.
280
+
281
+ ### The provider's empty answer
282
+
283
+ Four cases hold a recorded answer with no data in it: CelesTrak's HTTP 404 body, `No GP data found` or `No SupGP data found`, for a group with nothing in that format. The runner hands that body to the parser under test (check `empty-answer-yields-no-records`): zero records and no error is the pass, an exception or an invented record is a failure, and a parser without the format skips. The point is the distinction: an empty but valid answer is one of three outcomes a parser must keep apart from "this file is unreadable" and "this value cannot be represented", and an error here collapses the first into the second.
284
+
285
+ ### The gate: this month's launches
286
+
287
+ Below the case table the runner prints one gate, a headline read across existing cases rather than a new case: given the objects launched in the last 30 days, as a provider serves them, does the parser return them with the right identity? Its inputs are the CelesTrak CSV capture of the last-30-days group (256 objects, every one above 99999, captured 2026-09-21) and the corpus's Alpha-5 rendering of the same records, the form Space-Track's TLE output carries. Per format the parser reads, records are counted as loaded (returned with the expected integer id), misidentified (returned as 0, NaN, a string or a wrong number) or dropped (no record came back). The wording names the behaviour and never grades the project: "reads this month's launches", "only via CSV", "not in any format it reads", and, for a parser that reads TLE only, "nothing to load from this feed", because CelesTrak's TLE output omits these objects. A format that was never tried, because its file was not fetched or its case was not selected, limits the claim: the headline then says "every format measured here" or "not in any format measured here", names the format left out before the numbers, and never says "nothing to load from this feed", which would claim the parser reads TLE only. A freshly fetched CSV holds different records from the frozen capture; it is judged against its own record count and labelled as a live capture (D-148). Every headline carries the snapshot's date, count and id range, and the letters its Alpha-5 fields begin with: a decoder that is wrong from J upward passes a snapshot whose ids all begin with A. The JSON report carries the same under `gates`, with the counts, and each values item now carries its record counts under `counts`.
288
+
289
+ ## The seventeen cases
290
+
291
+ | case | what it covers |
292
+ |---|---|
293
+ | `epoch-year-19xx` | first ISS record (1998) in seven renderings; two-digit year 98; negative first derivative; non-zero second derivative |
294
+ | `baseline-iss-five-formats` | a current object in every rendering (live) |
295
+ | `six-digit-omm-saramago` | catalog number 100000, stable and live; TLE request 404 |
296
+ | `tle-omits-six-digit-objects` | a group that is entirely six-digit: 256 OMM records, TLE request 404 |
297
+ | `analyst-objects` | empty `OBJECT_ID`, `UNKNOWN` names, 8xxxx and 27xxxx ids, TLE keeps only the five-digit ones |
298
+ | `nine-digit-supgp-launch-nominals` | nine-digit ids (7995016xx) in CelesTrak supplemental data; perishable |
299
+ | `supgp-celestrak-classification-c` | classification `C`, element set 0, extra columns, 72000-series ids |
300
+ | `bstar-and-derivative-forms` | negative BSTAR and first derivative, non-zero second derivative, implied-decimal fields |
301
+ | `satcat-70000-cutoff` | legacy SATCAT stops at 69999; CSV/JSON SATCAT continues |
302
+ | `csv-json-omitted-mandatory-fields` | CelesTrak CSV/JSON omit the constant CCSDS-mandatory keywords |
303
+ | `mean-motion-derivative-convention` | OMM `MEAN_MOTION_DOT` equals the TLE field as printed (304/304) |
304
+ | `tle-vs-omm-precision-loss` | eccentricity truncated, mantissa rounded half up, SupGP epochs quantised; TLE→OMM→TLE round trip tested (the 24-character name limit is a format rule, not observed in provider data) |
305
+ | `omm-xml-schema` | valid NDM/XML 2.0, invalid against 3.0 on the version attribute alone |
306
+ | `alpha5-encoding-vectors` | the Space-Track table, official examples, boundaries, invalid inputs |
307
+ | `alpha5-tle-derived` | 604 derived Alpha-5 lines (letters A and T) from real CelesTrak records |
308
+ | `kvn-syntax-variants` | six CCSDS-legal KVN renderings CelesTrak never emits |
309
+ | `tle-writer-alpha5` | writer side: 606 frozen records (603 Alpha-5 ids, three five-digit) written through `write_tle`, plus three numbers the TLE field cannot carry, for which a refusal is the correct output |
310
+
311
+ Full detail, sources and per-case gaps: `MANIFEST.md`; per case: `fixtures/<case>/case.md`.
312
+
313
+ ## Known gaps (summary; each is also stated in the case that has it)
314
+
315
+ - Real catalog numbers exist only for Alpha-5 letters **A** (100000-100789) and **T**
316
+ (270000-270449, Space Fence analyst objects). Other letters are covered by vectors only.
317
+ - The renderer behind the derived Alpha-5 lines was validated (304 of 304 lines byte for byte)
318
+ only against sub-100000 CelesTrak output, because CelesTrak emits no Alpha-5. The encoding
319
+ step is corroborated by python-sgp4's independent implementation, by the official vectors, and
320
+ by the maintainer's run of `tools/verify_against_spacetrack.py` against their own Space-Track
321
+ account (44 records, zero defects, letters A and T; D-070). The derived lines remain
322
+ CelesTrak-style renderings: Space-Track's own TLE lines for the same records differ in the sign
323
+ written for a zero second derivative and in the last eccentricity digits, so they are not
324
+ byte-identical to Space-Track output. The rendering rule the corpus applies (eccentricity
325
+ truncated, BSTAR and second-derivative mantissa rounded half up) is CelesTrak's, derived
326
+ empirically from those 304 records and documented in no specification we located.
327
+ - No positive-exponent BSTAR (>= 1.0) exists in the fetched data. One synthetic-derived writer input (id 99999,
328
+ BSTAR 1.2345, TLE field ` 12345+1`, rendered by the corpus, D-125) covers the field's form on the writer side only.
329
+ - Nine-digit ids exist only in supplemental launch nominals for roughly a week after a launch;
330
+ outside that window the case reports `not-exercised`. What happens to a nominal's identity after
331
+ that week is an open question the corpus states and does not answer: it checks that the nine-digit
332
+ and six-digit forms both parse, holds no object in both, and has no ground truth for how a tracker
333
+ should correlate a nominal with the catalogued object that follows it; the shared launch
334
+ designator is not one-to-one. The nine-digit case's gaps say why that is out of scope rather than
335
+ missing.
336
+ - Stability of CelesTrak's `gp-first.php` over time is assumed, not yet observed; `tools/fetch.py`
337
+ compares every fetched file with the manifest after each run and reports `DRIFT` for stable
338
+ sources whose bytes changed.
339
+ - The writer case's inputs are the corpus's own frozen records; no output of an external tool is
340
+ shipped. strf's `rffit` (the writer behind satno2tle) was exercised at function level only, in a
341
+ scratch build outside the repository, and has no adapter because it is an interactive X11 program
342
+ (`DECISIONS.md` D-097).
343
+
344
+ ## Library behaviour found while building the corpus
345
+
346
+ Documented in `docs/CROSSCHECK.md`, with draft upstream reports in `docs/upstream/`:
347
+
348
+ - python-sgp4 2.27 and Skyfield 1.55 raise `ValueError` for every nine-digit `NORAD_CAT_ID`
349
+ loaded from OMM data.
350
+ - python-sgp4's `sgp4.omm.parse_xml` turns an empty `<OBJECT_ID/>` into `None` and
351
+ `initialize()` raises `TypeError` on 564 of the 566 analyst XML records in the case (563 of the 565
352
+ group records, plus the single 270449 first record).
353
+ - python-sgp4's `export_tle` writes a zero second derivative as ` 00000-0`, which matches
354
+ Space-Track's rendering; CelesTrak writes ` 00000+0`, so byte-exact round trips of CelesTrak
355
+ lines fail while values agree. A provider divergence, not a library defect; documented in
356
+ `docs/upstream/`, not filed (D-072).
357
+ - python-sgp4's `omm.initialize` sets the classification and then `sgp4init` resets it to `U`,
358
+ so CelesTrak supplemental records (`C`) come back as `U`; and its Alpha-5 decoder accepts
359
+ `I`, `O`, lowercase and four-character input that Space-Track's definition excludes.
360
+ - As a writer, python-sgp4's `export_tle` encodes Alpha-5 correctly and `omm.initialize` refuses
361
+ 340000 and nine-digit numbers, the correct output for a TLE writer; `to_alpha5(-1)` still yields
362
+ `-0001`, so a negative number is written rather than refused.
363
+
364
+ ## Writer-side tools
365
+
366
+ The writer case (`tle-writer-alpha5`) and `check-tle` exist because every newly catalogued object
367
+ needs an Alpha-5 field and amateur tools generate TLEs for new launches. Three writers were run
368
+ (`docs/FAILURES.md`): the corpus's own renderer passes every check; a writer that formats the catalog
369
+ number as an integer fails every Alpha-5 input; python-sgp4 2.27's `export_tle` passes every real
370
+ record and refuses 340000 and nine-digit numbers, failing only the synthetic -1 vector.
371
+
372
+ strf's `rffit`, the writer behind satno2tle, could not be built here and is interactive, so it has no
373
+ adapter; its two formatting functions were exercised unmodified in an isolated build outside the
374
+ repository. Tested at function level: correct Alpha-5 across the representable range and valid lines
375
+ for real records; above 339999 no range check, so the lines carry a blank catalog field, on a route only
376
+ the interactive Satellite ID entry takes and that no real catalog number reaches today. Labels, details
377
+ and the reproduction recipe are in `docs/WRITERS.md`. No bug is claimed for the binary; a short
378
+ hardening suggestion, `docs/upstream/strf-number-to-alpha5-range-check.md`, was filed as
379
+ [cbassa/strf#88](https://github.com/cbassa/strf/issues/88) on 2026-09-23. Users of rffit or satno2tle can check the file it wrote with `python3 -m gpconf check-tle`.
380
+
381
+ ## Upstream
382
+
383
+ Status of the findings above with python-sgp4, as of 2026-09-24:
384
+
385
+ - **Empty `<OBJECT_ID/>` import failure**: filed by the maintainer of this corpus as
386
+ [brandon-rhodes/python-sgp4#171](https://github.com/brandon-rhodes/python-sgp4/issues/171)
387
+ (draft and prepared patch in `docs/upstream/`); the patch,
388
+ [PR #172](https://github.com/brandon-rhodes/python-sgp4/pull/172), opened 2026-09-21 and amended
389
+ 2026-09-23 after the maintainer's review, was merged by the maintainer on 2026-09-24 as commit
390
+ `8126f77`, and #171 is closed. No release carries the fix yet (the latest is 2.27 of 2026-07-03), so
391
+ the python-sgp4 results in this README and in `docs/FAILURES.md`, 7 of 17 cases, are against 2.27
392
+ and stand until one does.
393
+ - **Nine-digit `NORAD_CAT_ID` rejected**: independently reported before this corpus existed as
394
+ [#169](https://github.com/brandon-rhodes/python-sgp4/issues/169), with
395
+ [PR #170](https://github.com/brandon-rhodes/python-sgp4/pull/170) open. Not filed again. PR #170
396
+ at head `5e4f308` was tested locally against the sixteen cases the corpus had at the time, on both the accelerated and the
397
+ pure-Python build: it adds two tests and no library code change, the nine-digit reproducer still
398
+ raises, and the runner results are identical to the 2.27 baseline (0 fixed, 0 regressions). The
399
+ test report is in `docs/upstream/pr170-test-comment.md` (DECISIONS D-088).
400
+ - **`export_tle` zero second derivative** (` 00000-0`): not a library defect, not filed (D-072).
401
+ - **`omm.initialize` classification reset to `U`**: draft note only, not filed.
402
+
403
+ For strf, `docs/upstream/strf-number-to-alpha5-range-check.md` holds a hardening suggestion, not a
404
+ bug report, drafted 2026-09-22 (D-101) and filed as [cbassa/strf#88](https://github.com/cbassa/strf/issues/88)
405
+ on 2026-09-23 (D-108).
406
+
407
+ ## Verifying the derived Alpha-5 lines against Space-Track yourself
408
+
409
+ CelesTrak emits no Alpha-5 TLEs, so the corpus's Alpha-5 lines are rendered from CelesTrak OMM
410
+ records (`derived/alpha5-tle/`). Space-Track does emit Alpha-5 for catalog numbers 100000–339999.
411
+ The corpus redistributes no Space-Track data, and its Space-Track name guard keeps any such data
412
+ out of the repository; but anyone with their own Space-Track account can check the encoding
413
+ against provider output:
414
+
415
+ ```bash
416
+ python3 tools/verify_against_spacetrack.py
417
+ ```
418
+
419
+ The tool asks for your Space-Track username and password interactively (never as arguments, never
420
+ logged or stored), logs in, runs exactly four queries at least three seconds apart (the current
421
+ records for 100000–100020 and 270000–270020, and the first historical record of 100000 and of
422
+ 270449), logs out, saves the raw responses under `~/spacetrack-verify/` (it refuses to run if that
423
+ directory would be inside the repository), and compares each returned TLE with the derived lines. It
424
+ prints only catalog fields, verdicts, differing column numbers and summary counts, never element
425
+ values or lines. In the maintainer's run (2026-09-21, D-070) no line was byte-identical and none
426
+ was expected to be: Space-Track writes a zero second derivative as `00000-0` (column 51, and the
427
+ checksum at 69) where CelesTrak writes `00000+0`, and its eccentricity field differs in the last
428
+ one or two digits (columns 32–33) from CelesTrak's truncated rendering on exactly the 8 of 21
429
+ same-epoch records where rounding and truncation disagree (D-071); what the run verified is
430
+ that every Alpha-5 field decoded to the queried id, every line-2 field matched line 1, and
431
+ designators and inclinations agreed. Use of Space-Track is governed by its user agreement; keep the
432
+ saved responses to yourself.
433
+
434
+ ## Reproducing or refreshing the corpus
435
+
436
+ The build is a pipeline of small scripts, all in `tools/`, all offline except `fetch.py`:
437
+ `fetch.py` → `inventory.py` → `validate_render.py` → `derive_alpha5.py`,
438
+ `derive_kvn_variants.py` → `make_expected.py` → `crosscheck.py` (needs python-sgp4, Skyfield and
439
+ xmlschema, pinned in `tools/crosscheck-requirements.lock.txt`: `pip install -r` it into the environment you build in)
440
+ → `validate_xml.py` → `make_manifest.py`
441
+ → `make_failures.py`. Refreshing live sources produces new snapshot values and bumps the
442
+ corpus minor version; frozen values of a released version are never rewritten.
443
+
444
+ ## Versioning
445
+
446
+ `corpus_version` in `manifest.json`. Patch: documentation and tooling only. Minor: refreshed
447
+ live snapshots, added cases, or an additive protocol change. Major: changed `expected.json` schema or
448
+ check semantics. Expected values published under a version are frozen; corrections arrive as new versions
449
+ with a `DECISIONS.md` entry. The `gpconf` package carries the version of the corpus it ships, and the runner
450
+ prints both on its first line.
451
+
452
+ ## How this corpus was built
453
+
454
+ The work ran in phases, each ending in a review by the project owner before the next began,
455
+ under a written decision policy (`CLAUDE.md`) that required every non-trivial choice, reversal
456
+ and correction to be recorded.
457
+
458
+ - **Audit trail:** `DECISIONS.md` is the complete, append-only log of decisions, including the
459
+ ones that were reversed. Read it first if you want to know why something is the way it is.
460
+ - **Sources:** `docs/RESEARCH.md` records every document consulted, with verbatim quotations,
461
+ URLs and retrieval dates.
462
+ - **Data provenance:** no element set was invented. Every value traces to a CelesTrak response
463
+ whose URL, retrieval time and SHA-256 are recorded in the manifest, to a published standard
464
+ cited by clause, or to a committed, labelled transformation of such a record (`derived/`,
465
+ each with a `.provenance.json`).
466
+ - **Verification:** the expected values were produced by reference readers written from the
467
+ format documents and cross-checked, record by record, against python-sgp4 and Skyfield
468
+ (`docs/CROSSCHECK.md`); the renderer used for derived TLE lines was validated by reproducing
469
+ 304 of 304 CelesTrak TLE lines byte for byte. Where a specification and the provider's
470
+ practice disagree, both are recorded (`manifest.json` → `ambiguities`) and neither is silently
471
+ chosen.
472
+ - **Independent audit:** completed on 2026-09-21, before publication; the report is
473
+ [`AUDIT.md`](https://github.com/hneogy/gp-omm-conformance/blob/v0.3.0/AUDIT.md). It was performed by a separate AI session that had no access to the
474
+ building session's context, was instructed to trust no document in the repository and to recompute
475
+ values independently; it was not a human review. It found no wrong expected value and a number of
476
+ documentation and tooling defects; every finding's resolution is recorded at the end of `AUDIT.md`
477
+ and in `DECISIONS.md` (D-054 onward). The public copy of `AUDIT.md` withholds one row of its
478
+ appendix table (a sample entry from the SupGP case, per D-033/D-049) and says so in a notice; the
479
+ auditor's text is otherwise unchanged and the private original is intact. It covered v0.1.0; neither
480
+ the writer-side case of v0.2.0, the fixes of v0.2.1 nor the packaging and protocol changes of v0.3.0 have
481
+ been separately audited.
482
+
483
+ If you find an error, the most useful report names the case id, the source file's SHA-256 and
484
+ the field, so that the discrepancy can be traced to a specific fetched byte sequence.
485
+
486
+ ## Licence and attribution
487
+
488
+ MIT, copyright NEOGY LLC (see `LICENSE`), for the corpus's code, documentation, vectors, derived
489
+ files and expected values. Provider data is not included; `tools/fetch.py` retrieves it under CelesTrak's own
490
+ terms. The schemas under `schemas/` are CCSDS/SANA publications redistributed unmodified.
491
+
492
+ Data source: CelesTrak (Dr. T.S. Kelso), https://celestrak.org, a 501(c)(3) non-profit that
493
+ makes this data freely available; please respect its usage policy. Standards: CCSDS 502.0-B-3
494
+ (Orbit Data Messages) and CCSDS 505.0-B-3 (XML Specification for Navigation Data Messages).
495
+ Alpha-5 definition: Space-Track, https://www.space-track.org/documentation.
496
+
497
+ To cite, use `CITATION.cff` (GitHub's "Cite this repository" reads it): *Neogy, H. (NEOGY LLC).
498
+ gp-omm-conformance, version 0.3.0, 2026-09-27, https://github.com/hneogy/gp-omm-conformance.*
499
+ Two Zenodo DOIs exist: the **concept DOI** [10.5281/zenodo.22867654](https://doi.org/10.5281/zenodo.22867654) refers to the
500
+ corpus as a whole and always resolves to the latest release; use it when you mean the corpus in
501
+ general. The **version DOI** for this release, v0.3.0, is added here and to `CITATION.cff` after Zenodo
502
+ mints it at the release; v0.2.1 keeps its own, [10.5281/zenodo.22926017](https://doi.org/10.5281/zenodo.22926017),
503
+ v0.2.0 its own, [10.5281/zenodo.22906966](https://doi.org/10.5281/zenodo.22906966), and v0.1.0, the release the
504
+ independent audit covered, keeps [10.5281/zenodo.22867655](https://doi.org/10.5281/zenodo.22867655). Use a version DOI
505
+ when your results depend on a specific set of expected values; each release gets its own under the same
506
+ concept DOI.