From c8c115b1fab37fa0fe486976f9c60c77039cbc97 Mon Sep 17 00:00:00 2001
From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com>
Date: Fri, 23 Oct 2026 00:11:49 +0000
Subject: [PATCH 1/2] fix(frontend): restore build and metadata tests
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
---
frontend/src/api/client.ts | 1 +
frontend/src/api/types.ts | 26 +++++++++-
.../src/components/PipelineStages.test.ts | 6 +--
.../src/components/differentialTable.test.ts | 4 +-
frontend/src/components/differentialTable.ts | 49 ++++++++++++++++---
frontend/src/views/StudyView.tsx | 4 +-
test/unit/test_numeric_policy.jl | 4 +-
7 files changed, 76 insertions(+), 18 deletions(-)
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index 98a97087..5b58aaa7 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -23,6 +23,7 @@ import type {
ReferenceTreeDoc, ReferenceTreeSummary, ReferenceTreeDetail, AlignmentQC, TrimSettings,
CladeCumulusSourcesResponse, CladeCumulusRequest, CladeCumulusResponse,
CladeMoveValidationRequest, CladeMoveValidationResponse,
+ CladisticsSummary, CladisticsDetail, CladisticsCreateRequest,
} from './types'
import type { FigureDoc } from '../figure/types'
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index e668807d..593f7dc2 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -423,7 +423,10 @@ export interface DifferentialRow {
prevalence: number
}
-export type DifferentialMethod = 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb'
+export type DifferentialMethod =
+ | 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb'
+ | 'multinomial' | 'dirichlet_multinomial'
+ | 'ancom_bc' | 'aldex2' | 'songbird'
/** Which row field holds the effect size, and what to call it. */
export interface DifferentialEffect {
@@ -453,6 +456,16 @@ export interface DifferentialResult {
replacement_delta?: number
/** Null uses GBM's estimated concentration; positive values override it. */
bayesian_alpha?: number | null
+ /** multinomial and dirichlet_multinomial only. */
+ mn_reference_taxon?: string | null
+ /** multinomial and dirichlet_multinomial only. */
+ mn_penalty?: number
+ /** dirichlet_multinomial only. */
+ dm_overdispersion_method?: 'mle' | 'mom'
+ /** Multinomial optimiser settings. */
+ tolerance?: number
+ max_iter?: number
+ max_seconds?: number
/** aldex2 only. */
mc_samples?: number
/** aldex2 only. */
@@ -462,6 +475,17 @@ export interface DifferentialResult {
n_taxa: number; n_tested: number; n_failed: number; n_boundary: number; n_filtered: number
/** Safety note for two-part models. */
model_warning?: string
+ /** Diagnostics returned by the joint multinomial models. */
+ model?: {
+ reference_taxon: string
+ penalty: number
+ n_parameters: number
+ iterations: number
+ converged: boolean
+ log_likelihood: number
+ runtime_seconds: number
+ n_taxa_fitted: number
+ }
/** clr_welch only. */
zero_replacement?: {
method: string
diff --git a/frontend/src/components/PipelineStages.test.ts b/frontend/src/components/PipelineStages.test.ts
index 0037ce55..2c46743a 100644
--- a/frontend/src/components/PipelineStages.test.ts
+++ b/frontend/src/components/PipelineStages.test.ts
@@ -24,11 +24,9 @@ test('advanced zero handling exposes both methods, bounded delta and nullable al
expect(CONFIG_DESCRIPTIONS['analysis.differential.bayesian_alpha']).toContain('no posterior draws')
})
-test('differential method enum lists all five compositional and count models', () => {
+test('differential method enum lists exactly the methods accepted by the backend', () => {
expect(CONFIG_TYPES['analysis.differential.method']).toEqual({
kind: 'enum',
- options: ['nb_glm', 'clr_welch', 'ancom_bc', 'aldex2', 'songbird'],
+ options: ['nb_glm', 'clr_welch', 'zinb', 'hurdle_nb'],
})
- expect(CONFIG_TYPES['analysis.differential.mc_samples']).toEqual({ kind: 'int' })
- expect(CONFIG_DESCRIPTIONS['analysis.differential.mc_samples']).toContain('Dirichlet')
})
diff --git a/frontend/src/components/differentialTable.test.ts b/frontend/src/components/differentialTable.test.ts
index 2bae9b2c..0090f3d1 100644
--- a/frontend/src/components/differentialTable.test.ts
+++ b/frontend/src/components/differentialTable.test.ts
@@ -99,7 +99,7 @@ describe('multinomial results', () => {
test('the reference taxon is exported with an empty statistic set, never a substitute', () => {
const lines = differentialCsv(mn).trimEnd().split('\r\n')
- expect(lines[4].startsWith('Bacteroides,reference,0,0,,,,,,1,')).toBe(true)
+ expect(lines[4].startsWith('Bacteroides,reference,0,0,,,,,,,1,')).toBe(true)
expect(effectValue(mn, mn.rows[1])).toBe(1.6)
})
})
@@ -163,7 +163,7 @@ describe('advanced compositional methods (ancom_bc, aldex2, songbird)', () => {
expect(effectValue(ax, ax.rows[0])).toBe(1.85)
expect(configLine(ax)).toBe('method aldex2, mc_samples 128, seed 123, min_prevalence 0.05; estimate is an ALDEx2 standardized CLR effect size, not a fold change')
const csvRows = differentialCsv(ax).trimEnd().split('\r\n')
- expect(csvRows[4]).toBe('Bacteroides,ok,,1.85,,5.545,2.9e-8,5.8e-8,,1,')
+ expect(csvRows[4]).toBe('Bacteroides,ok,,1.85,,5.545,2.9e-8,5.8e-8,,,1,')
})
test('songbird reports CLR-centred multinomial differential metadata', () => {
diff --git a/frontend/src/components/differentialTable.ts b/frontend/src/components/differentialTable.ts
index 2743553e..fd4b3980 100644
--- a/frontend/src/components/differentialTable.ts
+++ b/frontend/src/components/differentialTable.ts
@@ -46,13 +46,48 @@ export function formatStat(value: number | null): string {
*/
export function configLine(result: DifferentialResult): string {
const c = result.config
- if (c.method !== 'clr_welch')
- return `method ${c.method}, offset ${c.offset}, min_prevalence ${c.min_prevalence}`
- const replacement = c.replacement_method ?? 'bayesian_multiplicative'
- const alpha = c.bayesian_alpha === null || c.bayesian_alpha === undefined
- ? 'estimated' : String(c.bayesian_alpha)
- return `method clr_welch, replacement_method ${replacement}, replacement_delta ${c.replacement_delta}, ` +
- `bayesian_alpha ${alpha}, min_prevalence ${c.min_prevalence}; estimate is a CLR difference, not a fold change`
+
+ if (c.method === 'multinomial' || c.method === 'dirichlet_multinomial') {
+ const settings = [`method ${c.method}`]
+ if (c.method === 'dirichlet_multinomial' && c.dm_overdispersion_method)
+ settings.push(`overdispersion ${c.dm_overdispersion_method}`)
+ if (c.mn_reference_taxon) settings.push(`reference ${c.mn_reference_taxon}`)
+ if (c.mn_penalty !== undefined) settings.push(`penalty ${c.mn_penalty}`)
+ settings.push(`min_prevalence ${c.min_prevalence}`)
+ return `${settings.join(', ')}; estimates are log2 fold changes against the reference taxon`
+ }
+
+ if (c.method === 'clr_welch') {
+ const replacement = c.replacement_method ?? 'bayesian_multiplicative'
+ const settings = [
+ `method clr_welch`,
+ `replacement_method ${replacement}`,
+ `replacement_delta ${c.replacement_delta}`,
+ ]
+ // CZM has no Bayesian concentration parameter; do not claim one was used.
+ if (replacement === 'bayesian_multiplicative') {
+ const alpha = c.bayesian_alpha === null || c.bayesian_alpha === undefined
+ ? 'estimated' : String(c.bayesian_alpha)
+ settings.push(`bayesian_alpha ${alpha}`)
+ }
+ settings.push(`min_prevalence ${c.min_prevalence}`)
+ return `${settings.join(', ')}; estimate is a CLR difference, not a fold change`
+ }
+
+ if (c.method === 'ancom_bc')
+ return `method ancom_bc, min_prevalence ${c.min_prevalence}; effect is bias-corrected log2 fold change (Lin & Peddada 2020)`
+
+ if (c.method === 'aldex2')
+ return `method aldex2, mc_samples ${c.mc_samples}, seed ${c.seed}, min_prevalence ${c.min_prevalence}; estimate is an ALDEx2 standardized CLR effect size, not a fold change`
+
+ if (c.method === 'songbird')
+ return `method songbird, min_prevalence ${c.min_prevalence}; estimate is a CLR-centred multinomial differential, not an absolute fold change`
+
+ const settings = [`method ${c.method}`]
+ if (c.offset !== undefined) settings.push(`offset ${c.offset}`)
+ if (c.method === 'nb_glm' && c.dispersion !== undefined) settings.push(`dispersion ${c.dispersion}`)
+ settings.push(`min_prevalence ${c.min_prevalence}`)
+ return settings.join(', ')
}
/** The effect size of one row, from the field the result names in `effect`. */
diff --git a/frontend/src/views/StudyView.tsx b/frontend/src/views/StudyView.tsx
index 89f32231..553d948d 100644
--- a/frontend/src/views/StudyView.tsx
+++ b/frontend/src/views/StudyView.tsx
@@ -15,7 +15,7 @@ import { ConfigAccordion } from '../components/ConfigAccordion'
import { TreesPanel } from '../components/TreesPanel'
import { PlacementPanel } from '../components/PlacementPanel'
import { EvidenceModeToggle } from '../components/EvidenceModeToggle'
-import { CladeCumulusPanel } from '../components/CladeCumulusPanel'
+import { CladeCumulusPanel as EvidenceCladeCumulusPanel } from '../components/CladeCumulusPanel'
import { ReportPanel } from '../components/ReportPanel'
import { useNavRefresh } from '../hooks/useNavRefresh'
import { CladeCumulusPanel } from '../cladistics/CladeCumulusPanel'
@@ -304,7 +304,7 @@ export function StudyView() {
{view === 'trees' && <>
- {evidenceMode && }
+ {evidenceMode && }
setTreesVersion(v => v + 1)} />
>}
diff --git a/test/unit/test_numeric_policy.jl b/test/unit/test_numeric_policy.jl
index 2f64ff8c..5c2cdf5b 100644
--- a/test/unit/test_numeric_policy.jl
+++ b/test/unit/test_numeric_policy.jl
@@ -51,8 +51,8 @@ end
tight = numeric_policy(mode=:exact_counts, max_denominator_bits=32)
@test_throws ResourceLimitError exact_fraction(1, big(2)^40; policy=tight)
@test_throws ResourceLimitError exact_fraction(big(2)^40, 1; policy=tight)
- # Each addend fits the 32-bit budget on its own; their sum 1/2^31 + 1/3 needs
- # 33 bits, so the limit must fire on the running total, not only on inputs.
+ # Each addend fits the 32-bit budget on its own; their reduced sum has a
+ # 62-bit denominator, so the limit must fire on the running total.
@test_throws ResourceLimitError exact_rational_sum(
[big(1)//big(2)^31, big(1)//(big(2)^31 - 1)]; policy=tight)
end
From fd342911d5b89af6dc114ef943d6e8537782b035 Mon Sep 17 00:00:00 2001
From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com>
Date: Fri, 23 Oct 2026 00:17:39 +0000
Subject: [PATCH 2/2] test(stats): verify 1024-bit references independently
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
---
test/references/check_high_precision_references.py | 2 +-
test/unit/test_high_precision.jl | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/references/check_high_precision_references.py b/test/references/check_high_precision_references.py
index 3d644c8f..c63ffa23 100644
--- a/test/references/check_high_precision_references.py
+++ b/test/references/check_high_precision_references.py
@@ -14,7 +14,7 @@
import re
import sys
-PRECISION = 220
+PRECISION = 460
def references() -> dict[str, str]:
diff --git a/test/unit/test_high_precision.jl b/test/unit/test_high_precision.jl
index 1b903e08..9f4c9e96 100644
--- a/test/unit/test_high_precision.jl
+++ b/test/unit/test_high_precision.jl
@@ -10,9 +10,9 @@ const HP_LABELS = ["A", "B"]
const HP_FEATURES = ["f1", "f2", "f3"]
const REF_LN2 = "0.6931471805599453094172321214581765680755001343602552541206800094933936219696947156058633269964186875420014810205706857336855202357581305570326707516350759619307275708283714351903070386238916734711233501153644979552391204751726815749320651555247341395258829504530070953263666426541042391578149520437404303855008019441706416715186447128399681717845469570262716310645461502572074024816377733896385506952606683411372738737229289564935470257626520988596932019650586"
-const REF_LN3 = "1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615754813732088787970029065957865742368004225930519821052801870767277410603162769183381367179373698844360959903742570316795911521145592"
-const REF_TWO_THIRDS = "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667"
-const REF_H_1234 = "1.279854225833667467182185762149429928131354186921692886392239591382930699742075074568049151649059984163575294301928203177013603961545520253317776056233522504371230806742168835499680746549033237086147384098562106321046048"
+const REF_LN3 = "1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615754813732088787970029065957865742368004225930519821052801870767277410603162769183381367179373698844360959903742570316795911521145591917750671347054940166775580222203170252946897560690106521505642868138036317373298577782366991654792131818149020030103823630122248652748198225991097452490896458053467008845965085748444119018857087647494867079613085829411602166121184001409826"
+const REF_TWO_THIRDS = "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667"
+const REF_H_1234 = "1.279854225833667467182185762149429928131354186921692886392239591382930699742075074568049151649059984163575294301928203177013603961545520253317776056233522504371230806742168835499680746549033237086147384098562106321046048112868339308649672681189135461287570216937288791515535715833594380020178552966375097993963900928200120047744717834903206362014300458629735272704036751340598445797090470318251133760850603018005627582297102594277585911107434593371009711254916"
function test_bigfloat_close(actual, reference, bits, ulps=16)
setprecision(BigFloat, bits) do