Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/types/analysis_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const DANGER_ACK_TOKEN = 'I_UNDERSTAND_THE_RISK_AND_WANT_TO_OVERRIDE_BH'

export const SCHEMA_VERSION = '1.0.0'

/** Report whether the current fields match a client-recognised risky configuration. */
export function isDangerous(config: AnalysisConfig): boolean {
if (config.correction.allow_no_correction) return true
if (config.advanced.zero_handling === 'refuse' || config.advanced.zero_policy === 'refuse') return true
Expand All @@ -95,6 +96,7 @@ export function isDangerous(config: AnalysisConfig): boolean {
return false
}

/** Return a provenance-rich warning banner for a dangerous configuration, otherwise null. */
export function dangerBanner(config: AnalysisConfig): string | null {
if (!isDangerous(config)) return null
const reasons: string[] = []
Expand Down Expand Up @@ -134,6 +136,7 @@ ${reasons.map(r => `║ - ${r}`).join('\n')}
`
}

/** Return help for a recognised analysis field path, or a path-specific fallback. */
export function contextHelp(fieldPath: string): string {
const helpDb: Record<string, string> = {
method: `Analysis Method (required, explicit, no auto-selection) — v1: NB GLM, CLR/ILR+Gaussian, logistic
Expand Down
140 changes: 113 additions & 27 deletions src/analysis/AnalysisConfig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ const VALID_NORMALIZATION_FOR_METHOD = Dict{AnalysisMethod, Vector{String}}(
# --------------------------------------------------------------------------

"""
NormalizationConfig — explicit normalization / compositional transform
NormalizationConfig(; method, ...)

Heavy validation, context-sensitive help, refusal of meaningless inputs.
For CLR/ILR, pseudocount is mandatory and must be >0. For NB_GLM, size_factors preferred.
Create a normalisation or compositional-transform configuration. The method name
is normalised to lower case and the zero-policy name is parsed case-insensitively.
Incompatible options, non-positive CLR/ILR pseudocounts, and invalid epsilon or
replacement-delta values raise `ArgumentError`; unusual accepted values may emit
warnings.
"""
struct NormalizationConfig
method::String
Expand Down Expand Up @@ -189,9 +192,12 @@ struct NormalizationConfig
end

"""
CorrectionConfig — BH mandatory, hard-stop with DANGER banner
CorrectionConfig(; method="BH", alpha=0.05, ...)

BH is only sane default for high-dimensional microbiome data. Any override triggers DANGER.
Create a multiple-testing correction configuration. Non-BH methods are accepted
only when `allow_no_correction` is true and `acknowledgment_token` matches
`DANGER_ACK_TOKEN`. Empty or unsafe method strings, invalid tokens, and alpha
values outside `(0, 1)` raise `ArgumentError`.
"""
struct CorrectionConfig
method::String
Expand Down Expand Up @@ -235,9 +241,12 @@ struct CorrectionConfig
end

"""
AdvancedConfig — all behind Advanced Analysis expander, hidden unless Evidence Mode
AdvancedConfig(; ...)

Heavy validation, context-sensitive help, refusal of meaningless inputs, warnings for custom pseudocount/epsilon/zero_policy/etc.
Create the advanced analysis settings. Invalid categorical choices and values
outside their supported ranges raise `ArgumentError`; risky but accepted values
emit warnings. Either zero-handling option set to `"refuse"` also requires the
danger acknowledgement token.
"""
struct AdvancedConfig
dispersion_method::String
Expand Down Expand Up @@ -352,25 +361,15 @@ end
# --------------------------------------------------------------------------

"""
AnalysisConfig — immutable, versioned, explicit, provenance-rich

Fields (all explicit, no silent defaults except documented):
- schema_version: "1.0.0" (from DEED :schema-version first)
- id: UUID4 string, immutable
- created_at: DateTime, immutable
- created_by: String
- method: AnalysisMethod (NB_GLM, CLR_LM, ILR_LM, LOGISTIC in v1)
- formula: String R-style must contain ~, e.g. "~ group" or "disease ~ group + batch", forbids semicolon backtick dollar injection
- outcome_column: Union{String,Nothing} required for logistic
- metadata_columns: Vector{String} explicit, min 1, unique, pattern ^[a-zA-Z0-9_.\\-]+\$
- normalization: NormalizationConfig (method-dependent, pseudocount, epsilon, zero_policy, ilr_basis)
- correction: CorrectionConfig BH mandatory hard-stop DANGER banner on overrides
- advanced: AdvancedConfig behind Advanced Analysis expander heavy validation/help/warnings custom pseudocount/epsilon/zero_policy/etc.
- provenance: OrderedDict provenance-rich
- hash: SHA256 hex content-addressed, immutable
- dangerous: Bool computed is_dangerous

No silent switching. Every field explicit.
AnalysisConfig(; method, formula, metadata_columns, ...)

Create an immutable analysis configuration after validating its schema version,
UUID, formula, metadata columns, outcome, and method/normalisation pairing. The
constructor enriches a copy of `provenance`, computes a content hash when none is
supplied, and derives the stored danger flag from correction and advanced
settings; an explicitly supplied `dangerous=true` also marks the configuration.
Invalid combinations raise `ArgumentError`, while accepted but questionable
settings may emit warnings.
"""
struct AnalysisConfig
schema_version::String
Expand Down Expand Up @@ -563,7 +562,12 @@ const AnalysisConfigStruct = AnalysisConfig
const AdvancedOverrides = AdvancedConfig

"""
AnalysisResult — immutable result with provenance
AnalysisResult(; config_id, config_hash, method, ...)

Create an immutable result linked to an analysis configuration. The constructor
adds the configuration identifiers and method to a copy of `provenance` and
computes a content hash unless one is supplied. Invalid UUIDs or an empty
configuration hash raise `ArgumentError`.
"""
struct AnalysisResult
id::String
Expand Down Expand Up @@ -616,6 +620,13 @@ end
# Validators — refuse meaningless inputs
# --------------------------------------------------------------------------

"""
validate_config(config, available_columns; strict=true)

Return validation messages for missing metadata columns and formula references.
With `strict=true`, also recheck the method/normalisation pairing. A dangerous
configuration emits a warning but does not add a validation message.
"""
function validate_config(config::AnalysisConfig, available_columns::Vector{String}; strict::Bool=true)
errors = String[]

Expand Down Expand Up @@ -679,10 +690,18 @@ function validate_config(config::AnalysisConfig, available_columns::Vector{Strin
return errors
end

"""Return the content hash stored on `config`."""
function config_hash(config::AnalysisConfig)
return config.hash
end

"""
canonical_json(config)

Serialise selected fields from `config` in a fixed order. Unlike
[`to_json`](@ref), this representation omits provenance and several advanced and
normalisation fields.
"""
function canonical_json(config::AnalysisConfig)
return JSON3.write(OrderedDict(
"schema_version" => config.schema_version,
Expand Down Expand Up @@ -721,6 +740,12 @@ end
# Context-sensitive help — for UI
# --------------------------------------------------------------------------

"""
context_help(field_path)

Return the UI help text for a recognised analysis field path. Unknown paths
return a fallback message that includes the requested path.
"""
function context_help(field_path::String)
help_db = Dict{String,String}(
"method" => """
Expand Down Expand Up @@ -945,6 +970,13 @@ end
# DANGER banner — scary for paper writers on overrides
# --------------------------------------------------------------------------

"""
danger_banner(config)

Return a formatted warning for a configuration whose stored danger flag is set,
or `nothing` for a safe configuration. The banner lists the risky settings
recognised by this formatter and includes configuration provenance identifiers.
"""
function danger_banner(config::AnalysisConfig)
if !is_dangerous(config)
return nothing
Expand Down Expand Up @@ -990,10 +1022,18 @@ function danger_banner(config::AnalysisConfig)
return banner
end

"""Return the danger flag stored on `config`."""
function is_dangerous(config::AnalysisConfig)
return config.dangerous
end

"""
log_danger_banner(config)

Log an informational message and return `nothing` for a safe configuration. For
a dangerous configuration, log the generated banner at error and warning levels
and return the banner text.
"""
function log_danger_banner(config::AnalysisConfig)
banner = danger_banner(config)
if isnothing(banner)
Expand All @@ -1011,6 +1051,7 @@ end
# Serialization — JSON, Nickel, DEED, with standards
# --------------------------------------------------------------------------

"""Serialise `config` to the JSON representation used by the analysis API."""
function to_json(config::AnalysisConfig)
return JSON3.write(OrderedDict(
"schema_version" => config.schema_version,
Expand Down Expand Up @@ -1054,6 +1095,13 @@ function to_json(config::AnalysisConfig)
))
end

"""
from_json(json_str)

Parse a JSON analysis configuration and rebuild its nested immutable objects.
Optional fields absent from older representations receive the constructor
defaults. JSON parsing and constructor validation errors are propagated.
"""
function from_json(json_str::String)
data = JSON3.read(json_str)

Expand Down Expand Up @@ -1117,6 +1165,7 @@ function from_json(json_str::String)
return cfg
end

"""Render `config` as the module's generated Nickel source text."""
function to_nickel(config::AnalysisConfig)
# Nickel contract from hyperpolymath/standards 1-formats/k9/*.ncl style
# Uses TagOrString for enums, contracts for validation
Expand Down Expand Up @@ -1176,6 +1225,14 @@ function to_nickel(config::AnalysisConfig)
"""
end

"""
from_nickel(nickel_str)

Build a minimal configuration from the first method and formula assignments in
Nickel-like source. This is a partial parser: metadata and most settings use
defaults. Raise `ArgumentError` when either assignment cannot be found or the
derived configuration is invalid.
"""
function from_nickel(nickel_str::String)
# For now, parse via simple regex — full Nickel evaluation would require nickel binary
# This is a placeholder that extracts method and formula and validates via our validators
Expand All @@ -1196,6 +1253,13 @@ function from_nickel(nickel_str::String)
)
end

"""
validate_nickel(nickel_str)

Return messages for required field or contract markers missing from generated
Nickel text. This performs textual checks only; it does not parse or evaluate
Nickel.
"""
function validate_nickel(nickel_str::String)
# Validate via contracts — for now check that it contains required fields and contracts pass
errors = String[]
Expand All @@ -1217,6 +1281,7 @@ function validate_nickel(nickel_str::String)
return errors
end

"""Render `config` as a `repo-deed` document using DEED syntax."""
function to_deed(config::AnalysisConfig)
# DEED from hyperpolymath/standards 1-formats/deed/spec/DEED-GRAMMAR-SPEC.adoc v0.2.0
# :schema-version first, only () brackets, #t/#f booleans, :kebab-case keywords, #u5 UUID5, SPDX header mandatory
Expand Down Expand Up @@ -1299,6 +1364,12 @@ function to_deed(config::AnalysisConfig)
"""
end

"""
validate_deed(deed_str)

Return messages for required DEED markers, forbidden bracket characters, and a
missing DEED boolean. These are textual checks rather than full DEED parsing.
"""
function validate_deed(deed_str::String)
errors = String[]
if !occursin(":schema-version", deed_str)
Expand Down Expand Up @@ -1330,6 +1401,15 @@ end
# DOI-ready JSON manifest bundles — DataCite
# --------------------------------------------------------------------------

"""
create_doi_bundle(config, [result]; output_dir, authors, title, license, description)

Create or update `output_dir` with DataCite metadata, JSON, Nickel, DEED,
provenance, and content-hash files, then return the directory path. An optional
result is embedded in the DataCite document. Dangerous configurations also write
`DANGER_BANNER.txt` and emit a warning; existing files with the same names are
overwritten.
"""
function create_doi_bundle(config::AnalysisConfig, result::Union{AnalysisResult,Nothing}=nothing; output_dir::String="doi_bundle_$(config.id)", authors::Vector{String}=String[], title::String="MetaManifold Analysis Bundle", license::String="CC-BY-4.0", description::String="Differential abundance analysis")
mkpath(output_dir)

Expand Down Expand Up @@ -1427,6 +1507,12 @@ end
# Epistemic bridge — present_in_every_admissible_world (finite model)
# --------------------------------------------------------------------------

"""
present_in_every_admissible_world(candidates, query)

Return whether `query` is true for every candidate world. An empty candidate
collection therefore returns `true`, following `all` semantics.
"""
function present_in_every_admissible_world(candidates::Vector, query::Function)
# Finite model from residual-evidence-types: Case inhabited, Holds c P = (x:Candidate) -> P (fst x)
# Returns true if query holds for every candidate world consistent with observation
Expand Down
Loading