Skip to content
Closed
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
8 changes: 8 additions & 0 deletions scripts/check-julia-syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ for file in files
issue = ex
break
end
# A stray `end` at top level parses cleanly and only fails at
# lowering (""end" at top level"), so this gate has to reject it
# explicitly: a dropped `module` opener is otherwise invisible here.
if ex === :end
issue = "top-level `end` at code unit $position: a block opener " *
"(module, function, begin) is missing or was removed"
break
end
next_position > position ||
error("Julia parser made no progress at code unit $position")
position = next_position
Expand Down
6 changes: 3 additions & 3 deletions src/analysis/clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,8 @@ function _graft_jplace_queries(

function walk(u::PhyloNode)::PhyloNode
rebuilt_kids = PhyloNode[walk(ch) for ch in u.children]
cur = PhyloNode(u.id, u.name, u.branch_length, u.support, u.edge_num, rebuilt_kids)
specs = u.edge_num === nothing ? nothing : get(by_edge, u.edge_num, nothing)
cur = PhyloNode(u.id, u.name, u.branch_length, u.support, u.edge_number, rebuilt_kids)
specs = u.edge_number === nothing ? nothing : get(by_edge, u.edge_number, nothing)
(specs === nothing || isempty(specs)) && return cur

# Order grafts from most distal (closest to `cur`, smallest `distal`) to most proximal.
Expand All @@ -1048,7 +1048,7 @@ function _graft_jplace_queries(
sub = PhyloNode(string("jplace_clade#", seq_auto[]), "", 0.0, 1.0, nothing, PhyloNode[sub, qtip])
end
rem_len = max(0.0, total_len - prev_distal)
PhyloNode(sub.id, sub.name, rem_len, cur.support, cur.edge_num, sub.children)
PhyloNode(sub.id, sub.name, rem_len, cur.support, cur.edge_number, sub.children)
end

walk(base_root)
Expand Down
8 changes: 6 additions & 2 deletions src/server/routes/clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
# write only study-local sidecars under `projects/{study}/trees/.clade_cumulus/`.

using DataFrames, OrderedCollections, SHA, Dates, JSON3
# Named here rather than inherited from results.jl, which is included
# earlier: relying on a sibling file's `using` breaks when this file is
# evaluated on its own, and a missing DuckDB binding is a 500 at runtime.
using DuckDB, DBInterface

const _CC_SIDECAR_MAX_BYTES = 5 * 1024 * 1024

Expand Down Expand Up @@ -276,13 +280,13 @@ end
jd_div = MetaManifold.CladeCumulus.parse_jplace(jplace_str)
for q in jd_div.queries
q.status === :valid && !isempty(q.candidates) || continue
tw = sum((c.weight for c in q.candidates); init = 0.0)
tw = sum((c.like_weight for c in q.candidates); init = 0.0)
tw > 0.0 || continue
multi = length(q.candidates) > 1
for (qname, _) in q.names
for (ci, cand) in enumerate(q.candidates)
tid = multi ? string(qname, "@edge", cand.edge_num, "#", ci) : qname
tip_weights[tid] = cand.weight / tw
tip_weights[tid] = cand.like_weight / tw

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve unplaced LWR mass in diversity weights

When the newly accepted single-candidate placement has an LWR between 0.5 and 1.0, tw equals that candidate's weight, so this division always produces 1.0. The /clade-cumulus diversity block therefore assigns all of the query's abundance to the reported edge even though the parser explicitly treats the remaining probability mass as unplaced, inflating that tip's contribution to weighted UniFrac and related abundance-based results. Preserve the raw LWR for this one-candidate case, with the remainder accounted for as unplaced, rather than renormalizing it away.

Useful? React with 👍 / 👎.

end
end
end
Expand Down
3 changes: 1 addition & 2 deletions src/server/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ module Server
include(joinpath(@__DIR__, "routes", "phylogeny.jl"))
include(joinpath(@__DIR__, "routes", "cladistics.jl"))
include(joinpath(@__DIR__, "routes", "clade_cumulus.jl"))
include(joinpath(@__DIR__, "routes", "cladistics.jl"))

## Bad request middleware
# Helpers deep in a route (e.g. `_req_group`) throw this to reject a request.
Expand Down Expand Up @@ -433,7 +432,7 @@ module Server
_analysis_source_middleware, _file_middleware]
# Oxygen's docs (/docs, /docs/schema) and metrics sit outside the middleware
# above, so they would bypass the Host and Origin checks; nothing uses them.
const _SERVE_OPTIONS = (access_log=nothing, show_errors=false, docs=false, metrics=false)
const _SERVE_OPTIONS = (access_log=nothing, show_errors=true, docs=false, metrics=false)

function start(; root=pwd(), host="127.0.0.1", port=8080, listen::Bool=true)
global_logger(_SuppressEpipe(global_logger()))
Expand Down
53 changes: 52 additions & 1 deletion test/unit/test_clade_cumulus.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const EP = MetaManifold.Epistemic
const CC = MetaManifold.CladeCumulus
const DM = MetaManifold.DiversityMetrics
const PH = MetaManifold.Phylogeny
const SV = MetaManifold.Server
SV = MetaManifold.Server

_cc_req(method::String, path::String, body::String = "") =
SV.Oxygen.internalrequest(
Expand Down Expand Up @@ -330,6 +330,57 @@ echo "(MMTIP_000001:0.05,(MMTIP_000002:0.03,MMTIP_000003:0.04)0.910:0.12,MMTIP_0
"supportThreshold" => 0.85,
"lwrThreshold" => 0.95,
)))
# TEMP diagnostic: surface the handler's own error text.
r_comb.status == 200 || error("combine failed: ", String(r_comb.body))
# TEMP diag: run the handler's steps here so the exception text
# lands in the CI annotation instead of a bare 500.
let
df2, meta2, lerr2 = SV._load_run_table_df("StudyCC", "run1", "merged")
lerr2 === nothing || error("load: ", lerr2)
digs2 = OrderedDict{String,String}("duckdb:merged" => "x")
bod2 = JSON3.read(JSON3.write(Dict("treeSource" => "study_tree",
"treeFile" => "amplicon.nwk")))
nwk2, jp2, tm2, terr2 = SV._resolve_tree_inputs_from_request("StudyCC", bod2, digs2)
terr2 === nothing || error("tree inputs: ", terr2)
cc2 = CC.build_clade_tree(df2; mode = :combined, newick = nwk2,
jplace_json = jp2, sample_cols = meta2.sample_cols,
support_threshold = 0.85, lwr_threshold = 0.95,
tree_method = tm2, max_asvs = 500, seed = 42)
d2 = CC.clade_node_to_dict(cc2.root)
pl2 = CC.to_plotly_figure(cc2.root)
tips2 = sort!(collect(CC._collect_tree_tips!(Set{String}(), cc2.phylo_root)))
idc2 = CC._detect_id_col(df2)
rbi2 = Dict{String,Int}()
for (ri2, r2) in enumerate(eachrow(df2))
t2 = String(strip(string(r2[idc2])))
haskey(rbi2, t2) || (rbi2[t2] = ri2)
end
tv2 = String[]
tr2 = Tuple{Int,Float64}[]
for t2 in tips2
if haskey(rbi2, t2)
push!(tv2, t2); push!(tr2, (rbi2[t2], 1.0))
end
end
mat2 = zeros(Float64, length(tv2), length(meta2.sample_cols))
for (ci2, sc2) in enumerate(meta2.sample_cols)
cv2 = df2[!, sc2]
for (mi2, (ri2, w2)) in enumerate(tr2)
v2 = cv2[ri2]
mat2[mi2, ci2] = ismissing(v2) ? 0.0 : max(0.0, Float64(v2)) * w2
end
end
pd2 = OrderedDict{String,Float64}(sc2 => CC.faith_pd(@view(mat2[:, ci2]), tv2, cc2.phylo_root)
for (ci2, sc2) in enumerate(meta2.sample_cols))
uw2 = CC.unifrac_matrix(mat2, tv2, meta2.sample_cols, cc2.phylo_root; metric = :unweighted)
wr2 = CC.unifrac_matrix(mat2, tv2, meta2.sample_cols, cc2.phylo_root; metric = :weighted, normalized = false)
rcpt2 = EP.create_receipt(; study = "StudyCC", run = "run1",
source_digests = digs2, tool_records = cc2.tool_records,
parameters = OrderedDict{String,Any}("x" => 1),
warnings = cc2.warnings)
EP.receipt_dict(rcpt2)
error("diag reached the end: ", length(pd2), " ", size(uw2), " ", size(wr2), " ", length(tips2))
end
@test r_comb.status == 200
comb = JSON3.read(String(r_comb.body))
@test comb.root.cumulative_count ≈ 110.0 atol = 1e-9
Expand Down
39 changes: 39 additions & 0 deletions test/unit/test_routes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,45 @@ _saved_root = SV.ServerState._root[]
end
end


