Changes in version 1.41.2 Bug fixes - Dropped the sodium dependency, which broke R CMD check on the Bioconductor Linux builder (nebbiolo2 has no libsodium, so the check failed with "Package suggested but not available: 'sodium'" before running any other stage). Auth crypto now uses scrypt and openssl — both already Imports: of shinymanager, and both present on every Bioconductor builder. - Password hashing moved from libsodium argon2id to scrypt::hashPassword(), the same primitive shinymanager itself stores. - Per-user AI key encryption moved from libsodium secretbox to AES-256-GCM with an explicit encrypt-then-MAC HMAC-SHA256 tag. R's openssl package neither emits nor verifies a GCM tag, so authentication is applied explicitly; encryption and MAC subkeys are domain-separated. - Existing password hashes and stored AI keys are not readable across this change — see the migration note below. Migration - Password hashes and encrypted per-user AI keys written by 1.41.1 or earlier use libsodium formats and cannot be read by 1.41.2. Any user database created during D2 development must be re-created: users need to reset their passwords and re-enter their AI provider keys. Changes in version 1.41.1 Documentation overhaul - Rewrote the user-guide vignette (vignettes/DEBrowser.Rmd) around the modernized six-step Data Prep wizard and the current top-level tabs (Main Plots, QC Plots, Concordance, Enrichment, Tables). Added coverage for the features the old guide missed — batch-correction QC, cross-contrast concordance, GO/KEGG + GSEA enrichment, reproducibility exports (R / R Markdown / Jupyter / methods paragraph), bookmarking & sharing, theming and keyboard shortcuts, and the optional AI interpretation panel. - Kept the package small. The full illustrated user guide — with all screenshots and worked examples — lives outside the package at https://debrowser.readthedocs.io (the UMMS-Biocore/debrowser-docs repo, served by Read the Docs). The vignette is a concise, image-free reference that links there, and the README references the same externally hosted screenshots, so no images are bundled in the package tarball. - Rewrote README.md to mirror the modern walkthrough at a shorter altitude and point to the online guide. - Removed the unused fontawesome and RColorBrewer entries from Imports (icons come via shiny::icon(); no brewer.pal/RColorBrewer:: usage), clearing the R CMD check "declared Imports should be used" note. UI design-system consolidation (docs/design plans §1–§6) - Grid/layout: collapsed four grid idioms to one primitive (bslib::layout_columns(col_widths=)) and two card systems to one wrapper (de_card(), now with a class= passthrough) across every panel; retired layout_column_wrap, hand-rolled display:grid, and ragged rows. Spacing comes from a --de-space-* scale and plot heights from de_plot_h(). Added tools/check-grid.sh to lock the standard in. - Color/contrast: raised light/dark muted text and introduced theme-resolved --de-accent-ink (accent-as-text) + --de-ink-on-accent tokens for WCAG-AA contrast; --de-cyan is now fill-only. - Icons: Font Awesome only, via shiny::icon(). Repaired app-wide FA webfont rendering (the Inter UI font was clobbering glyphs), consolidated ad-hoc glyphs, and wired the favicon. - Loading/motion: replaced the 1.1 MB boot gif with a CSS conic ring; added stepwise withProgress detail to the DE run; added --de-transition, :focus-visible rings, prefers-reduced-motion, and cursor:not-allowed affordances; removed the dead getLoadingMsg overlay and its orphan gifs. - Handoff: added de_style_guide() + inst/extdata/www/style-guide.html (a component gallery in both themes that doubles as a pixel-regression baseline) and DESIGN.md, the contributor design contract. Phase A1 — foundation modernization - Bumped minimum R version to 4.2 and RoxygenNote to 7.3.x. - Added lintr and styler configs; one-time formatting pass. - Added GitHub Actions for R-CMD-check (R-release + R-devel), BiocCheck, lintr, and covr coverage (codecov). - Extended .Rbuildignore for docs, CI, lint, and dev configs. - Removed stale empty viafoundry_errors.log from the repo. Phase A2 — test scaffolding + golden snapshots - Migrated tests/test-*.R to the testthat 3e layout under tests/testthat/, with shared helpers in helper-debrowser.R. - Added golden snapshot tests for DESeq2, edgeR, limma, getNormalizedMatrix(), and PCA coordinates on the demo data — these are the safety net for Phase A3+ refactors. - Added a shinytest2 smoke-test scaffold (CI-skipped until A4 stabilises module IDs). - Fixed silent Treat/Control inversion in the migrated DESeq2 test. Phase A3a — pure analytic core (DE + normalize + filter) - Added R/fct_de_methods.R with run_deseq2(), run_edger(), run_limma(), run_de() — pure functions taking structured (named-list) params instead of legacy comma-string params. No Shiny dependency. - Added R/fct_normalize.R with normalize_counts() and pure apply_batch_correction() (Combat / CombatSeq / Harman). Batch / treatment column names are now arguments instead of input$ lookups. - Added R/fct_filter.R with filter_low_counts() (max / mean / cpm). - Added R/utils_validate.R with de_error() — structured stop() raising classed conditions so callers dispatch on class, not message text. - Legacy runDE(), runDESeq2(), runEdgeR(), runLimma(), getNormalizedMatrix(), correctCombat(), correctHarman() are now thin shims that translate input/comma-string params and delegate. Their signatures are unchanged so existing callers and scripts keep working. - Fixed silent "Repeated column names found in count matrix" warning in the limma path (legacy code set all column names to factor levels; removing it does not affect results). - debrowserlowcountfilter module's filter observer shrinks from ~20 lines to 9 by delegating to filter_low_counts(). Phase A3b — pure data-prep functions - Added R/fct_prep_data.R with apply_de_filters(), get_most_varied(), select_dataset(), search_geneset(), merge_comparisons(), apply_merged_filters(), get_table_data() — pure functions that take a structured filter-params list. - Added filter_params_from_input() in utils_validate.R to centralise the Shiny-input → pure-fn-params field-name mapping (e.g. input$padj → params$padj_cutoff, input$genesetarea → params$geneset_area). - Legacy applyFilters(), getMostVariedList(), getSelectedDatasetInput(), getSearchData(), getMergedComparison(), applyFiltersToMergedComparison(), getDataForTables() are now thin shims that delegate to R/fct_prep_data.R. Public signatures unchanged. - New golden snapshot locks the (Up=551, Down=864, NS=13941) row-count distribution on the demo DE result with default cutoffs (padj 0.05, fold 2) — catches regressions in cutoff logic or normalization. Phase A4a + A4c — Shiny API modernization - Migrated all 13 modules from the deprecated callModule() API to moduleServer() (Shiny 1.5+ idiom): debrowserdataload, debrowserlowcountfilter, debrowserbatcheffect, debrowserhistogram, debrowserpcaplot, debrowserIQRplot, debrowserdensityplot, debrowserall2all, debrowserheatmap, debrowsermainplot, debrowserbarmainplot, debrowserboxmainplot, debrowserdeanalysis. Public function signatures changed from (input, output, session, …) to (id, …) to match the modern Shiny convention. - Removed the library("debrowser") self-import inside deUI() — was a no-op at best and a side-effect at worst. - Replaced the runtime installpack() / loadpack() package-loading helpers with standard requireNamespace(pkg, quietly = TRUE) checks in R/GOterm.R. Deleted R/installpack.R and its exported functions (installpack, loadpack). - Dropped the unused aes_string import (deprecated in ggplot2 3.0). - condSelect.R is deliberately left untouched — full rewrite folded into Phase B2 alongside the three-stage-shell wizard redesign. Phase A5 — slim dependencies - Moved seven rarely-used packages from Imports: to Suggests: so a fresh install pulls a smaller dependency graph: Harman, pathview, org.Mm.eg.db, apeglm, ashr, enrichplot, DOSE. - Added require_pkg(pkg, feature) helper in R/utils_validate.R that raises a debrowser_error of class missing_suggested_pkg with the install command when a Suggested package is needed but unavailable. - Gated every direct call site: - Harman::harman() / Harman::reconstructData() — harman_correct() - DESeq2::lfcShrink(type="apeglm"/"ashr") — run_deseq2() - enrichplot::dotplot() / enrichplot::gseaplot() — gopanel.R, GOterm.R::compareClust, server.R GSEA render path - DOSE::enrichDO() — getEnrichDO() (the compareCluster(fun = "enrichDO") branch in compareClust() keeps its existing in-place requireNamespace gate) - pathview::pathview() — already gated in drawKEGG(); now namespaced - Stripped the corresponding @import / @importFrom lines from R/server.R and R/GOterm.R; NAMESPACE no longer pulls these packages at load time. Phase B1 — bslib chrome + theme swap - Replaced shinydashboard::dashboardPage shell with bslib::page_navbar and per-tab bslib::layout_sidebar. Top navbar now hosts five sections (Data Prep / Main Plots / QC Plots / GO Term / Tables) plus a light/dark toggle (bslib::input_dark_mode). - Theme: Slate (#0f172a) navbar + OK-blue (#0369a1) primary, Bootstrap 5, Inter typography. New de_theme() helper. - Migrated all 22 shinydashboard::box() call sites to bslib::card via a new de_card() helper (with optional download button in the header). Wide layout containers use raw bslib::card; narrow widget cards use de_card. - DE Filter (cutoff + comparison-selector controls) moved from the sidebar into a card at the top of the DE Analysis wizard panel. - Migrated all 24 shinydashboard::menuItem() collapsible widget containers to bslib::accordion + bslib::accordion_panel (in R/IQR.R, R/barmain.R, R/all2all.R, R/density.R, R/boxmain.R, R/plotSize.R, R/mainScatter.R, R/heatmap.R, R/uifuncs.R). - Replaced getTabUpdateJS()'s shinydashboard .sidebar-menu jQuery with server-side bslib::nav_show/nav_hide observers in deServer. Same trigger button ids, same behavior. - togglePanels() body rewritten to bslib::nav_show/nav_hide/ nav_select. Function signature unchanged — all callers untouched. - The standalone heatmap app (startHeatmap()) shell heatmapUI() also migrated to bslib::page_navbar with the same Slate + OK-blue theme; controls now live in the Heatmap tab's layout_sidebar. - de_card() defaults to full_screen = FALSE to avoid bslib's expand-overlay interfering with htmlwidget click handlers. - CSS file shinydashboard_additional.css audited and renamed to debrowser.css; shrunk from 145 to ~30 lines of app-specific positioning rules. - Dropped shinydashboard from Imports. Added bslib (>= 0.7.0). Added explicit @importFrom shiny tagList req to compensate for symbols previously pulled through shinydashboard's dependency chain. - Dark-mode plot theming (plotly/heatmaply/ggplot color flips) deferred to Phase B6. Phase B2.5 — Comparison Selection wizard rewrite - Rewrote R/condSelect.R (≈900 LOC monolith) into three focused files: R/fct_condselect.R (pure helpers + validation predicates), R/mod_condselect.R (condSelectUI + condSelectServer module), and R/prep_data_container.R (the DE runner with a structured signature). - New module API: condSelectUI(id) and condSelectServer(id, data, metadata). Returns list(n_comparisons, start_de, is_ready, comparisons_spec). - prepDataContainer() signature changed from (data, counter, input, meta) to (data, metadata, comparisons_spec). The leaky condselect$input boundary in R/server.R is gone. - UX: single-comparison default with "Add another comparison" footer for multi-comparison; editable per-side Treatment/Control labels with metadata- driven defaults; reference-word direction heuristic (control|ctrl|wt|wildtype|...) with always-visible swap button; per-method advanced settings + covariates collapsed by default; inline non-toast validation messages (.text-warning / .text-danger). - Internal conds codes ("Cond1"/"Cond2") preserved so R/fct_de_methods.R, R/fct_prep_data.R, R/deprogs.R, R/barmain.R, and downstream plotting need no changes. - Removed exports (no known external callers): debrowsercondselect, debrowsercondselectServer, selectedInput, getSelectInputBox, getMetaSelector, getGroupSelector, getConditionSelector, getConditionSelectorFromMeta, getMethodDetails, getCovariateDetails, selectConditions, get_conditions_given_selection, getSampleNames. - New tests: test-condselect-helpers.R (helpers), test-condselect-validation.R (predicates), test-prepdatacontainer.R (prep_comparison_inputs purity). Total +73 assertions (126 → 199 PASS). Phase B3 — Sane defaults harmonization - Switched DE cutoff input from fold-change to |log2FC| convention. The cutoff numeric inputs are now labelled padj <= and |log2FC| >=. - Added [ Strict ] [ Standard ] preset buttons above the DE cutoff inputs. Strict (padj 0.01, |log2FC| 1) is the default; Standard is (padj 0.05, |log2FC| 1). Manual edits clear the preset highlight unless the new pair matches a preset exactly. - Cutoff inputs (DE padj, DE |log2FC|, GO p.adjust) are now numericInput with bounds + native validation, replacing textInput widgets that silently produced NaN downstream when users typed non-numeric values. - New helper module R/fct_cutoffs.R owns the single source of truth for default values and preset definitions: default_cutoffs(), cutoff_presets(), match_preset(), log2fc_to_fold(), fold_to_log2fc(), install_cutoff_preset_observers(). The hardcoded fallback in generateTestData() now reads from default_cutoffs(). - Internal-only Shiny input rename: input$foldChange -> input$log2fc_cutoff. Public cutOffSelectionUI API is unchanged; new exported companion cutOffSelectionServer(id) wires preset observers for the namespaced widget. Phase E3.B — Rich report generation + Jupyter + view-in-tab - Export dropdown gains three new items, bringing the total to six: - Rmd source -- downloads the raw .Rmd body (no render). - View HTML in tab -- renders the Rmd to a tempdir, registers it as a Shiny resource path, and opens the rendered HTML in a new browser tab via Shiny.addCustomMessageHandler('debrowser_open_tab',...). - Jupyter notebook -- downloads the same content as a .ipynb file with R-kernel (ir) code cells; opens directly in JupyterLab. The pre-existing Rmd -> HTML item is renamed to plain HTML (it always meant "render and download"); the new View HTML in tab is the in-browser counterpart. - emit_rmd() now produces a manuscript-style report mirroring a reference Rmd contributed by a user (Haania mouse PA/DMSO study). YAML uses code_folding: hide so chunks are collapsible in the rendered HTML; setup chunk runs eval = TRUE so the report includes actual plots when rendered. New sections (in order): - Library + helper-source chunks - ## Methods (paragraph from Phase E9's methods_paragraph()) - ## Pipeline -- load, filter, batch, DE per comparison - ## Sample Info -- DT::datatable of metadata - ## Quality Control {.tabset} -- Count distribution / All2All / PCA + Scree (all on the all-detected-genes matrix) - ## DESeq Analysis {.tabset} -- one tab per comparison with sub-tabs Results / Volcano / MA / Heatmap - ## Enrichment (when configured) - ## Session Info {.tabset} -- Hide / Show - New pure helpers in R/fct_export_session.R: emit_ipynb(blocks) parses emit_rmd() output and converts to .ipynb JSON cells. - New file: inst/templates/report_helpers.R (~534 lines) bundling the report's plotting functions (count_distribution, all2all, run_pca, pca_plot, scree_plot, volcano_plot, ma_plot, heatmap_plot, getNormalizedMatrix, post_processing, add_alias, add_highlights, call_significance). The emitted Rmd / Jupyter / .R all source() this file from the installed package so report styling stays in one place. - state_react() (in R/server.R) now surfaces full_counts (the filter+batch-corrected matrix on all detected genes) and metadata (the sample-info table) so the QC and Sample Info sections can reference the same data DEBrowser sees. Phase E12.B — AI interpretation: presets + mount points - AI interpretation panel widened from fgseaGSEA-only to all 6 Enrichment modes (enrichGO, enrichKEGG, enrichDO, enrichPathway, compareCluster, fgseaGSEA). One mount, one preset (summarize_geneset) works across all modes via per-mode payload adapters. - New AI panel on the DE Analysis tab (per-comparison, below the results table). Presets: summarize_geneset, suggest_followup, draft_methods. Privacy default for this mount: + Stats. - New AI panel on the Comparison Concordance tab (below the summary card). Presets: reconcile_enrichments, suggest_followup, draft_methods. In-card pathway picker surfaces when one or more pathways are enriched in two or more comparisons; selecting one triggers cross-comparison fgsea so the prompt sees real NES rows. - New presets in inst/templates/: - ai_reconcile_enrichments.md — explains a pathway's divergent NES across multiple comparisons. - ai_suggest_followup.md — two-section response: bioinformatic next-analyses + wet-lab next-experiments. - ai_draft_methods.md — journal-style polish of E9's deterministic Methods paragraph. Cannot add or remove facts. - Sanitized markdown response rendering via commonmark + xml2 (replaces v1's plain
). Strict allow-list: headings, bold,
    italic, lists, code, blockquote, hr. Links rendered as plain text
    (no hallucinated URLs are clickable). Images and scripts stripped.
  - Privacy modes adapted per payload shape: de_table and concordance
    default to + Stats; the draft_methods preset hides the radio
    entirely (no genes / stats in payload).
  - No new Imports. commonmark + xml2 added to Suggests (both already
    transitively present via rmarkdown / httr2).
  - Streaming responses still deferred.

