From 61662605501e98f9a151f0a7f3a8117d0fd41b10 Mon Sep 17 00:00:00 2001 From: Ehsan ESTAJI <71376358+ehsanestaji@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:01:11 +0200 Subject: [PATCH] feat: surface submission fields in multiqc plugin --- docs/adoption-plan.md | 2 ++ docs/packaging.md | 2 +- integrations/multiqc/README.md | 10 ++++-- .../src/fastaguard_multiqc/multiqc_module.py | 32 +++++++++++++++++ .../multiqc/src/fastaguard_multiqc/parser.py | 6 ++++ tests/python/test_adoption_assets.py | 36 +++++++++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/adoption-plan.md b/docs/adoption-plan.md index d03342d..69d901e 100644 --- a/docs/adoption-plan.md +++ b/docs/adoption-plan.md @@ -42,6 +42,8 @@ Goal: make FastaGuard visible in standard pipeline reports. - Continue emitting `fastaguard_mqc.json` custom content. - Develop `integrations/multiqc/` into a packaged MultiQC plugin. - Test the plugin against multiple sample reports. +- Keep v0.5 gate, readiness, and submission fields visible in the native + MultiQC summary table. - Decide whether to submit upstream to MultiQC once public adoption begins. Done when: diff --git a/docs/packaging.md b/docs/packaging.md index 9d2c9aa..aafaea4 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -183,4 +183,4 @@ cd ../../examples/reports multiqc . ``` -This is intentionally compact: it parses `fastaguard_mqc.json`, adds key metrics to MultiQC general stats, and adds a FastaGuard summary section. The full evidence remains in FastaGuard's own HTML and JSON reports. +This is intentionally compact: it parses `fastaguard_mqc.json`, adds key metrics to MultiQC general stats, and adds a FastaGuard summary section with gate, readiness, and submission-readiness fields. The full evidence remains in FastaGuard's own HTML and JSON reports. diff --git a/integrations/multiqc/README.md b/integrations/multiqc/README.md index 26fbdce..43148d6 100644 --- a/integrations/multiqc/README.md +++ b/integrations/multiqc/README.md @@ -3,8 +3,9 @@ This directory contains a dedicated MultiQC plugin starter for FastaGuard. FastaGuard already emits MultiQC custom-content JSON as `fastaguard_mqc.json`. -This plugin is the next step: a native module that can add FastaGuard verdicts -and key assembly preflight metrics directly to MultiQC reports. +This plugin is the next step: a native module that can add FastaGuard verdicts, +gate status, readiness status, submission readiness, and key assembly preflight +metrics directly to MultiQC reports. ## Local Install @@ -42,7 +43,10 @@ multiqc --strict --module fastaguard examples/reports - Parse FastaGuard custom-content JSON. - Add verdict and summary metrics to the MultiQC general stats table. -- Add one FastaGuard summary table section. +- Add one FastaGuard summary table section with gate, readiness, and v0.5 + submission fields including `submission_target`, `submission_status`, + unsafe identifier counts, long identifier counts, duplicate first-token ID + counts, and gap-like N-run counts. Keep the module compact. MultiQC should summarize many FastaGuard reports, not replicate every field from the full FastaGuard HTML report. diff --git a/integrations/multiqc/src/fastaguard_multiqc/multiqc_module.py b/integrations/multiqc/src/fastaguard_multiqc/multiqc_module.py index e4cb75b..1dea8e4 100644 --- a/integrations/multiqc/src/fastaguard_multiqc/multiqc_module.py +++ b/integrations/multiqc/src/fastaguard_multiqc/multiqc_module.py @@ -204,6 +204,38 @@ def _summary_headers() -> dict: "title": "Readiness blockers", "description": "Readiness categories and finding IDs blocking downstream use", }, + "submission_target": { + "title": "Submission target", + "description": "Submission target profile used by FastaGuard", + }, + "submission_status": { + "title": "Submission status", + "description": "FASTA-level submission readiness status", + }, + "unsafe_identifier_count": { + "title": "Unsafe IDs", + "description": "Number of identifiers with submission-unsafe characters", + "min": 0, + "scale": "OrRd", + }, + "long_identifier_count": { + "title": "Long IDs", + "description": "Number of identifiers longer than the submission advisory threshold", + "min": 0, + "scale": "YlOrBr", + }, + "duplicate_first_token_id_count": { + "title": "Duplicate first-token IDs", + "description": "Number of duplicate first-token FASTA identifiers", + "min": 0, + "scale": "OrRd", + }, + "gap_like_n_run_count": { + "title": "Gap-like N runs", + "description": "Number of records with gap-like N runs worth submission review", + "min": 0, + "scale": "YlOrBr", + }, "duplicate_id_count": { "title": "Duplicate IDs", "description": "Number of duplicate FASTA record IDs", diff --git a/integrations/multiqc/src/fastaguard_multiqc/parser.py b/integrations/multiqc/src/fastaguard_multiqc/parser.py index 128266f..ce0a3db 100644 --- a/integrations/multiqc/src/fastaguard_multiqc/parser.py +++ b/integrations/multiqc/src/fastaguard_multiqc/parser.py @@ -24,6 +24,12 @@ "gate_blocking_findings", "readiness_status", "readiness_blockers", + "submission_target", + "submission_status", + "unsafe_identifier_count", + "long_identifier_count", + "duplicate_first_token_id_count", + "gap_like_n_run_count", "duplicate_id_count", "invalid_sequence_count", "high_n_sequence_count", diff --git a/tests/python/test_adoption_assets.py b/tests/python/test_adoption_assets.py index d543cd4..640ede6 100644 --- a/tests/python/test_adoption_assets.py +++ b/tests/python/test_adoption_assets.py @@ -155,6 +155,9 @@ def test_multiqc_parser_reads_fastaguard_custom_content(self): self.assertEqual(summary["valid_assembly"]["gate_status"], "WARN") self.assertEqual(summary["valid_assembly"]["readiness_status"], "WARN") self.assertEqual(summary["valid_assembly"]["readiness_blockers"], "") + self.assertEqual(summary["valid_assembly"]["submission_target"], ".") + self.assertEqual(summary["valid_assembly"]["submission_status"], "WARN") + self.assertEqual(summary["valid_assembly"]["duplicate_first_token_id_count"], 0) self.assertEqual(summary["valid_assembly"]["sequence_count"], 3) self.assertEqual(summary["valid_assembly"]["n50"], 16) @@ -172,6 +175,12 @@ def test_multiqc_parser_reads_expanded_fields_from_cli_example(self): "gate_blocking_findings": "duplicate_ids,duplicate_first_token_ids,invalid_chars", "readiness_status": "FAIL", "readiness_blockers": "index.duplicate_ids,submission.duplicate_ids,index.duplicate_first_token_ids,submission.duplicate_first_token_ids,alphabet.invalid_chars,submission.invalid_chars", + "submission_target": ".", + "submission_status": "FAIL", + "unsafe_identifier_count": 0, + "long_identifier_count": 0, + "duplicate_first_token_id_count": 1, + "gap_like_n_run_count": 0, "sequence_count": 5, "total_length": 145, "n50": 110, @@ -221,6 +230,12 @@ def test_multiqc_parser_reads_expanded_summary_fields(self): "finding_count": 4, "readiness_status": "WARN", "readiness_blockers": "assembly.high_n_rate", + "submission_target": "ncbi", + "submission_status": "WARN", + "unsafe_identifier_count": 1, + "long_identifier_count": 1, + "duplicate_first_token_id_count": 2, + "gap_like_n_run_count": 3, } }, } @@ -240,6 +255,10 @@ def test_multiqc_parser_reads_expanded_summary_fields(self): "n_percent": 2.5, "finding_count": 4, "duplicate_id_count": 1, + "unsafe_identifier_count": 1, + "long_identifier_count": 1, + "duplicate_first_token_id_count": 2, + "gap_like_n_run_count": 3, "invalid_sequence_count": 0, "high_n_sequence_count": 2, "tiny_contig_count": 1, @@ -249,6 +268,8 @@ def test_multiqc_parser_reads_expanded_summary_fields(self): "composite_anomaly_count": 1, "readiness_status": "WARN", "readiness_blockers": "assembly.high_n_rate", + "submission_target": "ncbi", + "submission_status": "WARN", }, ) @@ -400,8 +421,23 @@ def test_multiqc_plugin_summary_headers_include_gate_fields(self): self.assertIn('"gate_blocking_findings"', module_source) self.assertIn('"readiness_status"', module_source) self.assertIn('"readiness_blockers"', module_source) + self.assertIn('"submission_target"', module_source) + self.assertIn('"submission_status"', module_source) + self.assertIn('"unsafe_identifier_count"', module_source) + self.assertIn('"long_identifier_count"', module_source) + self.assertIn('"duplicate_first_token_id_count"', module_source) + self.assertIn('"gap_like_n_run_count"', module_source) self.assertIn("Finding IDs blocking the FastaGuard gate", module_source) self.assertIn("FastaGuard readiness status", module_source) + self.assertIn("FASTA-level submission readiness status", module_source) + + def test_multiqc_docs_describe_submission_fields(self): + readme = (ROOT / "integrations" / "multiqc" / "README.md").read_text() + + self.assertIn("submission readiness", readme) + self.assertIn("`submission_target`", readme) + self.assertIn("`submission_status`", readme) + self.assertIn("duplicate first-token", readme) def test_multiqc_plugin_registers_filename_first_fastaguard_search_pattern(self): patterns = getattr(multiqc_parser, "FASTAGUARD_SEARCH_PATTERN", {})