0b293869739f3112a74d2ee38f47785a7c245884
max
  Wed Sep 16 06:29:25 2026 -0700
HPRC v2 contrib hub: pcLAI defaults to dense, and genark addContrib wires the beta/public hub tiers

Mark reported that the pcLAI track shows only one color on HG00408 pat
(GCA_041900255.1) and that you cannot see where ancestry changes. The colors
are correct: a fresh download of the HPRC source BED matches the built
pclai.bb on (chrom, start, end, itemRgb) for all 25,475 windows. That
haplotype is simply 99.8% one ancestry cluster, which is common in this set
(71 of 460 haplotypes have a single centroid, 228 are >=99% one centroid).

The display was the real problem, so hprcPclai goes from visibility pack to
visibility dense plus onlyVisibility dense. At chromosome scale pack stacks
the 100 kb windows into ~50 rows of 1-2 px slivers; dense draws one colored
bar where the ancestry blocks are legible.

While deploying that, beta.hub.txt turned out to be a week stale: it still had
the pre-2026-09-09 pcaSegment field name and the double-prefixed
pclaiRefPanel.json dataUrl, so the details-page scatterplot was not drawing on
hgwbeta. genark addContrib only ever wired alpha.hub.txt, leaving beta and
public to the next clade build. It now wires those two as well for a
collection already named in betaGenArk.txt / publicGenArk.txt. This never
promotes anything - the tier argument still owns the release lists - and
mkGenomes.pl inlines the same per-assembly trackDb, so a later clade build
converges on the same content.

Ran over 462 assemblies, alpha and beta; genark checkContrib reports no
contrib problems, refs #35415

diff --git src/utils/genark/genark src/utils/genark/genark
index 030f75ba1be..fdd7ae6652c 100755
--- src/utils/genark/genark
+++ src/utils/genark/genark
@@ -324,75 +324,84 @@
         if want:
             text = text.rstrip("\n") + "\n" + name + "\n"
         else:
             keep = [l for l in text.split("\n") if l.strip() != name]
             text = "\n".join(keep).rstrip("\n") + "\n"
         with open(path, "w") as fh:
             fh.write(text)
         changed.append(key)
     return changed
 
 
 def addContrib(args):
     """Install a contrib track collection into the GenArk assembly hubs:
     symlink its data files + docs into <buildDir>/contrib/<name>/, write a
     per-assembly <name>.trackDb.txt with hub-root-relative paths, and wire that
-    block into each assembly's served hub.txt. Everything is written into the
+    block into each assembly's alpha.hub.txt, plus beta.hub.txt / public.hub.txt
+    for a collection already on those release lists. Everything is written into the
     assembly's GenArk build directory (see buildDir); the served /gbdb/genark
     and asmHubs/<acc> symlink trees are left alone. --remove undoes all of it."""
     name = args.name.rstrip("/")
     root = os.path.join(CONTRIB, name)
     if not os.path.isdir(root):
         sys.exit("error: no such contrib collection: %s" % root)
     docsDir = os.path.join(root, "docs")
     docs = []
     if os.path.isdir(docsDir):
         docs = sorted(f for f in os.listdir(docsDir) if f.endswith(".html"))
     # Files at the collection root that all assemblies share, such as a scatterplot
     # background panel named by a detailsScript dataUrl. Linked flat next to the
     # docs, so contrib/<name>/<file> resolves for every assembly.
     shared = sorted(f for f in os.listdir(root)
                     if f.endswith((".json", ".tsv"))
                     and os.path.isfile(os.path.join(root, f)))
 
     accs = sorted(d for d in os.listdir(root) if ACC_RE.match(d)
                   and os.path.isdir(os.path.join(root, d)))
     if not accs:
         sys.exit("error: no accession directories (GCA_*/GCF_*) under %s" % root)
     trackNames = contribTrackNames(root, accs)
+    # Which single-file hub tiers to wire. alpha always; beta and public only for a
+    # collection already on the matching release list (this never promotes anything,
+    # setTierMembership below is what changes the lists).
+    tiers = ["alpha"] + [k for k in ("beta", "public")
+                         if name in listMembers(TIER_LISTS[k])]
 
     done = 0
     skipped = 0
     for acc in accs:
         accDir = os.path.join(root, acc)
         asmDir = buildDir(acc)
         if asmDir is None:
             sys.stderr.write("skip %s: no GenArk build directory under "
                              "%s/{genbankBuild,refseqBuild}\n" % (acc, ASMHUBS))
             skipped += 1
             continue
         dest = os.path.join(asmDir, "contrib", name)
