From b6ab9325b8d29f1d1c266fbf5b0e81f3732d6d3b Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:24:24 +0000 Subject: [PATCH 1/2] fix(clade-cumulus): repair field names, NHX support capture, LWR tolerance, route table loading The CladeCumulus testset (the last red testset in the Julia suite: 817 pass / 3 fail / 14 error, all inside it) shipped in #207 with defects that the long-broken build had masked: - _graft_jplace_queries read QueryCandidate.weight/.distal_length/ .pendant_length; the struct fields are like_weight/distal_len/ pendant_len (FieldError on any valid jplace graft). - A single retained placement at LWR 0.98 was marked undecided: PLACEMENT_WEIGHT_SUM_TOL=1e-5 rejects truncated candidate lists whose retained ratios legitimately sum to slightly less than 1. Tolerance is now 0.05; a 0.40 sum stays undecided. - The Newick label reader consumed an NHX comment after ')' without capturing it, dropping [&&NHX:B=92] support; capture refs are now threaded through read_label_and_quoted. - The sources route listed views via SHOW TABLES and then failed to bind a session-registered DataFrame view on a fresh connection (500); it now enumerates real tables via duckdb_tables(). - _load_run_table_df rejected the SubString table names JSON3 returns; arguments are normalised to String at the boundary. - Test used PH.Tools, but Phylogeny imports only selected names from Tools; the mock-tool override now addresses MetaManifold.Tools. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- src/analysis/clade_cumulus.jl | 24 ++++++++++++++++-------- src/server/routes/clade_cumulus.jl | 12 ++++++++++-- test/unit/test_clade_cumulus.jl | 8 ++++---- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/analysis/clade_cumulus.jl b/src/analysis/clade_cumulus.jl index a702fde2..63cbdf24 100644 --- a/src/analysis/clade_cumulus.jl +++ b/src/analysis/clade_cumulus.jl @@ -28,7 +28,11 @@ const DEFAULT_TAXONOMIC_RANKS = [ "Class", "Order", "Family", "Genus", "Species", ] -const PLACEMENT_WEIGHT_SUM_TOL = 1e-5 +# Placement tools may truncate a query's candidate list, so retained +# like-weight ratios can legitimately sum to slightly less than 1 (a single +# retained placement at 0.98 is complete evidence). Sums far from 1 indicate +# a malformed or partial record and stay undecided. +const PLACEMENT_WEIGHT_SUM_TOL = 0.05 ## Phylogenetic tree structures & Newick / jplace parsers mutable struct PhyloNode @@ -202,8 +206,12 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]): end end - function read_label_and_quoted()::Tuple{String,Bool} - skip_ws_and_leading_comments!() + function read_label_and_quoted(; capture_nhx::Union{Ref{Union{Float64,Nothing}},Nothing} = nothing, + capture_edge::Union{Ref{Union{Int,Nothing}},Nothing} = nothing)::Tuple{String,Bool} + # An NHX comment immediately after ')' precedes the (usually empty) + # label, so the capture refs must be threaded through this first skip + # or the support tag is consumed as an anonymous comment. + skip_ws_and_leading_comments!(capture_nhx = capture_nhx, capture_edge = capture_edge) pos[] > n && return ("", false) if bytes[pos[]] == UInt8('\'') pos[] += 1 @@ -260,7 +268,7 @@ function parse_newick(src::AbstractString; warnings::Vector{String} = String[]): nhx_sup = Ref{Union{Float64,Nothing}}(nothing) edge_num = Ref{Union{Int,Nothing}}(nothing) - raw_lbl, was_quoted = read_label_and_quoted() + raw_lbl, was_quoted = read_label_and_quoted(capture_nhx = nhx_sup, capture_edge = edge_num) skip_ws_and_leading_comments!(capture_nhx = nhx_sup, capture_edge = edge_num) branch_len = 0.0 @@ -991,7 +999,7 @@ function _graft_jplace_queries( for q in jd.queries q.status === :valid && !isempty(q.candidates) || continue - total_w = sum((c.weight for c in q.candidates); init = 0.0) + total_w = sum((c.like_weight for c in q.candidates); init = 0.0) total_w > 0.0 || continue multi = length(q.candidates) > 1 for (qname, mult) in q.names @@ -1000,7 +1008,7 @@ function _graft_jplace_queries( tx = get(asv_deepest_taxon, qname, nothing) tconf = get(asv_tax_conf, qname, 1.0) for (ci, cand) in enumerate(q.candidates) - share = cand.weight / total_w + share = cand.like_weight / total_w tip_id = multi ? string(qname, "@edge", cand.edge_num, "#", ci) : qname if multi asv_reads[tip_id] = get(asv_reads, tip_id, 0.0) + base_cnt * share @@ -1009,9 +1017,9 @@ function _graft_jplace_queries( elseif !haskey(asv_reads, tip_id) asv_reads[tip_id] = base_cnt end - query_lwr[tip_id] = cand.weight + query_lwr[tip_id] = cand.like_weight push!(get!(by_edge, cand.edge_num, _GraftedQuerySpec[]), - _GraftedQuerySpec(tip_id, qname, share, max(0.0, cand.distal_length), max(0.0, cand.pendant_length), cand.weight)) + _GraftedQuerySpec(tip_id, qname, share, max(0.0, cand.distal_len), max(0.0, cand.pendant_len), cand.like_weight)) end end end diff --git a/src/server/routes/clade_cumulus.jl b/src/server/routes/clade_cumulus.jl index 45825b61..0dd6918d 100644 --- a/src/server/routes/clade_cumulus.jl +++ b/src/server/routes/clade_cumulus.jl @@ -64,7 +64,11 @@ function _probe_optional_phylo_tool(tool_key::String, fallbacks::Vector{String}) end end -function _load_run_table_df(study::String, run::String, table::String; group::Union{String,Nothing}=nothing) +function _load_run_table_df(study::AbstractString, run::AbstractString, table::AbstractString; group::Union{AbstractString,Nothing}=nothing) + # JSON3 hands request fields back as SubStrings; the DuckDB helpers are + # String-typed, so normalise once at this boundary. + study = String(study); run = String(run); table = String(table) + group = isnothing(group) ? nothing : String(group) dir = _require_duckdb(study, run; group) isnothing(dir) && return (nothing, nothing, json_error(404, "no_results", "No results for run '$run' - run the pipeline first")) @@ -142,7 +146,11 @@ end db = DuckDB.DB(db_path) con = DBInterface.connect(db) try - names_df = DataFrame(DBInterface.execute(con, "SHOW TABLES")) + # Real tables only: SHOW TABLES also lists views, and a view backed + # by a session-registered DataFrame cannot be bound (let alone + # counted) from this fresh connection. + names_df = DataFrame(DBInterface.execute(con, + "SELECT table_name AS name FROM duckdb_tables() WHERE NOT internal")) tlist = sort(String[string(r.name) for r in eachrow(names_df)]) for t in tlist cols = _duckdb_columns(con, t) diff --git a/test/unit/test_clade_cumulus.jl b/test/unit/test_clade_cumulus.jl index da09bc18..6e12a0c9 100644 --- a/test/unit/test_clade_cumulus.jl +++ b/test/unit/test_clade_cumulus.jl @@ -252,8 +252,8 @@ done echo "(MMTIP_000001:0.05,(MMTIP_000002:0.03,MMTIP_000003:0.04)0.910:0.12,MMTIP_000004:0.08);" > "$OUT" """) chmod(mock_ft, 0o755) - old_ft = get(PH.Tools._tools, "fasttree", nothing) - PH.Tools._tools["fasttree"] = mock_ft + old_ft = get(MetaManifold.Tools._tools, "fasttree", nothing) + MetaManifold.Tools._tools["fasttree"] = mock_ft try s4 = [ "ASV_1" => "ACGTACGTACGTACGTACGT", @@ -267,9 +267,9 @@ echo "(MMTIP_000001:0.05,(MMTIP_000002:0.03,MMTIP_000003:0.04)0.910:0.12,MMTIP_0 r["sha256"] == bytes2hex(open(sha256, mock_ft)), ft_res.tool_records) finally if old_ft === nothing - delete!(PH.Tools._tools, "fasttree") + delete!(MetaManifold.Tools._tools, "fasttree") else - PH.Tools._tools["fasttree"] = old_ft + MetaManifold.Tools._tools["fasttree"] = old_ft end end end From 0e409ed7791b480ead7f6ac37a778b6d33bef673 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:34:01 +0000 Subject: [PATCH 2/2] fix(doi): restore missing module DOI opener src/core/doi.jl ended with 'end # module DOI' but never opened the module, so including it into MetaManifold failed to parse (ParseError at doi.jl:681) and no Julia test could even load the package. Routes and tests address MetaManifold.DOI, so the file is a submodule whose opener was lost in an earlier repair. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- src/core/doi.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/doi.jl b/src/core/doi.jl index 6a2f926a..f5b0bd95 100644 --- a/src/core/doi.jl +++ b/src/core/doi.jl @@ -16,6 +16,8 @@ # * Durable write-ahead journal; immutable DOI/provenance receipts # * DOI insertion never rewrites scientific objects +module DOI + using Dates, SHA, HTTP, JSON3, Logging export DOIConfig, Deposition, PublicationReceipt,