gff3-validator 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Wright and David Molik (FAIR-bioHeaders contributors)
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.
@@ -0,0 +1,331 @@
1
+ Metadata-Version: 2.4
2
+ Name: gff3-validator
3
+ Version: 0.1.0
4
+ Summary: One-stop GFF3 validator: GFF3 1.26 syntax, structure and Sequence Ontology rules, plus optional FAIR-bioHeaders (FHGFF3) header checks
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: GFF3,validator,Sequence Ontology,genome annotation,FAIR
8
+ Author: Adam Wright
9
+ Author-email: adam.wright@oicr.on.ca
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Provides-Extra: headers
20
+ Requires-Dist: fair-bioheaders (>=0.4,<0.5) ; extra == "headers"
21
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
22
+ Project-URL: Bug Tracker, https://github.com/FAIR-bioHeaders/gff3-validator/issues
23
+ Project-URL: Documentation, https://github.com/FAIR-bioHeaders/gff3-validator/blob/main/docs/rules.md
24
+ Project-URL: Homepage, https://github.com/FAIR-bioHeaders/gff3-validator
25
+ Project-URL: Repository, https://github.com/FAIR-bioHeaders/gff3-validator
26
+ Project-URL: Specification, https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md
27
+ Description-Content-Type: text/markdown
28
+
29
+ # gff3-validator
30
+
31
+ A one-stop validator for [GFF3](https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md)
32
+ annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.
33
+
34
+ > **Status: early release (0.1.0).** The rule catalogue is a draft under
35
+ > review with the Sequence Ontology group
36
+ > ([FHR-Specification #62](https://github.com/FAIR-bioHeaders/FHR-Specification/issues/62)).
37
+ > The core syntax, attribute, directive and structure rules that the GFF3
38
+ > specification settles are implemented, and the optional biology rules run
39
+ > with `--genome`; Sequence Ontology and header rules are not yet. Install
40
+ > with `pip install gff3-validator`, or use it in the browser at
41
+ > https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and
42
+ > output formats may still change before 1.0.
43
+
44
+ ## Goal
45
+
46
+ One place, one name, three interfaces (web page, CLI, library) that tell you
47
+ whether a GFF3 file is valid against the GFF3 specification (version 1.26) and
48
+ the Sequence Ontology, and that explain every problem in plain language with
49
+ the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders
50
+ (FHGFF3) provenance header, the validator also checks it and its link to the
51
+ genome. See [spec 009](https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md)
52
+ (copied in [specs/001-gff3-validator](specs/001-gff3-validator/spec.md)).
53
+
54
+ Plain GFF3 without a header is a first-class input: core validation does not
55
+ depend on FAIR-bioHeaders.
56
+
57
+ ## Rule catalogue
58
+
59
+ Every check is a numbered rule in [rules/catalogue.yaml](rules/catalogue.yaml),
60
+ rendered as [docs/rules.md](docs/rules.md). Each rule has an id, level
61
+ (error, warning, info), layer (core, FHGFF3, biology), a reference into
62
+ GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO
63
+ review state. Open questions for SO are collected in
64
+ [docs/questions-for-SO.md](docs/questions-for-SO.md). A suggested fix is
65
+ guidance only; the validator never rewrites a file.
66
+
67
+ [docs/EVALUATION.md](docs/EVALUATION.md) reviews the 2024 FHGFF3 prototype, the
68
+ LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the
69
+ SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken
70
+ files.
71
+
72
+ ### Implemented rules
73
+
74
+ <!-- implemented-rules:start (generated by scripts/render_rules.py; do not edit) -->
75
+ 68 of 88 catalogue rules are implemented:
76
+
77
+ - `GFF-SYN-001` (error): First line is the ##gff-version 3 directive
78
+ - `GFF-SYN-002` (error): Only one ##gff-version directive
79
+ - `GFF-SYN-003` (error): Feature lines have nine tab-separated columns
80
+ - `GFF-SYN-004` (error): Undefined columns are ".", not empty
81
+ - `GFF-SYN-005` (error): No unescaped control characters
82
+ - `GFF-SYN-006` (warning): File is UTF-8
83
+ - `GFF-SYN-007` (info): Possible end-of-line comment
84
+ - `GFF-SYN-008` (error): Percent-encoding is well formed
85
+ - `GFF-SYN-009` (warning): Characters encoded that need not be
86
+ - `GFF-SYN-010` (error): seqid uses the allowed characters
87
+ - `GFF-SYN-012` (error): type is defined
88
+ - `GFF-SYN-013` (error): start and end are integers
89
+ - `GFF-SYN-014` (error): start is at least 1
90
+ - `GFF-SYN-015` (error): start is not greater than end
91
+ - `GFF-SYN-016` (error): score is a number or "."
92
+ - `GFF-SYN-017` (error): strand is +, -, . or ?
93
+ - `GFF-SYN-018` (error): phase is 0, 1, 2 or "."
94
+ - `GFF-SYN-019` (error): CDS features have a phase
95
+ - `GFF-ATT-001` (error): Attributes are tag=value pairs separated by semicolons
96
+ - `GFF-ATT-002` (error): Tags and values are not empty
97
+ - `GFF-ATT-003` (info): Empty attribute pair or trailing semicolon
98
+ - `GFF-ATT-004` (error): Each tag appears once per line
99
+ - `GFF-ATT-005` (error): Reserved characters are escaped in tags and values
100
+ - `GFF-ATT-006` (error): Single-valued reserved tags have one value
101
+ - `GFF-ATT-007` (warning): Unknown reserved (upper-case) tag
102
+ - `GFF-ATT-008` (warning): Tag differs from a reserved tag only by case
103
+ - `GFF-ATT-009` (info): Quoted attribute value
104
+ - `GFF-ATT-010` (error): Target has the form "target_id start end [strand]"
105
+ - `GFF-ATT-011` (error): Gap is a list of operations
106
+ - `GFF-ATT-012` (warning): Gap is given with Target
107
+ - `GFF-ATT-013` (warning): Gap lengths agree with the feature and Target lengths
108
+ - `GFF-ATT-014` (error): Dbxref and Ontology_term values are DBTAG:ID
109
+ - `GFF-ATT-016` (warning): Is_circular is "true"
110
+ - `GFF-DIR-001` (error): ##sequence-region has the form: seqid start end
111
+ - `GFF-DIR-002` (error): One ##sequence-region per seqid
112
+ - `GFF-DIR-003` (error): ### closes all forward references
113
+ - `GFF-DIR-004` (error): Only FASTA after ##FASTA
114
+ - `GFF-DIR-005` (warning): FASTA section starts with ##FASTA
115
+ - `GFF-DIR-006` (warning): FASTA records are well formed
116
+ - `GFF-DIR-007` (warning): ##species is an NCBI Taxonomy URL
117
+ - `GFF-DIR-008` (warning): ##genome-build has a source and a build name
118
+ - `GFF-DIR-009` (info): Ontology directives are recorded but not fetched
119
+ - `GFF-DIR-010` (info): Unknown directive
120
+ - `GFF-STR-001` (error): IDs are unique, except for the lines of one discontinuous feature
121
+ - `GFF-STR-002` (warning): Lines of one feature share seqid and strand
122
+ - `GFF-STR-003` (warning): Lines of one feature have the same Parent
123
+ - `GFF-STR-004` (error): Parent refers to an ID in the file
124
+ - `GFF-STR-005` (error): Derives_from refers to an ID in the file
125
+ - `GFF-STR-006` (error): No Parent cycles
126
+ - `GFF-STR-007` (warning): No Derives_from cycles
127
+ - `GFF-STR-008` (error): Features are within their ##sequence-region
128
+ - `GFF-STR-009` (info): seqid without ##sequence-region
129
+ - `GFF-STR-010` (warning): seqids appear in the ##FASTA section
130
+ - `GFF-STR-011` (error): Features fit within the embedded sequence
131
+ - `BIO-001` (error): seqid is in the genome
132
+ - `BIO-002` (error): Features fit within the genome sequence
133
+ - `BIO-003` (warning): ##sequence-region agrees with the genome
134
+ - `BIO-004` (warning): CDS phases are consistent
135
+ - `BIO-005` (warning): CDS segments lie within exons
136
+ - `BIO-006` (warning): CDS starts with a start codon
137
+ - `BIO-007` (warning): CDS ends with a stop codon
138
+ - `BIO-008` (warning): No internal stop codons
139
+ - `BIO-009` (info): CDS length is a multiple of three
140
+ - `BIO-010` (warning): Strand is consistent within a gene
141
+ - `BIO-011` (info): Biology checks skipped
142
+ - `HDR-001` (error): FHGFF3 header present when required
143
+ - `HDR-002` (info): FHGFF3 header present but not validated
144
+ - `HDR-003` (info): Header checks skipped (--no-header)
145
+ <!-- implemented-rules:end -->
146
+
147
+ ## Planned interfaces
148
+
149
+ - **CLI** `gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif]`, reading
150
+ plain, gzip/BGZF or stdin (`-`) input. Exit codes: 0 no errors, 1 errors
151
+ found, 2 usage error or unreadable input (the run is then incomplete).
152
+ - **Library** `gff3_validator.validate(path)` returning findings with rule id,
153
+ level, line, column, message and fix.
154
+ - **Web page** running the same engine in the browser through Pyodide, so
155
+ files never leave the computer (see [Use it in your browser](#use-it-in-your-browser)).
156
+ - **Reports** in text, JSON, HTML (one self-contained file) and SARIF 2.1.0
157
+ (for code-scanning annotations in CI).
158
+ - **Repository profiles** (NCBI, Ensembl, Alliance) layered on the core rules,
159
+ later and only with each repository's review.
160
+ - **Distribution** on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow
161
+ module and a container, after the first reviewed release.
162
+
163
+ Headers: a FAIR-bioHeaders header is validated when present; `--require-header`
164
+ makes it mandatory and `--no-header` skips header checks. Header checks need the
165
+ optional extra, `pip install "gff3-validator[headers]"` (FAIR-bioHeaders
166
+ toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the
167
+ skeleton only detects `#~` header lines and reports that header validation is
168
+ not implemented.
169
+
170
+ ## Current skeleton
171
+
172
+ ```bash
173
+ poetry install
174
+ poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
175
+ poetry run gff3-validate --format json annotation.gff3.gz
176
+ ```
177
+
178
+ The reader streams line by line and stops reading features at `##FASTA`;
179
+ FASTA records are read for their ids and lengths only, never kept. Per-line
180
+ rules keep no state. Findings for problems that can occur on most lines
181
+ (GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at
182
+ the first occurrence, with a count, so they cannot crowd errors out of the
183
+ `--max-findings` cap.
184
+
185
+ ### Memory
186
+
187
+ The structure rules (IDs, Parent and Derives_from references, cycles, bounds)
188
+ need state. Memory is O(number of distinct IDs) plus O(number of seqids):
189
+
190
+ - each ID gets an integer index and four packed array entries (first line,
191
+ seqid/type/strand combination, first Parent, Parent-list hash); features with
192
+ several Parents or with Derives_from keep the extra edges in sparse maps;
193
+ - features without an ID are not remembered after their line, except for a
194
+ reference to an ID not yet defined;
195
+ - forward references are kept per referenced ID (first line and count), not
196
+ per line, and are resolved when the ID appears or at the end of the file;
197
+ `###` only advances a counter, so a reference that crosses it is detected
198
+ when the ID is finally defined (GFF-DIR-003);
199
+ - per seqid: its first line, furthest start and end, and at most 100 lines
200
+ beyond its `##sequence-region` end (then a count);
201
+ - Parent and Derives_from cycles are found at the end of the file by an
202
+ iterative depth-first search (one byte of state per ID, no recursion).
203
+
204
+ On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five
205
+ exons and four CDS segments each, 2.1 million distinct IDs) a run takes about
206
+ 22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9
207
+ (roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and
208
+ reports the numbers:
209
+
210
+ ```bash
211
+ GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
212
+ # GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)
213
+ ```
214
+
215
+ ### Biology checks (`--genome`)
216
+
217
+ ```bash
218
+ gff3-validate --genome genome.fa.gz annotation.gff3
219
+ gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3
220
+ ```
221
+
222
+ The biology rules (BIO-001 to BIO-011) run only with `--genome`; without it
223
+ the report lists the biology layer as not checked. They compare seqids and
224
+ lengths with the genome, check CDS phases, CDS within exons, start, stop and
225
+ internal stop codons, coding length and strand consistency. Codon checks use
226
+ an NCBI translation table: 1 by default (the table is never inferred from the
227
+ organism; bacteria, archaea and plastids need `--translation-table 11`).
228
+ Partial CDS have no standard marker in GFF3, so the codon rules are warnings.
229
+ Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops
230
+ completed by polyadenylation) are exempt when the codon is covered by a
231
+ `recoded_codon` child of the CDS (or a subtype), following the SO/NCBI
232
+ discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy
233
+ `transl_except` attribute.
234
+
235
+ The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes;
236
+ not stdin). Leading `;` lines, such as a FAIR-bioHeaders `;~` header, are
237
+ skipped. The validator reads the file once to build an in-memory index like
238
+ samtools faidx (name, length, offset, bases and bytes per line; memory
239
+ O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks
240
+ with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first
241
+ decompressed to an anonymous temporary file in `TMPDIR`, which needs as much
242
+ free disk space as the uncompressed genome and is removed at the end. A genome
243
+ that cannot be indexed reliably (sequence before the first `>`, duplicate
244
+ names, lines of different lengths inside a record, non-sequence characters)
245
+ stops the run with exit code 2.
246
+
247
+ CDS segments (four integers per line), the exons of each transcript and the
248
+ recoded codons of each CDS are kept until the next `###` or the end of the
249
+ file, then judged and dropped. With `###` after each gene this is a few
250
+ genes' worth; without `###` it is about 40 bytes per CDS line and 20 bytes per
251
+ exon, plus about 300 bytes per transcript. On a synthetic file of 524,000
252
+ lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a
253
+ 231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13
254
+ (14 s and 104 MiB on 3.9); without `###` lines, 140 MiB. The opt-in
255
+ performance test includes this case
256
+ (`GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES` sets its size).
257
+
258
+ ## Use it in your browser
259
+
260
+ <https://fair-bioheaders.github.io/gff3-validator/> (once GitHub Pages is
261
+ enabled for this repository) runs the validator inside the page: the
262
+ `gff3-validator` wheel built from this repository, in Python compiled to
263
+ WebAssembly ([Pyodide](https://pyodide.org/)). Drop or pick a `.gff3` or
264
+ `.gff3.gz` file, optionally add a genome FASTA for the biology checks and a
265
+ translation table, choose how to treat a FAIR-bioHeaders header, and the page
266
+ shows the HTML report and offers it, the JSON and the SARIF report for
267
+ download. The reports are the ones `gff3-validate --format html|json|sarif`
268
+ writes for the same file and options; CI checks this on every fixture
269
+ (`web/test/parity.mjs`).
270
+
271
+ **Privacy.** Your files are read by the page and never uploaded. After the
272
+ page has loaded, the only requests are for the Python runtime and PyYAML from
273
+ one pinned Pyodide release on `cdn.jsdelivr.net` and for the validator wheel
274
+ from the page's own site. The page's Content-Security-Policy (a meta tag; the
275
+ validator runs in a Web Worker started from a `blob:` URL, which inherits it)
276
+ allows connections only to the site itself and to that Pyodide release, so the
277
+ page could not send a file elsewhere even by mistake. `pyodide.js` and
278
+ `pyodide-lock.json` are loaded with Subresource Integrity (sha384, in
279
+ [web/pyodide.json](web/pyodide.json)); the lock file pins the sha256 of
280
+ PyYAML, and the page checks the wheel against the sha256 in its
281
+ `manifest.json`. The rest of the Pyodide runtime (`pyodide.asm.mjs`, the
282
+ WebAssembly binary and the standard library) is loaded by `pyodide.js` from
283
+ the same pinned release without SRI, because Pyodide offers no way to pass
284
+ hashes for them.
285
+
286
+ **Size limits.** The annotation is read from disk in 1 MiB slices and is never
287
+ held in memory; memory grows with the number of feature IDs, as on the command
288
+ line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the
289
+ synthetic files of the performance test:
290
+
291
+ | Input | Time | WebAssembly memory |
292
+ |---|---|---|
293
+ | 1.1 million lines, 90 MB plain (1.05 million IDs) | 18 s | 155 MiB |
294
+ | 3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) | 56 s | 387 MiB |
295
+ | 524,000 lines (42 MB) with a 231 MiB genome, plain or gzip | 18 s | 90 to 108 MiB, plus the genome |
296
+
297
+ That is two to three times slower than the CLI. A full vertebrate annotation
298
+ (about 3 million lines) works. Memory grows by about 120 bytes per ID and
299
+ WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so
300
+ files several times larger than those measured here (not tested) need the
301
+ command line. The genome is copied into the page's memory (a gzip genome is also
302
+ decompressed there), so genomes of a few hundred MB are fine and multi-GB
303
+ genomes are not; use `gff3-validate --genome` for those.
304
+
305
+ Build and try the page locally:
306
+
307
+ ```bash
308
+ poetry run python scripts/build_web.py # builds the wheel and web/dist
309
+ python -m http.server -d web/dist 8000 # then open http://localhost:8000/
310
+ ```
311
+
312
+ `.github/workflows/pages.yml` builds `web/dist`, runs the parity check and
313
+ deploys to GitHub Pages on every push to `main` (and on demand). To bump
314
+ Pyodide, run `poetry run python scripts/build_web.py --pin VERSION` (it
315
+ downloads `pyodide.js` and `pyodide-lock.json` of that release, computes the
316
+ SRI hashes, checks that PyYAML is included and rewrites `web/pyodide.json`),
317
+ then `cd web/test && npm install --save-exact pyodide@VERSION`, rebuild and run
318
+ `node web/test/parity.mjs --python "poetry run python"`.
319
+
320
+ ## Development
321
+
322
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The rule catalogue is the source of
323
+ truth: edit `rules/catalogue.yaml`, then run `python scripts/render_rules.py` to
324
+ regenerate `docs/rules.md`, the packaged copy and the list above (CI checks for
325
+ drift with `--check`).
326
+
327
+ ## License
328
+
329
+ [MIT](LICENSE). Copyright (c) 2026 Adam Wright and David Molik
330
+ (FAIR-bioHeaders contributors).
331
+
@@ -0,0 +1,302 @@
1
+ # gff3-validator
2
+
3
+ A one-stop validator for [GFF3](https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md)
4
+ annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.
5
+
6
+ > **Status: early release (0.1.0).** The rule catalogue is a draft under
7
+ > review with the Sequence Ontology group
8
+ > ([FHR-Specification #62](https://github.com/FAIR-bioHeaders/FHR-Specification/issues/62)).
9
+ > The core syntax, attribute, directive and structure rules that the GFF3
10
+ > specification settles are implemented, and the optional biology rules run
11
+ > with `--genome`; Sequence Ontology and header rules are not yet. Install
12
+ > with `pip install gff3-validator`, or use it in the browser at
13
+ > https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and
14
+ > output formats may still change before 1.0.
15
+
16
+ ## Goal
17
+
18
+ One place, one name, three interfaces (web page, CLI, library) that tell you
19
+ whether a GFF3 file is valid against the GFF3 specification (version 1.26) and
20
+ the Sequence Ontology, and that explain every problem in plain language with
21
+ the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders
22
+ (FHGFF3) provenance header, the validator also checks it and its link to the
23
+ genome. See [spec 009](https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md)
24
+ (copied in [specs/001-gff3-validator](specs/001-gff3-validator/spec.md)).
25
+
26
+ Plain GFF3 without a header is a first-class input: core validation does not
27
+ depend on FAIR-bioHeaders.
28
+
29
+ ## Rule catalogue
30
+
31
+ Every check is a numbered rule in [rules/catalogue.yaml](rules/catalogue.yaml),
32
+ rendered as [docs/rules.md](docs/rules.md). Each rule has an id, level
33
+ (error, warning, info), layer (core, FHGFF3, biology), a reference into
34
+ GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO
35
+ review state. Open questions for SO are collected in
36
+ [docs/questions-for-SO.md](docs/questions-for-SO.md). A suggested fix is
37
+ guidance only; the validator never rewrites a file.
38
+
39
+ [docs/EVALUATION.md](docs/EVALUATION.md) reviews the 2024 FHGFF3 prototype, the
40
+ LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the
41
+ SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken
42
+ files.
43
+
44
+ ### Implemented rules
45
+
46
+ <!-- implemented-rules:start (generated by scripts/render_rules.py; do not edit) -->
47
+ 68 of 88 catalogue rules are implemented:
48
+
49
+ - `GFF-SYN-001` (error): First line is the ##gff-version 3 directive
50
+ - `GFF-SYN-002` (error): Only one ##gff-version directive
51
+ - `GFF-SYN-003` (error): Feature lines have nine tab-separated columns
52
+ - `GFF-SYN-004` (error): Undefined columns are ".", not empty
53
+ - `GFF-SYN-005` (error): No unescaped control characters
54
+ - `GFF-SYN-006` (warning): File is UTF-8
55
+ - `GFF-SYN-007` (info): Possible end-of-line comment
56
+ - `GFF-SYN-008` (error): Percent-encoding is well formed
57
+ - `GFF-SYN-009` (warning): Characters encoded that need not be
58
+ - `GFF-SYN-010` (error): seqid uses the allowed characters
59
+ - `GFF-SYN-012` (error): type is defined
60
+ - `GFF-SYN-013` (error): start and end are integers
61
+ - `GFF-SYN-014` (error): start is at least 1
62
+ - `GFF-SYN-015` (error): start is not greater than end
63
+ - `GFF-SYN-016` (error): score is a number or "."
64
+ - `GFF-SYN-017` (error): strand is +, -, . or ?
65
+ - `GFF-SYN-018` (error): phase is 0, 1, 2 or "."
66
+ - `GFF-SYN-019` (error): CDS features have a phase
67
+ - `GFF-ATT-001` (error): Attributes are tag=value pairs separated by semicolons
68
+ - `GFF-ATT-002` (error): Tags and values are not empty
69
+ - `GFF-ATT-003` (info): Empty attribute pair or trailing semicolon
70
+ - `GFF-ATT-004` (error): Each tag appears once per line
71
+ - `GFF-ATT-005` (error): Reserved characters are escaped in tags and values
72
+ - `GFF-ATT-006` (error): Single-valued reserved tags have one value
73
+ - `GFF-ATT-007` (warning): Unknown reserved (upper-case) tag
74
+ - `GFF-ATT-008` (warning): Tag differs from a reserved tag only by case
75
+ - `GFF-ATT-009` (info): Quoted attribute value
76
+ - `GFF-ATT-010` (error): Target has the form "target_id start end [strand]"
77
+ - `GFF-ATT-011` (error): Gap is a list of operations
78
+ - `GFF-ATT-012` (warning): Gap is given with Target
79
+ - `GFF-ATT-013` (warning): Gap lengths agree with the feature and Target lengths
80
+ - `GFF-ATT-014` (error): Dbxref and Ontology_term values are DBTAG:ID
81
+ - `GFF-ATT-016` (warning): Is_circular is "true"
82
+ - `GFF-DIR-001` (error): ##sequence-region has the form: seqid start end
83
+ - `GFF-DIR-002` (error): One ##sequence-region per seqid
84
+ - `GFF-DIR-003` (error): ### closes all forward references
85
+ - `GFF-DIR-004` (error): Only FASTA after ##FASTA
86
+ - `GFF-DIR-005` (warning): FASTA section starts with ##FASTA
87
+ - `GFF-DIR-006` (warning): FASTA records are well formed
88
+ - `GFF-DIR-007` (warning): ##species is an NCBI Taxonomy URL
89
+ - `GFF-DIR-008` (warning): ##genome-build has a source and a build name
90
+ - `GFF-DIR-009` (info): Ontology directives are recorded but not fetched
91
+ - `GFF-DIR-010` (info): Unknown directive
92
+ - `GFF-STR-001` (error): IDs are unique, except for the lines of one discontinuous feature
93
+ - `GFF-STR-002` (warning): Lines of one feature share seqid and strand
94
+ - `GFF-STR-003` (warning): Lines of one feature have the same Parent
95
+ - `GFF-STR-004` (error): Parent refers to an ID in the file
96
+ - `GFF-STR-005` (error): Derives_from refers to an ID in the file
97
+ - `GFF-STR-006` (error): No Parent cycles
98
+ - `GFF-STR-007` (warning): No Derives_from cycles
99
+ - `GFF-STR-008` (error): Features are within their ##sequence-region
100
+ - `GFF-STR-009` (info): seqid without ##sequence-region
101
+ - `GFF-STR-010` (warning): seqids appear in the ##FASTA section
102
+ - `GFF-STR-011` (error): Features fit within the embedded sequence
103
+ - `BIO-001` (error): seqid is in the genome
104
+ - `BIO-002` (error): Features fit within the genome sequence
105
+ - `BIO-003` (warning): ##sequence-region agrees with the genome
106
+ - `BIO-004` (warning): CDS phases are consistent
107
+ - `BIO-005` (warning): CDS segments lie within exons
108
+ - `BIO-006` (warning): CDS starts with a start codon
109
+ - `BIO-007` (warning): CDS ends with a stop codon
110
+ - `BIO-008` (warning): No internal stop codons
111
+ - `BIO-009` (info): CDS length is a multiple of three
112
+ - `BIO-010` (warning): Strand is consistent within a gene
113
+ - `BIO-011` (info): Biology checks skipped
114
+ - `HDR-001` (error): FHGFF3 header present when required
115
+ - `HDR-002` (info): FHGFF3 header present but not validated
116
+ - `HDR-003` (info): Header checks skipped (--no-header)
117
+ <!-- implemented-rules:end -->
118
+
119
+ ## Planned interfaces
120
+
121
+ - **CLI** `gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif]`, reading
122
+ plain, gzip/BGZF or stdin (`-`) input. Exit codes: 0 no errors, 1 errors
123
+ found, 2 usage error or unreadable input (the run is then incomplete).
124
+ - **Library** `gff3_validator.validate(path)` returning findings with rule id,
125
+ level, line, column, message and fix.
126
+ - **Web page** running the same engine in the browser through Pyodide, so
127
+ files never leave the computer (see [Use it in your browser](#use-it-in-your-browser)).
128
+ - **Reports** in text, JSON, HTML (one self-contained file) and SARIF 2.1.0
129
+ (for code-scanning annotations in CI).
130
+ - **Repository profiles** (NCBI, Ensembl, Alliance) layered on the core rules,
131
+ later and only with each repository's review.
132
+ - **Distribution** on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow
133
+ module and a container, after the first reviewed release.
134
+
135
+ Headers: a FAIR-bioHeaders header is validated when present; `--require-header`
136
+ makes it mandatory and `--no-header` skips header checks. Header checks need the
137
+ optional extra, `pip install "gff3-validator[headers]"` (FAIR-bioHeaders
138
+ toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the
139
+ skeleton only detects `#~` header lines and reports that header validation is
140
+ not implemented.
141
+
142
+ ## Current skeleton
143
+
144
+ ```bash
145
+ poetry install
146
+ poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
147
+ poetry run gff3-validate --format json annotation.gff3.gz
148
+ ```
149
+
150
+ The reader streams line by line and stops reading features at `##FASTA`;
151
+ FASTA records are read for their ids and lengths only, never kept. Per-line
152
+ rules keep no state. Findings for problems that can occur on most lines
153
+ (GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at
154
+ the first occurrence, with a count, so they cannot crowd errors out of the
155
+ `--max-findings` cap.
156
+
157
+ ### Memory
158
+
159
+ The structure rules (IDs, Parent and Derives_from references, cycles, bounds)
160
+ need state. Memory is O(number of distinct IDs) plus O(number of seqids):
161
+
162
+ - each ID gets an integer index and four packed array entries (first line,
163
+ seqid/type/strand combination, first Parent, Parent-list hash); features with
164
+ several Parents or with Derives_from keep the extra edges in sparse maps;
165
+ - features without an ID are not remembered after their line, except for a
166
+ reference to an ID not yet defined;
167
+ - forward references are kept per referenced ID (first line and count), not
168
+ per line, and are resolved when the ID appears or at the end of the file;
169
+ `###` only advances a counter, so a reference that crosses it is detected
170
+ when the ID is finally defined (GFF-DIR-003);
171
+ - per seqid: its first line, furthest start and end, and at most 100 lines
172
+ beyond its `##sequence-region` end (then a count);
173
+ - Parent and Derives_from cycles are found at the end of the file by an
174
+ iterative depth-first search (one byte of state per ID, no recursion).
175
+
176
+ On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five
177
+ exons and four CDS segments each, 2.1 million distinct IDs) a run takes about
178
+ 22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9
179
+ (roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and
180
+ reports the numbers:
181
+
182
+ ```bash
183
+ GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
184
+ # GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)
185
+ ```
186
+
187
+ ### Biology checks (`--genome`)
188
+
189
+ ```bash
190
+ gff3-validate --genome genome.fa.gz annotation.gff3
191
+ gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3
192
+ ```
193
+
194
+ The biology rules (BIO-001 to BIO-011) run only with `--genome`; without it
195
+ the report lists the biology layer as not checked. They compare seqids and
196
+ lengths with the genome, check CDS phases, CDS within exons, start, stop and
197
+ internal stop codons, coding length and strand consistency. Codon checks use
198
+ an NCBI translation table: 1 by default (the table is never inferred from the
199
+ organism; bacteria, archaea and plastids need `--translation-table 11`).
200
+ Partial CDS have no standard marker in GFF3, so the codon rules are warnings.
201
+ Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops
202
+ completed by polyadenylation) are exempt when the codon is covered by a
203
+ `recoded_codon` child of the CDS (or a subtype), following the SO/NCBI
204
+ discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy
205
+ `transl_except` attribute.
206
+
207
+ The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes;
208
+ not stdin). Leading `;` lines, such as a FAIR-bioHeaders `;~` header, are
209
+ skipped. The validator reads the file once to build an in-memory index like
210
+ samtools faidx (name, length, offset, bases and bytes per line; memory
211
+ O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks
212
+ with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first
213
+ decompressed to an anonymous temporary file in `TMPDIR`, which needs as much
214
+ free disk space as the uncompressed genome and is removed at the end. A genome
215
+ that cannot be indexed reliably (sequence before the first `>`, duplicate
216
+ names, lines of different lengths inside a record, non-sequence characters)
217
+ stops the run with exit code 2.
218
+
219
+ CDS segments (four integers per line), the exons of each transcript and the
220
+ recoded codons of each CDS are kept until the next `###` or the end of the
221
+ file, then judged and dropped. With `###` after each gene this is a few
222
+ genes' worth; without `###` it is about 40 bytes per CDS line and 20 bytes per
223
+ exon, plus about 300 bytes per transcript. On a synthetic file of 524,000
224
+ lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a
225
+ 231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13
226
+ (14 s and 104 MiB on 3.9); without `###` lines, 140 MiB. The opt-in
227
+ performance test includes this case
228
+ (`GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES` sets its size).
229
+
230
+ ## Use it in your browser
231
+
232
+ <https://fair-bioheaders.github.io/gff3-validator/> (once GitHub Pages is
233
+ enabled for this repository) runs the validator inside the page: the
234
+ `gff3-validator` wheel built from this repository, in Python compiled to
235
+ WebAssembly ([Pyodide](https://pyodide.org/)). Drop or pick a `.gff3` or
236
+ `.gff3.gz` file, optionally add a genome FASTA for the biology checks and a
237
+ translation table, choose how to treat a FAIR-bioHeaders header, and the page
238
+ shows the HTML report and offers it, the JSON and the SARIF report for
239
+ download. The reports are the ones `gff3-validate --format html|json|sarif`
240
+ writes for the same file and options; CI checks this on every fixture
241
+ (`web/test/parity.mjs`).
242
+
243
+ **Privacy.** Your files are read by the page and never uploaded. After the
244
+ page has loaded, the only requests are for the Python runtime and PyYAML from
245
+ one pinned Pyodide release on `cdn.jsdelivr.net` and for the validator wheel
246
+ from the page's own site. The page's Content-Security-Policy (a meta tag; the
247
+ validator runs in a Web Worker started from a `blob:` URL, which inherits it)
248
+ allows connections only to the site itself and to that Pyodide release, so the
249
+ page could not send a file elsewhere even by mistake. `pyodide.js` and
250
+ `pyodide-lock.json` are loaded with Subresource Integrity (sha384, in
251
+ [web/pyodide.json](web/pyodide.json)); the lock file pins the sha256 of
252
+ PyYAML, and the page checks the wheel against the sha256 in its
253
+ `manifest.json`. The rest of the Pyodide runtime (`pyodide.asm.mjs`, the
254
+ WebAssembly binary and the standard library) is loaded by `pyodide.js` from
255
+ the same pinned release without SRI, because Pyodide offers no way to pass
256
+ hashes for them.
257
+
258
+ **Size limits.** The annotation is read from disk in 1 MiB slices and is never
259
+ held in memory; memory grows with the number of feature IDs, as on the command
260
+ line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the
261
+ synthetic files of the performance test:
262
+
263
+ | Input | Time | WebAssembly memory |
264
+ |---|---|---|
265
+ | 1.1 million lines, 90 MB plain (1.05 million IDs) | 18 s | 155 MiB |
266
+ | 3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) | 56 s | 387 MiB |
267
+ | 524,000 lines (42 MB) with a 231 MiB genome, plain or gzip | 18 s | 90 to 108 MiB, plus the genome |
268
+
269
+ That is two to three times slower than the CLI. A full vertebrate annotation
270
+ (about 3 million lines) works. Memory grows by about 120 bytes per ID and
271
+ WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so
272
+ files several times larger than those measured here (not tested) need the
273
+ command line. The genome is copied into the page's memory (a gzip genome is also
274
+ decompressed there), so genomes of a few hundred MB are fine and multi-GB
275
+ genomes are not; use `gff3-validate --genome` for those.
276
+
277
+ Build and try the page locally:
278
+
279
+ ```bash
280
+ poetry run python scripts/build_web.py # builds the wheel and web/dist
281
+ python -m http.server -d web/dist 8000 # then open http://localhost:8000/
282
+ ```
283
+
284
+ `.github/workflows/pages.yml` builds `web/dist`, runs the parity check and
285
+ deploys to GitHub Pages on every push to `main` (and on demand). To bump
286
+ Pyodide, run `poetry run python scripts/build_web.py --pin VERSION` (it
287
+ downloads `pyodide.js` and `pyodide-lock.json` of that release, computes the
288
+ SRI hashes, checks that PyYAML is included and rewrites `web/pyodide.json`),
289
+ then `cd web/test && npm install --save-exact pyodide@VERSION`, rebuild and run
290
+ `node web/test/parity.mjs --python "poetry run python"`.
291
+
292
+ ## Development
293
+
294
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The rule catalogue is the source of
295
+ truth: edit `rules/catalogue.yaml`, then run `python scripts/render_rules.py` to
296
+ regenerate `docs/rules.md`, the packaged copy and the list above (CI checks for
297
+ drift with `--check`).
298
+
299
+ ## License
300
+
301
+ [MIT](LICENSE). Copyright (c) 2026 Adam Wright and David Molik
302
+ (FAIR-bioHeaders contributors).