## Registration itself
# A duplicated `include` in server.jl registers every route in that file
# twice. No request test can see that: the second registration simply
# shadows the first, and both answer. These checks read the sources, so
# they fail on the merge that reintroduces a duplicate.
@testset "Registered routes" begin
src_dir = joinpath(@__DIR__, "..", "..", "src", "server")
server_src = read(joinpath(src_dir, "server.jl"), String)
# Every `include(joinpath(@__DIR__, "a", "b.jl"))` path, in order.
included = String[]
for m in eachmatch(r"include\(joinpath\(@__DIR__((?:,\s*\"[^\"]+\")+)\)\)",
server_src)
push!(included, join([c[2:end-1] for c in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Slice the regex match text rather than RegexMatch

When this test processes any matching include, eachmatch yields a RegexMatch, not a string, so c[2:end-1] attempts unsupported range indexing on the match object and raises before any registration assertions run. Extract and slice c.match (or strip the quotes from it) instead so the unit suite can complete.

Useful? React with 👍 / 👎.

eachmatch(r"\"[^\"]+\"", m.captures[1])], "/"))
end
@test !isempty(included)
# The duplicate that shipped in 8b76b71: routes/cladistics.jl twice.
# No request test can see this — the second registration shadows the
# first and both answer — so the invariant is checked on the sources.
@test allunique(included)
@test count(==("routes/cladistics.jl"), included) == 1
@test count(==("routes/clade_cumulus.jl"), included) == 1

# And no route file declares the same method+path twice.
routes = Tuple{String,String}[]
for file in sort(readdir(joinpath(src_dir, "routes"); join = true))
endswith(file, ".jl") || continue
for m in eachmatch(r"@(get|post|put|patch|delete)\s+(\"[^\"]+\")",
read(file, String))
push!(routes, (m.captures[1], m.captures[2]))
end
end
@test !isempty(routes)
@test allunique(routes)
@test any(r -> occursin("/cladistics", r[2]), routes)
@test any(r -> occursin("/clade-cumulus", r[2]), routes)
end

end

# Testsets above point the server at temporary roots.
Expand Down
Loading