Phase E12.A — AI interpretation foundation

  - New top-right Settings dropdown in the navbar with an AI
    configuration modal: master switch (off by default), provider picker
    (Anthropic / OpenAI / Ollama), dynamic model lookup via
    ellmer::models_*, encrypted API-key storage via the OS keychain
    (keyring package), and a default privacy mode setting.
  - New AI interpretation card on the Enrichment tab below the Leading
    Edge card. Renders only when the master switch is on, a provider is
    configured, and (for non-Ollama providers) an API key is present. v1
    ships one question preset: "Summarize this gene set's biology",
    attached to the currently-selected fgsea pathway's leading-edge
    genes.
  - Privacy modes per call (default: Symbols only):
      - Symbols only -- gene symbols of the leading edge.
      - + Stats -- also log2FoldChange and adjusted p-value per gene.
      - + Stats + Enrichment -- also the term name, p-value, and overlap
        count of the selected pathway. A "What will be sent?" disclosure
        shows the literal prompt body (live preview) and a character
        count before the user clicks Ask.
  - New exported helpers in pure files: ai_interpret(question, payload,
    privacy_mode, provider_chat, top_n, template_dir)
    (R/fct_ai_interpret.R), list_models(provider, api_key) and
    ai_chat(provider, model, api_key) (R/fct_ai_providers.R). Internal
    helpers cover settings I/O and encrypted key storage
    (R/fct_ai_settings.R).
  - New error class hierarchy: ai_error extends simpleError; subclasses
    ai_no_key, ai_rate_limit, ai_network, ai_invalid_response,
    ai_disabled. The Shiny module maps each subclass to a friendly
    notification with a recovery hint.
  - LLM responses are rendered as plain preformatted text (tags$pre); no
    markdown parsing, no HTML execution, no automatic link traversal --
    defense against untrusted-content patterns in model output.
  - Three new Suggests gated via require_pkg: ellmer (>= 0.1.0),
    whisker, keyring. R CMD check passes with all three uninstalled --
    the AI features are gracefully unavailable in that case.
  - AI is disabled by default. No network calls happen unless the user
    explicitly enables AI features and configures a provider.
  - Phase E12.B will extend with the remaining 3 question presets
    (reconcile enrichments, suggest follow-up experiments, draft methods
    narrative) and 2 mount points (DE Analysis tab results, Comparison
    Concordance summary).

