3c814b674f49f9a30d4b8d227e0fe7061a18766a
braney
Tue Sep 1 09:21:28 2026 -0700
registryPages: correlate the trackDb settings docs with the cart, refs #37908 #37838
Two files in the tree say which track types a setting applies to, and they were
written from different evidence. trackDbLibrary.shtml carries a hand-written
types list per setting, which is what #37908 has been correcting.
cartTrackVarCatalog files each cart variable under the config function that
reads it and records the trackDb types that function serves, which came from
reading hui.c and the per-type Ui functions.
Where a trackDb setting and a cart variable are the same knob, the two are
answering the same question, so they can be compared. 60 of the 261 documented
settings have a runtime override. 46 of those pairs are comparable, 12 agree
exactly, and 28 have a type the config code serves that the docs do not list.
The output is a candidate list, not a verdict, and the page says so. Two known
false-positive shapes are called out on it: a variable read by two config
functions collects the types of both, and a pair joined by tdbDefault rather
than by name is weaker evidence, so those are reported separately.
Also factors the palette and the shared reset out of venn.css and index.css
into tokens.css, since a third page now needs them.
diff --git src/hg/utils/registryPages/registryPages.py src/hg/utils/registryPages/registryPages.py
index 024479459e3..ee67bab65f5 100755
--- src/hg/utils/registryPages/registryPages.py
+++ src/hg/utils/registryPages/registryPages.py
@@ -1,716 +1,967 @@
#!/usr/bin/env python3
"""registryPages.py - draw two web pages from the four configuration catalogs.
Refs #37838, #37923, #37925, #37623. The four catalogs each answer for one
part of the browser's configuration surface, and each one prints its own page.
This draws the two pages that need all four at once:
registryVenn.html a four-set Venn of the registries, so the shape of the
whole surface is visible at once: which registry owns
what, and the handful of names more than one describes.
registryIndex.html every name in all four catalogs, readable either in each
catalog's own groups or as one alphabetical list, with
the description on hover.
Everything on both pages is computed from the catalogs. No count is typed in
here, so the pages cannot drift from the tree the way a hand-written summary
would. registryData.py does the reading and the matching; this file does the
drawing.
Usage:
registryPages.py --outDir ~/public_html # both pages
registryPages.py --venn v.html --index i.html # or name them
registryPages.py --check # no output, just the audit
registryPages.py --outDir DIR --audit # add the saved-session count
--check is the mode for a nightly cron. It writes nothing and fails when a name
starts or stops being shared between two registries, which is the one thing here
that needs a person to read a call site. See KNOWN_SHARED in registryData.py.
--audit runs sessionCartAudit, which needs the database and takes about fifteen
seconds. Without it the pages leave out the one paragraph that talks about real
saved sessions.
"""
import argparse
import datetime
import html
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import registryData as rd # noqa: E402
+import trackDbData as td # noqa: E402
NUM_WORD = {0: "no", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve",
13: "thirteen", 14: "fourteen", 15: "fifteen", 16: "sixteen",
17: "seventeen", 18: "eighteen", 19: "nineteen", 20: "twenty"}
def word(n):
"""Small numbers read better spelled out in a sentence."""
return NUM_WORD.get(n, "{:,}".format(n))
def esc(s):
return html.escape(s, quote=False)
def shortPath(path):
"""Write a path under the user's home as ~/... so provenance is readable."""
home = os.path.expanduser("~")
return "~" + path[len(home):] if path.startswith(home + os.sep) else path
def asset(name):
"""Read one of the stylesheet or script files that sits next to this one."""
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), name)) as f:
return f.read().rstrip("\n")
+def style(name):
+ """The shared tokens plus one page's own rules, as a single stylesheet."""
+ return asset("tokens.css") + "\n\n" + asset(name)
+
+
# ============================================================ the Venn page ==
# Four congruent ellipses in the classic four-set arrangement: two rotated one
# way, two the other, so all fifteen regions exist. Order matters, and it is
# the order in rd.REG_ORDER: the two outer ellipses are the first and last.
ELLIPSES = {
"track": (350, 418, 360, 225, -140),
"url": (450, 318, 360, 225, -140),
"file": (544, 318, 360, 225, -40),
"conf": (644, 418, 360, 225, -40),
}
VIEWBOX = (1000, 730)
# Where each region's label goes. Found by rasterizing the four ellipses and
# taking the deepest point of each region, then checked so that every corner of
# every text block lands inside the region it belongs to. Do not nudge these by
# eye: change the ellipses and these have to be found again.
# big the exclusive region of one registry: rule, count, name, ticket
# pair two registries: the count with a colored dot per registry, or a zero
# small three or four registries: a zero, since none of them has ever held a name
REGION_LABEL = {
("track",): ("big", 150, 417),
("url",): ("big", 338, 80),
("file",): ("big", 655, 80),
("conf",): ("big", 843, 417),
("track", "url"): ("pair", 257, 166),
("url", "file"): ("pair", 495, 166),
("track", "file"): ("pair", 290, 511),
("url", "conf"): ("pair", 703, 511),
("file", "conf"): ("pair", 737, 166),
("track", "conf"): ("pair", 497, 614),
("track", "url", "file"): ("small", 400, 252),
("url", "file", "conf"): ("small", 593, 252),
("track", "file", "conf"): ("small", 401, 588),
("track", "url", "conf"): ("small", 593, 588),
("track", "url", "file", "conf"): ("small", 497, 514),
}
REG_SHORT = {"track": "CART / TRACK", "url": "URL PARAMS",
"file": "CART / FILE", "conf": "HG.CONF"}
REG_PHRASE = {"track": "track cart variables", "url": "URL parameters",
"file": "file cart variables", "conf": "hg.conf settings"}
# The same four names at the head of a sentence or a table cell. A plain
# .capitalize() would turn "URL parameters" into "Url parameters".
REG_ONLY = {"track": "Track cart variables only", "url": "URL parameters only",
"file": "File cart variables only", "conf": "hg.conf settings only"}
REG_BRIEF = {"track": "Track cart", "url": "URL", "file": "File cart", "conf": "hg.conf"}
def vennSvg(regs, counts):
"""The four-set diagram, with the count of every region drawn in it."""
byKey = {r["key"]: r for r in regs}
out = ['')
return "\n".join(out)
def vennAria(regs, counts):
"""One sentence saying what the picture shows, for a reader who cannot see it."""
only = ", ".join(str(counts.get((r["key"],), 0)) for r in regs)
pairs = sorted((n, r) for r, n in counts.items() if len(r) == 2 and n)
pairText = ", ".join(str(n) for n, _ in pairs)
names = [REG_PHRASE[r["key"]] for r in regs]
listed = ", ".join(names[:-1]) + " and " + names[-1]
return ("A four-ellipse Venn diagram of the browser's configuration registries: %s. "
"The exclusive regions hold %s names. %s pairwise regions hold %s. The other "
"regions, including every three-way and four-way region, are empty."
% (listed, only, word(len(pairs)).capitalize(), pairText))
def sliverList(regs, shared):
"""The shared names, one block per pair of registries, with the catalogs' own notes."""
byKey = {r["key"]: r for r in regs}
note = {}
for reg in regs:
for group in reg["groups"]:
for r in group["rows"]:
if r["desc"]:
note.setdefault((reg["key"], r["name"]), r["desc"])
pairs = {}
for name, keys in shared.items():
pairs.setdefault(keys, []).append(name)
out = ['
']
for keys in sorted(pairs, key=lambda k: (-len(pairs[k]), k)):
names = sorted(pairs[keys], key=lambda n: n.lower())
out.append('
')
out.append('
')
out.append('
%s'
% "".join('' % k for k in keys))
out.append(' %s
'
% esc(" + ".join(REG_PHRASE[k] for k in keys)))
out.append('
')
for name in names:
parts = []
for key in keys:
text = note.get((key, name))
if text:
parts.append('%s: %s' % (esc(byKey[key]["title"]), esc(text)))
out.append('
%s%s
'
% (esc(name), " ".join(parts) or "—"))
out.append('
')
out.append('
')
out.append('
')
return "\n".join(out)
def regionTable(regs, counts):
"""The same figure as a table, so identity is never carried by color alone."""
total = sum(counts.values())
rows = []
order = sorted(REGION_LABEL, key=lambda r: (len(r), -counts.get(r, 0), r))
for region in order:
n = counts.get(region, 0)
dots = "".join('' % (k if k in region else "off")
for k in rd.REG_ORDER)
if len(region) == 1:
label = REG_ONLY[region[0]]
elif len(region) == len(rd.REG_ORDER):
label = "All four"
else:
label = " + ".join(REG_BRIEF[k] for k in region)
rows.append('
%s
%s
'
'
%d
'
% ('' if n else ' class="z"', dots, esc(label), n))
return ('
\n'
'
Region counts, %s distinct names
\n'
' \n'
'
Registries
Region
'
'
Names
\n'
' \n'
' \n%s\n \n'
'
' % ("{:,}".format(total), "\n".join(rows)))
def collisionNote(coll):
"""The names that match across registries without being the same variable."""
if not coll:
return ("
No name is spelled the same way in two registries while meaning two "
"different variables.
")
lines = []
for name, where in coll:
keys = [k for k in rd.REG_ORDER if k in where]
lines.append("%s is %s" % (esc(name), " and ".join(
"%s in the %s" % (", ".join("%s" % esc(v) for v in where[k]),
esc(REG_PHRASE[k])) for k in keys)))
return ("
%s %s in two registries and mean two different variables. They are "
"counted as separate names above.
\n
%s.
"
% (word(len(coll)).capitalize(),
"spelling appears" if len(coll) == 1 else "spellings appear",
". ".join(lines)))
-def vennPage(regs, counts, shared, coll, baseline, audit, indexName, today):
+def vennPage(regs, counts, shared, coll, baseline, audit, indexName, corrName, today):
"""The whole Venn page."""
byKey = {r["key"]: r for r in regs}
total = sum(counts.values())
rowTotal = sum(r["rows"] for r in regs)
alone = sum(n for r, n in counts.items() if len(r) == 1)
nShared = total - alone
track = byKey["track"]
prefixes = next(g for g in track["groups"] if g["title"].endswith("the track name"))
exceptions = next((g for g in track["groups"]
if g["title"].startswith("Exceptions")), {"rows": []})
plain = track["rows"] - len(prefixes["rows"]) - len(exceptions["rows"])
confShared = sorted(n for n, keys in shared.items() if "conf" in keys)
if confShared == ["textSize"]:
confExcept = ("textSize is the exception, and it is deliberate. The setting "
"names the site default and the cart holds the visitor's choice.")
elif confShared:
confExcept = ("The %s a visitor can also set: %s."
% ("one" if len(confShared) == 1 else "ones",
", ".join("%s" % esc(n) for n in confShared)))
else:
confExcept = "Nothing a mirror admin sets in hg.conf can be set by a visitor."
auditPara = ""
if audit:
auditPara = ("\n
The session audit reads {sessions:,} saved sessions and finds "
"{names:,} distinct variable names in them. {unknown:,} match nothing in "
"any catalog.
Four catalogs in hg/utils/ describe what can be configured in the
browser: the settings in hg.conf, the parameters on a CGI URL,
the track-scoped cart variables, and the cart variables that hold a
file name. Together they hold %(rowTotal)s rows covering %(total)s distinct names. Only
%(sharedWord)s names appear in more than one registry, and no name appears in three.
A name is in a registry when that catalog gives it a
row. For the track catalog that means its %(plain)d variables, its %(prefixes)d track-name
prefixes, and the %(exceptions)d names spelled out in its exceptions list. Every name is listed,
with its description, in the Registry Name Index.
generated %(today)ssource: each catalog's --jsontree: %(tree)s
%(svg)s
Every name in the four registries, placed by which registries hold it. A pair of
dots marks which two registries share a region. The four ellipses are nearly disjoint:
%(alone)s of the %(total)s names sit alone in one registry, %(sharedWord)s sit in a pair, and
every three-way and four-way region is empty.
The %(sharedWord)s shared names
Each of these is one variable that two registries both describe, not two
variables that happen to share a spelling. The text is each catalog's own.
%(slivers)s
All %(nRegions)s regions
The same figure as a table. Filled dots mark the registries that hold the names
in that region.
%(table)s
Reading the empty regions
%(emptyWord)s of the %(nRegions)s regions are empty, and the %(fullWord)s that
are not hold %(sharedWord)s names between them. Three things explain that, and one of them is
a gap.
Some names collide but are not shared
%(collisions)s
A bare name is not an identity. Matching on the name alone would have missed the
track-scoped names, which one catalog spells <track>_sel and the other
spells _sel, and would have claimed those collisions instead.
hg.conf barely touches the rest
%(confNames)d settings, %(confShared)s of which a visitor can also set. That is the
shape you want: what a mirror admin configures and what a visitor configures are two
different sets.
%(confExcept)s
What sits outside all four
%(urlBaseline)d URL names and %(trackBaseline)d cart variable names are recorded in the
baseline files as out of scope. Those files were accepted wholesale on the day they were
written, so a name being in one is not evidence that anybody reviewed it.
Global cart variables, the ones scoped to no track, have no registry at all.
textSize is one of them, which is why it enters the picture through the URL
registry rather than a cart one.
%(auditPara)s
""" % {
- "css": asset("venn.css"),
+ "css": style("venn.css"),
"today": today,
"tree": esc(shortPath(rd.kentSrc())),
"svg": "\n".join(" " + line for line in vennSvg(regs, counts).splitlines()),
"rowTotal": "{:,}".format(rowTotal),
"total": "{:,}".format(total),
"alone": "{:,}".format(alone),
"sharedWord": word(nShared),
"plain": plain,
"prefixes": len(prefixes["rows"]),
"exceptions": len(exceptions["rows"]),
"slivers": sliverList(regs, shared),
"table": regionTable(regs, counts),
"nRegions": word(len(REGION_LABEL)),
"emptyWord": word(sum(1 for r in REGION_LABEL if not counts.get(r, 0))).capitalize(),
"fullWord": word(sum(1 for r in REGION_LABEL
if counts.get(r, 0) and len(r) > 1)),
"collisions": collisionNote(coll),
"confNames": len(byKey["conf"]["names"]),
"confShared": word(len(confShared)),
"confExcept": confExcept,
"urlBaseline": baseline["url"],
"trackBaseline": baseline["track"],
"auditPara": auditPara,
"indexName": esc(indexName),
+ "corrName": esc(corrName),
"footRegs": " · ".join("%s #%s" % (r["tool"], r["ticket"]) for r in regs),
}
# =========================================================== the index page ==
def sortKey(name):
"""Sort a name by the part that varies, setting the shared