-        # the alpha-tier single-file hub in the build dir (asmHubs/<acc>/alpha.hub.txt
-        # and /gbdb/genark/<acc>/alpha.hub.txt are symlinks to this); asmId is the
-        # build directory basename, which carries the assembly-name suffix.
-        asmId = os.path.basename(asmDir)
-        # Only the alpha tier for now: <asmId>.singleFile.hub.txt is what
-        # /gbdb/genark/<acc>/hub.txt points at and is served as the assembly's
-        # default hub, so a contrib collection under test stays out of it.
-        hubTxt = os.path.join(asmDir, "alpha.hub.txt")
+        # The single-file hub tiers in the build dir. alpha.hub.txt is what
+        # asmHubs/<acc>/alpha.hub.txt and /gbdb/genark/<acc>/alpha.hub.txt point at.
+        # beta.hub.txt and public.hub.txt are normally composed by mkGenomes.pl at the
+        # next clade build, which inlines exactly the per-assembly trackDb written just
+        # below, so wiring them here as well only makes a trackDb fix reach hgwbeta (and
+        # the public tier) without waiting for that rebuild; the rebuild then converges
+        # on the same content rather than fighting it. <asmId>.singleFile.hub.txt is
+        # what /gbdb/genark/<acc>/hub.txt points at and is served as the assembly's
+        # default hub, so it is deliberately never touched.
+        hubTxts = [os.path.join(asmDir, "%s.hub.txt" % t) for t in tiers]
 
         if args.remove:
+            for hubTxt in hubTxts:
                 if os.path.exists(hubTxt):
                     wireHubTxt(hubTxt, name, "", remove=True, dryRun=args.dry_run)
             if args.dry_run:
                 print("  rm -rf %s" % dest)
             elif os.path.isdir(dest):
                 shutil.rmtree(dest)
             done += 1
             continue
 
         if args.dry_run:
             print("# %s -> %s" % (acc, dest))
         else:
             os.makedirs(dest, exist_ok=True)
 
         # symlink data files (.bb / .bw) from the collection's accession dir
@@ -404,40 +413,43 @@
             symlink(os.path.join(docsDir, d), os.path.join(dest, d), args.dry_run)
         # and the collection's shared root-level data files
         for f in shared:
             symlink(os.path.join(root, f), os.path.join(dest, f), args.dry_run)
 
         # per-assembly trackDb with hub-root-relative paths, and wire it into hub.txt
         srcTdb = os.path.join(accDir, "trackDb.txt")
         if os.path.isfile(srcTdb):
             tdb = rewriteTrackDb(srcTdb, name)
             destTdb = os.path.join(dest, "%s.trackDb.txt" % name)
             if args.dry_run:
                 print("  write %s (%d bytes)" % (destTdb, len(tdb)))
             else:
                 with open(destTdb, "w") as fh:
                     fh.write(tdb)
+            for hubTxt in hubTxts:
                 if os.path.exists(hubTxt):
                     wireHubTxt(hubTxt, name, tdb, remove=False, dryRun=args.dry_run,
                                trackNames=trackNames)
                 else:
-                sys.stderr.write("warn %s: no hub.txt to wire at %s\n" % (acc, hubTxt))
+                    sys.stderr.write("warn %s: no hub.txt to wire at %s\n"
+                                     % (acc, hubTxt))
         done += 1
 
     verb = "removed from" if args.remove else "installed into"
-    print("addContrib %s: %s %d assemblies, skipped %d (no assembly hub)"
-          % (name, verb, done, skipped))
+    print("addContrib %s: %s %d assemblies, skipped %d (no assembly hub), "
+          "hub tiers wired: %s"
+          % (name, verb, done, skipped, " ".join(tiers)))
 
     tier = getattr(args, "tier", "alpha")
     changed = setTierMembership(name, tier, args.remove, args.dry_run)
     if args.remove:
         if changed:
             print("  also removed from: %s"
                   % ", ".join(os.path.basename(TIER_LISTS[k]) for k in changed))
     elif tier == "alpha":
         print("  tier alpha: visible now at "
               "https://genome-test.gi.ucsc.edu/h/<accession>, and nowhere else. "
               "The pushes exclude every hub.txt tier file, so this cannot leak.")
     else:
         if changed:
             print("  tier %s: added to %s" % (tier,
                   ", ".join(os.path.basename(TIER_LISTS[k]) for k in changed)))