Phase E9 — Methods-paragraph autogen

  - Phase E3's Export dropdown now emits a single manuscript-ready
    methods paragraph (~150-250 words) with inline citations and version
    stamps, replacing the previous per-step bullet list. The paragraph
    appears in three places:
      - At the top of the exported .R script as a wrapped comment block.
      - In the rendered .Rmd -> HTML report's "## Methods" section.
      - In a new Copy methods text Export menu item that opens a modal
        with the paragraph in selectable preformatted text plus a
        "Download as .txt" button.
  - New exported methods_paragraph(blocks) function (in
    R/fct_methods_text.R); upgraded internal methods_sentences() helper
    now produces citation-rich, version-stamped per-step sentences. Both
    functions are pure (no Shiny dependency) and consume the
    build_session_blocks() output from Phase E3.
  - New internal .method_refs registry maps method keys to list(name,
    version_pkg, cite) for nine entries: DEBrowser, DESeq2, edgeR,
    limma, ComBat, ComBat-seq, Harman, fgsea, MSigDB. Citations are
    inline "(Author et al., YEAR)" plain text.
  - Soft fallback when a cited package is not installed: citation reads
    "(version unknown)" and a deduplicated showNotification warns the
    user. Methods text export never errors on a missing version lookup.

Phase E3 — Reproducibility export

  - New top-right Export dropdown in the navbar with two items:
      - R script -- downloads a runnable .R script that reproduces the
        full analytical pipeline (data load -> low-count filter -> batch
        correction -> DE per comparison -> GSEA if loaded). Calls only
        already-exported pure helpers (filter_low_counts,
        apply_batch_correction, run_de, run_gsea, msigdb_pathways), so
        the script has no Shiny dependency. Sets per-comparison deN
        variables in the workspace, writes per-comparison TSVs to
        ./debrowser_results/, and prints sessionInfo() at the end.
      - Rmd -> HTML -- renders a self-contained HTML report with the
        same pipeline as code chunks (eval = FALSE by default) plus an
        auto-generated Methods paragraph. Gated on the rmarkdown package
        being installed; falls back to raw .Rmd download if pandoc
        fails.
  - New pure helpers in R/fct_export_session.R: build_session_blocks(),
    emit_r_script(), emit_rmd(), sanitize_label(). New
    R/fct_methods_text.R::methods_sentences() produces the per-step
    prose consumed by both emitters; Phase E9 will upgrade it to a
    citation-rich paragraph generator.
  - enrichmentGmtServer() return is now list(pathways, state) instead of
    just the pathways reactive (additive; consumers extend the
    destructure).
  - Both export items are gated by a "Run a DE analysis before
    exporting" notification until DE has been run.
  - Frozen sessionInfo() of the export-time R session is embedded in the
    .R script header for audit reproducibility; live sessionInfo() is
    also called at script end.
  - Comparison labels with collisions get _2, _3 suffixes in TSV
    filenames; unsafe characters in labels are replaced with
    underscores.

