diff --git a/.github/REFACTOR_ISSUES.md b/.github/REFACTOR_ISSUES.md new file mode 100644 index 0000000..df0c8e7 --- /dev/null +++ b/.github/REFACTOR_ISSUES.md @@ -0,0 +1,167 @@ +# Refactor issue tracker + +Source-of-truth backlog for the DSL2 modularization (see `REFACTORING_PLAN.md`). +Create these on GitHub with `scripts/create_refactor_issues.sh` (uses `gh`). +Each non-epic issue closes one `## Phase N` of the plan and must pass the +stub-run gate before merge: + +``` +nextflow config . -profile test && nextflow run . -profile test -stub-run && nextflow run . --help +``` + +--- + +## EPIC: Modularize nf_funannotate1 into DSL2 modules + subworkflows +labels: refactor, epic + +Break the 2,470-line `funannotate.nf` monolith into one-process-per-file modules +composed by subworkflows, on a `meta`-map data contract. nf-core-*inspired*, not +nf-core-submitted (see `REFACTORING_PLAN.md` for the verdict). Child issues #1–#12 +below; they are ordered — #1 and #2 block the rest. + +Definition of done: monolith replaced by `main.nf` + `workflows/` + +`subworkflows/local/` + `modules/local/`; every process emits `versions.yml`; +earlgrey/funannotate share `INPUT_CHECK`; CI green at every step. + +--- + +## 1. Adopt `meta`-map data contract (Phase 0 — BLOCKER) +labels: refactor + +Replace the positional 10-tuple +`tuple(out, asmid, species, strain, locustag, busco, header_length, transl_table, gz, taxonid)` +with `tuple val(meta), path(genome)` where `meta` is the map defined in +`REFACTORING_PLAN.md` (Principle 0). `meta.id` is the only naming field; +`header_length` becomes `params.header_length`. No more extraction until this lands. + +- [ ] Build `meta` in the workflow channel construction +- [ ] Update every call site to consume `meta` +- [ ] `params.header_length` added to schema with default 24 +- [ ] Stub-run gate green + +## 2. Shared `INPUT_CHECK` subworkflow (dedupe earlgrey) (Phase 0) +labels: refactor + +Extract the samplesheet `splitCsv` + taxon/asmid/suppress filtering (duplicated +in `funannotate.nf` and `earlgrey_mask.nf`) into `subworkflows/local/input_check.nf` +emitting the `meta` channel. Both entrypoints call it. + +- [ ] `subworkflows/local/input_check.nf` emits `ch_genomes` (meta + genome) +- [ ] `funannotate.nf` and `earlgrey_mask.nf` both consume it; no duplicated parse +- [ ] Stub-run gate green for both entrypoints + +## 3. Repo skeleton + relocate existing modules (Phase 1) +labels: refactor + +Create `main.nf`, `workflows/funannotate.nf`, `subworkflows/local/`, +`modules/local/`. Move `modules/asm_stats.nf` and `modules/annotation_tools.nf` +to one-process-per-file under `modules/local/` (split `annotation_tools.nf` into +`antismash.nf` / `signalp.nf` / `interproscan.nf`). + +- [ ] Directory skeleton in place; `main.nf` is a thin entrypoint +- [ ] `annotation_tools.nf` split into 3 single-process modules +- [ ] Stub-run gate green + +## 4. `versions.yml` + `conf/base.config` + `conf/modules.config` (Phase 2) +labels: refactor + +Establish the conventions every later module copies: each process emits +`versions.yml`; resources move to label-based `conf/base.config` +(`process_low/medium/high`); per-process `publishDir`/`ext.args` move to +`conf/modules.config`. + +- [ ] `conf/base.config` with resource labels +- [ ] `conf/modules.config` with publishDir + ext.args +- [ ] At least one module emits and the workflow collects `versions.yml` +- [ ] Stub-run gate green + +## 5. Setup modules (Phase 3) +labels: refactor, good first issue + +Extract `SETUP_TAXONDB`, `SETUP_FUNANNOTATE_DB`, `SETUP_AUGUSTUS_CONFIG` into +`modules/local/` + `subworkflows/local/setup_dbs.nf`. Preserve `storeDir` +(run-at-most-once) caching. Good first real extraction — validates the gate. + +- [ ] 3 modules + `setup_dbs.nf` subworkflow; storeDir preserved +- [ ] Stub-run gate green + +## 6. Genome clean + `prepare_genome` subworkflow (Phase 4) +labels: refactor + +Extract `GENOME_CLEAN` / `GENOME_CLEAN_BATCH` into modules and a +`subworkflows/local/prepare_genome.nf` (clean → asm_stats → mask). +**Preserve the FCS-GX `/dev/shm` staging** and the "skip already-cleaned" batch +gating so a fully-cleaned batch never pays the ~30-min staging cost. + +- [ ] Modules + `prepare_genome.nf`; FCS-GX /dev/shm staging preserved +- [ ] Batch padding/skip behavior unchanged +- [ ] Stub-run gate green + +## 7. Masking subworkflow + per-tool modules (Phase 5) +labels: refactor + +Replace the planned single masking mega-process. One module per masker +(`mask_tantan.nf` now; `mask_repeatmodeler.nf`, `mask_repeatmasker.nf`, +`mask_earlgrey.nf` as stubs/follow-ups); selection logic in +`subworkflows/local/mask.nf` keyed on `params.mask_tool`. + +- [ ] `mask.nf` selects one masker by param; `NONE` path supported +- [ ] `mask_tantan.nf` extracted; others stubbed with clear TODO +- [ ] Stub-run gate green + +## 8. RNA-seq fetch subworkflow (Phase 6 — hardest) +labels: refactor + +Extract `SRA_QUERY` / `SRA_QUERY_BATCH` / `COLLECT_SRA_QUERY` / +`WRITE_EMPTY_READS` / `SRA_FETCH` / `SRA_FETCH_SE` / `RNASEQ_PREPARE` into modules ++ `subworkflows/local/rnaseq.nf`. Done **after** the pattern is proven on easier +processes. Preserve per-species shared Trinity-GG output and `maxForks` limits. + +- [ ] 7 modules + `rnaseq.nf`; shared Trinity-GG semantics preserved +- [ ] maxForks / rate limits preserved +- [ ] Stub-run gate green + +## 9. Funannotate predict subworkflow (Phase 7) +labels: refactor + +Extract `FUNANNOTATE_TRAIN` / `FUNANNOTATE_PREDICT` / `FUNANNOTATE_UPDATE` into +modules + `subworkflows/local/predict.nf`. Keep the pre-flight assembly +size/fragmentation validation and post-flight "not enough models" guard. + +- [ ] 3 modules + `predict.nf`; pre/post-flight checks preserved +- [ ] `update` is optional (param-gated) +- [ ] Stub-run gate green + +## 10. Annotation subworkflow (Phase 8) +labels: refactor + +`subworkflows/local/annotate.nf` composing optional `antismash` / `signalp` / +`interproscan` modules → `funannotate_annotate.nf`. Each optional tool +independently param-gated. + +- [ ] `annotate.nf` with per-tool gating; merges results into funannotate annotate +- [ ] Stub-run gate green + +## 11. Consolidate `ucr_hpcc` institutional profile + portable container path (Phase 9) +labels: refactor + +The `module`→`ucr_hpcc` rename is done. Finish the repivot: fold UCR SLURM +partitions / `clusterOptions` into the institutional profile, and ensure a fully +portable run works via per-module conda/biocontainer directives (no Lmod). +Document the "copy to `conf/provision_.config`" path for new sites. + +- [ ] UCR partition config consolidated under the institutional profile +- [ ] Portable container/conda path runs without any UCR modules +- [ ] `docs/` note for adding a new institution + +## 12. nf-core hygiene (stretch) (Phase 9) +labels: refactor, documentation + +`docs/usage.md` + `docs/output.md`, `assets/schema_input.json` (samplesheet +schema), pipeline naming decision (nf-core forbids underscores/digits), optional +MultiQC + `nf-test`. Decide explicitly whether to pursue nf-core submission or +stay nf-core-inspired. + +- [ ] `docs/usage.md`, `docs/output.md` +- [ ] `assets/schema_input.json` +- [ ] naming + submission decision recorded in `REFACTORING_PLAN.md` diff --git a/README.md b/README.md index d376e06..36690fb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The pipeline lives at the repo root (`funannotate.nf` + `nextflow.config`), so i runs directly from GitHub — no clone required: ```bash -nextflow run stajichlab/nf_funannotate1 -profile annotate,slurm,module -resume +nextflow run stajichlab/nf_funannotate1 -profile annotate,slurm,ucr_hpcc -resume ``` Nextflow caches the repo under `~/.nextflow/assets/`; add `-r ` to pin @@ -26,7 +26,7 @@ a revision and `-latest` to pull updates. Outputs and the `samples.csv` / nextflow run stajichlab/nf_funannotate1 -profile test -stub-run # real run on SLURM with environment modules (from your launch dir, with samples.csv) -nextflow run stajichlab/nf_funannotate1 -profile annotate,slurm,module -resume --n_test 1 +nextflow run stajichlab/nf_funannotate1 -profile annotate,slurm,ucr_hpcc -resume --n_test 1 # or, from a local checkout, use the sbatch launcher sbatch /path/to/nf_funannotate1/run_annotate.sh --n_test 1 @@ -43,7 +43,7 @@ Compose one option from each of three axes: `-profile ,, This is the **single source of truth** for the DSL2 modularization effort. It +> supersedes the earlier `IMPLEMENTATION_SUMMARY.md` and `MODULE_STRUCTURE.txt` +> (removed). Progress is tracked in GitHub issues — see +> `.github/REFACTOR_ISSUES.md` and `scripts/create_refactor_issues.sh`. + +## Where we actually are + +- `funannotate.nf`: **2,470-line monolith, 20 inline processes.** +- `earlgrey_mask.nf`: separate 394-line pipeline that **duplicates** the + samplesheet parse + taxonomy/asmid/suppress filtering from `funannotate.nf`. +- Extracted so far: `modules/asm_stats.nf`, `modules/annotation_tools.nf` (2 of 20). +- Strong base already present: nf-schema + `nextflow_schema.json`, orthogonal + profiles, manifest, CITATIONS/COC/CHANGELOG/LICENSE, stub-run CI. + +The goal is an **nf-core-*inspired*** layout (not nf-core submission): adopt the +parts that are pure engineering wins, skip the parts that fight our HPC reality. + +--- + +## Principle 0 — the data contract (do this FIRST; blocks everything else) + +Every process is currently wired with a fragile positional 10-tuple: + +```groovy +tuple(out, asmid, species, strain, locustag, busco, header_length, transl_table, gz, taxonid) +``` + +Adding an 11th field touches every process. **Replace it with a `meta` map**, the +standard DSL2 idiom. Genome travels as a separate `path`: + +```groovy +// Canonical channel element: tuple val(meta), path(genome) +meta = [ + id : out, // unique sample tag — used for tag{} and file naming + asmid : asmid, + species : species, + strain : strain, + locustag : locustag, + busco : busco, // BUSCO_LINEAGE + transl_table: transl_table, // default '1' + taxonid : taxonid, +] +``` + +Rules: +- `meta.id` is the **only** field used for naming/`tag`; everything else is payload. +- `header_length` (constant 24) becomes `params.header_length`, **not** a meta field. +- Build `meta` once, in the `INPUT_CHECK` subworkflow (below). No process + re-parses the samplesheet. +- A module's `input:` declares `tuple val(meta), path(x)` and never positionally + unpacks fields it doesn't use. + +Until `meta` is adopted, **do not extract more modules** — every module written +against the old tuple is rework. + +--- + +## Target architecture + +``` +main.nf # thin entrypoint: parse args, call workflow +workflows/ + funannotate.nf # wires the subworkflows (was the monolith) + earlgrey.nf # curated-mask entry, reuses shared subworkflows +subworkflows/local/ + input_check.nf # samplesheet -> meta channel + taxon/asmid/suppress filters (SHARED) + setup_dbs.nf # SETUP_TAXONDB / FUNANNOTATE_DB / AUGUSTUS_CONFIG gating + prepare_genome.nf # clean -> asm_stats -> mask + mask.nf # selects ONE masker module by params.mask_tool + rnaseq.nf # sra_query -> sra_fetch(_se) -> rnaseq_prepare + predict.nf # train -> predict -> (update) + annotate.nf # antismash|signalp|interpro -> funannotate annotate +modules/local/ # ONE process per file + setup_taxondb.nf setup_funannotate_db.nf setup_augustus_config.nf + genome_clean.nf genome_clean_batch.nf asm_stats.nf + mask_tantan.nf mask_repeatmodeler.nf mask_repeatmasker.nf mask_earlgrey.nf + sra_query.nf sra_query_batch.nf collect_sra_query.nf write_empty_reads.nf + sra_fetch.nf sra_fetch_se.nf rnaseq_prepare.nf + funannotate_train.nf funannotate_predict.nf funannotate_update.nf + antismash.nf interproscan.nf signalp.nf funannotate_annotate.nf + select_reps.nf # earlgrey representative selection +conf/ + base.config # resources by label (process_low/medium/high/...) + modules.config # per-process publishDir + ext.args (nf-core idiom) +``` + +### Why this and not the old plan + +- **One tool = one process = one module file.** The old plan bundled 3 processes + per file (`sra_query.nf`, `annotation_tools.nf`, `databases.nf`). That is the + *opposite* of the convention and kills reuse. Group sequences with + **subworkflows**, which the old plan never mentioned. +- **Masking is a subworkflow, not a mega-process.** A single process with an + `if/else` over NONE/TANTAN/REPEATMODELER/REPEATMASKER/EARLGREY is an + anti-pattern. Each masker is its own module; the *selection* lives in + `subworkflows/local/mask.nf`. +- **Kill the funannotate/earlgrey duplication.** Both entrypoints parse the same + samplesheet and apply the same filters. Extract that into `input_check.nf` once + and call it from both. EarlGrey is a whole pipeline (SELECT_REPS + asm_stats + + representative-per-species), not a masking flavor — it reuses shared + subworkflows rather than being folded into one module. + +--- + +## Per-process extraction checklist (the stub-run gate) + +Apply to **one process per commit/PR**. The monolith must stay runnable at every +commit. + +For process `P`: + +- [ ] Create `modules/local/

