| status | in-force |
|---|---|
| summary | Where promoted registry globals live (/sys/FNS_Registries) and the six invariants that govern the home. |
| since | a019218 2026-08-21 |
| skill | fns-registry |
Status: in force since a019218 (2026-08-21). Minor architecture change
— location only, no API change. Read this before touching registry promotion
code; the full scheme (roles, publishing, surfaces) is in
RegistryScheme.md.
Promoted registry globals used to sit as direct children of /sys
(/sys/FNS_ToolbarRegistry, …). They now live in one container:
/sys/FNS_Registries/
├── FNS_ConfigRegistry op.FNS_CONFIGREGISTRY
├── FNS_MainMenuRegistry op.FNS_MAINMENUREGISTRY
├── FNS_NavbarRegistry op.FNS_NAVBARREGISTRY
├── FNS_OpMenuRegistry op.FNS_OPMENUREGISTRY
├── FNS_PaneTypeRegistry op.FNS_PANETYPEREGISTRY
└── FNS_ToolbarRegistry op.FNS_TOOLBARREGISTRY
Nothing that consumes a registry changed: global OP shortcuts resolve from any depth.
C1 — One home. /sys/FNS_Registries. A bare baseCOMP: no
extension, no DATs, no custom pages or pars, no tags, no shortcut, no clone,
no externaltox. It is a folder. Do not give it behavior — if you find
yourself wanting to, the logic belongs on RegistryBase or on a registry.
C2 — One resolver. RegistryBase._sys_comp(create=False) is the only
thing that knows the path, via SYS_HOME = 'FNS_Registries'. Never hardcode
/sys/FNS_Registries in registry code — go through _sys_comp(), and reach
a specific global through its shortcut. (Two deliberate exceptions, both
outside RegistryBase: InstallerExt.SYS_REGISTRY_HOME and the literal in
registry_presave_exec.promotedRegistries(). Keep them in step.)
C3 — Reads never create. Only the promotion path passes create=True.
Asking "is there a home?" must not grow one in a project that has no
registries.
C4 — Consumers use the shortcut, never a path. op.FNS_TOOLBARREGISTRY,
guarded — no registry is guaranteed to exist:
reg = getattr(op, 'FNS_TOOLBARREGISTRY', None)
if reg is not None and hasattr(reg, 'Register'):
...C5 — This is per-process state. /sys never saves with the .toe.
The home and everything in it are rebuilt on every project open by whichever
master promotes first. Never treat a global as durable storage.
C6 — Bare /sys is legacy and gets absorbed. A global found as a direct
child of /sys predates this change. It is relocated regardless of
version: merge its entries → destroy it → re-promote into the home. Two
branches, both verified:
| trigger | path |
|---|---|
| a host inits and sees it | _installGlobalRegistry sets relocating, calls _replace_global_registry(force=True) |
| the legacy copy inits itself | _isLegacySysCopy() → promote into home, then retire self on run(..., delayFrames=5) |
_find_sys_registries() deliberately scans both locations so parked
(shortcut-less) legacy copies are reconciled too, and
_reconcile_parked_sys_registries refuses to promote a winner that is not
already in the home.
-
Equal versions: the incumbent wins.
_check_version_againststands down onour_version <= their_version— the whole version, not just the major. A same-version reinstall leaves the old global live inside the home. Only the bare-/syscase is unconditional (C6). -
A choice about which version to promote. There is none to make: newest wins, silently. The comparison used to raise a
ui.messageBoxon a major mismatch; because promotion runs during project load once per registry copy, that surfaced as a stack of modal dialogs and wedged extension init (2026-08-24). Nothing in this path may prompt. -
The installer reports, it does not force.
InstallerExt.PromotedRegistries()— exported frominstall.py, and on everyInstallPlanresult under theregistrieskey — returns{name, path, version, shortcut}per global. It is a snapshot of the running process. Forced re-promotion was considered and deliberately not built; add it explicitly if you need it. -
Shipped artifacts lag, but do not guess how much. Ask the tooling, not your instincts:
exec(open('packaging/release_one.py').read()); Preflight()is read-only and authoritative. It distinguishes registry ripple (a vendored host copy merely looking newer — a warning, not a blocker; empty as of790be63) fromunlanded(a package whose own code is genuinely newer than its.tox— a blocker). After the move that was six packages: CustomParTools and the five registry packages. Landing them is PI's per-package Save button; there is no script API, and every scriptable path inrelease_one.pypublishes. See packaging/RELEASING.md.Mixed old/new hosts converge safely regardless (an old one promotes to bare
/sys, a new one relocates it), which is what C6 buys. C6 can be deleted once no shipped artifact predates the move.
scripts/shared/RegistryBase.py is shared by 83 DATs with
syncfile=True. Saving it hot-reloads every one and reinitializes the whole
registry family — including the live globals driving the toolbar, navbar,
main menu and op menu. So:
- Patch a copy,
py_compileit, then move it into place. A syntax error landed directly takes down every registry at once. - Check
get_sessionsfirst — one writer per checkout. - After landing, verify (below) before doing anything else.
If a shared-source edit does not reach a host, check its file par. The
par value is the carrier — 62 of the 83 DATs have no tags at all and still
resolve to the shared file, so tags prove nothing. button_hog's
FNS_ToolbarRegistry/RegistryBase was the one host pointing at a per-op copy
and therefore sat out this whole change; repaired in 790be63 (par re-pointed,
stray text tag dropped, duplicate .py deleted). Worth re-running the
census in the recipe below after any bulk registry work.
FNS_CommandRegistry does not subclass RegistryBase. Its source lives
in the TDXLPP repo
(utility/TDXLauncherUtility/FNS_CommandRegistry/FNSCommandRegistryExt.py,
its own Envoy on port 9879) and it reaches this project only through
../TDXLPP/release/TDXLauncherUtility.tox. Corrected 2026-09-08: the
source now lives here
(modules/suspects/FNSTools/FNS_CommandRegistry/FNSCommandRegistryExt.py), it
ships as the FNS_CommandRegistry store package, and the launcher mirrors the
released artifact (packaging/launcher_mirror.json); the promotion shape and
the C1–C6 duty are unchanged. It carries a hand-ported copy of
the same shape — SYS_HOME, _sysHome(create=False), and the same
relocate-if-parked rule — so it lands in the same home. A source edit there
does nothing here until TDXLPP re-releases its tox; Repromote() is its
dev helper.
Any future non-RegistryBase member must honour C1–C6 the same way.
Every block below was run against the live project at a019218 and passes.
home = op('/sys/FNS_Registries')
assert home and not home.extensions[0] # C1: bare folder
for c in home.children: # shortcut points into home
assert getattr(op, c.par.opshortcut.eval()).path.startswith(home.path)
# C6: nothing left parked in bare /sys. FNS_CommandRegistry is the one
# expected holdout until TDXLPP re-releases -- see "Family members" above.
strays = [c.name for c in op('/sys').children
if 'Registry' in c.name and c is not home]
assert strays in ([], ['FNS_CommandRegistry']), straysThen confirm entries survived and surfaces still render — a relocation that loses registrations looks identical to a healthy one from the outside:
{c.name: len(next(e for e in c.extensions if hasattr(e, 'stored'))
.stored['PaneRegistry']) for c in home.children}
# baseline at a019218: Config 45, Toolbar 37, Navbar 13,
# MainMenu 11, OpMenu 3, PaneType 2
len([c for c in op('/ui/dialogs/bookmark_bar').children
if c.name.startswith('tbmirror_')]) # 22 at a019218op('/FNSTools/registry_presave_exec').module.healAllRegistries() should
return all six names. get_op_errors on /sys/FNS_Registries must be clean.
Finally, confirm no host has drifted off the shared source — this is what caught button_hog:
import collections
c = collections.Counter(d.par.file.eval()
for d in op('/').findChildren(name='RegistryBase'))
assert list(c) == ['scripts/shared/RegistryBase.py'], dict(c)