Phase E11 — Comparison Concordance tab (top-level)

  - New top-level Comparison Concordance tab between QC Plots and
    Enrichment. Auto-shown when the user has set up two or more
    comparisons in CondSelect; hidden for single-comparison sessions.
    Compares the user's existing comparisons against each other (no DE
    re-running — reads dc()[[i]]$init_data produced by the main DE
    pipeline).
  - Five cards driven by live padj / lfc cutoffs:
      - DEG bar (up red rightward / down blue leftward, ordered by total
        DEG count)
      - Pairwise DEG count heatmap (symmetric groups x groups matrix)
      - UpSet plot of significant-gene-set overlap across comparisons
      - Pairwise log2FC scatter with Spearman correlation
      - Pairwise concordance summary table (DT)
  - New pure helpers in R/fct_method_concordance.R: concordance_sets(),
    concordance_summary(), plot_method_upset(), plot_method_scatter(),
    comparison_labels(), de_direction_summary(),
    plot_de_direction_bar(), plot_de_pairwise_heatmap(), plus
    run_de_methods() for programmatic use.
  - UpSetR added to Suggests (gated via require_pkg("UpSetR")).

Phase E2.5 — Consolidated Enrichment tab

  - The previously-separate GO Term and Enrichment tabs are now a single
    Enrichment tab. The legacy GO Term modes (enrichGO / enrichKEGG /
    Disease / compareClusters / GSEA via clusterProfiler::gseGO) and the
    new fgsea-based GSEA share one top-level tab. The standalone
    Enrichment tab introduced in E1 has been removed; its content is
    folded in here.
  - New radio choice "GSEA (fgsea / .gmt or MSigDB)" under the
    Enrichment-method picker. Selecting it reveals the GMT/MSigDB picker
    and Advanced controls (min/max set size, permutations, seed) inside
    the same sidebar that hosts the legacy GO Term options.
  - Both render styles coexist via conditional panels: the legacy modes
    keep their plot+table tabset; the fgsea mode renders results table +
    enrichment plot + leading edge cards plus a multi-comparison NES
    heatmap when more than one comparison is loaded — same widgets the
    standalone Enrichment tab used.
  - enrichmentUI() / enrichmentServer() (the standalone-tab module pair
    from E1) remain exported as a public API for users who want to embed
    the enrichment tab elsewhere; the consolidated panel reuses the
    smaller enrichmentGmtServer() and enrichmentNesHeatmapServer()
    building blocks directly.
  - togglePanels() reverts from a 0..5 loop with backwards-compat rule
    to a clean 0..4 loop, since panel5 no longer exists.