.nf` with `process P { ... }`. +- [ ] `input:` uses `tuple val(meta), path(...)` (no positional field unpacking). +- [ ] Add `tag "${meta.id}"` and a resource `label` (`process_low|medium|high`). +- [ ] Keep the existing `stub:` block; keep `storeDir`/`publishDir` behavior. +- [ ] Emit a version: `path "versions.yml", emit: versions` + a `cat <<-END_VERSIONS` + block capturing the tool version. (Foundational — see Issue 3.) +- [ ] Move the process's resource/`withName` block out of + `conf/profile_annotate.config` into `conf/modules.config` (name unchanged, + so existing `withName:` selectors keep matching). +- [ ] In the workflow, replace the inline `process P {}` with + `include { P } from '../modules/local/p'` and adapt the call site to pass `meta`. +- [ ] **Gate (must pass before commit):** + ``` + nextflow config . -profile test + nextflow run . -profile test -stub-run + nextflow run . --help + ``` +- [ ] Commit. One process. Repeat. + +--- + +## Migration order (corrected) + +The old plan started with RNA-seq fetch ("least interdependent") — but +`SRA_FETCH` is the **single most complex** process (~270 lines). Prove the +pattern on a leaf first, then attack the hard pieces. + +| Phase | Work | Why here | +|------|------|----------| +| 0 | `meta` map contract + `INPUT_CHECK` subworkflow | Blocks all extraction; dedupes earlgrey | +| 1 | Skeleton: `main.nf`, `workflows/`, `subworkflows/local/`, `modules/local/`; move existing `asm_stats` + `annotation_tools` to convention | Establishes layout cheaply | +| 2 | `versions.yml` + `conf/base.config` + `conf/modules.config` | Pattern every later module copies | +| 3 | Setup modules (3 leaf processes) | Easiest real extraction; validates gate | +| 4 | Genome clean + `prepare_genome` subworkflow | Preserve FCS-GX `/dev/shm` staging | +| 5 | `mask` subworkflow + per-tool masker modules | Replaces the mega-process design | +| 6 | `rnaseq` subworkflow (the hard one) | Done *after* pattern is proven | +| 7 | `predict` subworkflow (train/predict/update) | Core | +| 8 | `annotate` subworkflow | Composition of optional tools | +| 9 | nf-core hygiene (docs/usage, docs/output, schema_input, naming, MultiQC) | Stretch | + +--- + +## Provisioning repivot (done) + +The `module` provisioning profile is renamed to **`ucr_hpcc`** — an +*institutional* profile in the nf-core/configs sense. The Lmod module names and +`/bigdata` paths only exist at UCR, so the name now says so. Portable runs use +`singularity` (containers) or `pixi`. New sites copy +`conf/provision_ucr_hpcc.config` to `conf/provision_.config` and register a +matching profile. + +``` +-profile annotate,slurm,ucr_hpcc # institutional (default on UCR HPCC) +-profile annotate,local,singularity # portable +``` + +Issue 11 covers the fuller consolidation (folding UCR SLURM partitions / +`clusterOptions` into the same institutional profile). + +--- + +## Distance from nf-core + +| Area | State | +|------|-------| +| Scaffolding (schema, CITATIONS, COC, CHANGELOG, LICENSE, CI) | ~30–40% there | +| `meta` map | none (Phase 0) | +| Structure (`main.nf`/`workflows`/`subworkflows`/`modules`) | monolith | +| `versions.yml` per module + MultiQC | none (mandatory for nf-core) | +| Containers per module | **biggest gap** — relies on Lmod/pixi; nf-core needs conda+biocontainer per process | +| Naming | `nf_funannotate1` violates nf-core naming (underscores/digits) | +| nf-test, `docs/usage.md`+`output.md`, `assets/schema_input.json`, `.nf-core.yml` | missing | + +**Verdict:** ~30–40% on peripheral scaffolding, ~0% on the two load-bearing items +(meta-maps + container-per-module). Full `nf-core lint` compliance is a multi-week +rewrite, much of which fights our HPC reality. **Recommendation: nf-core-inspired, +not nf-core-submitted** — adopt meta-maps, one-tool-per-module, subworkflows, +`versions.yml`, `conf/modules.config`; keep `ucr_hpcc` as an institutional profile +but add a real container path so the pipeline is portable. diff --git a/conf/profile_annotate.config b/conf/profile_annotate.config index 04e3288..a8ed12b 100644 --- a/conf/profile_annotate.config +++ b/conf/profile_annotate.config @@ -2,8 +2,8 @@ * annotate pipeline profile: funannotate.nf * * Provides the pipeline params and per-process SLURM resources. Combine with an - * executor profile (slurm|local) and a provisioning profile (module|pixi| - * singularity), e.g. -profile annotate,slurm,module + * executor profile (slurm|local) and a provisioning profile (ucr_hpcc|pixi| + * singularity), e.g. -profile annotate,slurm,ucr_hpcc * * Defaults are fungal (busco fungi via BUSCO_LINEAGE column, antismash_taxon=fungi, * swissprot_fungi proteins) but every organism-specific value is a param, so other @@ -25,6 +25,14 @@ params { taxondb = "/bigdata/stajichlab/shared/projects/1KFG/2026/NCBI_fungi/tmp/taxa/" proteins = "${launchDir}/lib/swissprot_fungi.faa" seqcenter = "NCBI" + + // ── assembly statistics (for earlgrey_mask.nf SELECT_REPS) ──────────────── + tables_dir = "${launchDir}/tables" + gen_asm_stats = true // generate asm_stats.tsv.gz from clean genomes + // Directory ASM_STATS scans for cleaned genomes — the storeDir target that + // GENOME_CLEAN writes .fa into. Must match earlgrey_mask.nf's + // params.genome_dir so both pipelines read the same cleaned assemblies. + genome_dir = "${launchDir}/input_clean_genomes" // Writable AUGUSTUS_CONFIG copy, seeded once by SETUP_AUGUSTUS_CONFIG (storeDir-cached). // augustus writes new species parameter sets here during training, so it must be a // private writable copy of the install's read-only config. diff --git a/conf/provision_module.config b/conf/provision_ucr_hpcc.config similarity index 86% rename from conf/provision_module.config rename to conf/provision_ucr_hpcc.config index c6759e2..afc622d 100644 --- a/conf/provision_module.config +++ b/conf/provision_ucr_hpcc.config @@ -1,5 +1,12 @@ /* - * Provisioning: Lmod modules (DEFAULT, proven on UCR HPCC). + * Provisioning: UCR HPCC Lmod modules — INSTITUTIONAL profile (-profile ucr_hpcc). + * + * This is a site-specific profile: the module names (funannotate/dev-1.8.18, + * AAFTF, taxonkit, ...) and the conda bootstrap below exist on the UCR HPCC and + * will NOT resolve elsewhere. For a portable run use a container/env provisioning + * profile instead (-profile <...>,singularity or ,pixi). New sites should copy + * this file to conf/provision_.config and register a matching profile in + * nextflow.config — the nf-core/configs institutional-profile model. * * Each process label gets a `beforeScript` that loads the tools it needs. The * beforeScript and the process `script:` are concatenated into one job script, diff --git a/earlgrey_mask.nf b/earlgrey_mask.nf index 1c55180..d68e1fd 100644 --- a/earlgrey_mask.nf +++ b/earlgrey_mask.nf @@ -30,7 +30,10 @@ params.genome_dir = "${launchDir}/input_clean_genomes" params.genome_suffix = '.fa' // clean (unmasked) genome suffix -params.asm_stats = "${launchDir}/tables/asm_stats.tsv.gz" +params.tables_dir = "${launchDir}/tables" // where asm_stats.tsv.gz lives +params.asm_stats = "${params.tables_dir}/asm_stats.tsv.gz" +params.gen_asm_stats = true // generate asm_stats if missing +params.skip_select_reps = false // skip SELECT_REPS step (just do EarlGrey on all) params.cutoff_mb = 200 // species qualifies if rep > this params.repeat_taxon = 'fungi' // EarlGrey -r RepeatMasker search term params.earlgrey_version = '7.2.6' @@ -49,6 +52,12 @@ def genomeFile(String base) { return file(base, glob: false) } +// ════════════════════════════════════════════════════════════════════════════ +// INCLUDES +// ════════════════════════════════════════════════════════════════════════════ + +include { ASM_STATS } from './modules/asm_stats' + // ════════════════════════════════════════════════════════════════════════════ // PROCESSES // ════════════════════════════════════════════════════════════════════════════ @@ -288,11 +297,48 @@ process DELIVER_MASK { workflow { - // ── Select representatives ──────────────────────────────────────────────── - def reps = SELECT_REPS( - file(params.samples, glob: false), - file(params.asm_stats, glob: false), - ) + // ── Generate assembly statistics if needed ──────────────────────────────── + // ASM_STATS generates asm_stats.tsv.gz from clean genomes (used by SELECT_REPS). + // Only runs if gen_asm_stats=true and the file doesn't already exist. + if (params.gen_asm_stats.toBoolean()) { + def asm_stats_path = file(params.tables_dir).toAbsolutePath() + def asm_stats_gz = file("${asm_stats_path}/asm_stats.tsv.gz") + if (!asm_stats_gz.exists()) { + log.info "Generating assembly statistics: ${asm_stats_gz}" + ASM_STATS( + file(params.samples, glob: false), + file(params.genome_dir, glob: false) + ) + } else { + log.info "Assembly statistics already exist: ${asm_stats_gz}" + } + } + + // ── Select representatives (skip with --skip_select_reps) ──────────────── + // When skip_select_reps=true, all genomes are processed for EarlGrey + // without the size/N50 filtering applied by SELECT_REPS. + def reps + if (params.skip_select_reps.toBoolean()) { + log.info "Skipping SELECT_REPS; processing all genomes for EarlGrey" + reps = channel.fromPath(params.samples, glob: false) + .splitCsv(header: true) + .map { row -> + def species = (row.SPECIES?.trim() ?: '') + def asmid = (row.ASMID?.trim() ?: '') + if (species && asmid) { + "SPECIES,REP_ASMID,REP_SIZE_MB,N_MEMBERS,MEMBER_ASMIDS\n${species},${asmid},0.0,0," + } else { + null + } + } + .filter { it != null } + .collectFile(name: "${launchDir}/misc/repeat_representatives.csv", newLine: false) + } else { + reps = SELECT_REPS( + file(params.samples, glob: false), + file(params.asm_stats, glob: false), + ) + } // ── Per-species records (n_test limits *species*) ───────────────────────── def records = reps diff --git a/funannotate.nf b/funannotate.nf index 43873cf..280ad5c 100644 --- a/funannotate.nf +++ b/funannotate.nf @@ -10,7 +10,7 @@ * params.taxondb / params.funannotate_db are null and parsing fails): * sbatch nextflow/run_annotate.sh * nextflow run nextflow/funannotate.nf -c nextflow/nextflow.config \ - * -profile annotate,slurm,module -resume + * -profile annotate,slurm,ucr_hpcc -resume */ // Metadata tuple order used throughout: @@ -1878,6 +1878,7 @@ def staleRnaseq(String out, String species) { } include { validateParameters; paramsSummaryLog; paramsHelp } from 'plugin/nf-schema' +include { ASM_STATS } from './modules/asm_stats' workflow { // `--help` prints schema-driven parameter help (grouped, with types/defaults) and exits. @@ -2049,6 +2050,23 @@ workflow { genome_fa.toAbsolutePath().toString(), taxonid) } + // ── Generate assembly statistics (for earlgrey_mask.nf SELECT_REPS) ──────── + // Generate asm_stats.tsv if --gen_asm_stats is true and the file doesn't exist. + // This is used by earlgrey_mask.nf to select representative genomes per species. + if (params.gen_asm_stats.toBoolean()) { + def asm_stats_path = file(params.tables_dir).toAbsolutePath() + def asm_stats_gz = file("${asm_stats_path}/asm_stats.tsv.gz") + if (!asm_stats_gz.exists()) { + log.info "Generating assembly statistics: ${asm_stats_gz}" + ASM_STATS( + file(params.samples), + file(params.genome_dir) + ) + } else { + log.info "Assembly statistics already exist: ${asm_stats_gz}" + } + } + // ── Repeat masking ──────────────────────────────────────────────────────── // predict_genome_ch carries the genome path to use for prediction — either // the tantan soft-masked genome (default) or the clean unmasked genome diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..8b04458 --- /dev/null +++ b/modules/README.md @@ -0,0 +1,163 @@ +# Nextflow Modules Directory + +This directory contains reusable Nextflow process modules organized by functional area. Each module is designed to be independently importable and composable into different workflows. + +## Directory Structure + +``` +modules/ +├── README.md (this file) +├── asm_stats.nf +│ └── Currently at root level; move to AAFTF/ in Phase 1 +├── annotation_tools.nf +│ └── Currently at root level; move to annotate/ in Phase 3 +├── funannotate/ (Phase 2 - Gene Prediction) +│ ├── predict.nf +│ ├── train.nf +│ └── update.nf +├── AAFTF/ (Phase 1 - Genome Preprocessing) +│ ├── asm_stats.nf +│ ├── FCS_GX.nf +│ ├── sourpurge.nf +│ └── vecscreen.nf +├── repeatmasking/ (Phase 1 - Repeat Masking) +│ └── masking.nf (strategies: TANTAN, REPEATMODELER, REPEATMASKER, EARLGREY, NONE) +├── rnaseq_fetch/ (Phase 2 - RNA-seq Preparation) +│ ├── sra_query.nf +│ ├── sra_fetch.nf +│ └── prepare.nf +├── annotate/ (Phase 3 - Annotation) +│ ├── annotation_tools.nf +│ └── funannotate.nf +└── setup/ (Phase 4 - Utilities) + └── databases.nf +``` + +## Module Usage + +### Including a Module +```groovy +include { PROCESS_NAME } from './modules/category/module.nf' + +// In workflow: +PROCESS_NAME(input_channel) +``` + +### Example: Using Multiple Modules +```groovy +include { ASM_STATS } from './modules/AAFTF/asm_stats' +include { FUNANNOTATE_PREDICT } from './modules/funannotate/predict' +include { ANTISMASH_RUN; SIGNALP_RUN } from './modules/annotate/annotation_tools' + +workflow { + ASM_STATS(samples, genome_dir) + FUNANNOTATE_PREDICT(genome_channel) + ANTISMASH_RUN(predict_output) + SIGNALP_RUN(predict_output) +} +``` + +## Module Documentation Format + +Each module file should include: +1. **Header comment** describing the module's purpose +2. **List of included processes** +3. **Parameter requirements** (e.g., `params.augustus_config`) +4. **Example usage** in comments + +### Template +```groovy +/* + * module_name — Brief description of what this module does + * + * Processes: + * - PROCESS_1: What it does + * - PROCESS_2: What it does + * + * Parameters required: + * - params.param_name: Description + * + * Example usage: + * include { PROCESS_1; PROCESS_2 } from './modules/category/module' + * PROCESS_1(input_channel) + */ +``` + +## Development Guidelines + +### Before Creating a New Module +1. Check if similar functionality exists +2. Verify the process is truly independent from others +3. Document dependencies clearly + +### Module Design Principles +1. **Single Responsibility**: Each module focuses on one functional area +2. **Reusability**: Processes should work in different contexts +3. **Clear Contracts**: Explicit input/output tuples +4. **Minimal Dependencies**: Avoid tight coupling to specific params +5. **Standalone Testing**: Each module can be tested with `-stub-run` + +### Parameter Handling +- Prefer `params.param_name` over hardcoded values +- Document all expected params in module header +- Use sensible defaults when possible +- Avoid module-specific param namespacing (e.g., don't use `params.predict_*` in predict.nf) + +## Rollout Plan + +### Phase 1: Genome Preprocessing (Extract from funannotate.nf) +- `modules/AAFTF/asm_stats.nf` - Move from root +- `modules/AAFTF/FCS_GX.nf` - Extract GENOME_CLEAN +- `modules/AAFTF/sourpurge.nf` - Extract GENOME_CLEAN +- `modules/AAFTF/vecscreen.nf` - New +- `modules/repeatmasking/masking.nf` - Extract + MASKREPEAT_TANTAN_RUN + +### Phase 2: Gene Prediction (Extract from funannotate.nf) +- `modules/rnaseq_fetch/sra_query.nf` - SRA processes +- `modules/rnaseq_fetch/sra_fetch.nf` - SRA download processes +- `modules/rnaseq_fetch/prepare.nf` - RNASEQ_PREPARE +- `modules/funannotate/train.nf` - FUNANNOTATE_TRAIN +- `modules/funannotate/predict.nf` - FUNANNOTATE_PREDICT +- `modules/funannotate/update.nf` - FUNANNOTATE_UPDATE (optional) + +### Phase 3: Annotation (Extract from funannotate.nf) +- `modules/annotate/annotation_tools.nf` - Move from root + FUNANNOTATE_ANNOTATE +- `modules/annotate/funannotate.nf` - If needed + +### Phase 4: Setup & Utilities +- `modules/setup/databases.nf` - SETUP_* processes + +## Testing Modules + +### Stub Run +```bash +nextflow run funannotate.nf \ + -c nextflow/nextflow.config \ + -profile test,local \ + -stub-run +``` + +### Unit Test (Single Process) +```bash +nextflow run -c nextflow.config \ + -profile test,local \ + -stub-run \ + --only-module modules/funannotate/predict.nf +``` + +## Performance Notes + +### Module Extraction Impact +- Minimal performance change (include statements are compile-time) +- Negligible memory overhead from modularization +- DAG complexity unchanged + +### Caching & Resume +- storeDir and publishDir behavior unchanged +- Resume functionality works across module boundaries +- Workflow checkpoints unaffected + +## Links & References + +- [REFACTORING_PLAN.md](../REFACTORING_PLAN.md) - Detailed phase breakdown +- Nextflow Module Documentation: https://www.nextflow.io/docs/latest/modules.html diff --git a/modules/annotation_tools.nf b/modules/annotation_tools.nf new file mode 100644 index 0000000..14f0415 --- /dev/null +++ b/modules/annotation_tools.nf @@ -0,0 +1,145 @@ +/* + * annotation_tools — Post-prediction annotation workflows + * + * This module contains optional annotation tools that run after FUNANNOTATE_PREDICT: + * - ANTISMASH_RUN: antiSMASH for secondary metabolite detection + * - INTERPROSCAN_RUN: InterProScan for protein domain annotation + * - SIGNALP_RUN: SignalP for signal peptide prediction + * + * These are independent tools that can be run selectively via params: + * --run_antismash (default: false) + * --run_interpro (default: false) + * --run_signalp (default: false) + * + * Include in your workflow: + * include { ANTISMASH_RUN; INTERPROSCAN_RUN; SIGNALP_RUN } from './modules/annotation_tools' + */ + +process ANTISMASH_RUN { + label 'antismash' + tag "$out" + + cpus 8 + memory '16 GB' + time '60h' + + publishDir "${params.target}", mode: 'copy', overwrite: true + + input: + tuple val(out), val(asmid), val(species), val(strain), val(locustag), + val(busco_lineage), val(header_length), val(transl_table) + + output: + tuple val(out), path("${out}/antismash_local/**") + + script: + def gbk = "${params.target}/${out}/predict_results/${out}.gbk" + """ + # Accept a compressed prediction (.gbk.gz); antismash needs it uncompressed, so + # inflate a local copy in the work dir when only the gzipped form is present. + GBK="${gbk}" + if [ ! -f "\$GBK" ] && [ -f "${gbk}.gz" ]; then + zcat "${gbk}.gz" > ${out}.predict.gbk + GBK=${out}.predict.gbk + fi + if [ ! -f "\$GBK" ]; then + echo "ERROR: predict GBK not found: ${gbk}[.gz]" >&2 + exit 1 + fi + source /etc/profile.d/modules.sh 2>/dev/null || true + mkdir -p ${out}/antismash_local + antismash --taxon ${params.antismash_taxon} \\ + --output-dir ${out}/antismash_local \\ + --genefinding-tool none \\ + --fullhmmer --clusterhmmer --cb-general --pfam2go \\ + -c ${task.cpus} \\ + \$GBK + pigz ${out}/antismash_local/*.json + """ + + stub: + """ + mkdir -p ${out}/antismash_local + touch ${out}/antismash_local/${out}.json.gz + touch ${out}/antismash_local/index.html + """ +} + +// IPRSCAN5 - InterPro protein domain annotation +process INTERPROSCAN_RUN { + label 'interproscan' + tag "$out" + + cpus 8 + memory '32 GB' + time '60h' + + publishDir "${params.target}", mode: 'copy', overwrite: true + + input: + tuple val(out), val(asmid), val(species), val(strain), val(locustag), + val(busco_lineage), val(header_length), val(transl_table) + + output: + tuple val(out), path("${out}/annotate_misc/iprscan.xml") + + script: + def proteins = "${params.target}/${out}/predict_results/${out}.proteins.fa" + """ + if [ ! -f "${proteins}" ]; then + echo "ERROR: protein FASTA not found: ${proteins}" >&2 + exit 1 + fi + mkdir -p ${out}/annotate_misc + interproscan.sh -i ${proteins} -f XML -o ${out}/annotate_misc/iprscan.xml \\ + -dp -goterms -pa -t p -cpu ${task.cpus} + """ + + stub: + """ + mkdir -p ${out}/annotate_misc + touch ${out}/annotate_misc/iprscan.xml + """ +} + +// SignalP - Signal peptide prediction +process SIGNALP_RUN { + label 'signalp' + tag "$out" + + cpus 8 + memory '16 GB' + time '12h' + + publishDir "${params.target}", mode: 'copy', overwrite: true + + input: + tuple val(out), val(asmid), val(species), val(strain), val(locustag), + val(busco_lineage), val(header_length), val(transl_table) + + output: + tuple val(out), path("${out}/annotate_misc/signalp.results.txt") + + script: + def proteins = "${params.target}/${out}/predict_results/${out}.proteins.fa" + """ + if [ ! -f "${proteins}" ]; then + echo "ERROR: protein FASTA not found: ${proteins}" >&2 + exit 1 + fi + TMPDIR=\${SCRATCH:-/tmp} + signalp6 -od \$TMPDIR/${out}_signalp \\ + -org euk --mode fast -format txt \\ + -fasta ${proteins} \\ + --write_procs ${task.cpus} -bs 16 + mkdir -p ${out}/annotate_misc + cp \$TMPDIR/${out}_signalp/prediction_results.txt ${out}/annotate_misc/signalp.results.txt + rm -rf \$TMPDIR/${out}_signalp + """ + + stub: + """ + mkdir -p ${out}/annotate_misc + touch ${out}/annotate_misc/signalp.results.txt + """ +} diff --git a/modules/asm_stats.nf b/modules/asm_stats.nf new file mode 100644 index 0000000..26d8049 --- /dev/null +++ b/modules/asm_stats.nf @@ -0,0 +1,78 @@ +/* + * asm_stats — Generate assembly statistics for clean genomes + * + * This module generates asm_stats.tsv with columns: ASMID, total_length_bp, N50_bp, contig_count. + * Stats are used by earlgrey_mask.nf to select representative genomes per species (SELECT_REPS). + * + * Include in your workflow: + * include { ASM_STATS } from './modules/asm_stats' + * ASM_STATS(samples_csv, genome_dir) + */ + +process ASM_STATS { + label 'setup' + + storeDir { params.tables_dir } + + cpus 4 + memory '8 GB' + time '2h' + + input: + path samples + path genome_dir + + output: + path 'asm_stats.tsv.gz', emit: stats + + script: + """ + set -euo pipefail + + TMPFILE=\$(mktemp) + trap 'rm -f \$TMPFILE' EXIT + + printf 'ASMID\\ttotal_length_bp\\tN50_bp\\tcontig_count\\n' > \$TMPFILE + + # Extract ASMIDs from samples.csv + awk -F',' 'NR>1 {print \$2}' ${samples} | sort -u | while read asmid; do + [ -z "\$asmid" ] && continue + asmid="\$(echo "\$asmid" | xargs)" # trim whitespace + + # Look for genome file: prefer .fa.gz, fall back to .fa, then .masked.fasta.gz + if [ -f "${genome_dir}/\${asmid}.fa.gz" ]; then + genome="${genome_dir}/\${asmid}.fa.gz" + elif [ -f "${genome_dir}/\${asmid}.fa" ]; then + genome="${genome_dir}/\${asmid}.fa" + elif [ -f "${genome_dir}/\${asmid}.masked.fasta.gz" ]; then + genome="${genome_dir}/\${asmid}.masked.fasta.gz" + elif [ -f "${genome_dir}/\${asmid}.masked.fasta" ]; then + genome="${genome_dir}/\${asmid}.masked.fasta" + else + echo "[WARN] No genome file found for \${asmid} in ${genome_dir}" >&2 + continue + fi + + # Use seqkit to compute stats + total_bp=\$(seqkit stats -T "\$genome" 2>/dev/null | tail -n 1 | awk '{print \$4}') + n50=\$(seqkit fx2tab -l "\$genome" 2>/dev/null | sort -rn -k2 | \\ + awk -v total="\$total_bp" 'BEGIN{sum=0} {sum+=\$2; if(sum >= total/2) {print \$2; exit}}') + contigs=\$(seqkit stats -T "\$genome" 2>/dev/null | tail -n 1 | awk '{print \$3}') + + [ -z "\$total_bp" ] && total_bp="0" + [ -z "\$n50" ] && n50="0" + [ -z "\$contigs" ] && contigs="0" + + printf '%s\\t%s\\t%s\\t%s\\n' "\$asmid" "\$total_bp" "\$n50" "\$contigs" >> \$TMPFILE + done + + pigz -c \$TMPFILE > asm_stats.tsv.gz + echo "[INFO] Assembly statistics written: asm_stats.tsv.gz" + """ + + stub: + """ + printf 'ASMID\\ttotal_length_bp\\tN50_bp\\tcontig_count\\n' | pigz -c > asm_stats.tsv.gz + echo "[STUB] ASM_STATS" + """ +} diff --git a/nextflow.config b/nextflow.config index ae7182a..352219f 100644 --- a/nextflow.config +++ b/nextflow.config @@ -6,16 +6,23 @@ * * pipeline : annotate | earlgrey | test * executor : slurm | local - * provisioning : module | pixi | singularity + * provisioning : ucr_hpcc | pixi | singularity + * + * Provisioning is split into a PORTABLE path and an INSTITUTIONAL path: + * - singularity / pixi : portable — runnable at any site; tools come from + * containers or project-local pixi envs. + * - ucr_hpcc : institutional — UCR HPCC Lmod modules. Encodes + * site-specific module names/paths; analogue of an + * nf-core/configs institutional profile. * * Examples: - * -profile annotate,slurm,module (default / recommended on HPCC) - * -profile annotate,local,singularity (containers, run head locally) - * -profile annotate,local,module -stub-run (graph/dry test) + * -profile annotate,slurm,ucr_hpcc (default / recommended on UCR HPCC) + * -profile annotate,local,singularity (portable: containers, head local) + * -profile annotate,local,ucr_hpcc -stub-run (graph/dry test) * * The pipeline profile supplies params + per-label resources; the executor * profile selects SLURM vs local; the provisioning profile fills each process - * label's `beforeScript` (module/pixi) or `container` (singularity). Process + * label's `beforeScript` (ucr_hpcc/pixi) or `container` (singularity). Process * scripts themselves are tool-agnostic — they contain NO `module load` lines. */ @@ -95,7 +102,8 @@ profiles { } // ---- provisioning axis --------------------------------------------------- - module { includeConfig 'conf/provision_module.config' } + // Institutional (UCR HPCC Lmod). Portable alternatives: pixi, singularity. + ucr_hpcc { includeConfig 'conf/provision_ucr_hpcc.config' } pixi { includeConfig 'conf/provision_pixi.config' } singularity { singularity.enabled = true diff --git a/run_annotate.sh b/run_annotate.sh index b8d1b8c..127a302 100755 --- a/run_annotate.sh +++ b/run_annotate.sh @@ -16,8 +16,9 @@ # - or the current dir: PIPELINE=$PWD # REVISION git branch / tag / commit to run (default: pipeline default branch) # -# Default provisioning is Lmod modules on SLURM. Swap axes via env vars: -# PROVISION=singularity sbatch run_annotate.sh +# Default provisioning is the UCR HPCC institutional profile (Lmod modules) on +# SLURM. Swap axes via env vars: +# PROVISION=singularity sbatch run_annotate.sh # portable containers # EXECUTOR=local sbatch run_annotate.sh # head + tasks local # REVISION=v0.1.0 sbatch run_annotate.sh # pin a release # @@ -34,7 +35,7 @@ module load nextflow PIPELINE="${PIPELINE:-stajichlab/nf_funannotate1}" REVISION="${REVISION:-}" EXECUTOR="${EXECUTOR:-slurm}" -PROVISION="${PROVISION:-module}" +PROVISION="${PROVISION:-ucr_hpcc}" mkdir -p logs/nextflow diff --git a/samples.csv b/samples.csv new file mode 100644 index 0000000..9e7036e --- /dev/null +++ b/samples.csv @@ -0,0 +1,2 @@ +SPECIES,STRAIN,ASMID,LOCUSTAG,BUSCO_LINEAGE,TRANSL_TABLE,NCBI_TAXONID,GENOME +Debaryomyces hansenii,CDA1,CDA1_20250504,AC24UF,saccharomycetes_odb12,12,,test_run/genome/CDA1_20250504.fa.gz diff --git a/scripts/create_refactor_issues.sh b/scripts/create_refactor_issues.sh new file mode 100755 index 0000000..2ff0e82 --- /dev/null +++ b/scripts/create_refactor_issues.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# +# create_refactor_issues.sh — create the modularization backlog on GitHub. +# +# Idempotent-ish: skips any issue whose exact title already exists (open OR +# closed). Creates the `refactor`/`epic` labels and a milestone if missing. +# Requires: gh (authenticated), run from inside the repo. +# +# ./scripts/create_refactor_issues.sh # create everything +# DRY_RUN=1 ./scripts/create_refactor_issues.sh # print what would happen +# +# Mirrors .github/REFACTOR_ISSUES.md — keep them in sync. + +set -euo pipefail + +REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)" +MILESTONE="DSL2 modularization" +DRY_RUN="${DRY_RUN:-0}" + +say() { printf '%s\n' "$*"; } +run() { if [ "$DRY_RUN" = 1 ]; then say "DRY: $*"; else "$@"; fi; } + +ensure_label() { # name color description + if ! gh label list --repo "$REPO" --limit 200 | grep -qiE "^$1[[:space:]]"; then + run gh label create "$1" --repo "$REPO" --color "$2" --description "$3" || true + fi +} + +ensure_milestone() { + if ! gh api "repos/$REPO/milestones?state=all" -q '.[].title' | grep -qxF "$MILESTONE"; then + run gh api "repos/$REPO/milestones" -f title="$MILESTONE" \ + -f description="Break funannotate.nf monolith into modules + subworkflows" >/dev/null || true + fi +} + +issue_exists() { # title + gh issue list --repo "$REPO" --state all --limit 500 --json title -q '.[].title' \ + | grep -qxF "$1" +} + +make_issue() { # title labels body + local title="$1" labels="$2" body="$3" + if issue_exists "$title"; then + say "skip (exists): $title" + return + fi + run gh issue create --repo "$REPO" --title "$title" --label "$labels" \ + --milestone "$MILESTONE" --body "$body" +} + +ensure_label refactor 1d76db "DSL2 modularization work" +ensure_label epic 5319e7 "Tracking epic" +ensure_milestone + +make_issue "EPIC: Modularize nf_funannotate1 into DSL2 modules + subworkflows" "refactor,epic" \ +"Break the 2,470-line funannotate.nf monolith into one-process-per-file modules composed by subworkflows, on a meta-map data contract. See REFACTORING_PLAN.md and .github/REFACTOR_ISSUES.md. Child issues are ordered; the meta-map and INPUT_CHECK issues block the rest." + +make_issue "Adopt meta-map data contract (Phase 0, BLOCKER)" "refactor" \ +"Replace the positional 10-tuple with \`tuple val(meta), path(genome)\`. meta.id is the only naming field; header_length becomes params.header_length. No further extraction until this lands. See REFACTORING_PLAN.md Principle 0." + +make_issue "Shared INPUT_CHECK subworkflow (dedupe earlgrey)" "refactor" \ +"Extract samplesheet parse + taxon/asmid/suppress filtering (duplicated in funannotate.nf and earlgrey_mask.nf) into subworkflows/local/input_check.nf emitting the meta channel. Both entrypoints consume it." + +make_issue "Repo skeleton + relocate existing modules (Phase 1)" "refactor" \ +"Create main.nf, workflows/, subworkflows/local/, modules/local/. Move asm_stats.nf and split annotation_tools.nf into antismash/signalp/interproscan single-process modules." + +make_issue "versions.yml + conf/base.config + conf/modules.config (Phase 2)" "refactor" \ +"Each process emits versions.yml; resources move to label-based conf/base.config; per-process publishDir/ext.args move to conf/modules.config. The pattern every later module copies." + +make_issue "Extract setup modules (Phase 3)" "refactor,good first issue" \ +"Extract SETUP_TAXONDB / SETUP_FUNANNOTATE_DB / SETUP_AUGUSTUS_CONFIG into modules/local + subworkflows/local/setup_dbs.nf. Preserve storeDir run-at-most-once caching." + +make_issue "Genome clean + prepare_genome subworkflow (Phase 4)" "refactor" \ +"Extract GENOME_CLEAN / GENOME_CLEAN_BATCH + subworkflows/local/prepare_genome.nf (clean -> asm_stats -> mask). Preserve FCS-GX /dev/shm staging and the skip-already-cleaned batch gating." + +make_issue "Masking subworkflow + per-tool modules (Phase 5)" "refactor" \ +"Replace the single masking mega-process: one module per masker (tantan now; repeatmodeler/repeatmasker/earlgrey follow-ups), selection in subworkflows/local/mask.nf keyed on params.mask_tool. Support NONE." + +make_issue "RNA-seq fetch subworkflow (Phase 6, hardest)" "refactor" \ +"Extract SRA_QUERY/_BATCH, COLLECT_SRA_QUERY, WRITE_EMPTY_READS, SRA_FETCH/_SE, RNASEQ_PREPARE into modules + subworkflows/local/rnaseq.nf. Done after pattern proven. Preserve shared Trinity-GG and maxForks." + +make_issue "Funannotate predict subworkflow (Phase 7)" "refactor" \ +"Extract FUNANNOTATE_TRAIN/PREDICT/UPDATE into modules + subworkflows/local/predict.nf. Keep pre-flight size/fragmentation checks and post-flight not-enough-models guard. update is optional/param-gated." + +make_issue "Annotation subworkflow (Phase 8)" "refactor" \ +"subworkflows/local/annotate.nf composing optional antismash/signalp/interproscan modules -> funannotate_annotate.nf, each independently param-gated." + +make_issue "Consolidate ucr_hpcc institutional profile + portable container path (Phase 9)" "refactor" \ +"module->ucr_hpcc rename is done. Fold UCR SLURM partitions/clusterOptions into the institutional profile; ensure a fully portable container/conda run works without Lmod. Document the new-site path." + +make_issue "nf-core hygiene (Phase 9, stretch)" "refactor,documentation" \ +"docs/usage.md + docs/output.md, assets/schema_input.json, naming decision (nf-core forbids underscores/digits), optional MultiQC + nf-test. Record nf-core submission vs nf-core-inspired decision in REFACTORING_PLAN.md." + +say "Done." diff --git a/test_run/genome/CDA1_20250504.fa.gz b/test_run/genome/CDA1_20250504.fa.gz new file mode 100644 index 0000000..89dd126 Binary files /dev/null and b/test_run/genome/CDA1_20250504.fa.gz differ