You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
frs_wsg_drainage() computes drainage closure with a single-column ltree ancestry test:
WHEREf.outlet<@ w.outlet
against public.wsg_outlet — a table that no repo builds. It was materialized ad-hoc in a psql session on 2026-05-24 (link#175); the CREATE statement was never committed and survives only in a session transcript. Consumers that connect to a database without it fail hard (relation "public.wsg_outlet" does not exist), which is NewGraphEnvironment/link#227.
Worse, the predicate is wrong, and the missing table was hiding it.
Why the predicate is wrong
A single ltree per group cannot express where along a stem a group sits. Its wscode_ltree names the stream; it does not say whether you are above or below a given confluence. Consequences, measured against the local FWA load (246 groups):
25% of groups (62/246) resolve to an outlet shallower than their own polygon's wscode_ltree, because the ad-hoc derivation took nlevel(wscode_ltree) ASC — the shallowest code appearing anywhere in the group. A group that merely clips a larger river inherits that river's code and becomes a false ancestor of everything upstream on it.
Only 167 distinct outlets for 246 groups. The largest false-ancestor cluster is 14 Fraser groups all sharing outlet 100 (BBAR CHWK COTR DOGC FRCN HARR LFRA MFRA MORK NARC SETN TABR TWAC UFRA). Because <@ is inclusive, each is an "ancestor" of every other, so any single Fraser focal pulls in all 14.
Ordering hazard. Equal outlets give equal nlevel, so the query falls through to its w.wsg ASC tiebreak — alphabetical. In that Fraser cluster, LFRA (the most downstream group) sorts seventh, so six groups upstream of it order first. Downstream-first ordering is load-bearing for cross-WSG ;DAM propagation in link, so this is a correctness hazard, not just wasted compute.
Concrete example — Morice and Bulkley. The Morice (Wedzin Kwa) is hydrologically the mainstem, but FWA carried the "Bulkley" name down through the confluence, so the Morice is filed as a tributary of the thing it feeds. Under the current predicate MORR resolves to outlet 400.431358 — identical toBULK — so the Bulkley appears to drain through the Morice. Backwards.
Fix
Use FWA's own topology function and the polygon table that ships with fwapg:
localcode_ltree supplies the along-stem position the single code lacks, and fwa_downstream is the same primitive family as the fwa_upstream already used across fresh/link.
This also removes the public.wsg_outlet dependency entirely — whse_basemapping.fwa_watershed_groups_poly is standard FWA infrastructure, verified present with 246 rows and fwa_downstream available in both the local docker fwapg and the tunnelled bcfishpass database. Nothing to build, nothing to ship, no reproducibility hole.
Province-wide run: 0.3 s, 1032 edges, 208 of 246 groups with at least one downstream group (the remaining 38 are coastal outlets).
Behaviour changes
Default table becomes whse_basemapping.fwa_watershed_groups_poly; the expected columns change from (wsg, outlet) to (watershed_group_code, wscode_ltree, localcode_ltree). Breaking for any caller passing a custom table.
Closures get tighter. frs_wsg_drainage(conn, c("PARS","BULK")) goes from 15 groups to 10, correctly dropping LKEL (separate tributary), MSKE / USKE (upstream of the confluence), LBTN (sibling), MORR (upstream).
fwa_downstream excludes self, so the focal group must be unioned in explicitly — inclusive <@ gave that for free.
Problem
frs_wsg_drainage()computes drainage closure with a single-column ltree ancestry test:against
public.wsg_outlet— a table that no repo builds. It was materialized ad-hoc in a psql session on 2026-05-24 (link#175); theCREATEstatement was never committed and survives only in a session transcript. Consumers that connect to a database without it fail hard (relation "public.wsg_outlet" does not exist), which isNewGraphEnvironment/link#227.Worse, the predicate is wrong, and the missing table was hiding it.
Why the predicate is wrong
A single ltree per group cannot express where along a stem a group sits. Its
wscode_ltreenames the stream; it does not say whether you are above or below a given confluence. Consequences, measured against the local FWA load (246 groups):wscode_ltree, because the ad-hoc derivation tooknlevel(wscode_ltree) ASC— the shallowest code appearing anywhere in the group. A group that merely clips a larger river inherits that river's code and becomes a false ancestor of everything upstream on it.100(BBAR CHWK COTR DOGC FRCN HARR LFRA MFRA MORK NARC SETN TABR TWAC UFRA). Because<@is inclusive, each is an "ancestor" of every other, so any single Fraser focal pulls in all 14.nlevel, so the query falls through to itsw.wsg ASCtiebreak — alphabetical. In that Fraser cluster, LFRA (the most downstream group) sorts seventh, so six groups upstream of it order first. Downstream-first ordering is load-bearing for cross-WSG;DAMpropagation in link, so this is a correctness hazard, not just wasted compute.Concrete example — Morice and Bulkley. The Morice (Wedzin Kwa) is hydrologically the mainstem, but FWA carried the "Bulkley" name down through the confluence, so the Morice is filed as a tributary of the thing it feeds. Under the current predicate
MORRresolves to outlet400.431358— identical toBULK— so the Bulkley appears to drain through the Morice. Backwards.Fix
Use FWA's own topology function and the polygon table that ships with fwapg:
localcode_ltreesupplies the along-stem position the single code lacks, andfwa_downstreamis the same primitive family as thefwa_upstreamalready used across fresh/link.This also removes the
public.wsg_outletdependency entirely —whse_basemapping.fwa_watershed_groups_polyis standard FWA infrastructure, verified present with 246 rows andfwa_downstreamavailable in both the local dockerfwapgand the tunnelledbcfishpassdatabase. Nothing to build, nothing to ship, no reproducibility hole.Province-wide run: 0.3 s, 1032 edges, 208 of 246 groups with at least one downstream group (the remaining 38 are coastal outlets).
Behaviour changes
tablebecomeswhse_basemapping.fwa_watershed_groups_poly; the expected columns change from(wsg, outlet)to(watershed_group_code, wscode_ltree, localcode_ltree). Breaking for any caller passing a customtable.frs_wsg_drainage(conn, c("PARS","BULK"))goes from 15 groups to 10, correctly dropping LKEL (separate tributary), MSKE / USKE (upstream of the confluence), LBTN (sibling), MORR (upstream).fwa_downstreamexcludes self, so the focal group must be unioned in explicitly — inclusive<@gave that for free.@examplesblock and the acceptance criterion in frs_wsg_drainage: FWA WSG drainage-closure primitive #211 both record the old 15-group answer. Both need updating; that 15-group result is not a correct oracle.Acceptance
Hydrology fixtures, all verified passing against the proposed predicate:
MORR → BULKTRUE,BULK → MORRFALSEBULK → LSKETRUE,BULK → KISPTRUE (the Skeena does flow through the Kispiox group)BULK → MSKEFALSE,BULK → USKEFALSE (upstream of the confluence)BULK → LKELFALSE,PARS → LBTNFALSE (siblings)@exampleRefs
NewGraphEnvironment/link#227— the missing-table symptom; this removes its first half entirely