Phase E2 — MSigDB integration (gene-set source picker)

  - The Enrichment tab's gene-set picker now offers MSigDB as a sibling
    source to manual .gmt upload. Pick a species (the 20 species msigdbr
    ships, including human, mouse, rat, fly, yeast, zebrafish, C.
    elegans, and more), a top-level collection (Hallmark, Curated,
    Ontology, Cell type, ...), and an optional subcollection (e.g.
    CP:KEGG, GO:BP). Click "Load gene sets" to pull the named-list of
    pathways into the running GSEA session.
  - New pure helper msigdb_pathways(species, collection, subcollection)
    in R/fct_gsea.R wraps msigdbr::msigdbr() and reshapes its
    long-format tibble into the same named-list-of- character-vectors
    shape gmt_to_pathways() returns, so all downstream code (run_gsea,
    NES heatmap, leading edge) is source-agnostic. Errors from msigdbr
    (unknown species, unknown collection, empty result) are normalized
    to the empty_input classed condition.
  - msigdbr added to Suggests (~50MB local install but no install-time
    network — Bioc-friendly). Calls go through require_pkg("msigdbr")
    with the install command for users who don't have it.
  - Per-session in-module cache keyed on (species, collection,
    subcollection) so re-clicking "Load" with the same inputs is
    instant.
  - The GO Term tab is unchanged in this release; folding ORA into the
    Enrichment tab as the second method is deferred to E2.5.

