diff --git a/.gitignore b/.gitignore index f6650df..7fed34e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,18 @@ __pycache__/ /.nextflow.log* /.snakemake/ /work/ + +# LaTeX build artifacts (keep paper/*.tex, *.bib, Makefile, README) +paper/*.aux +paper/*.bbl +paper/*.blg +paper/*.fdb_latexmk +paper/*.fls +paper/*.log +paper/*.out +paper/*.synctex.gz +paper/*.toc +paper/*.lof +paper/*.lot +paper/*.pdf +paper/build/ diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..48d476e --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,36 @@ +{ + "title": "FastaGuard: FASTA preflight QC for modern bioinformatics pipelines", + "description": "FastaGuard is a streaming-first Rust CLI for FASTA preflight quality control in assembly pipelines. It validates structure, flags FASTA-level red flags, and emits a stable HTML/JSON/TSV/MultiQC report contract with explicit pipeline gates before expensive downstream QC (QUAST, BUSCO, BlobToolKit, CheckM, annotation, or official validators). This Zenodo deposit archives a citable software release accompanying the Bioinformatics Application Note.", + "upload_type": "software", + "access_right": "open", + "license": "MIT", + "creators": [ + { + "name": "Estaji, Ehsan", + "orcid": "0009-0005-0838-228X", + "affiliation": "Umeå Plant Science Centre, Department of Plant Physiology, Umeå University" + }, + { + "name": "Mao, Jian-Feng", + "orcid": "0000-0001-9735-8516", + "affiliation": "Umeå Plant Science Centre, Department of Plant Physiology, Umeå University" + } + ], + "keywords": [ + "FASTA", + "quality control", + "genome assembly", + "preflight", + "bioinformatics", + "Rust", + "pipelines" + ], + "related_identifiers": [ + { + "identifier": "https://github.com/ehsanestaji/FastaGuard/releases/tag/v1.0.0", + "relation": "isSupplementTo", + "resource_type": "software" + } + ], + "notes": "Deposit the existing GitHub release tag v1.0.0 (Reference Contract Gate / Bioconda-published line). Do not invent DOIs. After Zenodo mints a version DOI, paste it into paper/submission.yml and the Availability sentence in paper/main.tex, then rebuild the PDF." +} diff --git a/docs/evidence/results/v1.0.0-rerun/evidence_summary.json b/docs/evidence/results/v1.0.0-rerun/evidence_summary.json new file mode 100644 index 0000000..d89f7da --- /dev/null +++ b/docs/evidence/results/v1.0.0-rerun/evidence_summary.json @@ -0,0 +1,153 @@ +{ + "binary_sha256": "8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f", + "binary_to_source_reproducibility_attested": false, + "cases": [ + { + "accession": null, + "category": "synthetic", + "downstream_route": "If this fails, fix the FastaGuard run before comparing with QUAST, BUSCO, or other downstream tools.", + "elapsed_seconds": 0.0256, + "evidence_role": "offline valid FASTA smoke case", + "exit_code": 0, + "expected_scale": "tiny synthetic fixture", + "finding_count": 0, + "finding_ids": [], + "gate_blocking_findings": "", + "gate_mode": "pipeline", + "gate_status": "PASS", + "id": "synthetic_valid", + "input_bytes": 676, + "input_sha256": "71152ba646d1f2d077e0a3a185524c2138a0657ab62f5f0694709c15420c3e5d", + "label": "Deterministic synthetic FASTA", + "n50": 160, + "n90": 140, + "sequence_count": 4, + "source": "local", + "source_url": null, + "total_length": 604, + "verdict": "PASS" + }, + { + "accession": null, + "category": "fixture", + "downstream_route": "Fix FASTA-level blockers before QUAST, BUSCO, BlobToolKit, CheckM, annotation, or submission validators.", + "elapsed_seconds": 0.0304, + "evidence_role": "local blocker case for duplicate IDs, invalid characters, high-N records, and tiny contigs", + "exit_code": 0, + "expected_scale": "tiny intentionally problematic fixture", + "finding_count": 8, + "finding_ids": [ + "duplicate_ids", + "duplicate_first_token_ids", + "invalid_chars", + "high_n_rate", + "gap_runs", + "terminal_ns", + "length_outliers", + "composite_anomalies" + ], + "gate_blocking_findings": "duplicate_ids,duplicate_first_token_ids,invalid_chars,high_n_rate", + "gate_mode": "pipeline", + "gate_status": "FAIL", + "id": "problem_fixture", + "input_bytes": 187, + "input_sha256": "4b8551daeda739b62c8e7aaa1ebf300e4118167ca582c51eeccfa1549c96f0a5", + "label": "Problem assembly fixture", + "n50": 110, + "n90": 8, + "sequence_count": 5, + "source": "local", + "source_url": null, + "total_length": 145, + "verdict": "FAIL" + }, + { + "accession": null, + "category": "fixture", + "downstream_route": "If this passes, the pipeline can accept compressed FASTA before heavier QC tools.", + "elapsed_seconds": 0.025, + "evidence_role": "offline gzipped FASTA input smoke case", + "exit_code": 0, + "expected_scale": "tiny gzipped fixture", + "finding_count": 1, + "finding_ids": [ + "terminal_ns" + ], + "gate_blocking_findings": "", + "gate_mode": "pipeline", + "gate_status": "WARN", + "id": "gzipped_valid", + "input_bytes": 81, + "input_sha256": "544240d141ede11647a5cbdce60b23793dde1cc0559f2e918ecc80e9cc3fb48e", + "label": "Gzipped valid assembly fixture", + "n50": 16, + "n90": 15, + "sequence_count": 3, + "source": "local", + "source_url": null, + "total_length": 47, + "verdict": "WARN" + }, + { + "accession": "GCF_000005845.2", + "category": "bacterial", + "downstream_route": "If FastaGuard passes, route to QUAST for assembly metrics and BUSCO when biological completeness is in scope.", + "elapsed_seconds": 0.0885, + "evidence_role": "small bacterial reference assembly", + "exit_code": 0, + "expected_scale": "about 4.6 Mbp", + "finding_count": 0, + "finding_ids": [], + "gate_blocking_findings": "", + "gate_mode": "pipeline", + "gate_status": "PASS", + "id": "ecoli_k12_mg1655", + "input_bytes": 4699745, + "input_sha256": "53bb6a51b6e92139ced1e38f74b7938781027c52200922ff03718c2237d23bb4", + "label": "Escherichia coli K-12 MG1655", + "n50": 4641652, + "n90": 4641652, + "sequence_count": 1, + "source": "public_ncbi", + "source_url": "https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_000005845.2/", + "total_length": 4641652, + "verdict": "PASS" + }, + { + "accession": "GCF_000182925.2", + "category": "fungal", + "downstream_route": "If FastaGuard passes, route to QUAST, BUSCO, and BlobToolKit-style follow-up when assembly quality or composition signals need deeper review.", + "elapsed_seconds": 0.8113, + "evidence_role": "small eukaryotic fungal reference assembly", + "exit_code": 0, + "expected_scale": "tens of Mbp", + "finding_count": 2, + "finding_ids": [ + "gap_runs", + "gap_pattern_warnings" + ], + "gate_blocking_findings": "", + "gate_mode": "pipeline", + "gate_status": "WARN", + "id": "neurospora_crassa_or74a", + "input_bytes": 41618285, + "input_sha256": "8176a4ac98fe05dffa4fa92df030ce0f15af23fbc382c8d8176ffedd3012d847", + "label": "Neurospora crassa OR74A", + "n50": 6000761, + "n90": 4218384, + "sequence_count": 21, + "source": "public_ncbi", + "source_url": "https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_000182925.2/", + "total_length": 41102378, + "verdict": "WARN" + } + ], + "fastaguard_version": "1.0.0", + "generated_at": "2026-10-24T18:11:39+00:00", + "platform": "macOS-26.7.1-arm64-arm-64bit-Mach-O", + "provenance_scope": "source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested.", + "python": "3.14.5", + "runtime_context": "Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees.", + "schema_version": 1, + "source_commit": "103ee343737e7f4d5719a6f8bda7c05c4676e9a9" +} diff --git a/docs/evidence/results/v1.0.0-rerun/evidence_summary.tsv b/docs/evidence/results/v1.0.0-rerun/evidence_summary.tsv new file mode 100644 index 0000000..ca830ad --- /dev/null +++ b/docs/evidence/results/v1.0.0-rerun/evidence_summary.tsv @@ -0,0 +1,6 @@ +schema_version generated_at fastaguard_version source_commit binary_sha256 provenance_scope binary_to_source_reproducibility_attested platform python runtime_context id label category source accession source_url evidence_role expected_scale downstream_route input_bytes input_sha256 elapsed_seconds exit_code verdict gate_mode gate_status gate_blocking_findings sequence_count total_length n50 n90 finding_ids finding_count +1 2026-10-24T18:11:39+00:00 1.0.0 103ee343737e7f4d5719a6f8bda7c05c4676e9a9 8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested. false macOS-26.7.1-arm64-arm-64bit-Mach-O 3.14.5 Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees. synthetic_valid Deterministic synthetic FASTA synthetic local offline valid FASTA smoke case tiny synthetic fixture If this fails, fix the FastaGuard run before comparing with QUAST, BUSCO, or other downstream tools. 676 71152ba646d1f2d077e0a3a185524c2138a0657ab62f5f0694709c15420c3e5d 0.0256 0 PASS pipeline PASS 4 604 160 140 0 +1 2026-10-24T18:11:39+00:00 1.0.0 103ee343737e7f4d5719a6f8bda7c05c4676e9a9 8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested. false macOS-26.7.1-arm64-arm-64bit-Mach-O 3.14.5 Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees. problem_fixture Problem assembly fixture fixture local local blocker case for duplicate IDs, invalid characters, high-N records, and tiny contigs tiny intentionally problematic fixture Fix FASTA-level blockers before QUAST, BUSCO, BlobToolKit, CheckM, annotation, or submission validators. 187 4b8551daeda739b62c8e7aaa1ebf300e4118167ca582c51eeccfa1549c96f0a5 0.0304 0 FAIL pipeline FAIL duplicate_ids,duplicate_first_token_ids,invalid_chars,high_n_rate 5 145 110 8 duplicate_ids,duplicate_first_token_ids,invalid_chars,high_n_rate,gap_runs,terminal_ns,length_outliers,composite_anomalies 8 +1 2026-10-24T18:11:39+00:00 1.0.0 103ee343737e7f4d5719a6f8bda7c05c4676e9a9 8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested. false macOS-26.7.1-arm64-arm-64bit-Mach-O 3.14.5 Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees. gzipped_valid Gzipped valid assembly fixture fixture local offline gzipped FASTA input smoke case tiny gzipped fixture If this passes, the pipeline can accept compressed FASTA before heavier QC tools. 81 544240d141ede11647a5cbdce60b23793dde1cc0559f2e918ecc80e9cc3fb48e 0.025 0 WARN pipeline WARN 3 47 16 15 terminal_ns 1 +1 2026-10-24T18:11:39+00:00 1.0.0 103ee343737e7f4d5719a6f8bda7c05c4676e9a9 8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested. false macOS-26.7.1-arm64-arm-64bit-Mach-O 3.14.5 Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees. ecoli_k12_mg1655 Escherichia coli K-12 MG1655 bacterial public_ncbi GCF_000005845.2 https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_000005845.2/ small bacterial reference assembly about 4.6 Mbp If FastaGuard passes, route to QUAST for assembly metrics and BUSCO when biological completeness is in scope. 4699745 53bb6a51b6e92139ced1e38f74b7938781027c52200922ff03718c2237d23bb4 0.0885 0 PASS pipeline PASS 1 4641652 4641652 4641652 0 +1 2026-10-24T18:11:39+00:00 1.0.0 103ee343737e7f4d5719a6f8bda7c05c4676e9a9 8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f source_commit records verified release-tag and source-tree equality. fastaguard_version and binary_sha256 record the observed executable. Binary-to-source reproducibility was not independently attested. false macOS-26.7.1-arm64-arm-64bit-Mach-O 3.14.5 Elapsed seconds are contextual measurements from one local run, not cross-platform performance guarantees. neurospora_crassa_or74a Neurospora crassa OR74A fungal public_ncbi GCF_000182925.2 https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_000182925.2/ small eukaryotic fungal reference assembly tens of Mbp If FastaGuard passes, route to QUAST, BUSCO, and BlobToolKit-style follow-up when assembly quality or composition signals need deeper review. 41618285 8176a4ac98fe05dffa4fa92df030ce0f15af23fbc382c8d8176ffedd3012d847 0.8113 0 WARN pipeline WARN 21 41102378 6000761 4218384 gap_runs,gap_pattern_warnings 2 diff --git a/docs/paper-readiness.md b/docs/paper-readiness.md new file mode 100644 index 0000000..7f2a590 --- /dev/null +++ b/docs/paper-readiness.md @@ -0,0 +1,64 @@ +# FastaGuard paper readiness (*Bioinformatics* Application Note) + +Venue: **Bioinformatics (OUP) — Application Note** (user-confirmed). + +Branch/PR: `cursor/paper-prep-venues-198e` / [#21](https://github.com/ehsanestaji/FastaGuard/pull/21). + +Control manifest: [`paper/submission.yml`](../paper/submission.yml). + +Machine check: `cd paper && make check`. + +## DONE (machine / repo complete) + +| Item | Status | +| --- | --- | +| Full Application Note draft (`paper/main.tex`) | DONE | +| Structured abstract (Summary / Availability / Contact / SI) | DONE | +| Authors, ORCID, Umeå affiliation, emails (from VariantFlow front-matter) | DONE | +| Funding (WIFORCE/KAW), COI, AI disclosure, CRediT, acknowledgements | DONE | +| Evidence freeze: Table 1 numbers match `docs/evidence/results/v0.6/evidence_summary.tsv` | DONE | +| Cited software release for Availability: GitHub **`v1.0.0`** (Bioconda-published) | DONE | +| Forbidden phrase guard (“FastQC for FASTA”) in `check_submission.py` | DONE | +| `.zenodo.json` metadata prepared (no fake DOI) | DONE | +| Zenodo DOI minted from `v1.0.0` (`10.5281/zenodo.23622311`) and cited in Availability + manifest | DONE | +| Cover letter draft (`paper/cover-letter.md`) incl. preprint + AI disclosure + archive DOI (OUP policy) | DONE | +| Upstream claims verified (Bioconda, BioContainers, nf-core module, Snakemake wrapper) | DONE | +| Pre-submission review: claims re-tested against a v1.0.0 build; related work + references audited; fixes applied (see `paper/HANDOFF.md`) | DONE | +| Zenodo record creators corrected (Estaji + Mao, ORCIDs, affiliation) | DONE | +| Validator comparison vs py_fasta_validator, fa-lint, table2asn, SeqKit (Table 2, `paper/comparison/`) | DONE | +| NCBI rows re-run with a v1.0.0 build (`docs/evidence/results/v1.0.0-rerun/`) | DONE | +| AI-disclosure wording, funding line, SNIC acknowledgement settled with the author | DONE | +| bioRxiv build target + checklist | DONE | +| PDF builds (`make`) | DONE | +| Word count ~2.0–2.6k band | DONE (~2.0k+) | + +## USER-ACTION (requires Ehsan / credentials / co-author) + +| Item | Notes | +| --- | --- | +| Co-author final OK (Jian-Feng Mao) | DONE 2026-10-24: reported in chat by Ehsan Estaji (Jian-Feng agreed and knew of the work); no written message seen by the agent | +| Merge PR #21 | `paper/comparison/` is not in the v1.0.0 archive; merge before submission so paths in the paper resolve | +| Record bioRxiv DOI | Submitted 2026-10-24 as `BIORXIV/2026/761563`; after screening (24-72 h) add the DOI to `paper/submission.yml` and the cover letter | +| ScholarOne upload | Format-free first submission OK; upload PDF + cover letter; OUP General Template deferred to **revision** | +| APC / Read & Publish / ISCB discount | Check before acceptance | +| Suggested editors (optional) | Cover letter suggests assembly/sequence-analysis tools area | + +## Pre-submit command checklist + +```bash +cd paper +make check # must exit 0 +make # builds main.pdf +make biorxiv # optional preprint PDF copy +``` + +## Evidence vs software versions + +- **Availability / packaging cite:** FastaGuard **`v1.0.0`** + (). +- **Table 1 evidence freeze:** committed **v0.6.0** public-evidence pack + (`docs/evidence/…`); timings are contextual contract illustrations, not a + claim that v1.0.0 was re-benchmarked on that host. +- Do **not** invent a new tag solely for the paper; `v1.0.0` already exists. + Latest GitHub “Latest” may be newer (e.g. v1.1.0) — paper cites v1.0.0 as the + Bioconda reference-contract release unless Availability is intentionally retargeted. diff --git a/paper/HANDOFF.md b/paper/HANDOFF.md new file mode 100644 index 0000000..46e1bd2 --- /dev/null +++ b/paper/HANDOFF.md @@ -0,0 +1,120 @@ +# Paper track handoff (cold-start) + +Paper-only track. Do not change product CLI/schema/packaging unless asked. +Venue: ***Bioinformatics* (OUP) Application Note** — **user-confirmed**. +PR: [#21](https://github.com/ehsanestaji/FastaGuard/pull/21) · branch `cursor/paper-prep-venues-198e`. + +## Machine-ready vs USER-ACTION + +| Status | Items | +| --- | --- | +| **DONE / submit-package ready** | Full draft; front-matter; evidence freeze; `submission.yml`; `check_submission.py`; cover letter; `.zenodo.json`; bioRxiv build target; `docs/paper-readiness.md` | +| **DONE (2026-10-24)** | Zenodo DOI minted and pasted into Availability + manifest; OUP guideline review (AI + preprint disclosure added to cover letter); external claims verified (see below); PDF metadata + Fig. 1 label overlap fixed | +| **DONE (2026-10-24)** | bioRxiv preprint submitted as `BIORXIV/2026/761563` (acknowledgement email received); in screening, DOI pending | +| **USER-ACTION only** | Record the bioRxiv DOI when issued; ScholarOne upload; APC/R&P/ISCB; merge PR #21 | + +Do **not** claim bioRxiv or ScholarOne submitted from this agent track until the +confirmation page / DOI is seen. Never click final Submit without explicit user OK. +Order: co-author OK -> bioRxiv -> ScholarOne (cover letter says the preprint was submitted). + +## Commands + +```bash +cd paper +make check # must exit 0 +make # main.pdf +make biorxiv # build/fastaguard-biorxiv.pdf +``` + +## Freezes + +- **Software Availability cite:** GitHub **`v1.0.0`** (Bioconda `fastaguard=1.0.0`) +- **Table 1 evidence:** `docs/evidence/results/v0.6/evidence_summary.tsv` (fastaguard **0.6.0**, commit `cf27295…`) +- **Forbidden phrase:** never use “FastQC for FASTA” affirmatively + +## Copied from VariantFlow (front-matter only) + +Authors/ORCID/Umeå/emails; WIFORCE funding; COI; AI disclosure; CRediT; SNIC ack; sectioning conventions. + +## Not copied + +VariantFlow VCF claims, benchmarks, tables, Zenodo/bioRxiv DOIs. + +## Key paths + +- Manifest: `paper/submission.yml` +- Readiness: `docs/paper-readiness.md` +- Store context: `/cursor/stores/bc-53e26eb2-1f92-41f8-9fa2-384c56a6ad53/docs/project-context.md` + +## Verified 2026-10-24 (do not re-derive) + +- `fastaguard=1.0.0` is Bioconda latest; BioContainers `quay.io/biocontainers/fastaguard:1.0.0--hfa8f182_0` exists. +- nf-core module `modules/nf-core/fastaguard` and `snakemake-wrappers/bio/fastaguard` exist upstream. +- Zenodo DOI resolves (HTTP 200 -> zenodo.org/records/23622311). +- Body ~2,160 words excl. references (OUP limit ~2,600 words, or ~2,000 + one figure). +- GitHub "Latest" is v1.1.0 but the paper intentionally cites v1.0.0 (Bioconda latest). + +## Pre-submission review (2026-10-24) - applied + +Two independent reviews (claims vs a binary built from `git archive v1.0.0`; related work and +references) plus an editorial pass. Applied to `main.tex` / `references.bib`: + +- Table 1 caption now states runs used `--min-contig-length 1` (default => synthetic fixture WARNs on + `tiny_contigs`; verified), the N. crassa count includes mitochondrion NC_026614.1 (64,840 bp; verified + against NCBI Datasets), and the v1.0.0 binary reproduces both local fixtures (verdict, gate, SHA-256, exit 0). +- Removed the unsupported "value-benchmark" sentence (source was v0.3.0 in `docs/value-benchmark.md`, cited to v1.0.0). +- Gate text lists first-token duplicate IDs; binaries claim fixed (no Linux aarch64 GitHub binary; Bioconda has it); + "deterministic bundle" narrowed (timestamps/durations vary); NCBI policy described as conservative (stricter than + table2asn on contig_199 / seqid_50 boundary cases) with the NCBI FASTA-format citation. +- Related work added (py_fasta_validator, fa-lint, AssemblyQC, SeqFu, gfastats, SeqKit2) with hedged "to our + knowledge" wording; missing citations added (CheckM, Nextflow, Snakemake, nf-core, Bioconda, BioContainers, FCS-GX, + RefSeq and genome papers). All DOIs checked against Crossref/DataCite; 24 bib entries, 0 BibTeX warnings. +- Removed: "fast", "product thesis", "future tool-using agents", "falsifiable", SNIC/HPC2N acknowledgement (all + reported runs were macOS arm64), and the `paper/` supplementary pointer (not in the v1.0.0 tree). +- Zenodo record 23622311 creators fixed on zenodo.org (Estaji + Mao, ORCIDs, UPSC affiliation); DOI unchanged. +- PDF: `--flag` ligature fixed (microtype `\DisableLigatures`), PDF metadata filled, Fig. 1 label overlap fixed. + +Resolved after the user's answers (same day): +- AI disclosure rewritten (paper + cover letter): software, code base and benchmarking results came first; the draft + was written solely from them, not from a free-form prompt; tools named (Cursor-based agents, Anthropic Claude Code). +- Funding unchanged: GraphPop, GraphMana and VariantFlow preprints carry the identical WIFORCE/KAW sentence. +- SNIC/HPC2N not used for FastaGuard: acknowledgement stays removed. +- NCBI rows re-run with a v1.0.0 build (clean clone at the tag + NCBI Datasets 18.39.0): all five evidence-pack rows + match the frozen v0.6.0 values on verdict, gate, blocking findings, lengths, sequence counts, SHA-256, exit code + (timings differ). Portable summary: `docs/evidence/results/v1.0.0-rerun/`. Binary was built from the v1.0.0 tag, not + the release asset. +- Validator comparison added (Table 2, `paper/comparison/`): py_fasta_validator 0.6, fa-lint 1.2.0, table2asn 1.29.324, + SeqKit 2.14.0, FastaGuard 1.0.0 on 30 constructed files (expectations fixed first) + the two RefSeq assemblies. + Reproduce with `python3 -I paper/comparison/run_comparison.py --help`; tool SHA-256s in `tool_versions.json`. + Author-constructed corpus favours FastaGuard; capability, not accuracy. A first real-data run was discarded (label + collision + stale report read); the script was fixed and the 30-file results were byte-identical afterwards. + +Co-author OK: reported by Ehsan in chat 2026-10-24 (no message from Jian-Feng seen by the agent). +Still open: merge PR #21 so `paper/comparison/` resolves on main (it is not in the v1.0.0 +Zenodo archive; consider a small separate deposit for test data); `CITATION.cff` lists Estaji only (product repo); Zenodo +description still says Bioconda "remains at v0.7.0"; length (Introduction-Discussion ~2,260 words; ~2,700 incl. +abstract, captions and declarations; ~850 more in references; draft PDF is 8 pages). + +## bioRxiv submission record (2026-10-24) + +- ID `BIORXIV/2026/761563`; New Results; Bioinformatics; CC BY 4.0; funder Knut and Alice Wallenberg Foundation (no award number). +- The uploaded PDF is `paper/build/fastaguard-biorxiv.pdf` (SHA-256 `8eb58ec7...2269`); bioRxiv served it back byte-identical. +- Form notes: one corresponding author only (Ehsan); accented letters must be HTML entities (`Umeå`); the abstract field takes the full + abstract; the built-in browser has no file-upload tool, so the file is picked by the user in the native dialog. +- Next: when bioRxiv issues the DOI, record it in `paper/submission.yml`, the cover letter and the academic dashboard + (`research-monitor`, entry SUB-03), then upload to ScholarOne. + +## OUP policy points that shaped the cover letter + +- Preprints allowed; must be disclosed in the cover letter; update with published DOI after acceptance; + do not submit the preprint to more than one journal at once. +- AI use must be disclosed in the cover letter and in the manuscript. OUP lists "drafting papers from a + prompt" as unacceptable; the disclosure must stay an accurate description of what was done. +- Software must be archived (Zenodo) with the URL in the Availability abstract, and stay available 2 years. +- ScholarOne asks for ORCID, COI, CRediT roles; reviewer suggestions optional. + +## Zenodo DOI (2026-10-24) + +- Version DOI: `10.5281/zenodo.23622311` (https://zenodo.org/records/23622311) +- Concept DOI: `10.5281/zenodo.23622310` +- Source: GitHub release `v1.0.0` via Zenodo GitHub integration diff --git a/paper/Makefile b/paper/Makefile new file mode 100644 index 0000000..f452b3e --- /dev/null +++ b/paper/Makefile @@ -0,0 +1,38 @@ +# Build and validate the FastaGuard Application Note PDF. +MAIN := main +LATEXMK ?= latexmk +BUILDDIR := build +BIORXIV_PDF := $(BUILDDIR)/fastaguard-biorxiv.pdf + +.PHONY: all pdf check biorxiv clean distclean view + +all: pdf + +pdf: + $(LATEXMK) -pdf -interaction=nonstopmode -halt-on-error $(MAIN).tex + +check: + python3 check_submission.py + +biorxiv: pdf + mkdir -p $(BUILDDIR) + cp $(MAIN).pdf $(BIORXIV_PDF) + @echo "Wrote $(BIORXIV_PDF)" + +# Fallback if latexmk is unavailable: +pdflatex-only: + pdflatex -interaction=nonstopmode $(MAIN).tex + bibtex $(MAIN) + pdflatex -interaction=nonstopmode $(MAIN).tex + pdflatex -interaction=nonstopmode $(MAIN).tex + +clean: + $(LATEXMK) -c $(MAIN).tex || true + rm -f $(MAIN).bbl $(MAIN).blg $(MAIN).synctex.gz + +distclean: clean + rm -f $(MAIN).pdf + rm -rf $(BUILDDIR) + +view: pdf + @echo "Built $(MAIN).pdf" diff --git a/paper/README.md b/paper/README.md new file mode 100644 index 0000000..e6758c1 --- /dev/null +++ b/paper/README.md @@ -0,0 +1,40 @@ +# FastaGuard paper (LaTeX) + +**Venue (user-confirmed):** [*Bioinformatics*](https://academic.oup.com/bioinformatics) — **Application Note** + +**PR:** [#21](https://github.com/ehsanestaji/FastaGuard/pull/21) · `cursor/paper-prep-venues-198e` + +Paper-prep only. Cold-start: [`HANDOFF.md`](HANDOFF.md). Readiness checklist: [`../docs/paper-readiness.md`](../docs/paper-readiness.md). + +## Build & check + +```bash +cd paper +make check # submission control (must pass) +make # → main.pdf +make biorxiv # → build/fastaguard-biorxiv.pdf +``` + +Requires TeX Live (`pdflatex`, `bibtex`, `latexmk`, TikZ, `microtype`) and Python 3. + +## Submission package files + +| File | Role | +| --- | --- | +| `main.tex` / `references.bib` | Manuscript | +| `submission.yml` | Control manifest (versions, freezes, blockers) | +| `check_submission.py` | Machine gate | +| `cover-letter.md` | ScholarOne cover letter draft | +| `ZENODO.md` | USER-ACTION DOI mint from `v1.0.0` | +| `biorxiv-checklist.md` | Optional preprint steps | +| `../.zenodo.json` | Zenodo metadata (no DOI invented) | + +## Freezes + +- Availability software: **`v1.0.0`** +- Table 1 evidence: **v0.6.0** pack under `docs/evidence/results/v0.6/` +- Format-free first submission OK; OUP General Template deferred to revision + +## Framing + +Never pitch as “FastQC for FASTA.” diff --git a/paper/ZENODO.md b/paper/ZENODO.md new file mode 100644 index 0000000..0be80b5 --- /dev/null +++ b/paper/ZENODO.md @@ -0,0 +1,40 @@ +# Zenodo archive steps (USER-ACTION) + +No Zenodo DOI is invented in the manuscript. GitHub release **`v1.0.0`** already +exists and is the cited Availability / Bioconda line +(). + +## Mint a DOI from the existing release + +1. Sign in to [Zenodo](https://zenodo.org/) with an account that can access the + `ehsanestaji/FastaGuard` GitHub repository (enable the Zenodo GitHub + integration under Zenodo → GitHub if needed). +2. Enable the **FastaGuard** repository in Zenodo’s GitHub listing. +3. Prefer depositing the **existing** `v1.0.0` release (do not invent a new + version number for the paper). Options: + - If Zenodo is linked: create a *new* GitHub release only if you intentionally + want a fresh archive tag; otherwise use Zenodo’s UI to refresh/archive the + `v1.0.0` release asset / tag already published. + - Or upload the `v1.0.0` source tarball from GitHub releases manually and + apply metadata from repo-root [`.zenodo.json`](../.zenodo.json). +4. Publish the Zenodo record. Copy: + - **Concept DOI** (optional, always resolves to latest) + - **Version DOI** for `v1.0.0` (preferred for Availability) +5. Paste into: + - `paper/submission.yml` → `release.archive_doi` / `release.version_doi` + - `paper/main.tex` Availability sentence (add + `\href{https://doi.org/10.5281/zenodo.XXXX}{doi:10.5281/zenodo.XXXX}`) +6. Rebuild: `cd paper && make && make check` + +## Do not + +- Invent or placeholder a `10.5281/zenodo.…` DOI in the submit-ready PDF. +- Retag a different software version unless the manuscript Availability is + updated to match. + +## Minted (2026-10-24) + +- **Version DOI:** `10.5281/zenodo.23622311` — https://doi.org/10.5281/zenodo.23622311 +- **Concept DOI:** `10.5281/zenodo.23622310` +- **Record:** https://zenodo.org/records/23622311 +- Pasted into `paper/main.tex` Availability and `paper/submission.yml`. diff --git a/paper/biorxiv-checklist.md b/paper/biorxiv-checklist.md new file mode 100644 index 0000000..99c6e2c --- /dev/null +++ b/paper/biorxiv-checklist.md @@ -0,0 +1,57 @@ +# bioRxiv upload checklist (USER-ACTION) + +Deposit the preprint **before** the ScholarOne submission: the cover letter +states that the same manuscript has been submitted to bioRxiv (OUP requires +preprint disclosure in the cover letter). OUP allows preprints; update the +preprint with the published DOI after acceptance. + +## Build the bioRxiv-ready PDF + +```bash +cd paper +make biorxiv +# -> paper/build/fastaguard-biorxiv.pdf +``` + +This is the same Application Note PDF with a stable filename for upload. +bioRxiv takes a single PDF with text, figures, and tables; no separate figure +files are needed. + +## Gate before starting + +- Jian-Feng Mao has confirmed in writing that he consents to deposition and + submission (bioRxiv requires all authors' consent). +- Ehsan is logged in to bioRxiv himself (registration needs an affiliation: + Umeå Plant Science Centre, Umeå University). Agents never enter passwords or + create accounts. + +## Fields (paste-ready) + +| Field | Value | +| --- | --- | +| Title | `FastaGuard: FASTA preflight QC for modern bioinformatics pipelines` | +| Authors | Ehsan Estaji (ORCID 0009-0005-0838-228X); Jian-Feng Mao (ORCID 0000-0001-9735-8516) | +| Affiliation | Umeå Plant Science Centre, Department of Plant Physiology, Umeå University, SE-901 87 Umeå, Sweden | +| Corresponding | ehsan.estaji@umu.se, jianfeng.mao@umu.se | +| Article type | New Results | +| Subject area | Bioinformatics (confirm the name in bioRxiv's dropdown) | +| Abstract | Summary paragraph from `main.tex` (single block); Availability stays in the PDF | +| License | CC BY (recommended: matches the open-access CC BY licence OUP offers) | +| Funding | WIFORCE (Wallenberg Initiatives in Forest Research), Knut and Alice Wallenberg Foundation | +| Competing interests | None | +| Code / data | https://github.com/ehsanestaji/FastaGuard ; Zenodo doi:10.5281/zenodo.23622311 | + +## Upload steps + +1. Start a new submission, upload `paper/build/fastaguard-biorxiv.pdf`. +2. Fill the fields above; review the PDF as bioRxiv renders it. +3. Stop at the final confirm screen and get Ehsan's explicit OK before clicking + it: a posted preprint is citable and cannot be removed. +4. After bioRxiv screening, record the bioRxiv DOI in `paper/submission.yml` + (`biorxiv_status`) and add it to the ScholarOne record / revision. + +## Do not + +- Claim the preprint is posted until the bioRxiv confirmation shows a DOI. +- Put a fake bioRxiv DOI in `main.tex`. +- Submit the preprint to more than one journal at once (OUP policy). diff --git a/paper/check_submission.py b/paper/check_submission.py new file mode 100644 index 0000000..68f7f12 --- /dev/null +++ b/paper/check_submission.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Validate the FastaGuard Bioinformatics Application Note submission package. + +Fails (exit 1) on manuscript TODOs/placeholders, forbidden framing, missing +Availability URLs, missing author emails, or evidence-table drift vs the frozen +v0.6 public-evidence TSV. Zenodo DOI absence is reported as a human blocker, +not a machine failure (no fake DOIs). + +Usage: + python3 paper/check_submission.py + make -C paper check +""" + +from __future__ import annotations + +import csv +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PAPER = ROOT / "paper" +MANIFEST = PAPER / "submission.yml" +MANUSCRIPT = PAPER / "main.tex" +EVIDENCE_TSV = ROOT / "docs" / "evidence" / "results" / "v0.6" / "evidence_summary.tsv" +FORBIDDEN = "FastQC for FASTA" +PLACEHOLDER_RES = ( + re.compile(r"\bTODO\b"), + re.compile(r"\bTBD\b"), + re.compile(r"\[institutional-email@", re.I), + re.compile(r"\[Affiliation to be completed\]", re.I), + re.compile(r"Funding:\s*\[to be completed\]", re.I), + re.compile(r"example\.org", re.I), +) + + +def read(path: Path) -> str: + if not path.exists(): + raise SystemExit(f"ERROR: missing required file: {path.relative_to(ROOT)}") + return path.read_text(encoding="utf-8") + + +def require_tokens(label: str, text: str, tokens: tuple[str, ...]) -> list[str]: + return [f"{label} missing {token!r}" for token in tokens if token not in text] + + +def manifest_list(manifest: str, section: str) -> list[str]: + pattern = rf"^{re.escape(section)}:\n((?: - .+\n)+)" + match = re.search(pattern, manifest, flags=re.MULTILINE) + if not match: + return [] + return [line.strip()[2:] for line in match.group(1).splitlines() if line.strip().startswith("- ")] + + +def load_evidence_rows() -> dict[str, dict[str, str]]: + with EVIDENCE_TSV.open(encoding="utf-8") as handle: + reader = csv.DictReader(handle, delimiter="\t") + return {row["id"]: row for row in reader} + + +def main() -> int: + errors: list[str] = [] + warnings: list[str] = [] + human_blockers: list[str] = [] + + manifest = read(MANIFEST) + manuscript = read(MANUSCRIPT) + + errors += require_tokens( + "paper/submission.yml", + manifest, + ( + "target_journal: Bioinformatics", + "article_type: Application Note", + "tag: v1.0.0", + "fastaguard_version: \"0.6.0\"", + "forbidden_phrase: \"FastQC for FASTA\"", + "performance_claim_source: docs/evidence_only", + "ehsan.estaji@umu.se", + "jianfeng.mao@umu.se", + ), + ) + + errors += require_tokens( + "paper/main.tex", + manuscript, + ( + r"\begin{abstract}", + r"\textbf{Summary:}", + r"\textbf{Availability and Implementation:}", + r"\textbf{Contact:}", + r"\textbf{Supplementary information:}", + "https://github.com/ehsanestaji/FastaGuard", + "ehsan.estaji@umu.se", + "jianfeng.mao@umu.se", + "0009-0005-0838-228X", + "0000-0001-9735-8516", + r"\section*{Funding}", + r"\section*{Conflict of interest}", + r"\section*{AI usage disclosure}", + "WIFORCE", + "v1.0.0", + "0.0194", + "0.0185", + "0.0569", + "0.3441", + "GCF\\_000005845.2", + "GCF\\_000182925.2", + ), + ) + + if FORBIDDEN.lower() in manuscript.lower(): + # Allow negation contexts that explicitly reject the phrase. + for match in re.finditer(re.escape(FORBIDDEN), manuscript, flags=re.IGNORECASE): + window = manuscript[max(0, match.start() - 40) : match.end() + 10].lower() + if "not" in window or "never" in window or "avoid" in window or "rather than" in window: + continue + errors.append(f"paper/main.tex contains forbidden phrase {FORBIDDEN!r}") + break + + for pattern in PLACEHOLDER_RES: + if pattern.search(manuscript): + errors.append(f"paper/main.tex still has placeholder matching {pattern.pattern}") + + # Evidence freeze: every table timing/accession must match the TSV. + if not EVIDENCE_TSV.exists(): + errors.append(f"missing evidence freeze file: {EVIDENCE_TSV.relative_to(ROOT)}") + else: + rows = load_evidence_rows() + expected = { + "synthetic_valid": ("0.0194", "PASS", "604", "4"), + "problem_fixture": ("0.0185", "FAIL", "145", "5"), + "ecoli_k12_mg1655": ("0.0569", "PASS", "4641652", "1"), + "neurospora_crassa_or74a": ("0.3441", "WARN", "41102378", "21"), + } + for row_id, (elapsed, verdict, length, nseq) in expected.items(): + row = rows.get(row_id) + if not row: + errors.append(f"evidence TSV missing row id={row_id}") + continue + if row["elapsed_seconds"] != elapsed: + errors.append( + f"evidence drift for {row_id}: elapsed_seconds={row['elapsed_seconds']!r} != {elapsed!r}" + ) + if row["verdict"] != verdict or row["gate_status"] != verdict: + errors.append( + f"evidence drift for {row_id}: verdict/gate={row['verdict']}/{row['gate_status']}" + ) + if row["total_length"] != length or row["sequence_count"] != nseq: + errors.append(f"evidence drift for {row_id}: length/count mismatch") + if elapsed not in manuscript: + errors.append(f"paper/main.tex missing frozen elapsed value {elapsed} for {row_id}") + + for rel in manifest_list(manifest, "submission_docs"): + path = ROOT / rel + if not path.exists(): + errors.append(f"manifest path does not exist: {rel}") + + for required in ( + PAPER / "cover-letter.md", + PAPER / "ZENODO.md", + PAPER / "biorxiv-checklist.md", + ROOT / "docs" / "paper-readiness.md", + ROOT / ".zenodo.json", + ): + if not required.exists(): + errors.append(f"missing required submission file: {required.relative_to(ROOT)}") + + # Rough word-count band for Application Note body. + body = re.sub(r"\\begin\{tikzpicture\}.*?\\end\{tikzpicture\}", " ", manuscript, flags=re.S) + body = re.sub(r"\\begin\{verbatim\}.*?\\end\{verbatim\}", " ", body, flags=re.S) + plain = re.sub(r"\\[a-zA-Z]+\*?(?:\[[^\]]*\])?", " ", body) + plain = re.sub(r"[{}]", " ", plain) + words = re.findall(r"[A-Za-z0-9']+", plain) + n_words = len(words) + if n_words < 1800 or n_words > 3200: + warnings.append( + f"approximate manuscript token count {n_words} outside soft band 1800–3200 " + "(Application Note target ~2000–2600 words)" + ) + + if "archive_doi: null" in manifest or "version_doi: null" in manifest: + human_blockers.append("Zenodo DOI not yet minted (see paper/ZENODO.md) — USER-ACTION") + human_blockers.append("ScholarOne upload — USER-ACTION") + human_blockers.append("Co-author final confirmation — USER-ACTION") + human_blockers.append("APC / Read & Publish / ISCB discount check — USER-ACTION") + + for warning in warnings: + print(f"WARNING: {warning}") + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + print("submission package machine checks PASSED") + print(f"approximate word/token count: {n_words}") + print("remaining USER-ACTION blockers (not machine failures):") + for blocker in human_blockers: + print(f"- {blocker}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/paper/comparison/real_results.tsv b/paper/comparison/real_results.tsv new file mode 100644 index 0000000..679a576 --- /dev/null +++ b/paper/comparison/real_results.tsv @@ -0,0 +1,14 @@ +# Real files: no expectation, nothing scored. Same decision rules as results.tsv. +file scope expectation tool flagged strict exit_code detail +ecoli_GCF_000005845.2 real n/a py_fasta_validator no no 0 valid +ecoli_GCF_000005845.2 real n/a fa-lint no no 0 Fasta is valid: $REAL/genomic.fna +ecoli_GCF_000005845.2 real n/a table2asn no no 0 only baseline warnings (2) +ecoli_GCF_000005845.2 real n/a seqkit no no 0 no message +ecoli_GCF_000005845.2 real n/a fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +ecoli_GCF_000005845.2 real n/a fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +neurospora_GCF_000182925.2 real n/a py_fasta_validator no no 0 valid +neurospora_GCF_000182925.2 real n/a fa-lint no no 0 Fasta is valid: $REAL/genomic.fna +neurospora_GCF_000182925.2 real n/a table2asn yes yes 0 error [SEQ_INST.InternalNsInSeqRaw] Run of 100 Ns in raw sequence starting at base 72205 BIOSEQ: lcl|NC_02; error [SEQ_INST.InternalNsInSeqRaw] Run of 100 Ns in raw sequence starting at base 77053 BIOSEQ: lcl|NC_02; error [SEQ_INST.InternalNsInSeqRaw] Run of 1 +neurospora_GCF_000182925.2 real n/a seqkit no no 0 no message +neurospora_GCF_000182925.2 real n/a fastaguard_pipeline yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=gap_runs,gap_pattern_warnings +neurospora_GCF_000182925.2 real n/a fastaguard_ncbi yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=gap_runs,gap_pattern_warnings diff --git a/paper/comparison/results.tsv b/paper/comparison/results.tsv new file mode 100644 index 0000000..386a7f1 --- /dev/null +++ b/paper/comparison/results.tsv @@ -0,0 +1,184 @@ +# NOTE: the corpus is author-constructed and favours FastaGuard's own check list. +# Expectations (valid/error/advisory) were fixed before any tool ran; advisory rows are not scored. +# The tools were not designed for the same purpose. flagged: yes|no|crash|n.a.; strict: blocked / error-level. +file scope expectation tool flagged strict exit_code detail +valid_simple control valid py_fasta_validator no no 0 valid +valid_simple control valid fa-lint no no 0 Fasta is valid: $CORPUS/valid_simple.fa +valid_simple control valid table2asn no no 0 only baseline warnings (4) +valid_simple control valid seqkit no no 0 no message +valid_simple control valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_simple control valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_single_line control valid py_fasta_validator no no 0 valid +valid_single_line control valid fa-lint no no 0 Fasta is valid: $CORPUS/valid_single_line.fa +valid_single_line control valid table2asn no no 0 only baseline warnings (4) +valid_single_line control valid seqkit no no 0 no message +valid_single_line control valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_single_line control valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_softmasked control valid py_fasta_validator no no 0 valid +valid_softmasked control valid fa-lint no no 0 Fasta is valid: $CORPUS/valid_softmasked.fa +valid_softmasked control valid table2asn no no 0 only baseline warnings (4) +valid_softmasked control valid seqkit no no 0 no message +valid_softmasked control valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_softmasked control valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_iupac control valid py_fasta_validator no no 0 valid +valid_iupac control valid fa-lint no no 0 Fasta is valid: $CORPUS/valid_iupac.fa +valid_iupac control valid table2asn no no 0 only baseline warnings (4) +valid_iupac control valid seqkit no no 0 no message +valid_iupac control valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_iupac control valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_no_final_newline control valid py_fasta_validator no no 0 valid +valid_no_final_newline control valid fa-lint no no 0 Fasta is valid: $CORPUS/valid_no_final_newline.fa +valid_no_final_newline control valid table2asn no no 0 only baseline warnings (4) +valid_no_final_newline control valid seqkit no no 0 no message +valid_no_final_newline control valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +valid_no_final_newline control valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +empty_file syntax error py_fasta_validator no no 0 valid +empty_file syntax error fa-lint yes yes 1 Fasta file is empty +empty_file syntax error table2asn crash crash 1 console: Cannot open $WORK/table2asn/empty_file/empty_file.fa +empty_file syntax error seqkit no no 0 no message +empty_file syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +empty_file syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +seq_before_header syntax error py_fasta_validator yes yes 1 $CORPUS/seq_before_header.fa does not start with a > +seq_before_header syntax error fa-lint yes yes 1 Fasta file must start with a header line +seq_before_header syntax error table2asn yes yes 1 console Error: CFastaReader: Input doesn't start with a defline or comment around line 1 +seq_before_header syntax error seqkit yes yes 255 [ERRO] fastx: invalid FASTA/Q format +seq_before_header syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +seq_before_header syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +empty_header syntax error py_fasta_validator no no 0 valid +empty_header syntax error fa-lint yes yes 1 Fasta header does not contain a valid ID near line #12: > +empty_header syntax error table2asn no no 0 only baseline warnings (4) +empty_header syntax error seqkit yes yes 255 [ERRO] seq: invalid DNAredundant letter: +empty_header syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +empty_header syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +empty_record syntax error py_fasta_validator no no 0 valid +empty_record syntax error fa-lint yes yes 1 Empty sequence for record near line #12: ctg2_empty +empty_record syntax error table2asn yes yes 1 console Error: FASTA-Reader: No residues given +empty_record syntax error seqkit no no 0 no message +empty_record syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +empty_record syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +dup_id_identical_header identifier error py_fasta_validator yes yes 2 $CORPUS/dup_id_identical_header.fa has multiple sequences with the same identifier +dup_id_identical_header identifier error fa-lint yes yes 1 Duplicate fasta ID found near line #12: >ctg1 isolate X +dup_id_identical_header identifier error table2asn crash crash - timeout after 30s (process killed) +dup_id_identical_header identifier error seqkit no no 0 no message +dup_id_identical_header identifier error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=duplicate_ids,duplicate_first_token_ids advisory=none +dup_id_identical_header identifier error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=duplicate_ids,duplicate_first_token_ids advisory=none +dup_first_token_id identifier error py_fasta_validator yes yes 2 $CORPUS/dup_first_token_id.fa has multiple sequences with the same identifier +dup_first_token_id identifier error fa-lint yes yes 1 Duplicate fasta ID found near line #12: >ctg1 isolate B +dup_first_token_id identifier error table2asn crash crash - timeout after 30s (process killed) +dup_first_token_id identifier error seqkit no no 0 no message +dup_first_token_id identifier error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=duplicate_ids,duplicate_first_token_ids advisory=none +dup_first_token_id identifier error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=duplicate_ids,duplicate_first_token_ids advisory=none +dup_sequence_diff_ids content advisory py_fasta_validator no no 0 valid +dup_sequence_diff_ids content advisory fa-lint no no 0 Fasta is valid: $CORPUS/dup_sequence_diff_ids.fa +dup_sequence_diff_ids content advisory table2asn no no 0 only baseline warnings (4) +dup_sequence_diff_ids content advisory seqkit no no 0 no message +dup_sequence_diff_ids content advisory fastaguard_pipeline yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=duplicate_sequences +dup_sequence_diff_ids content advisory fastaguard_ncbi yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=duplicate_sequences +digit_in_sequence syntax error py_fasta_validator yes yes 4 $CORPUS/digit_in_sequence.fa has non-sequence characters in it +digit_in_sequence syntax error fa-lint yes yes 1 Invalid sequence character near line #7 +digit_in_sequence syntax error table2asn yes yes 1 console Error: CFastaReader: There are invalid residue(s) in input sequence +digit_in_sequence syntax error seqkit yes yes 255 [ERRO] seq: invalid DNAredundant letter: 7 +digit_in_sequence syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +digit_in_sequence syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +stop_star_in_sequence syntax error py_fasta_validator yes yes 4 $CORPUS/stop_star_in_sequence.fa has non-sequence characters in it +stop_star_in_sequence syntax error fa-lint yes yes 1 Invalid sequence character near line #7 +stop_star_in_sequence syntax error table2asn yes yes 1 console Error: CFastaReader: There are invalid residue(s) in input sequence +stop_star_in_sequence syntax error seqkit yes yes 255 [ERRO] seq: invalid DNAredundant letter: * +stop_star_in_sequence syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +stop_star_in_sequence syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +gap_dash_in_sequence syntax advisory py_fasta_validator yes yes 4 $CORPUS/gap_dash_in_sequence.fa has non-sequence characters in it +gap_dash_in_sequence syntax advisory fa-lint yes yes 1 Invalid sequence character near line #7 +gap_dash_in_sequence syntax advisory table2asn yes no 2 console Warning: CFastaReader: Hyphens are invalid and will be ignored around line 7 +gap_dash_in_sequence syntax advisory seqkit no no 0 no message +gap_dash_in_sequence syntax advisory fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +gap_dash_in_sequence syntax advisory fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +non_iupac_letter_x syntax error py_fasta_validator no no 0 valid +non_iupac_letter_x syntax error fa-lint no no 0 Fasta is valid: $CORPUS/non_iupac_letter_x.fa +non_iupac_letter_x syntax error table2asn yes yes 1 console Error: CFastaReader: There are invalid residue(s) in input sequence +non_iupac_letter_x syntax error seqkit yes yes 255 [ERRO] seq: invalid DNAredundant letter: X +non_iupac_letter_x syntax error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +non_iupac_letter_x syntax error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +space_in_sequence syntax advisory py_fasta_validator yes yes 4 $CORPUS/space_in_sequence.fa has non-sequence characters in it +space_in_sequence syntax advisory fa-lint yes yes 1 Invalid sequence character near line #2 +space_in_sequence syntax advisory table2asn no no 0 only baseline warnings (2) +space_in_sequence syntax advisory seqkit no no 0 no message +space_in_sequence syntax advisory fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +space_in_sequence syntax advisory fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_chars advisory=none +blank_line_in_record syntax advisory py_fasta_validator no no 0 valid +blank_line_in_record syntax advisory fa-lint yes yes 1 Empty sequence line near line #4 +blank_line_in_record syntax advisory table2asn no no 0 only baseline warnings (2) +blank_line_in_record syntax advisory seqkit no no 0 no message +blank_line_in_record syntax advisory fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +blank_line_in_record syntax advisory fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +crlf_line_endings syntax advisory py_fasta_validator no no 0 valid +crlf_line_endings syntax advisory fa-lint no no 0 Fasta is valid: $CORPUS/crlf_line_endings.fa +crlf_line_endings syntax advisory table2asn no no 0 only baseline warnings (4) +crlf_line_endings syntax advisory seqkit no no 0 no message +crlf_line_endings syntax advisory fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +crlf_line_endings syntax advisory fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +inconsistent_wrapping syntax advisory py_fasta_validator no no 0 valid +inconsistent_wrapping syntax advisory fa-lint yes yes 1 Sequence near line #4 violates preceding line wrapping length +inconsistent_wrapping syntax advisory table2asn no no 0 only baseline warnings (2) +inconsistent_wrapping syntax advisory seqkit no no 0 no message +inconsistent_wrapping syntax advisory fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +inconsistent_wrapping syntax advisory fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +utf8_bom syntax advisory py_fasta_validator yes yes 1 $CORPUS/utf8_bom.fa does not start with a > +utf8_bom syntax advisory fa-lint yes yes 1 Fasta file must start with a header line +utf8_bom syntax advisory table2asn yes yes 1 console Error: Object type not supported +utf8_bom syntax advisory seqkit no no 0 no message +utf8_bom syntax advisory fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +utf8_bom syntax advisory fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=invalid_fasta_structure advisory=none +high_n_record content advisory py_fasta_validator no no 0 valid +high_n_record content advisory fa-lint no no 0 Fasta is valid: $CORPUS/high_n_record.fa +high_n_record content advisory table2asn yes yes 2 error [SEQ_INST.ContigsTooShort] Maximum contig length is 2 bases BIOSEQ: lcl|ctg1: raw, dna len= 600 +high_n_record content advisory seqkit no no 0 no message +high_n_record content advisory fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=high_n_rate advisory=none +high_n_record content advisory fastaguard_ncbi yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=high_n_rate +all_n_record content error py_fasta_validator no no 0 valid +all_n_record content error fa-lint yes yes 1 Sequence near line #1 is comprised entirely of N, n, '.' or '*': ctg1 +all_n_record content error table2asn yes yes 2 critical [SEQ_INST.AllNs] Sequence is all Ns BIOSEQ: lcl|ctg1: raw, dna len= 600 +all_n_record content error seqkit no no 0 no message +all_n_record content error fastaguard_pipeline yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=high_n_rate advisory=gap_runs,terminal_ns,composite_anomalies +all_n_record content error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=terminal_ns advisory=high_n_rate,gap_runs,composite_anomalies +tiny_contig_50 content advisory py_fasta_validator no no 0 valid +tiny_contig_50 content advisory fa-lint no no 0 Fasta is valid: $CORPUS/tiny_contig_50.fa +tiny_contig_50 content advisory table2asn no no 0 only baseline warnings (4) +tiny_contig_50 content advisory seqkit no no 0 no message +tiny_contig_50 content advisory fastaguard_pipeline yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=tiny_contigs +tiny_contig_50 content advisory fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=ncbi_genome_short_contigs advisory=tiny_contigs +seqid_len_49 ncbi valid py_fasta_validator no no 0 valid +seqid_len_49 ncbi valid fa-lint no no 0 Fasta is valid: $CORPUS/seqid_len_49.fa +seqid_len_49 ncbi valid table2asn no no 0 only baseline warnings (2) +seqid_len_49 ncbi valid seqkit no no 0 no message +seqid_len_49 ncbi valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +seqid_len_49 ncbi valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +seqid_len_50 ncbi error py_fasta_validator no no 0 valid +seqid_len_50 ncbi error fa-lint no no 0 Fasta is valid: $CORPUS/seqid_len_50.fa +seqid_len_50 ncbi error table2asn no no 0 only baseline warnings (2) +seqid_len_50 ncbi error seqkit no no 0 no message +seqid_len_50 ncbi error fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +seqid_len_50 ncbi error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=ncbi_genome_seqid advisory=none +seqid_disallowed_char ncbi error py_fasta_validator no no 0 valid +seqid_disallowed_char ncbi error fa-lint no no 0 Fasta is valid: $CORPUS/seqid_disallowed_char.fa +seqid_disallowed_char ncbi error table2asn no no 0 only baseline warnings (2) +seqid_disallowed_char ncbi error seqkit no no 0 no message +seqid_disallowed_char ncbi error fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +seqid_disallowed_char ncbi error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=ncbi_genome_seqid advisory=none +contig_len_199 ncbi advisory py_fasta_validator no no 0 valid +contig_len_199 ncbi advisory fa-lint no no 0 Fasta is valid: $CORPUS/contig_len_199.fa +contig_len_199 ncbi advisory table2asn no no 0 only baseline warnings (2) +contig_len_199 ncbi advisory seqkit no no 0 no message +contig_len_199 ncbi advisory fastaguard_pipeline yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=tiny_contigs +contig_len_199 ncbi advisory fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=ncbi_genome_short_contigs advisory=tiny_contigs +contig_len_200 ncbi valid py_fasta_validator no no 0 valid +contig_len_200 ncbi valid fa-lint no no 0 Fasta is valid: $CORPUS/contig_len_200.fa +contig_len_200 ncbi valid table2asn no no 0 only baseline warnings (2) +contig_len_200 ncbi valid seqkit no no 0 no message +contig_len_200 ncbi valid fastaguard_pipeline no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +contig_len_200 ncbi valid fastaguard_ncbi no no 0 verdict=PASS gate=PASS can_continue=true blocking=none advisory=none +terminal_ns ncbi error py_fasta_validator no no 0 valid +terminal_ns ncbi error fa-lint no no 0 Fasta is valid: $CORPUS/terminal_ns.fa +terminal_ns ncbi error table2asn yes yes 0 error [SEQ_INST.TerminalNs] N at beginning of sequence BIOSEQ: lcl|ctg1: raw, dna len= 600; error [SEQ_INST.TerminalNs] N at end of sequence BIOSEQ: lcl|ctg1: raw, dna len= 600 +terminal_ns ncbi error seqkit no no 0 no message +terminal_ns ncbi error fastaguard_pipeline yes no 0 verdict=WARN gate=WARN can_continue=true blocking=none advisory=terminal_ns +terminal_ns ncbi error fastaguard_ncbi yes yes 0 verdict=FAIL gate=FAIL can_continue=false blocking=terminal_ns advisory=none diff --git a/paper/comparison/run_comparison.py b/paper/comparison/run_comparison.py new file mode 100644 index 0000000..d9e92dc --- /dev/null +++ b/paper/comparison/run_comparison.py @@ -0,0 +1,574 @@ +#!/usr/bin/env python3 +"""Capability comparison of FASTA validators on a small synthetic corpus. + +HONEST SCOPE. The corpus is AUTHOR-CONSTRUCTED and was designed around the check list of +FastaGuard (a FASTA preflight-QC CLI), so it favours FastaGuard. The tools were NOT built +for the same purpose: py_fasta_validator is a minimal syntax checker, fa-lint a protein/ +nucleotide FASTA linter, table2asn an NCBI submission converter whose validator is a side +effect, SeqKit a general toolkit whose `seq -v` checks residue letters. Expectations +(valid / error / advisory) were written down BEFORE any tool was run; advisory rows are +reported but never scored. Grounding: https://www.ncbi.nlm.nih.gov/genbank/fastaformat/ +(F) and https://www.ncbi.nlm.nih.gov/genbank/genomesubmit/ (G), read 2026-10-24. + +USAGE (stdlib only, Python 3.9+): + run_comparison.py --corpus-dir DIR [--out-dir DIR] [--work-dir DIR] [--timeout SEC] + [--fastaguard P] [--py-fasta-validator P] [--fa-lint P] [--table2asn P] [--seqkit P] + [--archives DIR] [--generate-only] [--real [LABEL=]FASTA [[LABEL=]FASTA ...]] + [--real-timeout SEC] +Tool location: flag, else env FASTAGUARD / PY_FASTA_VALIDATOR / FA_LINT / TABLE2ASN / +SEQKIT, else the same-named executable on PATH. A missing tool is reported as n.a. +--out-dir defaults to this script's directory (results.tsv, summary.tsv, +tool_versions.json, plus real_results.tsv with --real). --generate-only writes the corpus, +prints name/scope/expectation/sha256/rationale as TSV and exits. --archives DIR re-hashes +downloaded archives found anywhere below DIR against the pinned SHA-256 values below. +--real runs the same tools on real files (no expectation) into real_results.tsv; give each +file a unique LABEL=PATH (the label defaults to the file name) and a longer --real-timeout. + +DECISION RULES (flagged = yes | no | crash | n.a.; strict is a second, stricter level) + py_fasta_validator -f F: exit 1/2/4 (documented codes) -> yes; 0 -> no; Traceback, + signal, timeout or any other code -> crash. Exit code recorded. + fa-lint -fasta F: exit != 0 or a logrus level=error|fatal|warn|panic on stderr -> yes; + "panic:"/"goroutine" in stderr, signal or timeout -> crash. + seqkit seq -v -t dna F -o /dev/null: exit != 0 or [ERRO]/[WARN] on stderr -> yes; + panic text, signal or timeout -> crash. (-t dna is explicit: with -t auto the + alphabet is guessed from a prefix and tiny inputs are not validated.) + table2asn: repo invocation of scripts/verify_ncbi_genome_policy.py (-t synthetic + template -M n -j "[organism=synthetic construct][moltype=genomic DNA]" -V vb -Z). + Every file gets two template-induced baseline warnings, so a bare "warning" is not + attributable to the file. flagged = yes if (a) a console Error/Fatal/Critical block + (FASTA reader rejections print there, exit 1, no .val), or (b) a .val/.stats + message of level Error/Critical/Fatal/Reject (the repo's "rejected"), or (c) a + Warning whose rule code is absent from the valid_simple control run, or a console + Warning block with a Problem line (FASTA-reader warnings; added after the first run, + it only affects gap_dash_in_sequence). strict = yes only for (a)/(b). Non-zero exit + without such evidence, a console "Cannot open" (what it prints for an empty file), + signal, timeout (it hangs on duplicate SeqIDs: idle, >240 s checked by hand), or no + artifact at all -> crash (repo: tool_error). + fastaguard_pipeline: --profile assembly --gate pipeline; fastaguard_ncbi: --profile + assembly --gate submission --submission-target ncbi; defaults otherwise (no + --min-contig-length). flagged = verdict.status != PASS; strict = not + gate.can_continue ("blocked"). Missing/invalid JSON -> crash. FastaGuard exits 0 + even for FAIL, so the exit code carries no verdict. +For non-layered tools strict == flagged. Scoring: error rows -> flagged is TP (else miss), +valid rows -> flagged is FP (else TN), crash rows are counted separately, advisory rows +are reported only. Per-scope tallies: syntax, identifier, content, ncbi, control. +results.tsv columns: file, scope, expectation, tool, flagged, strict, exit_code, detail. +Reproduce: see tool_versions.json (pinned archives) and the command in the final report. +""" + +import argparse +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import zlib +from pathlib import Path + +TWO_LEVEL = {"table2asn", "fastaguard_pipeline", "fastaguard_ncbi"} +TOOLS = ["py_fasta_validator", "fa-lint", "table2asn", "seqkit", + "fastaguard_pipeline", "fastaguard_ncbi"] +ANSI = re.compile(r"\x1b\[[0-9;]*m") +ERR_RE = re.compile(r"(?i)(?:^|[\s\[])(?:FATAL|CRITICAL|ERROR)(?:\]|\s*:)") # from the repo script +VAL_RE = re.compile(r"^\s*(\w+):\s+\S+\s+\[([^\]]+)\]\s*(.*)$") +ERR_LEVELS = {"error", "critical", "fatal", "reject", "rejected"} +BAD_FALINT = re.compile(r"level=(?:error|fatal|warn|warning|panic)", re.I) +T2A_ARGS = ["-M", "n", "-j", "[organism=synthetic construct][moltype=genomic DNA]", "-V", "vb", "-Z"] +T2A_TEMPLATE = """Submit-block ::= { + contact { + contact { + name name { + last "Validation", + first "Local" + } + } + }, + cit { + authors { + names std { + { + name name { + last "Validation", + first "Local", + initials "L." + } + } + }, + affil std { + affil "Controlled local validation", + city "Brussels", + country "Belgium" + } + } + }, + subtype new +} + +Seqdesc ::= pub { + pub { + gen { + cit "unpublished", + authors { + names std { + { + name name { + last "Validation", + first "Local", + initials "L." + } + } + } + }, + title "Controlled FASTA validation fixture" + } + } +} +""" +# Pinned archives (macOS arm64 builds). Digests were checked at download time against the +# publisher's own value (GitHub release digest / PyPI JSON / repo evidence for table2asn). +SOURCES = { + "py_fasta_validator": ("https://files.pythonhosted.org/packages/source/p/py_fasta_validator/" + "py_fasta_validator-0.6.tar.gz", + "ca29c20e1be31ef190f923b62ddb7fccda8c1c3ba9cf699a6ccaccce10ea011a", + ["-v"], "PyPI sdist of project py-fasta-validator; built in a python3.11 venv " + "with setuptools==80.9.0 because the shipped CLI imports pkg_resources"), + "fa-lint": ("https://github.com/GallVp/fa-lint/releases/download/1.2.0/fa-lint-darwin-arm64.tar.gz", + "6edcb792489b18bcfd03156912d05227ac05cdf657c517041c40489566470436", + ["-version"], "official release binary"), + "table2asn": ("https://ftp.ncbi.nlm.nih.gov/asn1-converters/by_program/table2asn/mac.table2asn.gz", + "348b9d5f1b05065f4e5e61b6c2350e5ff044de2d6dc14765be6ac02c00e59341", + ["-version"], "digest equals the one pinned in docs/evidence/" + "fastaguard-v0.7-ncbi-genome-policy.md; NCBI publishes no separate digest"), + "seqkit": ("https://github.com/shenwei356/seqkit/releases/download/v2.14.0/seqkit_darwin_arm64.tar.gz", + "7b3a6a7c9be07a06c5b308d1aad769c09762b36b387690eed8b91b00d01503b7", + ["version"], "official release binary; md5 2b335eb27f69af97a252818a85145684 also matched"), + "fastaguard": ("local build of `git archive v1.0.0` of https://github.com/ehsanestaji/FastaGuard", + None, ["--version"], "no archive; executable hash recorded"), +} + + +# ----------------------------------------------------------------------------- corpus +def seq(label, n): + x = zlib.crc32(label.encode()) or 1 + out = [] + for _ in range(n): + x = (x * 1103515245 + 12345) & 0x7FFFFFFF + out.append("ACGT"[(x >> 16) & 3]) + return "".join(out) + + +def rec(name, s, w=60, desc=""): + lines = [s] if w == 0 else [s[i:i + w] for i in range(0, len(s), w)] + return "\n".join([">" + name + (" " + desc if desc else "")] + lines) + "\n" + + +def put(s, i, ch): + return s[:i] + ch + s[i + 1:] + + +def build_corpus(): + """Return [(name, scope, expectation, rationale, bytes)]; order is the report order.""" + a, b = seq("a", 600), seq("b", 800) + base = rec("ctg1", a) + rec("ctg2", b) + iupac = a + for pos, ch in ((100, "R"), (150, "Y"), (200, "K"), (250, "M"), (300, "S"), (350, "W"), + (400, "N"), (450, "B"), (480, "D"), (510, "H"), (540, "V")): + iupac = put(iupac, pos, ch) + soft = a[:100] + a[100:300].lower() + a[300:] + chunks, i, k = [], 0, 0 + while i < 600: + w = (60, 80, 45, 70)[k % 4] + chunks.append(a[i:i + w]) + i, k = i + w, k + 1 + nn = "".join("N" if j % 5 in (1, 2, 3) else a[j] for j in range(600)) + c = [] + + def add(name, scope, expect, why, data): + c.append((name, scope, expect, why, data.encode() if isinstance(data, str) else data)) + + add("valid_simple", "control", "valid", "F: '>' SeqID line plus IUPAC lines; 2 records, 600/800 nt, wrap 60", base) + add("valid_single_line", "control", "valid", "F: sequence 'can contain returns', does not require them", rec("ctg1", a, 0) + rec("ctg2", b, 0)) + add("valid_softmasked", "control", "valid", "soft-masking is ubiquitous; F and G are silent on case, so no basis to reject", rec("ctg1", soft) + rec("ctg2", b)) + add("valid_iupac", "control", "valid", "F: 'only use IUPAC symbols'; R,Y,K,M,S,W,N,B,D,H,V all are, one each in 600 nt", rec("ctg1", iupac) + rec("ctg2", b)) + add("valid_no_final_newline", "control", "valid", "missing last newline is legal text; robustness check", base.rstrip("\n")) + add("empty_file", "syntax", "error", "F/G: a file is a set of '>' records; zero bytes contains no sequence", b"") + add("seq_before_header", "syntax", "error", "F: the definition line must precede the sequence", seq("pre", 60) + "\n" + base) + add("empty_header", "syntax", "error", "F: '>' must be followed by a SeqID; a nameless record cannot be referenced", rec("ctg1", a) + ">\n" + "\n".join(wrap_(b)) + "\n") + add("empty_record", "syntax", "error", "F: a definition line is followed by its sequence; zero residues defines nothing", rec("ctg1", a) + ">ctg2_empty\n" + rec("ctg3", b)) + add("dup_id_identical_header", "identifier", "error", "F: 'SeqID must be unique'; G: 'unique within a genome'", rec("ctg1", a, desc="isolate X") + rec("ctg1", b, desc="isolate X")) + add("dup_first_token_id", "identifier", "error", "F/G: SeqID is the first token; differing descriptions do not make it unique", rec("ctg1", a, desc="isolate A") + rec("ctg1", b, desc="isolate B")) + add("dup_sequence_diff_ids", "content", "advisory", "legal, but byte-identical records under two IDs usually signal a merge error", rec("ctg1", a) + rec("ctg2", a)) + add("digit_in_sequence", "syntax", "error", "F: 'only use IUPAC symbols'; a digit is not one", rec("ctg1", put(a, 300, "7"))) + add("stop_star_in_sequence", "syntax", "error", "F: '*' is a protein stop symbol, not an IUPAC nucleotide symbol", rec("ctg1", put(a, 300, "*"))) + add("gap_dash_in_sequence", "syntax", "advisory", "F: outside alignments '-' 'will be stripped'; not rejected, but silently altered", rec("ctg1", put(a, 300, "-"))) + add("non_iupac_letter_x", "syntax", "error", "F: 'only use IUPAC symbols'; X is not a nucleotide code", rec("ctg1", put(a, 300, "X"))) + add("space_in_sequence", "syntax", "advisory", "not IUPAC, but many parsers strip blanks, so reasonable tools differ", rec("ctg1", put(a, 30, " "))) + add("blank_line_in_record", "syntax", "advisory", "F/G silent; some parsers skip blanks, indexers (faidx) object", ">ctg1\n" + "\n".join(wrap_(a)[:2] + [""] + wrap_(a)[2:]) + "\n") + add("crlf_line_endings", "syntax", "advisory", "F/G silent; tolerated by most parsers, but stray CR leaks into IDs of some tools", base.replace("\n", "\r\n")) + add("inconsistent_wrapping", "syntax", "advisory", "F: returns inside sequence are allowed (<=80 recommended); only line-indexed tools object", ">ctg1\n" + "\n".join(chunks) + "\n") + add("utf8_bom", "syntax", "advisory", "BOM makes line 1 not start with '>' for byte-exact readers; common Windows artefact", b"\xef\xbb\xbf" + base.encode()) + add("high_n_record", "content", "advisory", "valid FASTA; 60% scattered N is a quality signal, not a format error", rec("ctg1", nn)) + add("all_n_record", "content", "error", "no residue information; also violates G 'remove any Ns from the beginning or end'", rec("ctg1", "N" * 600)) + add("tiny_contig_50", "content", "advisory", "G: 'Contigs should be >199nt' is a recommendation; dropping short contigs is a QC choice", rec("ctg1", a) + rec("tiny", seq("t", 50))) + add("seqid_len_49", "ncbi", "valid", "G: SeqIDs must be <50 characters (F says <=25 for general GenBank; G governs genomes)", rec("ctg_" + "x" * 45, a)) + add("seqid_len_50", "ncbi", "error", "G: SeqIDs must be <50 characters", rec("ctg_" + "x" * 46, a)) + add("seqid_disallowed_char", "ncbi", "error", "F/G: SeqID 'can only include' letters, digits, - _ . : * #; '@' is not allowed", rec("ctg@1", a)) + add("contig_len_199", "ncbi", "advisory", "G: 'Contigs should be >199nt' (wording is a recommendation, so not scored)", rec("ctg1", seq("c", 199))) + add("contig_len_200", "ncbi", "valid", "G: >199 nt satisfied exactly", rec("ctg1", seq("c", 200))) + add("terminal_ns", "ncbi", "error", "G: 'Remove any Ns from the beginning or end of each sequence'", rec("ctg1", "N" * 10 + a[10:590] + "N" * 10)) + return c + + +def wrap_(s, w=60): + return [s[i:i + w] for i in range(0, len(s), w)] + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def corpus_sha(corpus): + return sha("".join("%s\t%s\n" % (n, sha(d)) for n, _, _, _, d in corpus).encode()) + + +# ----------------------------------------------------------------------------- runners +def sh(cmd, cwd, timeout, env=None): + try: + p = subprocess.run([str(x) for x in cmd], cwd=str(cwd), env=env, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, + text=True, errors="replace") + return p.returncode, p.stdout, p.stderr + except subprocess.TimeoutExpired: + return None, "", "timeout after %gs (process killed)" % timeout + except OSError as e: + return None, "", "cannot execute: %s" % e.strerror + + +def crashed(rc, err): + return rc is None or rc < 0 or "panic:" in err or "goroutine " in err or "Traceback" in err + + +def run_pyfv(exe, path, wd, to): + rc, out, err = sh([exe, "-f", path], wd, to, dict(os.environ, PYTHONWARNINGS="ignore")) + msg = err or out or "valid" + if crashed(rc, err) or rc not in (0, 1, 2, 4): + return "crash", "crash", rc, "crash or undocumented exit: " + msg + flag = "no" if rc == 0 else "yes" + return flag, flag, rc, msg + + +def run_falint(exe, path, wd, to): + rc, out, err = sh([exe, "-fasta", path], wd, to) + if crashed(rc, err): + return "crash", "crash", rc, err or out + m = re.search(r'msg="?([^"]*)"?', err) + flag = "yes" if rc != 0 or BAD_FALINT.search(err) else "no" + return flag, flag, rc, (m.group(1) if m else (err or out)) + + +def run_seqkit(exe, path, wd, to): + rc, out, err = sh([exe, "seq", "-v", "-t", "dna", path, "-o", os.devnull], wd, to) + err = ANSI.sub("", err) + if crashed(rc, err): + return "crash", "crash", rc, err + flag = "yes" if rc != 0 or "[ERRO]" in err or "[WARN]" in err else "no" + return flag, flag, rc, err or "no message" + + +def run_fg(exe, gate, path, wd, to): + wd.mkdir(parents=True, exist_ok=True) + for stale in ("fg.json", "fg.html", "fg.tsv", "fg_mqc.json"): + (wd / stale).unlink(missing_ok=True) # never parse a report left by an earlier run + cmd = [exe, path, "--profile", "assembly"] + gate + [ + "--json", wd / "fg.json", "--out", wd / "fg.html", "--tsv", wd / "fg.tsv", + "--multiqc", wd / "fg_mqc.json"] + rc, out, err = sh(cmd, wd, to) + if rc != 0: # FastaGuard exits 0 for PASS, WARN and FAIL once the reports are written + return "crash", "crash", rc, "fastaguard exit %s: %s" % (rc, (err or out).strip()) + try: + rep = json.loads((wd / "fg.json").read_text()) + g = rep["gate"] + verdict, blocked = rep["verdict"]["status"], not g["can_continue"] + except (OSError, ValueError, KeyError, TypeError): + return "crash", "crash", rc, "no usable JSON report: " + (err or out) + det = "verdict=%s gate=%s can_continue=%s blocking=%s advisory=%s" % ( + verdict, g["status"], str(g["can_continue"]).lower(), ",".join(g["blocking_findings"]) or "none", + ",".join(g["advisory_findings"]) or "none") + return ("yes" if verdict != "PASS" else "no"), ("yes" if blocked else "no"), rc, det + + +class Table2asn: + def __init__(self, exe, wd, to): + self.exe, self.wd, self.to, self.baseline = exe, wd, to, set() + self.template = wd / "table2asn-validation-template.sbt" + self.template.write_text(T2A_TEMPLATE) + + def parse_val(self, d, stem): + msgs = [] + val, stats = d / (stem + ".val"), d / (stem + ".stats") + if val.is_file(): + for line in val.read_text(errors="replace").splitlines(): + m = VAL_RE.match(line) + if m: + msgs.append((m.group(1).lower(), m.group(2), m.group(3))) + return msgs, True + if stats.is_file(): + txt = stats.read_text(errors="replace") + for n, lvl in re.findall(r"(\d+)\s+([A-Za-z]+)-level", txt): + if int(n) and lvl.lower() in ERR_LEVELS: + msgs.append((lvl.lower(), "stats", "%s %s-level message(s)" % (n, lvl))) + for code in re.findall(r"^(\w+\.\w+):\s+\d+", txt, re.M): + msgs.append(("warning", code, "(from .stats)")) + return msgs, True + return msgs, False + + def __call__(self, path, wd): + d = self.wd / "table2asn" / wd.name + d.mkdir(parents=True, exist_ok=True) + name = path.name[:-3] if path.name.endswith(".gz") else path.name + inp = d / name + if path.name.endswith(".gz"): + import gzip + with gzip.open(path, "rb") as src, open(inp, "wb") as dst: + shutil.copyfileobj(src, dst) + else: + shutil.copyfile(path, inp) + rc, out, err = sh([self.exe, "-t", self.template, "-i", inp] + T2A_ARGS, d, self.to) + console = ANSI.sub("", out + "\n" + err) + if crashed(rc, err): + return "crash", "crash", rc, err + msgs, have = self.parse_val(d, inp.stem if inp.suffix else inp.name) + errs = [m for m in msgs if m[0] in ERR_LEVELS] + cerr = ERR_RE.search(console) is not None + problem = " ".join(re.findall(r"Problem:\s*(.*)", console)) + if cerr and "Cannot open" in problem: + return "crash", "crash", rc, "console: " + problem + if cerr: + return "yes", "yes", rc, "console Error: " + (problem or console) + if errs: + return "yes", "yes", rc, "; ".join("%s [%s] %s" % (m[0], m[1], m[2][:70]) for m in errs[:3]) + cwarn = problem if re.search(r"(?i)(?:^|[\s\[])WARNING\s*:", console) else "" + new = [m for m in msgs if m[0].startswith("warn") and m[1] not in self.baseline] + if (rc != 0 or not have) and not (new or cwarn): + return "crash", "crash", rc, "no error evidence for non-zero exit / no artifact: " + console + if new or cwarn: + det = ["warning [%s] %s" % (m[1], m[2][:70]) for m in new[:3]] + return "yes", "no", rc, "; ".join(det + (["console Warning: " + cwarn] if cwarn else [])) + return "no", "no", rc, "only baseline warnings (%d)" % len(msgs) + + def set_baseline(self, path, wd): + self(path, wd) + d = self.wd / "table2asn" / wd.name + msgs, _ = self.parse_val(d, path.stem) + self.baseline = {m[1] for m in msgs if m[0].startswith("warn")} + + +# ----------------------------------------------------------------------------- reporting +def cell(row): + f = row["flagged"] + return ("yes" if row["strict"] == "yes" else "warn") if f == "yes" else f + + +def tally(rows, tool, key, scope): + t = {} + for e in ("error", "valid", "advisory"): + t.update({e + "_flagged": 0, e + "_total": 0, e + "_crash": 0}) + for r in rows: + if r["tool"] != tool or r[key] == "n.a." or r["expectation"] not in ("error", "valid", "advisory"): + continue + if scope != "all" and r["scope"] != scope: + continue + t[r["expectation"] + "_total"] += 1 + t[r["expectation"] + "_flagged"] += r[key] == "yes" + t[r["expectation"] + "_crash"] += r[key] == "crash" + return t + + +def clean(v, maps=()): + s = ANSI.sub("", str(v)) + for a, b in maps: + s = s.replace(a, b) + return " ".join(s.split())[:260].strip() or "-" + + +PREAMBLE = [ + "# NOTE: the corpus is author-constructed and favours FastaGuard's own check list.", + "# Expectations (valid/error/advisory) were fixed before any tool ran; advisory rows are not scored.", + "# The tools were not designed for the same purpose. flagged: yes|no|crash|n.a.;" + " strict: blocked / error-level.", +] + + +def write_tsv(path, header, rows, preamble=()): + lines = list(preamble) + ["\t".join(header)] + lines += ["\t".join(clean(x) for x in r) for r in rows] + path.write_text("\n".join(lines) + "\n") + + +def summary(rows, real_rows, tools_run): + out = ["# Author-constructed corpus favouring FastaGuard's check list; tools differ in purpose.", + "# block: scores (flagged = any report; strict = FastaGuard blocked / table2asn error-level)", + "\t".join(["tool", "definition", "scope", "error_flagged", "error_total", "error_crash", + "valid_flagged", "valid_total", "valid_crash", "advisory_flagged", + "advisory_total", "advisory_crash"])] + for tool in tools_run: + for key in ("flagged", "strict") if tool in TWO_LEVEL else ("flagged",): + for scope in ("all", "control", "syntax", "identifier", "content", "ncbi"): + t = tally(rows, tool, key, scope) + out.append("\t".join([tool, key, scope] + [str(t[k + s]) for k in ("error", "valid", "advisory") + for s in ("_flagged", "_total", "_crash")])) + out += ["# block: capability_matrix (yes = flagged and blocked/error-level or single-level tool;" + " warn = flagged below block/error level; no; crash; n.a.)", + "\t".join(["file", "scope", "expectation"] + TOOLS)] + for key in dict.fromkeys(r["file"] for r in rows): + by = {r["tool"]: r for r in rows if r["file"] == key} + first = by[TOOLS[0]] + out.append("\t".join([key, first["scope"], first["expectation"]] + [cell(by[t]) for t in TOOLS])) + if real_rows: + out += ["# block: real_files (no expectation; see real_results.tsv for detail)", + "\t".join(["file"] + TOOLS)] + for key in dict.fromkeys(r["file"] for r in real_rows): + by = {r["tool"]: r for r in real_rows if r["file"] == key} + out.append("\t".join([key] + [cell(by[t]) for t in TOOLS])) + return "\n".join(out) + "\n" + + +def tool_version(exe, args, maps): + rc, out, err = sh([exe] + args, exe.parent, 30, dict(os.environ, PYTHONWARNINGS="ignore")) + text = ANSI.sub("", (out or err).strip().splitlines()[0] if (out or err).strip() else "unknown") + m = re.search(r"version\s+(\S+)", text) + return clean(m.group(1) if m else text, maps) + + +def provenance(paths, archives, maps): + found = {} + if archives: + for p in Path(archives).rglob("*"): + if p.is_file(): + found.setdefault(p.name, []).append(p) + entries = [] + for tool, (url, digest, vargs, note) in SOURCES.items(): + exe = paths.get(tool) + verified = None + if digest: + hits = found.get(url.rsplit("/", 1)[-1], []) + verified = (sha(hits[0].read_bytes()) == digest) if hits else None + entries.append({ + "tool": tool, "version": tool_version(exe, vargs, maps) if exe else "not installed", + "source_url": url, "archive_sha256_pinned": digest, "archive_sha256_verified": verified, + "executable_sha256": sha(exe.read_bytes()) if exe else None, "notes": note}) + return entries + + +def resolve(flag, env, name): + cand = flag or os.environ.get(env) or shutil.which(name) + p = Path(cand).expanduser() if cand else None + return p.resolve() if p and p.is_file() and os.access(p, os.X_OK) else None + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--corpus-dir", type=Path, required=True) + ap.add_argument("--out-dir", type=Path, default=Path(__file__).resolve().parent) + ap.add_argument("--work-dir", type=Path) + ap.add_argument("--timeout", type=float, default=60.0) + ap.add_argument("--archives", type=Path) + ap.add_argument("--generate-only", action="store_true") + ap.add_argument("--real", nargs="+", default=[], metavar="[LABEL=]FASTA") + ap.add_argument("--real-timeout", type=float, default=600.0) + for flag in ("fastaguard", "py-fasta-validator", "fa-lint", "table2asn", "seqkit"): + ap.add_argument("--" + flag, type=Path) + args = ap.parse_args() + + corpus = build_corpus() + cdir = args.corpus_dir.resolve() + cdir.mkdir(parents=True, exist_ok=True) + for name, _, _, _, data in corpus: + (cdir / (name + ".fa")).write_bytes(data) + if args.generate_only: + print("# corpus_sha256\t" + corpus_sha(corpus)) + for name, scope, expect, why, data in corpus: + print("\t".join([name, scope, expect, sha(data)[:16], why])) + return 0 + + paths = {"py_fasta_validator": resolve(args.py_fasta_validator, "PY_FASTA_VALIDATOR", "py_fasta_validator"), + "fa-lint": resolve(args.fa_lint, "FA_LINT", "fa-lint"), + "table2asn": resolve(args.table2asn, "TABLE2ASN", "table2asn"), + "seqkit": resolve(args.seqkit, "SEQKIT", "seqkit"), + "fastaguard": resolve(args.fastaguard, "FASTAGUARD", "fastaguard")} + tmp = None if args.work_dir else tempfile.mkdtemp(prefix="fg-compare-") + work = (args.work_dir or Path(tmp)).resolve() + work.mkdir(parents=True, exist_ok=True) + maps = [(str(work), "$WORK"), (str(cdir), "$CORPUS")] + [ + (str(p.parent), "$TOOLDIR") for p in paths.values() if p] + to = [args.timeout] # one-element list so the real-file phase can raise it + t2a = Table2asn(paths["table2asn"], work, to[0]) if paths["table2asn"] else None + runners = { + "py_fasta_validator": paths["py_fasta_validator"] and (lambda p, w: run_pyfv(paths["py_fasta_validator"], p, w, to[0])), + "fa-lint": paths["fa-lint"] and (lambda p, w: run_falint(paths["fa-lint"], p, w, to[0])), + "table2asn": t2a, + "seqkit": paths["seqkit"] and (lambda p, w: run_seqkit(paths["seqkit"], p, w, to[0])), + "fastaguard_pipeline": paths["fastaguard"] and (lambda p, w: run_fg( + paths["fastaguard"], ["--gate", "pipeline"], p, w / "fgp", to[0])), + "fastaguard_ncbi": paths["fastaguard"] and (lambda p, w: run_fg( + paths["fastaguard"], ["--gate", "submission", "--submission-target", "ncbi"], p, w / "fgn", to[0])), + } + if t2a: + wd = work / "case" / "valid_simple" + wd.mkdir(parents=True, exist_ok=True) + t2a.set_baseline(cdir / "valid_simple.fa", wd) + + def run_all(label, scope, expect, path, extra=()): + wd = work / "case" / label + wd.mkdir(parents=True, exist_ok=True) + res = [] + for tool in TOOLS: + fn = runners[tool] + f, s, rc, det = fn(path, wd) if fn else ("n.a.", "n.a.", None, "tool not found") + res.append({"file": label, "scope": scope, "expectation": expect, "tool": tool, + "flagged": f, "strict": s, "exit_code": "-" if rc is None else rc, + "detail": clean(det, maps + list(extra))}) + return res + + rows = [] + for name, scope, expect, _, _ in corpus: + rows += run_all(name, scope, expect, cdir / (name + ".fa")) + real_rows = [] + to[0] = args.real_timeout + if t2a: + t2a.to = to[0] + seen = set() + for spec in args.real: + label, sep, rest = spec.partition("=") + p = Path(rest if sep else spec) + label = label if sep else p.name + if label in seen: + sys.exit("duplicate --real label %r: use LABEL=PATH to make labels unique" % label) + seen.add(label) + real_rows += run_all(label, "real", "n/a", p.resolve(), [(str(p.resolve().parent), "$REAL")]) + + cols = ["file", "scope", "expectation", "tool", "flagged", "strict", "exit_code", "detail"] + out = args.out_dir + out.mkdir(parents=True, exist_ok=True) + write_tsv(out / "results.tsv", cols, [[r[c] for c in cols] for r in rows], PREAMBLE) + if real_rows: + write_tsv(out / "real_results.tsv", cols, [[r[c] for c in cols] for r in real_rows], + ["# Real files: no expectation, nothing scored. Same decision rules as results.tsv."]) + tools_run = [t for t in TOOLS if runners[t]] + (out / "summary.tsv").write_text(summary(rows, real_rows, tools_run)) + info = {"note": "Author-constructed corpus favouring FastaGuard's check list; tools differ in purpose.", + "corpus_sha256": corpus_sha(corpus), "corpus_files": len(corpus), + "platform": "%s %s" % (platform.system(), platform.machine()), + "tools": provenance(paths, args.archives, maps)} + (out / "tool_versions.json").write_text(json.dumps(info, indent=2, sort_keys=True) + "\n") + if tmp: + shutil.rmtree(tmp, ignore_errors=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/paper/comparison/summary.tsv b/paper/comparison/summary.tsv new file mode 100644 index 0000000..4cd9138 --- /dev/null +++ b/paper/comparison/summary.tsv @@ -0,0 +1,93 @@ +# Author-constructed corpus favouring FastaGuard's check list; tools differ in purpose. +# block: scores (flagged = any report; strict = FastaGuard blocked / table2asn error-level) +tool definition scope error_flagged error_total error_crash valid_flagged valid_total valid_crash advisory_flagged advisory_total advisory_crash +py_fasta_validator flagged all 5 13 0 0 7 0 3 10 0 +py_fasta_validator flagged control 0 0 0 0 5 0 0 0 0 +py_fasta_validator flagged syntax 3 7 0 0 0 0 3 6 0 +py_fasta_validator flagged identifier 2 2 0 0 0 0 0 0 0 +py_fasta_validator flagged content 0 1 0 0 0 0 0 3 0 +py_fasta_validator flagged ncbi 0 3 0 0 2 0 0 1 0 +fa-lint flagged all 9 13 0 0 7 0 5 10 0 +fa-lint flagged control 0 0 0 0 5 0 0 0 0 +fa-lint flagged syntax 6 7 0 0 0 0 5 6 0 +fa-lint flagged identifier 2 2 0 0 0 0 0 0 0 +fa-lint flagged content 1 1 0 0 0 0 0 3 0 +fa-lint flagged ncbi 0 3 0 0 2 0 0 1 0 +table2asn flagged all 7 13 3 0 7 0 3 10 0 +table2asn flagged control 0 0 0 0 5 0 0 0 0 +table2asn flagged syntax 5 7 1 0 0 0 2 6 0 +table2asn flagged identifier 0 2 2 0 0 0 0 0 0 +table2asn flagged content 1 1 0 0 0 0 1 3 0 +table2asn flagged ncbi 1 3 0 0 2 0 0 1 0 +table2asn strict all 7 13 3 0 7 0 2 10 0 +table2asn strict control 0 0 0 0 5 0 0 0 0 +table2asn strict syntax 5 7 1 0 0 0 1 6 0 +table2asn strict identifier 0 2 2 0 0 0 0 0 0 +table2asn strict content 1 1 0 0 0 0 1 3 0 +table2asn strict ncbi 1 3 0 0 2 0 0 1 0 +seqkit flagged all 5 13 0 0 7 0 0 10 0 +seqkit flagged control 0 0 0 0 5 0 0 0 0 +seqkit flagged syntax 5 7 0 0 0 0 0 6 0 +seqkit flagged identifier 0 2 0 0 0 0 0 0 0 +seqkit flagged content 0 1 0 0 0 0 0 3 0 +seqkit flagged ncbi 0 3 0 0 2 0 0 1 0 +fastaguard_pipeline flagged all 11 13 0 0 7 0 7 10 0 +fastaguard_pipeline flagged control 0 0 0 0 5 0 0 0 0 +fastaguard_pipeline flagged syntax 7 7 0 0 0 0 3 6 0 +fastaguard_pipeline flagged identifier 2 2 0 0 0 0 0 0 0 +fastaguard_pipeline flagged content 1 1 0 0 0 0 3 3 0 +fastaguard_pipeline flagged ncbi 1 3 0 0 2 0 1 1 0 +fastaguard_pipeline strict all 10 13 0 0 7 0 4 10 0 +fastaguard_pipeline strict control 0 0 0 0 5 0 0 0 0 +fastaguard_pipeline strict syntax 7 7 0 0 0 0 3 6 0 +fastaguard_pipeline strict identifier 2 2 0 0 0 0 0 0 0 +fastaguard_pipeline strict content 1 1 0 0 0 0 1 3 0 +fastaguard_pipeline strict ncbi 0 3 0 0 2 0 0 1 0 +fastaguard_ncbi flagged all 13 13 0 0 7 0 7 10 0 +fastaguard_ncbi flagged control 0 0 0 0 5 0 0 0 0 +fastaguard_ncbi flagged syntax 7 7 0 0 0 0 3 6 0 +fastaguard_ncbi flagged identifier 2 2 0 0 0 0 0 0 0 +fastaguard_ncbi flagged content 1 1 0 0 0 0 3 3 0 +fastaguard_ncbi flagged ncbi 3 3 0 0 2 0 1 1 0 +fastaguard_ncbi strict all 13 13 0 0 7 0 5 10 0 +fastaguard_ncbi strict control 0 0 0 0 5 0 0 0 0 +fastaguard_ncbi strict syntax 7 7 0 0 0 0 3 6 0 +fastaguard_ncbi strict identifier 2 2 0 0 0 0 0 0 0 +fastaguard_ncbi strict content 1 1 0 0 0 0 1 3 0 +fastaguard_ncbi strict ncbi 3 3 0 0 2 0 1 1 0 +# block: capability_matrix (yes = flagged and blocked/error-level or single-level tool; warn = flagged below block/error level; no; crash; n.a.) +file scope expectation py_fasta_validator fa-lint table2asn seqkit fastaguard_pipeline fastaguard_ncbi +valid_simple control valid no no no no no no +valid_single_line control valid no no no no no no +valid_softmasked control valid no no no no no no +valid_iupac control valid no no no no no no +valid_no_final_newline control valid no no no no no no +empty_file syntax error no yes crash no yes yes +seq_before_header syntax error yes yes yes yes yes yes +empty_header syntax error no yes no yes yes yes +empty_record syntax error no yes yes no yes yes +dup_id_identical_header identifier error yes yes crash no yes yes +dup_first_token_id identifier error yes yes crash no yes yes +dup_sequence_diff_ids content advisory no no no no warn warn +digit_in_sequence syntax error yes yes yes yes yes yes +stop_star_in_sequence syntax error yes yes yes yes yes yes +gap_dash_in_sequence syntax advisory yes yes warn no yes yes +non_iupac_letter_x syntax error no no yes yes yes yes +space_in_sequence syntax advisory yes yes no no yes yes +blank_line_in_record syntax advisory no yes no no no no +crlf_line_endings syntax advisory no no no no no no +inconsistent_wrapping syntax advisory no yes no no no no +utf8_bom syntax advisory yes yes yes no yes yes +high_n_record content advisory no no yes no yes warn +all_n_record content error no yes yes no yes yes +tiny_contig_50 content advisory no no no no warn yes +seqid_len_49 ncbi valid no no no no no no +seqid_len_50 ncbi error no no no no no yes +seqid_disallowed_char ncbi error no no no no no yes +contig_len_199 ncbi advisory no no no no warn yes +contig_len_200 ncbi valid no no no no no no +terminal_ns ncbi error no no yes no warn yes +# block: real_files (no expectation; see real_results.tsv for detail) +file py_fasta_validator fa-lint table2asn seqkit fastaguard_pipeline fastaguard_ncbi +ecoli_GCF_000005845.2 no no no no no no +neurospora_GCF_000182925.2 no no yes no warn warn diff --git a/paper/comparison/tool_versions.json b/paper/comparison/tool_versions.json new file mode 100644 index 0000000..bbe461c --- /dev/null +++ b/paper/comparison/tool_versions.json @@ -0,0 +1,53 @@ +{ + "corpus_files": 30, + "corpus_sha256": "0d8aa8f02cbac6164750ad719b1bd36c973f55bd924ee733478e9b67934f9181", + "note": "Author-constructed corpus favouring FastaGuard's check list; tools differ in purpose.", + "platform": "Darwin arm64", + "tools": [ + { + "archive_sha256_pinned": "ca29c20e1be31ef190f923b62ddb7fccda8c1c3ba9cf699a6ccaccce10ea011a", + "archive_sha256_verified": true, + "executable_sha256": "3360f4602a710d3ce7d239fe89894c94a9cd07d8ed10612786c3cf0d9dccbeb7", + "notes": "PyPI sdist of project py-fasta-validator; built in a python3.11 venv with setuptools==80.9.0 because the shipped CLI imports pkg_resources", + "source_url": "https://files.pythonhosted.org/packages/source/p/py_fasta_validator/py_fasta_validator-0.6.tar.gz", + "tool": "py_fasta_validator", + "version": "0.6" + }, + { + "archive_sha256_pinned": "6edcb792489b18bcfd03156912d05227ac05cdf657c517041c40489566470436", + "archive_sha256_verified": true, + "executable_sha256": "5330aa5003cee754d26951c53f251a7546fe2006962cfeb1a281353fc079026d", + "notes": "official release binary", + "source_url": "https://github.com/GallVp/fa-lint/releases/download/1.2.0/fa-lint-darwin-arm64.tar.gz", + "tool": "fa-lint", + "version": "1.2.0" + }, + { + "archive_sha256_pinned": "348b9d5f1b05065f4e5e61b6c2350e5ff044de2d6dc14765be6ac02c00e59341", + "archive_sha256_verified": true, + "executable_sha256": "94f276a1283be339f10272f71963bce22053a8463987bac3c1ed55a7078583c1", + "notes": "digest equals the one pinned in docs/evidence/fastaguard-v0.7-ncbi-genome-policy.md; NCBI publishes no separate digest", + "source_url": "https://ftp.ncbi.nlm.nih.gov/asn1-converters/by_program/table2asn/mac.table2asn.gz", + "tool": "table2asn", + "version": "table2asn: 1.29.324" + }, + { + "archive_sha256_pinned": "7b3a6a7c9be07a06c5b308d1aad769c09762b36b387690eed8b91b00d01503b7", + "archive_sha256_verified": true, + "executable_sha256": "4939e4d15cca77bdfff5d1904447b7721ce93d2026462e7d302de95e6f664537", + "notes": "official release binary; md5 2b335eb27f69af97a252818a85145684 also matched", + "source_url": "https://github.com/shenwei356/seqkit/releases/download/v2.14.0/seqkit_darwin_arm64.tar.gz", + "tool": "seqkit", + "version": "seqkit v2.14.0" + }, + { + "archive_sha256_pinned": null, + "archive_sha256_verified": null, + "executable_sha256": "8a76785d89f2f7c7f569dba0bbc741124f8b506851f5bff25d5eb134d87b897f", + "notes": "no archive; executable hash recorded", + "source_url": "local build of `git archive v1.0.0` of https://github.com/ehsanestaji/FastaGuard", + "tool": "fastaguard", + "version": "fastaguard 1.0.0" + } + ] +} diff --git a/paper/cover-letter.md b/paper/cover-letter.md new file mode 100644 index 0000000..576da67 --- /dev/null +++ b/paper/cover-letter.md @@ -0,0 +1,63 @@ +# Cover letter — *Bioinformatics* Application Note (draft) + +**To:** The Editors, *Bioinformatics* (Oxford University Press) + +**From:** Ehsan Estaji and Jian-Feng Mao + +**Re:** Application Note submission — *FastaGuard: FASTA preflight QC for modern bioinformatics pipelines* + +Dear Editors, + +Please consider our manuscript as an **Application Note** in *Bioinformatics*. + +FastaGuard is an open-source (MIT), streaming-first Rust command-line tool that +provides **FASTA preflight quality control** for genome-assembly pipelines. It +validates FASTA structure, flags identifier and composition hazards, and emits a +versioned multi-format report contract (HTML, JSON, TSV, and MultiQC-compatible +JSON) with explicit pipeline gates (`gate.can_continue`) before expensive +downstream steps such as QUAST, BUSCO, BlobToolKit, CheckM, annotation, or +official repository validators. The contribution is a pipeline-native preflight +*contract*—not interpretive assembly QC and not a substitute for those tools—so +workflow authors can stop or route early on FASTA-level blockers. The software +is published on Bioconda and BioContainers (v1.0.0) with merged nf-core module +and Snakemake wrapper integrations. + +We believe *Bioinformatics* Application Notes are the appropriate venue for a +short, citable description of freely available bioinformatics software with clear +Availability and Implementation. The manuscript follows Application Note length +and abstract conventions (Summary / Availability and Implementation / Contact / +Supplementary information). Source and documentation: +https://github.com/ehsanestaji/FastaGuard + +**Suggested topical area:** genome assembly / sequence analysis software tools +(Editors may reassign as appropriate.) + +**Software availability.** The submitted release (v1.0.0) is open source (MIT), +available at the stable URL above, and archived on Zenodo +(doi:10.5281/zenodo.23622311) as required by the journal software policy. + +**Preprint disclosure.** The same manuscript has been submitted to bioRxiv as a +preprint. We will update the preprint with the published DOI after acceptance. + +**AI use disclosure.** AI coding assistants (Cursor-based agents and Anthropic +Claude Code) were used for planning, manuscript scaffolding, repository evidence +cross-checks, prose drafting support, and revision. The software, code base, and +benchmarking results were completed first; the manuscript was then drafted solely +from that code base and those results, not from a free-form prompt. The authors +reviewed all output and remain responsible for the manuscript, code, evidence +interpretation, and all scientific claims. The same statement appears in the +manuscript. + +Both corresponding authors can be reached at ehsan.estaji@umu.se and +jianfeng.mao@umu.se. We confirm that the work is original, is not under +consideration at another journal, and that all authors approve submission. + +Thank you for your consideration. + +Sincerely, + +Ehsan Estaji + +Jian-Feng Mao + +Umeå Plant Science Centre, Department of Plant Physiology, Umeå University diff --git a/paper/figures/README.md b/paper/figures/README.md new file mode 100644 index 0000000..2cb5746 --- /dev/null +++ b/paper/figures/README.md @@ -0,0 +1,17 @@ +# Figures + +Place submission-ready artwork here. + +Suggested primary figure for the Application Note: + +| File (suggested) | Content | +| --- | --- | +| `preflight-overview.pdf` | Edge-to-edge schematic: FASTA → FastaGuard (PASS/WARN/FAIL + report bundle) → QUAST / BUSCO / BlobToolKit / MultiQC / submission validators | + +Optional supplements (not counted against the tight main-text budget if moved online-only): + +- HTML report screenshot +- Compare-mode cohort table screenshot +- Gate JSON excerpt highlighting `gate.can_continue` + +Export vector PDF or 350+ dpi PNG/TIFF for journal upload. diff --git a/paper/main.tex b/paper/main.tex new file mode 100644 index 0000000..530e8aa --- /dev/null +++ b/paper/main.tex @@ -0,0 +1,476 @@ +% FastaGuard — Bioinformatics (OUP) Application Note +% USER-CONFIRMED venue: Application Note (~4 pages / ~2,000–2,600 words) +% Author / funding / disclosure front-matter aligned with VariantFlow +% (https://github.com/ehsanestaji/VariantFlow paper/bioinformatics-application-note/). +% Scientific claims and evidence remain FastaGuard-specific (docs/evidence/). +% Build: make (see README.md and HANDOFF.md) +\pdfminorversion=5 +\documentclass[11pt,a4paper]{article} + +% Draft layout approximating journal constraints without requiring oup-authoring-template. +% For camera-ready / revision, switch to the OUP General Template with Bioinformatics +% options: \documentclass[numsec,webpdf,modern,large]{...} and author-date bib style. +\usepackage[margin=1in]{geometry} +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage{times} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage{array} +\usepackage{hyperref} +\usepackage{xcolor} +\usepackage{natbib} +\usepackage{url} +\usepackage{microtype}\DisableLigatures[-]{encoding=*,family=tt*}% keep "--" in CLI flags as two hyphens +\usepackage{lineno} +\usepackage{amssymb} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,fit,calc} + +\definecolor{fgblue}{HTML}{1F4E79} + +\hypersetup{ + colorlinks=true, + linkcolor=fgblue, + citecolor=fgblue, + urlcolor=fgblue, + pdftitle={FastaGuard: FASTA preflight QC for modern bioinformatics pipelines}, + pdfauthor={Ehsan Estaji and Jian-Feng Mao}, + pdfkeywords={FASTA, quality control, genome assembly, preflight, bioinformatics pipelines, Rust} +} +\setlength{\emergencystretch}{2em} + +\title{FastaGuard: FASTA preflight QC for modern bioinformatics pipelines} +\author{ +Ehsan Estaji\textsuperscript{1,$\dag$} +\href{https://orcid.org/0009-0005-0838-228X}{\small(ORCID 0009-0005-0838-228X)} \qquad +Jian-Feng Mao\textsuperscript{1,$\dag$} +\href{https://orcid.org/0000-0001-9735-8516}{\small(ORCID 0000-0001-9735-8516)} \\[6pt] +\small \textsuperscript{1}Ume{\aa} Plant Science Centre, +Department of Plant Physiology, Ume{\aa} University, +SE-901\,87 Ume{\aa}, Sweden \\[4pt] +\small \textsuperscript{$\dag$}Correspondence: + \href{mailto:ehsan.estaji@umu.se}{\texttt{ehsan.estaji@umu.se}}, + \href{mailto:jianfeng.mao@umu.se}{\texttt{jianfeng.mao@umu.se}} +} +\date{} + +\begin{document} +\linenumbers +\maketitle + +\begin{abstract} +\noindent\textbf{Summary:} +FastaGuard is a streaming, explainable FASTA preflight QC tool that validates assembly +FASTA files, detects structural and composition red flags, and emits a versioned +multi-format report with an explicit pipeline gate before expensive downstream +analysis such as QUAST, +BUSCO, BlobToolKit, CheckM, annotation, or official repository validation. + +\noindent\textbf{Availability and Implementation:} +Source code and documentation are freely available under the MIT license at +\url{https://github.com/ehsanestaji/FastaGuard} +(release tag \texttt{v1.0.0}: +\url{https://github.com/ehsanestaji/FastaGuard/releases/tag/v1.0.0}; +archived at \href{https://doi.org/10.5281/zenodo.23622311}{doi:10.5281/zenodo.23622311}). +Install via Bioconda +(\texttt{mamba install -c conda-forge -c bioconda fastaguard=1.0.0}), +BioContainers, or GitHub release binaries. +Implemented in Rust; supported on Linux and macOS, including Bioconda builds for +\texttt{linux-64}, \texttt{linux-aarch64}, \texttt{osx-64}, and \texttt{osx-arm64}. + +\noindent\textbf{Contact:} +\href{mailto:ehsan.estaji@umu.se}{\texttt{ehsan.estaji@umu.se}}; +\href{mailto:jianfeng.mao@umu.se}{\texttt{jianfeng.mao@umu.se}} + +\noindent\textbf{Supplementary information:} +Finding-catalog excerpts, evidence packs, and reproducibility notes are maintained +with the software repository under \texttt{docs/} and \texttt{paper/comparison/}. +\end{abstract} + +\vspace{0.4em} +\noindent\textbf{Keywords:} FASTA, quality control, genome assembly, preflight, +bioinformatics pipelines, Rust + +\section{Introduction} +Genome assemblies and reference FASTA files are foundational inputs for +bioinformatics pipelines, yet FASTA-level checks are usually assembled from +separate utilities. +Teams commonly combine ad hoc scripts, general sequence utilities such as +SeqKit~\citep{shen2016seqkit,shen2024seqkit2}, assembly evaluators such as +QUAST~\citep{gurevich2013quast}, biological completeness tools such as +BUSCO~\citep{simao2015busco}, genome-quality estimation with +CheckM~\citep{parks2015checkm}, contamination-oriented exploration with +BlobToolKit~\citep{challis2020blobtoolkit}, and late-stage report aggregation with +MultiQC~\citep{ewels2016multiqc}. +Raw-read QC tools remain essential before assembly, but they do not answer +whether a finished assembly FASTA is structurally sound for the next stage. +Each of the tools above is valuable for its intended question; none is designed +as a first-step gate for a narrower but frequent need: +whether a FASTA file is structurally valid, alphabet-sane, interpretable, and +safe to pass into expensive downstream steps. + +The cost of skipping that earlier layer is practical. +Malformed headers, empty records, duplicate identifiers, invalid IUPAC +characters, extreme ambiguity, and high-N scaffolds may be caught only when a +downstream tool reaches the offending record, or be silently normalised along +the way; we did not quantify the resulting delay. +Pipeline authors then reimplement the same checks, with incompatible report +shapes and unclear stop/go semantics for workflow engines such as +Nextflow~\citep{ditommaso2017nextflow} and Snakemake~\citep{molder2021snakemake}. +Dedicated validators exist: py\_fasta\_validator~\citep{edwards2023pyfastavalidator} +and fa-lint~\citep{falint} check headers, alphabet, and identifier uniqueness +but, to our knowledge, provide no structured per-finding report, and AssemblyQC +embeds such input checks inside a single pipeline~\citep{rashid2024assemblyqc}. +Statistics toolkits such as SeqKit, SeqFu~\citep{telatin2021seqfu}, and +gfastats~\citep{formenti2022gfastats} mainly summarise sequences and define no +gate semantics. +To our knowledge, none combines severity-graded findings, an explicit +blocking-versus-advisory gate, and a schema-versioned machine-readable report +(JSON, TSV, MultiQC) that workflow engines can branch on. + +FastaGuard addresses that gap as a dedicated \emph{preflight} layer +(Fig.~\ref{fig:overview}). +It is intentionally not a competitor to interpretive assembly QC: +it does not claim biological completeness, contamination taxonomy, or repository +acceptance, and it does not replace official NCBI, ENA, or DDBJ validators. +Instead it answers whether the FASTA itself is ready for those tools, and it +returns a versioned PASS/WARN/FAIL contract, with verdicts, gates, and findings +that are deterministic for a given input and options, that humans and +Nextflow, Snakemake, or nf-core~\citep{ewels2020nfcore} workflows can consume +without scraping logs or HTML. +The design is therefore a contract as much as a report: +field names, documented finding identifiers, provenance checksums, and gate +continuation booleans are intended to remain stable within a schema major +version. + +\section{Implementation} +FastaGuard is a streaming-first command-line tool implemented in Rust and +distributed as a single binary. +The primary assembly workflow is: + +\begin{verbatim} +fastaguard sample.fa --profile assembly --gate pipeline \ + --outdir reports --prefix sample-01 +\end{verbatim} + +\noindent +which writes a four-report bundle +(\texttt{.html}, \texttt{.json}, \texttt{.tsv}, and MultiQC-compatible +\texttt{\_mqc.json}) under a chosen prefix. +Verdicts, gates, and findings are deterministic for a given input and options; +provenance timestamps and durations vary between runs. +JSON is the source of truth for machine routing. +A committed JSON Schema and finding catalog document field names, finding +identifiers, severity semantics, and suggested actions +(\texttt{fastaguard --schema}, \texttt{--finding-catalog}, +\texttt{--explain-finding}). + +\paragraph{Validity and composition checks.} +Under the assembly profile, FastaGuard reports structural failures (malformed +headers, empty records, sequence before the first header, empty input), +duplicate identifiers and duplicate sequences, invalid nucleotide/IUPAC +symbols, core length statistics including N50/N90 and L50/L90, and composition +signals (GC/AT/N and ambiguity rates, gap runs, tiny-contig advisories, and +composition or length outlier hints). +Findings are explainable: each carries an identifier, evidence bounds, and +routing language that distinguishes blockers from advisories. + +\paragraph{Pipeline gates and process exit contract.} +Optional gates make stop/go policy explicit. +The recommended workflow preset \texttt{--gate pipeline} marks duplicate identifiers +(full-header or first-token), invalid characters, invalid FASTA structure, and +high-N content as blocking by default, while GC and length outliers remain advisory routing signals unless +elevated with \texttt{--fail-on}. +JSON exposes \texttt{gate.status}, \texttt{gate.blocking\_findings}, +\texttt{gate.advisory\_findings}, \texttt{gate.can\_continue}, and +\texttt{provenance.input\_sha256}. +From v0.6 onward, process exit codes separate execution from QC status: +exit~0 means report generation completed for PASS, WARN, and FAIL results; +exit~2 covers argument errors; exit~3 covers configuration, I/O, or runtime +failures. +Workflows should therefore branch on \texttt{gate.can\_continue} (or related +gate fields), not on process status alone. +\texttt{machine\_summary.safe\_for\_downstream} is intentionally stricter (true +only for overall PASS) and is not a synonym for gate continuation. + +\paragraph{Submission-readiness, compare mode, and reference checks.} +\texttt{--gate submission} with \texttt{--submission-target generic|ncbi} +performs FASTA-level submission preflight before official validators. +For NCBI genome targeting, FastaGuard applies a documented, deliberately +conservative \texttt{ncbi\_genome} policy snapshot (first-token SeqID syntax and +length limits, minimum record length, terminal Ns) based on NCBI submission and +FASTA guidance~\citep{ncbi_genome_submit,ncbi_fasta_format}; in a local +differential check it was stricter than table2asn~\citep{ncbi_table2asn} on several boundary cases +(for example, a 199-bp record or a 50-character SeqID), and it does not replace +NCBI FCS~\citep{astashyn2024fcsgx}, annotation validation, or repository +acceptance decisions. +\texttt{fastaguard compare} triages many FASTA files into cohort HTML/JSON/TSV +outputs for starter cohort routing before interpretive QC. +A separate \texttt{reference} mode checks whether explicitly supplied companion +declarations (FASTA index, sequence dictionary, alignment/variant headers, +GFF3 sequence declarations) agree with a canonical FASTA under +\texttt{strict}, \texttt{coordinate}, or \texttt{advisory} policies. + +\paragraph{Readiness categories.} +Beyond a single verdict, reports expose readiness facets spanning file, +structure, alphabet, index, assembly, submission, and machine concerns. +That decomposition helps operators see \emph{why} a file is not yet ready +(for example, identifier hazards versus composition advisories) without reading +an unstructured log. +HTML reports remain human-oriented, while JSON/TSV remain stable for engines +and for MultiQC custom content. + +\paragraph{Packaging and workflow adoption.} +FastaGuard is published on Bioconda~\citep{gruning2018bioconda} and +BioContainers~\citep{daveigaleprevost2017biocontainers}, ships GitHub release +binaries for x86\_64 Linux and for Intel and Apple-silicon macOS (Linux aarch64 +is available through Bioconda), and has merged nf-core and Snakemake wrapper +integrations for assembly preflight. +A typical install path is +\texttt{mamba install -c conda-forge -c bioconda fastaguard=1.0.0} +(matching GitHub release tag \texttt{v1.0.0}), with a pinned BioContainers image +for containerized workflow engines. +These distribution paths are part of the Application Note claim: the tool is +meant to be installed and gated in pipelines with low friction, not only built +from source by specialists; we make no claim about third-party adoption. +The archived release is citable~\citep{fastaguard2026}, and source and +published packages remain openly reachable under the MIT license. + +\begin{figure}[t] + \centering + \begin{tikzpicture}[ + font=\scriptsize\sffamily, + >=Latex, + box/.style={draw, rounded corners=2pt, align=center, inner sep=5pt, + minimum height=0.85cm, minimum width=2.15cm}, + io/.style={box, fill=gray!12}, + fg/.style={box, fill=blue!8, thick, minimum width=2.6cm}, + down/.style={box, fill=orange!10, minimum width=1.55cm}, + arr/.style={->, thick}, + lab/.style={font=\scriptsize\sffamily\itshape, text=black!70} + ] + \node[io] (fa) {Assembly\\FASTA}; + \node[fg, right=0.55cm of fa] (fg) {FastaGuard\\preflight}; + \node[io, right=0.55cm of fg] (rep) {HTML / JSON\\TSV / MultiQC}; + \node[down, below left=0.85cm and -0.15cm of rep] (q) {QUAST}; + \node[down, below=0.85cm of rep] (b) {BUSCO}; + \node[down, below right=0.85cm and -0.15cm of rep] (bl) {BlobToolKit\\/ CheckM}; + \node[io, right=0.7cm of rep] (sub) {Official\\validators}; + \draw[arr] (fa) -- (fg); + \draw[arr] (fg) -- (rep); + \draw[arr] (rep.south west) -- (q.north); + \draw[arr] (rep.south) -- (b.north); + \draw[arr] (rep.south east) -- (bl.north); + \draw[arr] (rep) -- (sub); + \node[lab, above=0.05cm of fg] {PASS / WARN / FAIL; \texttt{gate.can\_continue}}; + \node[lab, below=2.0cm of fg] + {Fix FASTA-level blockers first; interpretive QC remains downstream}; + \end{tikzpicture} + \caption{FastaGuard as the FASTA preflight layer before interpretive assembly + QC and official submission validation. + Machine-readable reports support workflow stop/go decisions without replacing + QUAST, BUSCO, BlobToolKit, CheckM, MultiQC, or repository validators.} + \label{fig:overview} +\end{figure} + +\section{Results} +Committed public-evidence summaries for FastaGuard v0.6.0 illustrate the intended +behaviour on synthetic contract fixtures and two exact NCBI RefSeq +assemblies~\citep{oleary2016refseq} (Table~\ref{tab:evidence}). +Portable JSON and TSV summaries in the repository record input byte sizes, +SHA-256 digests, structural metrics, finding identifiers, and scoped provenance +separately for the observed executable and the verified source tree. +On a recorded macOS arm64 run, a clean \emph{Escherichia coli} K-12 +MG1655~\citep{blattner1997ecoli} assembly (\texttt{GCF\_000005845.2}; 4\,641\,652~bp) completed pipeline-gated +preflight in 0.0569~s with PASS and no findings. +A \emph{Neurospora crassa} OR74A~\citep{galagan2003neurospora} assembly (\texttt{GCF\_000182925.2}; 41\,102\,378~bp) +completed in 0.3441~s with WARN for gap-pattern advisories and +\texttt{gate.can\_continue} still true under the pipeline gate, routing to deeper +assembly or composition follow-up rather than hard failure. +An intentionally broken local fixture returned FAIL with blocking findings +(duplicate identifiers, invalid characters, high-N content) while still exiting~0 after +writing a complete FAIL report---matching the v0.6 workflow contract that +separates QC status from process success. +Re-running all five evidence-pack inputs with a build of the v1.0.0 tag reproduced +the same verdicts, gate statuses, blocking findings, lengths, sequence counts, +input SHA-256 digests, and exit status; timings differed between runs and are +indicative only. + +These examples are contract demonstrations, not a representative biological +benchmark across taxa or assemblers. +They show that clean references can pass quickly, that advisory gap patterns can +WARN without blocking continuation, and that FASTA-level blockers remain machine +visible in FAIL reports that workflow engines can parse. +Compare mode and submission gates extend the same contract to cohort triage and +FASTA-level submission hazards; reference mode adds companion-declaration +compatibility checks when indexes, dictionaries, or headers must agree with a +canonical FASTA before mapping or annotation workflows proceed. + +\begin{table}[t] + \centering + \caption{Frozen FastaGuard v0.6.0 public-evidence outcomes (assembly profile, + pipeline gate) from + \texttt{docs/evidence/results/v0.6/evidence\_summary.tsv}. + All runs used \texttt{--min-contig-length 1}; with the default minimum, the + 604-bp synthetic fixture returns WARN through an advisory tiny-contig finding. + Elapsed times are single-machine contextual measurements (macOS arm64), not + cross-platform guarantees. + The \emph{N.\ crassa} count includes the mitochondrial sequence NC\_026614.1 + (64\,840~bp). Software Availability cites the published \texttt{v1.0.0} + release.} + \label{tab:evidence} + \begin{tabular}{@{}llllr@{}} + \toprule + Input & Scale & Verdict & Gate & Time (s) \\ + \midrule + Synthetic valid fixture & 604~bp / 4 seqs & PASS & PASS & 0.0194 \\ + Problem fixture & 145~bp / 5 seqs & FAIL & FAIL & 0.0185 \\ + \emph{E.\ coli} \texttt{GCF\_000005845.2} & 4\,641\,652~bp / 1 seq & PASS & PASS & 0.0569 \\ + \emph{N.\ crassa} \texttt{GCF\_000182925.2} & 41\,102\,378~bp / 21 seqs & WARN & WARN & 0.3441 \\ + \bottomrule + \end{tabular} +\end{table} + +To place these behaviours against existing tools, we ran +py\_fasta\_validator~0.6, fa-lint~1.2.0, table2asn~1.29.324~\citep{ncbi_table2asn}, +SeqKit~2.14.0, and FastaGuard v1.0.0 on 30 small constructed FASTA files +(7 valid, 13 defective, and 10 advisory files that are not scored; +Table~\ref{tab:comparison}), with expectations written down before any tool was +run. +No tool flagged a valid file. +FastaGuard's NCBI gate flagged all 13 defective files and its pipeline gate 11 +(blocking 10); fa-lint flagged 9, table2asn 7, and py\_fasta\_validator and SeqKit +5 each. +table2asn returned no validation result on three inputs: an empty file, which it +could not open, and two duplicate-identifier files, on which it did not terminate +within the 30~s limit. +Only FastaGuard's NCBI gate flagged a 50-character SeqID and a SeqID containing +\texttt{@}; table2asn accepted both. +FastaGuard did not flag blank lines inside records or uneven line wrapping, which +fa-lint does. +On the two RefSeq assemblies, no tool flagged \emph{E.\ coli}; in \emph{N.\ crassa}, +py\_fasta\_validator, fa-lint, and SeqKit flagged nothing, whereas table2asn +reported error-level internal runs of 100~Ns (\texttt{InternalNsInSeqRaw}), which +FastaGuard reports as non-blocking gap-pattern advisories (WARN). +The corpus was built by the FastaGuard authors, its NCBI-rule rows encode the +policy that FastaGuard's NCBI gate implements, and the tools serve different +purposes, so Table~\ref{tab:comparison} compares capabilities, not accuracy on +real submissions. + +\begin{table}[t] + \centering + \caption{Capability comparison on 30 author-constructed FASTA files (13 + defective and 7 valid; 10 advisory files are not scored). Entries are defective + files flagged, out of the number shown; no valid file was flagged by any tool. + pyFV, py\_fasta\_validator; t2a, table2asn; FG, FastaGuard v1.0.0 with the + pipeline or NCBI submission gate (FG\textsubscript{pipe} blocked 10 of its 11). + The corpus favours FastaGuard's check list and the tools differ in purpose: this + is not a performance or accuracy benchmark. + $^{a}$No validation result on three inputs (an empty file and two + duplicate-identifier files).} + \label{tab:comparison} + \small + \begin{tabular}{@{}lcccccc@{}} + \toprule + Defect class (files) & pyFV & fa-lint & t2a & SeqKit + & FG\textsubscript{pipe} & FG\textsubscript{NCBI} \\ + \midrule + Syntax (7) & 3 & 6 & 5$^{a}$ & 5 & 7 & 7 \\ + Identifiers (2) & 2 & 2 & 0$^{a}$ & 0 & 2 & 2 \\ + All-N record (1) & 0 & 1 & 1 & 0 & 1 & 1 \\ + NCBI rules (3) & 0 & 0 & 1 & 0 & 1 & 3 \\ + \midrule + All defective (13) & 5 & 9 & 7$^{a}$ & 5 & 11 & 13 \\ + \bottomrule + \end{tabular} +\end{table} + +A minimal workflow pattern is therefore: run FastaGuard with +\texttt{--gate pipeline} and an output bundle; inspect +\texttt{gate.can\_continue} in JSON (for example with \texttt{jq}); only then +launch QUAST, BUSCO, BlobToolKit, CheckM, annotation, or official validators. +If the gate fails, fix the FASTA-level blockers first. +If the gate continues with WARN advisories, keep the findings as routing hints +for which interpretive tool to prioritize next. +In practice, a PASS or non-blocking WARN means only that the selected FastaGuard +gate policy was satisfied and that the FASTA can be routed to appropriate +downstream tools. +It does not establish assembly correctness, biological completeness, +contamination status, taxonomy, annotation quality, or archive acceptance. +That boundary is deliberate: FastaGuard owns FASTA-level preflight and triage; +interpretive tools own interpretive questions. + +\section{Discussion} +FastaGuard's contribution is a pipeline-native FASTA preflight \emph{contract}: +streaming validation, explainable findings, stable schemas, explicit gates, and +packaging paths that workflow authors can adopt without reinventing per-pipeline +scripts. +Relative to SeqKit and SeqFu, FastaGuard trades general sequence manipulation +for an opinionated QC verdict and gate policy; relative to syntax validators +such as py\_fasta\_validator and fa-lint, it adds severity-graded, explainable +findings and a structured report (Table~\ref{tab:comparison}). +Relative to QUAST, BUSCO, BlobToolKit, and CheckM, it runs earlier and answers a +different question: whether the FASTA is safe to spend those tools on. +Relative to MultiQC, it is a producer of aggregatable content rather than an +aggregator. +Positioning the tool as preflight---rather than as a substitute for those +ecosystem components---keeps it complementary to them. + +Limitations follow from that scope. +FastaGuard does not discover biological contamination, does not certify archive +acceptance, and does not replace official validators or FCS-style screens. +Public-evidence timings are contextual to one machine and release line; authors +should re-measure on their targets before citing absolute performance. +Finding thresholds are heuristics: we did not measure detection or +false-positive rates on real submissions, and the validator comparison +(Table~\ref{tab:comparison}) uses a small author-constructed corpus. +The behaviour described here corresponds to release v1.0.0; later changes are +recorded in the project changelog. + +\section*{Author contributions} +Following CRediT, E.E.: conceptualisation, software, validation, investigation, +data curation, and writing of the original draft. J.-F.M.: supervision and +writing through review and editing. Both authors read and approved the final +manuscript. + +\section*{Funding} +This work was partially supported by the Wallenberg Initiatives in Forest +Research (WIFORCE) funded by the Knut and Alice Wallenberg Foundation. + +\section*{Conflict of interest} +The authors declare no competing interests. + +\section*{AI usage disclosure} +AI coding assistants (Cursor-based agents and Anthropic Claude Code) were used for +planning, manuscript scaffolding, repository evidence cross-checks, prose drafting +support, and revision. +The software, code base, and benchmarking results were completed first; the +manuscript was then drafted solely from that code base and those results, not from +a free-form prompt. +The authors reviewed all output and remain responsible for the manuscript, code, +evidence interpretation, and all scientific claims. + +\section*{Acknowledgements} +We thank packaging and workflow maintainers who exercised Bioconda, +BioContainers, nf-core module, and Snakemake wrapper integrations for FastaGuard. + +\section*{Data availability} +FastaGuard source code, schemas, finding catalog, and documentation are openly +available at \url{https://github.com/ehsanestaji/FastaGuard} under the MIT +license (cited release tag \texttt{v1.0.0}). +Committed public-evidence summaries used in Table~\ref{tab:evidence} are +archived under \texttt{docs/evidence/} (v0.6.0 portable JSON/TSV pack, plus the +v1.0.0 re-run summary) in the repository; the rows can be regenerated with +\texttt{scripts/collect\_evidence.py}, which retrieves the NCBI assemblies by +accession with the NCBI Datasets command-line tool and records input SHA-256 +digests in the TSV. +The validator-comparison script, per-file results, and tool versions with SHA-256 +checksums are in \texttt{paper/comparison/}. +No new biological samples were generated for this study; public NCBI assemblies +cited in the evidence pack are used under their existing data-access terms. + +\bibliographystyle{apalike} +\bibliography{references} + +\end{document} diff --git a/paper/references.bib b/paper/references.bib new file mode 100644 index 0000000..7f2f79b --- /dev/null +++ b/paper/references.bib @@ -0,0 +1,257 @@ +% References for the FastaGuard Application Note. +% Journal-article DOIs checked against Crossref/DataCite on 2026-10-24. + +@article{gurevich2013quast, + author = {Gurevich, Alexey and Saveliev, Vladislav and Vyahhi, Nikolay and Tesler, Glenn}, + title = {{QUAST}: quality assessment tool for genome assemblies}, + journal = {Bioinformatics}, + year = {2013}, + volume = {29}, + number = {8}, + pages = {1072--1075}, + doi = {10.1093/bioinformatics/btt086} +} + +@article{simao2015busco, + author = {Sim{\~a}o, Felipe A. and Waterhouse, Robert M. and Ioannidis, Panagiotis and Kriventseva, Evgenia V. and Zdobnov, Evgeny M.}, + title = {{BUSCO}: assessing genome assembly and annotation completeness with single-copy orthologs}, + journal = {Bioinformatics}, + year = {2015}, + volume = {31}, + number = {19}, + pages = {3210--3212}, + doi = {10.1093/bioinformatics/btv351} +} + +@article{challis2020blobtoolkit, + author = {Challis, Richard and Richards, Edward and Rajan, Jeena and Cochrane, Guy and Blaxter, Mark}, + title = {{BlobToolKit} -- interactive quality assessment of genome assemblies}, + journal = {G3 Genes|Genomes|Genetics}, + year = {2020}, + volume = {10}, + number = {4}, + pages = {1361--1374}, + doi = {10.1534/g3.119.400908} +} + +@article{ewels2016multiqc, + author = {Ewels, Philip and Magnusson, M{\aa}ns and Lundin, Sverker and K{\"a}ller, Max}, + title = {{MultiQC}: summarize analysis results for multiple tools and samples in a single report}, + journal = {Bioinformatics}, + year = {2016}, + volume = {32}, + number = {19}, + pages = {3047--3048}, + doi = {10.1093/bioinformatics/btw354} +} + +@article{shen2016seqkit, + author = {Shen, Wei and Le, Shuai and Li, Yan and Hu, Fuquan}, + title = {{SeqKit}: a cross-platform and ultrafast toolkit for {FASTA}/{Q} file manipulation}, + journal = {PLOS ONE}, + year = {2016}, + volume = {11}, + number = {10}, + pages = {e0163962}, + doi = {10.1371/journal.pone.0163962} +} + +@article{parks2015checkm, + author = {Parks, Donovan H. and Imelfort, Michael and Skennerton, Connor T. and Hugenholtz, Philip and Tyson, Gene W.}, + title = {{CheckM}: assessing the quality of microbial genomes recovered from isolates, single cells, and metagenomes}, + journal = {Genome Research}, + year = {2015}, + volume = {25}, + number = {7}, + pages = {1043--1055}, + doi = {10.1101/gr.186072.114} +} + +@article{ditommaso2017nextflow, + author = {Di Tommaso, Paolo and Chatzou, Maria and Floden, Evan W. and Barja, Pablo Prieto and Palumbo, Emilio and Notredame, Cedric}, + title = {Nextflow enables reproducible computational workflows}, + journal = {Nature Biotechnology}, + year = {2017}, + volume = {35}, + number = {4}, + pages = {316--319}, + doi = {10.1038/nbt.3820} +} + +@article{molder2021snakemake, + author = {M{\"o}lder, Felix and Jablonski, Kim Philipp and Letcher, Brice and Hall, Michael B. and Tomkins-Tinch, Christopher H. and Sochat, Vanessa and Forster, Jan and Lee, Soohyun and Twardziok, Sven O. and Kanitz, Alexander and Wilm, Andreas and Holtgrewe, Manuel and Rahmann, Sven and Nahnsen, Sven and K{\"o}ster, Johannes}, + title = {Sustainable data analysis with {Snakemake}}, + journal = {F1000Research}, + year = {2021}, + volume = {10}, + pages = {33}, + doi = {10.12688/f1000research.29032.2} +} + +@article{ewels2020nfcore, + author = {Ewels, Philip A. and Peltzer, Alexander and Fillinger, Sven and Patel, Harshil and Alneberg, Johannes and Wilm, Andreas and Garcia, Maxime Ulysse and Di Tommaso, Paolo and Nahnsen, Sven}, + title = {The {nf-core} framework for community-curated bioinformatics pipelines}, + journal = {Nature Biotechnology}, + year = {2020}, + volume = {38}, + number = {3}, + pages = {276--278}, + doi = {10.1038/s41587-020-0439-x} +} + +@article{gruning2018bioconda, + author = {Gr{\"u}ning, Bj{\"o}rn and Dale, Ryan and Sj{\"o}din, Andreas and Chapman, Brad A. and Rowe, Jillian and Tomkins-Tinch, Christopher H. and Valieris, Renan and K{\"o}ster, Johannes and {The Bioconda Team}}, + title = {{Bioconda}: sustainable and comprehensive software distribution for the life sciences}, + journal = {Nature Methods}, + year = {2018}, + volume = {15}, + number = {7}, + pages = {475--476}, + doi = {10.1038/s41592-018-0046-7} +} + +@article{daveigaleprevost2017biocontainers, + author = {da Veiga Leprevost, Felipe and Gr{\"u}ning, Bj{\"o}rn A. and Alves Aflitos, Saulo and R{\"o}st, Hannes L. and Uszkoreit, Julian and Barsnes, Harald and Vaudel, Marc and Moreno, Pablo and Gatto, Laurent and Weber, Jonas and Bai, Mingze and Jimenez, Rafael C. and Sachsenberg, Timo and Pfeuffer, Julianus and Vera Alvarez, Roberto and Griss, Johannes and Nesvizhskii, Alexey I. and Perez-Riverol, Yasset}, + title = {{BioContainers}: an open-source and community-driven framework for software standardization}, + journal = {Bioinformatics}, + year = {2017}, + volume = {33}, + number = {16}, + pages = {2580--2582}, + doi = {10.1093/bioinformatics/btx192} +} + +@article{astashyn2024fcsgx, + author = {Astashyn, Alexander and Tvedte, Eric S. and Sweeney, Deacon and Sapojnikov, Victor and Bouk, Nathan and Joukov, Victor and Mozes, Eyal and Strope, Pooja K. and Sylla, Pape M. and Wagner, Lukas and Bidwell, Shelby L. and Brown, Larissa C. and Clark, Karen and Davis, Emily W. and Smith-White, Brian and Hlavina, Wratko and Pruitt, Kim D. and Schneider, Valerie A. and Murphy, Terence D.}, + title = {Rapid and sensitive detection of genome contamination at scale with {FCS-GX}}, + journal = {Genome Biology}, + year = {2024}, + volume = {25}, + pages = {60}, + doi = {10.1186/s13059-024-03198-7} +} + +@article{rashid2024assemblyqc, + author = {Rashid, Usman and Wu, Chen and Shiller, Jason and Smith, Ken and Crowhurst, Ross and Davy, Marcus and Chen, Ting-Hsuan and Carvajal, Ignacio and Bailey, Sarah and Thomson, Susan and Deng, Cecilia H.}, + title = {{AssemblyQC}: a {Nextflow} pipeline for reproducible reporting of assembly quality}, + journal = {Bioinformatics}, + year = {2024}, + volume = {40}, + number = {8}, + pages = {btae477}, + doi = {10.1093/bioinformatics/btae477} +} + +@article{formenti2022gfastats, + author = {Formenti, Giulio and Abueg, Linelle and Brajuka, Angelo and Brajuka, Nadolina and Gallardo-Alba, Crist{\'o}bal and Giani, Alice and Fedrigo, Olivier and Jarvis, Erich D.}, + title = {Gfastats: conversion, evaluation and manipulation of genome sequences using assembly graphs}, + journal = {Bioinformatics}, + year = {2022}, + volume = {38}, + number = {17}, + pages = {4214--4216}, + doi = {10.1093/bioinformatics/btac460} +} + +@article{shen2024seqkit2, + author = {Shen, Wei and Sipos, Botond and Zhao, Liuyang}, + title = {{SeqKit2}: a {Swiss} army knife for sequence and alignment processing}, + journal = {iMeta}, + year = {2024}, + volume = {3}, + number = {3}, + pages = {e191}, + doi = {10.1002/imt2.191} +} + +@article{telatin2021seqfu, + author = {Telatin, Andrea and Fariselli, Piero and Birolo, Giovanni}, + title = {{SeqFu}: a suite of utilities for the robust and reproducible manipulation of sequence files}, + journal = {Bioengineering}, + year = {2021}, + volume = {8}, + number = {5}, + pages = {59}, + doi = {10.3390/bioengineering8050059} +} + +@misc{edwards2023pyfastavalidator, + author = {Edwards, Rob}, + title = {py\_fasta\_validator: a {Python} extension of the {fasta\_validate} {FASTA} validator}, + year = {2023}, + howpublished = {\url{https://github.com/linsalrob/py_fasta_validator}}, + note = {Version 0.6; MIT License; doi:10.5281/zenodo.5002710} +} + +@misc{fastaguard2026, + author = {Estaji, Ehsan and Mao, Jian-Feng}, + title = {{FastaGuard}: {FASTA} preflight {QC} for assembly pipelines}, + year = {2026}, + publisher = {Zenodo}, + howpublished = {\url{https://doi.org/10.5281/zenodo.23622311}}, + note = {Version 1.0.0; MIT License; source: \url{https://github.com/ehsanestaji/FastaGuard/releases/tag/v1.0.0}} +} + +@misc{ncbi_genome_submit, + author = {{National Center for Biotechnology Information}}, + title = {Prokaryotic and Eukaryotic Genomes Submission Guide}, + year = {2025}, + howpublished = {\url{https://www.ncbi.nlm.nih.gov/genbank/genomesubmit/}}, + note = {Page last updated 2025-07-21; accessed 2026-10-24} +} + +@article{oleary2016refseq, + author = {O'Leary, Nuala A. and Wright, Mathew W. and Brister, J. Rodney and Ciufo, Stacy and Haddad, Diana and McVeigh, Rich and Rajput, Bhanu and Robbertse, Barbara and Smith-White, Brian and Ako-Adjei, Danso and Astashyn, Alexander and Badretdin, Azat and Bao, Yiming and Blinkova, Olga and Brover, Vyacheslav and Chetvernin, Vyacheslav and Choi, Jinna and Cox, Eric and Ermolaeva, Olga and Farrell, Catherine M. and Goldfarb, Tamara and Gupta, Tripti and Haft, Daniel and Hatcher, Eneida and Hlavina, Wratko and Joardar, Vinita S. and Kodali, Vamsi K. and Li, Wenjun and Maglott, Donna and Masterson, Patrick and McGarvey, Kelly M. and Murphy, Michael R. and O'Neill, Kathleen and Pujar, Shashikant and Rangwala, Sanjida H. and Rausch, Daniel and Riddick, Lillian D. and Schoch, Conrad and Shkeda, Andrei and Storz, Susan S. and Sun, Hanzhen and Thibaud-Nissen, Francoise and Tolstoy, Igor and Tully, Raymond E. and Vatsan, Anjana R. and Wallin, Craig and Webb, David and Wu, Wendy and Landrum, Melissa J. and Kimchi, Avi and Tatusova, Tatiana and DiCuccio, Michael and Kitts, Paul and Murphy, Terence D. and Pruitt, Kim D.}, + title = {Reference sequence ({RefSeq}) database at {NCBI}: current status, taxonomic expansion, and functional annotation}, + journal = {Nucleic Acids Research}, + year = {2016}, + volume = {44}, + number = {D1}, + pages = {D733--D745}, + doi = {10.1093/nar/gkv1189} +} + +@article{blattner1997ecoli, + author = {Blattner, Frederick R. and Plunkett, Guy and Bloch, Craig A. and Perna, Nicole T. and Burland, Valerie and Riley, Monica and Collado-Vides, Julio and Glasner, Jeremy D. and Rode, Christopher K. and Mayhew, George F. and Gregor, Jason and Davis, Nelson Wayne and Kirkpatrick, Heather A. and Goeden, Michael A. and Rose, Debra J. and Mau, Bob and Shao, Ying}, + title = {The complete genome sequence of {\textit{Escherichia coli}} {K-12}}, + journal = {Science}, + year = {1997}, + volume = {277}, + number = {5331}, + pages = {1453--1462}, + doi = {10.1126/science.277.5331.1453} +} + +@misc{falint, + author = {{GallVp}}, + title = {fa-lint: a {FASTA} linter and validator}, + year = {2025}, + howpublished = {\url{https://github.com/GallVp/fa-lint}}, + note = {Version 1.2.0; MIT License; accessed 2026-10-24} +} + +@misc{ncbi_fasta_format, + author = {{National Center for Biotechnology Information}}, + title = {{FASTA} Format for Nucleotide Sequences}, + year = {2026}, + howpublished = {\url{https://www.ncbi.nlm.nih.gov/genbank/fastaformat/}}, + note = {Accessed 2026-10-24} +} + +@article{galagan2003neurospora, + author = {Galagan, James E. and Calvo, Sarah E. and Borkovich, Katherine A. and Selker, Eric U. and others}, + title = {The genome sequence of the filamentous fungus {\textit{Neurospora crassa}}}, + journal = {Nature}, + year = {2003}, + volume = {422}, + pages = {859--868}, + doi = {10.1038/nature01554} +} + +@misc{ncbi_table2asn, + author = {{National Center for Biotechnology Information}}, + title = {What is table2asn?}, + year = {2026}, + howpublished = {\url{https://www.ncbi.nlm.nih.gov/genbank/table2asn/}}, + note = {Version 1.29.324; accessed 2026-10-24} +} diff --git a/paper/submission.yml b/paper/submission.yml new file mode 100644 index 0000000..774e95f --- /dev/null +++ b/paper/submission.yml @@ -0,0 +1,115 @@ +# FastaGuard Bioinformatics Application Note — submission control manifest +# Validated by: python3 paper/check_submission.py + +target_journal: Bioinformatics +publisher: Oxford University Press +article_type: Application Note +submission_portal: ScholarOne (Bioinformatics) +format_policy: format-free_first_submission_ok +oup_template_deferred_to: revision + +# Machine-check status after `make check` (human ScholarOne/bioRxiv steps remain USER-ACTION). +submission_status: ready_for_author_upload +author_gate_status: confirmed # 2026-10-24, reported in chat by Ehsan Estaji (Jian-Feng Mao knew of the work and agreed); no message from Jian-Feng seen by the agent +zenodo_gate_status: deposited +biorxiv_status: submitted_in_screening # BIORXIV/2026/761563, submitted 2026-10-24, acknowledgement received; DOI pending +scholarone_status: not_submitted +# OUP policy: AI use must be disclosed in the cover letter AND the manuscript; preprints must be +# disclosed in the cover letter; update the preprint with the published DOI after acceptance. +ai_disclosure_locations: [manuscript, cover_letter] +preprint_disclosure_location: cover_letter + +manuscript: + source: paper/main.tex + bibliography: paper/references.bib + pdf: paper/main.pdf + biorxiv_pdf: paper/build/fastaguard-biorxiv.pdf + title: "FastaGuard: FASTA preflight QC for modern bioinformatics pipelines" + cover_letter: paper/cover-letter.md + validator_comparison: paper/comparison/ + evidence_rerun_v1_0_0: docs/evidence/results/v1.0.0-rerun/ + +authors: + - name: Ehsan Estaji + affiliation: Umeå Plant Science Centre, Department of Plant Physiology, Umeå University + email: ehsan.estaji@umu.se + orcid: 0009-0005-0838-228X + corresponding: true + - name: Jian-Feng Mao + affiliation: Umeå Plant Science Centre, Department of Plant Physiology, Umeå University + email: jianfeng.mao@umu.se + orcid: 0000-0001-9735-8516 + corresponding: true + +# Software version cited for Availability / packaging (published GitHub + Bioconda release). +release: + tag: v1.0.0 + source_archive_url: https://github.com/ehsanestaji/FastaGuard/releases/tag/v1.0.0 + bioconda_package: fastaguard=1.0.0 + archive_doi: "10.5281/zenodo.23622310" + version_doi: "10.5281/zenodo.23622311" + zenodo_record_url: "https://zenodo.org/records/23622311" + zenodo_notes: "Minted 2026-10-24 from GitHub release v1.0.0 via Zenodo GitHub webhook replay; concept DOI 10.5281/zenodo.23622310; version DOI 10.5281/zenodo.23622311." + +# Public-evidence freeze used by Table 1 (contract illustrations; not a cross-platform benchmark). +evidence_freeze: + pack: docs/evidence/fastaguard-v0.6-public-evidence.md + portable_tsv: docs/evidence/results/v0.6/evidence_summary.tsv + portable_json: docs/evidence/results/v0.6/evidence_summary.json + fastaguard_version: "0.6.0" + source_commit: cf27295da0cb9b1a48318caa9e3b8739cfd0c104 + generated_at: "2026-08-21T08:11:21+00:00" + table_rows: + - id: synthetic_valid + elapsed_seconds: "0.0194" + verdict: PASS + gate_status: PASS + total_length: 604 + sequence_count: 4 + - id: problem_fixture + elapsed_seconds: "0.0185" + verdict: FAIL + gate_status: FAIL + total_length: 145 + sequence_count: 5 + - id: ecoli_k12_mg1655 + accession: GCF_000005845.2 + elapsed_seconds: "0.0569" + verdict: PASS + gate_status: PASS + total_length: 4641652 + sequence_count: 1 + - id: neurospora_crassa_or74a + accession: GCF_000182925.2 + elapsed_seconds: "0.3441" + verdict: WARN + gate_status: WARN + total_length: 41102378 + sequence_count: 21 + +claim_policy: + forbidden_phrase: "FastQC for FASTA" + broad_replacement_claim_allowed: false + performance_claim_source: docs/evidence_only + caveats_required: true + +submission_docs: + - docs/paper-readiness.md + - paper/cover-letter.md + - paper/ZENODO.md + - paper/biorxiv-checklist.md + - paper/HANDOFF.md + - paper/README.md + +required_human_inputs: + - coauthor_final_ok_jian_feng_mao + - biorxiv_deposit + - scholarone_upload + - apc_or_read_and_publish_check + - suggested_editors_or_accept_defaults + +blockers_for_machine_ready: [] +blockers_for_human_submit: + - bioRxiv screening clearance and preprint DOI (then add it to the cover letter) + - ScholarOne upload (credentials) + - APC / institutional Read & Publish / ISCB discount check diff --git a/paper/tables/README.md b/paper/tables/README.md new file mode 100644 index 0000000..72ffcf9 --- /dev/null +++ b/paper/tables/README.md @@ -0,0 +1,14 @@ +# Tables + +Optional working tables / CSV exports that feed the manuscript. + +Suggested frozen evidence table for `main.tex` Table 1: + +- input role / accession +- FastaGuard version + commit +- verdict / gate status +- wall time +- peak RSS +- platform + +Source evidence packs live under `docs/evidence/` in the repository root; copy summarized numbers here rather than inventing new claims.