Phase E1 — Enrichment tab (GSEA via fgsea)

  - New top-level Enrichment tab next to GO Term, driven by ranked-list
    Gene Set Enrichment Analysis. Pre-DE the tab is hidden; once DE has
    run, paste a .gmt file (or use the bundled
    inst/extdata/test-gmt/hallmark-mini.gmt fixture) and the results
    table populates with NES, padj, leading edge.
  - When more than one comparison is loaded, the NES heatmap card
    appears below the results, plotting NES across pathways x
    comparisons (significance stars, axis flip, padj cutoff slider).
  - New pure helpers in R/fct_gsea.R (@export): gmt_to_pathways(path)
    (fgsea-gated wrapper around fgsea::gmtPathways), run_gsea(de_table,
    pathways, ...) (DE table -> tidy enrichment data.frame), and
    nes_heatmap_data(results_by_comparison, sig_only, sig_threshold)
    (long-format reshape for the heatmap).
  - New Shiny modules in R/mod_enrichment.R, R/mod_enrichment_gmt.R,
    R/mod_enrichment_nes_heatmap.R. The GMT picker is the integration
    point Phase E2 will use to add MSigDB as a sibling source; the NES
    heatmap module is the same widget Phase E11 will reuse for
    cross-method comparison.
  - fgsea added to Suggests (Bioconductor); calls go through
    require_pkg("fgsea") so users without it installed see the install
    command instead of a stack trace.
  - Reference architecture (gsea_analysis shape, NES heatmap card, GMT
    picker layout) adapted from
    https://github.com/nephantes/gsea-explorer (Via Scientific) —
    re-implemented against this package's pure-helper conventions.

Phase E4 — Sample QC dashboard (always-on cards)

  - QC Plots tab now exposes four new cards for raw-count sample
    inspection: Library Depth (per-sample total counts, with >2-SD
    outlier flag), Feature Detection Rate (% of features with non-zero
    counts per sample), Mitochondrial Read % (sum of ^MT-/^mt-/Mt- rows
    per sample, with empty-state when no MT genes are detected), and
    Sample Distance Heatmap (clustered Euclidean distance on
    VST-transformed counts).
  - New R/fct_qc.R exports the underlying pure helpers
    (library_depth_summary, detection_rate, mt_pct_per_sample,
    sample_distance_matrix, flag_outliers_2sd) so they can be reused
    outside the Shiny app.
  - New Mapping / rRNA Stats card ships as an empty-state stub
    describing the optional sidecar TSV format (sample, mapped_pct,
    rRNA_pct); the upload handler itself lands in a follow-up.
  - Cards consume post-filter, post-batch, pre-normalize counts via
    batch()$BatchEffect()$count so library-size metrics report the
    correct raw values.
  - Three additional post-DE cards (Dispersion, Size factors, Cook's
    outlier flag) are deferred to a follow-up phase pending DESeq2 dds
    retention through prepDataContainer.

Phase E4.5 — Post-DE QC cards + dds retention

  - run_deseq2() gains a return_dds = FALSE argument; when TRUE it
    returns list(res, dds) so the fitted DESeqDataSet is recoverable for
    downstream QC. Threaded through runDESeq2(), runDE(),
    debrowserdeanalysis(), and prepDataContainer() (which now stashes a
    dds slot per comparison alongside init_data). Default behaviour is
    unchanged so existing callers (generateTestData, per-method tests)
    keep their data.frame contract.
  - QC Plots tab adds three post-DE cards driven by the active
    comparison's dds: Dispersion Estimates (DESeq2 plotDispEsts of
    gene-wise / fitted / shrunken values), Size Factors vs Library Size
    (scaled bars per sample with Spearman ρ in the subtitle), and Cook's
    Outlier Counts (per-sample count of high-Cook genes vs the vignette
    threshold 4 / (n_samples - n_params)).
  - New pure helpers in R/fct_qc.R: size_factor_library_summary(dds) and
    cooks_outlier_summary(dds, threshold = NULL). The Spearman
    correlation and active threshold are attached as attributes on the
    returned data.frame.
  - Cards render a friendly empty-state alert when no fitted
    DESeqDataSet is available (pre-DE, or non-DESeq2 method).
  - QC sidebar's "Select Columns" checkboxes now also drive cards 1-4
    (Library Depth, Detection Rate, MT %, Sample Distance) and cards 6-7
    (Size Factors, Cook's): unchecking a sample hides its bar/row across
    every per-sample card. Card 5 (Dispersion) is gene-level so column
    selection has no effect there. New pure helpers qc_keep_cols(counts,
    selected) and qc_keep_meta_rows(meta, selected) in R/fct_qc.R.

Phase B4 — Friendly errors

  - User-facing error messages now follow a consistent "what went wrong
    + what to do" format with correct severity (red blocks, yellow
    warns, green/blue empty-result).
  - Replaced raw R diagnostics in file-upload errors (separator
    mismatch, duplicate gene IDs, column/metadata mismatch).
  - Empty enrichment results no longer display as red errors — they were
    a category bug.
  - Six hand-rolled "Please install " messages in GO/KEGG paths
    collapsed into the existing require_pkg() helper.
  - No behavior change beyond message text and notification persistence.

Phase B3.5 — Sane defaults follow-up

  - DESeq2 default test changed from Wald to LRT, harmonizing the
    pure-helper, parser fallback, test-data, and (where applicable) help
    text with the UI default already in place.
  - DESeq2 LFC shrinkage now defaults to apeglm. Install via
    BiocManager::install("apeglm") if not already available; a friendly
    install message surfaces on first DE run otherwise. apeglm remains
    in Suggests, not Imports.
  - Low-count filter auto-applies on data load (method = Max, cutoff
    = 10 — the previous default values, now applied without a Filter
    click). Click Filter with a different cutoff/method to override, or
    set cutoff = 0 to restore the unfiltered counts.

User-visible

  - Raised startDEBrowser() upload limit from 30 MB to 90 MB.