| status | in-force |
|---|---|
| summary | How a user picks and chooses which tools to install: buckets, manifests, Pkgversion-driven updates, and the bootstrap rail. |
| since | 2026-08-13 (buckets + manifests decision) |
| skill | fns-packaging |
How a user picks and chooses which FunctionStore tools to install, instead of taking the whole toolkit. Companion to RegistryScheme.md, which owns the in-project runtime relationship between tools and registries.
Distribution model (decided 2026-08-13): buckets and manifests. A
bucket holds manifest.json and the per-release artifacts; the one-drop
FNSTools.tox is the bootstrap (native .exe/.dmg installers were deferred
on 2026-08-21, NativeInstallerDecision.md; this
line corrected 2026-09-08). Everything downstream —
picking, installing, updating — is manifest-driven. Update decisions are
made on a version parameter we govern (Pkgversion), read live off
the installed component; artifact hashes verify downloads only. See §4.2.
UvPackagingResearch.md remains as research only: the pip/uv rail it explores is not the plan (§3).
Started as design notes 2026-08-10; §1.1–1.3 and §4.1 record what has since been built and verified.
The redesign25 registry architecture already did the hard part. Tools are self-contained COMPs carrying stamped registry contribution DATs (Toolbar/Navbar/OpMenu/MainMenu/PaneType registries, HotkeyManager entries). Registries discover contributions; a missing tool just means missing entries. That is exactly the property "install any subset" requires. The pre-redesign monolith could not do partial installs — the registry scheme is the enabler, and this document is downstream of it.
Remaining coupling to audit before any of this works: /sys globals,
RegistryBase.py, the registry hosts themselves, tools_ui, and any
op.X global-shortcut references between tools.
Swept all 59 depth-1 tool COMPs: every descendant DAT's text plus every
custom/expression parameter, for op.<SHORTCUT> references resolving to a
DIFFERENT tool. Stamped registry hosts (*/ToolbarRegistry/*,
*/ConfigRegistry/*, …) were excluded — those reference core by design and
are what the clone rail updates.
Verdict: the "tools depend only on core" rule is already ~true. Fourteen cross-tool edges across 59 tools — 35 live reference lines — plus three dead ones. Nothing here is deep logic entanglement; it is almost entirely a widget living in the wrong package. No blocker to the core/tool split.
| Source → Target | Lines | Site(s) | Shape |
|---|---|---|---|
| CustomParPromoter → QuickExt, QuickParent, ClearPars, QuickCollapse, iopPromoter | 7 | button_custompar_tools/panelexec_lcick, panelexec_mclick, dragdrop |
hub button — one widget dispatching to 5 optional tools |
| FNS_Navbar (core) → CustomParPromoter, iopPromoter | 8 | containers/hijack_dragdrop/dragdrop |
core → optional (backwards) |
| FNS_Toolbar (core) → midiMapper | 7 | widgets/button_midi_learn/dragdrop, panelexec2, panelexec3, panelexec4 |
core → optional (backwards) |
| tools_ui (core) → GlobalOutSelect | 1 | valueParExec |
core → optional (backwards) |
| OpTemplates → AutoRes | 8 | OPTemplates1/{noiseTOP/noise1, noiseTOP/noise2_highp, circleTOP/circle1, displaceTOP/base1/noise1} .resolutionw/h |
par expressions inside template assets |
| ExprHotStrings → CustomParPromoter | 1 | extExprHotString |
real logic dep |
| MY_HOTKEYS → ResetPLS1 | 1 | keyboardin14_callbacks |
the only guarded one |
| SearchWords → FNS_OpMenu | 1 | .opviewer (expr) |
target is core — fine |
| openOp1 → ColorUI | 1 | .Op (expr) |
helper COMP, not a packaged tool |
Two findings that shape the work:
-
34 of the 35 lines are unguarded — bare
op.FNS_QUICKEXT.CreateExtension(...),op.FNS_CPP.Reference = .... With the target uninstalled these raiseAttributeErrorinside a panel/drag callback: a partial install fails at click time, not at load time. The one exception isMY_HOTKEYS/keyboardin14_callbacks, and it is a baretry: … except: pass— it survives a missing tool but swallows every other error too, so it is a pattern to replace, not to copy.Watch the OpTemplates block — it only looks guarded.
tdu.tryExcept(lambda: parent.Project.width, op.AUTO_RES.par.Resolutionw)protects the first argument, but the fallback is an ordinary argument evaluated eagerly, so a missingop.AUTO_RESraises beforetryExceptever runs. Eight parameter expressions, and being parameters they would error on every cook rather than only on interaction — the most visible failure of the set, and the least obvious in a code read. -
Three edges point the wrong way — core surfaces (Toolbar, Navbar,
tools_ui) reaching into optional tools. Core must never depend on a feature package, and the registry scheme already removes the need: a tool ships its own button as a contribution.button_midi_learnbelongs in midiMapper;hijack_dragdrop's CPP/iop branches belong in those tools.
Dead edges (delete, don't port): three commented-out
#op.FNS_RPLS.par.Reset.pulse() lines in CustomParPromoter/.../parexec1,
FNS_Toolbar/widgets/button_midi_learn/parexec1 and
ParOPDrop/button_ParOpPlace/parexec1 — copy-paste residue inflating
ResetPLS1's apparent fan-in from 1 to 4.
Recommended resolution (in order, each independently landable):
- Delete the three dead references — free, and removes ResetPLS1's only apparent fan-in beyond MY_HOTKEYS.
- Relocate the three backwards widgets into the tools they drive, as registry contributions. This is the only structural work, and it is exactly the packaging move anyway.
- Give the rest one guarded-optional idiom instead of eleven bare
attribute accesses. The global shortcut IS the feature-detect —
getattr(op, 'FNS_CPP', None), return quietly (or log once) when absent. Worth one core helper so all sites read the same and the "not installed" message is uniform. No dependency solver, no manifest edges — keeps §2.1's rule intact. - Merge the custom-par family — decided 2026-08-13, see §1.2.
Unrelated bug found in passing: ExprHotStrings/extExprHotString does
self.customParPromoter = op.FNS_CPP — a cached extension reference, which
goes stale on reinit (see .claude/rules/td-python.md). Fix independently of
packaging.
Per-tool packages, with exactly one deliberate bundle. This closes the granularity open question in §5 for the custom-par family; groups like MISC and OUTPUT are still open.
Merged: the CustomParTools package (~934 ops): CustomParPromoter
(renamed to CustomParTools, keeps shortcut FNS_CPP and the
Toolbar/Navbar hosts, which already published canonical CustomParTools)
now contains QuickExt + QuickParent + ClearPars + iopPromoter as children.
Hub-button callbacks call them sibling-relative (op('../QuickExt'));
FNS_QUICKEXT / FNS_QUICKPARENT / FNS_ClearPars / FNS_IOP retired.
Each child keeps its own ConfigRegistry host (canonical unchanged, so
saved config sections carried over); nested suspect toxes live under
modules/suspects/FunctionStore_tools_2025/CustomParTools/.
Hazard paid for during the merge — Embody move-detection vs shared
clone files. The tsv carried rows for the OLD CustomParPromoter host's
DATs whose file_path was the MASTER's shared
FNS_Toolbar/ToolbarRegistry/*.py (many host clones share that file).
Renaming the package orphaned those op paths; Embody's move-detection
re-matched them to a DIFFERENT clone (MISC/button_hog), "moved" the
shared file to a per-clone path — deleting the master file that ~20
clones sync from. Repair: restore master files from git, remove the
retargeted rows + stray files (remove_externalization_tag), re-bind the
hijacked clone's file par to the master. Before renaming/destroying
any COMP that contains stamped registry hosts, grep externalizations.tsv
for rows under its path first.
The evidence is entry points, not vibes. Only CustomParPromoter carries
Toolbar and Navbar hosts; the other four have a ConfigRegistry host and
nothing else — no toolbar button, no navbar item, no op-menu contribution,
and their keyboardins hold only esc/enter (dialog dismiss inside their
own popups, not invocation shortcuts). They have no independent entry
point: CustomParPromoter/button_custompar_tools is their entire
user-facing surface. They are method libraries that were packaged as tools.
Stay separate — both have real global invocation hotkeys, so they stand alone:
-
QuickCollapse (
ctrl+w,ctrl+shift+w) — also on the hub's middle-click, so that one branch becomes an optional feature-detect. -
QuickParCustom— this call was wrong, and merged 2026-08-24. "No cross-tool edges at all" was false: its extension resolvedgetattr(op, 'FNS_CPP', None)and drove promotion through CustomParTools' promoter. The scan missed it because a guardedgetattronopdoes not read as a reference — the same class that hidhijack_dragdrop. Installing it without CustomParTools gave hotkeys that could not promote. The other half of the reason ("has real global invocation hotkeys, so it stands alone") also stopped distinguishing anything once CustomParTools itself took on four global hotkeys from the MY_HOTKEYS retirement: owning hotkeys is not a reason to be a separate package. Folded in as a child, keeping its ConfigRegistry canonical and its Active toggle as the off-switch.Lesson for the rest of the audit: a
getattr(op, 'SHORTCUT', None)edge is invisible to a text/expression scan. Any remaining "already clean" verdict reached that way is worth re-checking by hand.
Three scans, because each reference class is invisible to the others:
| Class | Found by | Result |
|---|---|---|
| DAT text | shortcut substring | 76 hits, mostly noise |
| DAT text | code-shaped + word-bounded, tables excluded | 4 real tool->tool |
| Parameter expressions | Par.expr sweep |
8 live, 34 dead |
The 76 collapsed for two reasons worth remembering: FNS_TOOLBAR is a
substring of FNS_TOOLBARREGISTRY, so every stamped core registry host
read as a dependency on the Toolbar package; and the largest "edges" were
table DATs (GlobalOutSelect/treePanel, fastOpFind) that catalogue every
tool's shortcut and path as DATA. Any future scan must use word boundaries and
skip isTable DATs.
OpTemplates -> AutoRes is real, and lived where no text scan looks -- in
parameter expressions on 8 TOPs. It was also broken:
tdu.tryExcept(lambda: parent.Project.width, op.AUTO_RES.par.Resolutionw)
tryExcept's fallback is lazy only when it is a function. As a bare value,
op.AUTO_RES is evaluated eagerly as an argument, so the expression raised
whenever AutoRes was absent even though the primary path succeeded -- verified
empirically, not inferred. Fixed with a lambda plus a hasattr fallback.
The 34 toolbar "edges" were corpses. Every toolbar-button tool stored
tdu.tryExcept(lambda: op.FNS_TOOLBAR.par.Layoutstart.eval(), 666) +float(op.FNS_TOOLBAR.op('ToolbarDef').cell(...)) on alignorder/display --
but those pars are in CONSTANT mode, so the text was dormant, and
ToolbarDef no longer exists on the Toolbar package at all. Bar order is
governed by ToolbarRegistry/the configurator, which assigns alignorder to the
mirrors in /ui/dialogs/bookmark_bar, not to the source buttons inside
tools (which all sit at 0.0). The dead text is cleared; values untouched.
Rule this establishes: a stored expression on a CONSTANT-mode par is not an edge. Mode has to be read, or an audit counts dormant legacy as live coupling -- here it would have inflated the graph by 34.
Also fixed: /FNSTools/parexec1 called op.FNS_COLOR_UI.openParameters()
unguarded on the toolkit root; ColorUI is optional, so it is feature-detected
now.
Effect on §1.1's graph: 14 edges → 8. Four CustomParPromoter edges become
internal calls, and FNS_Navbar → CustomParPromoter + → iopPromoter
collapse into one. Remaining after this plus the MY_HOTKEYS move and the dead
-line deletion: the three backwards core→tool widgets, OpTemplates → AutoRes, ExprHotStrings → CustomParTools, CustomParTools → QuickCollapse.
Merging retires the FNS_QUICKEXT / FNS_QUICKPARENT / FNS_ClearPars /
FNS_IOP global shortcuts. No alias shim: redesign25 is a major version with
no back-compat obligation (same call as the FNS_Config v2 rewrite).
Assessed all three before touching them; they do not resolve the same way.
-
tools_ui → GlobalOutSelect: done. Never a widget relocation at all -- justif tab == op.GLOBAL_OUT_SELinside the tab switcher. Replaced with a capability test (hasattr(tab.par, 'Refresh')), which REMOVES the edge instead of guarding it. GlobalOutSelect is the only one of the seven app tabs with a Refresh par, so it is equivalent today, and a future tab opts in by adding the par. -
FNS_Navbar → CustomParPromoter+→ iopPromoter: RESOLVED by guarding, not moving (revised during the merge).hijack_dragdropstays a navbar citizen: it is a navbar-surface behavior (hijacks navbar drag-drop, probes a../panenavpane-context service — a constant-mode par reference the text/expression audit missed), and its CustomParTools calls are optional-feature delegation. Itsdragdropnow resolves the package once viagetattr(op, 'FNS_CPP', None), bails with adebug()when absent, and reaches iopPromoter THROUGH the package (cpt.op('iopPromoter')) soFNS_IOPcould still be retired. Note the audit lesson: constant-mode par VALUES can carry op references too — sweep those, not just DAT text and expressions.SUPERSEDED 2026-09-09: hijack_dragdrop MOVED after all, along with
PathCellClickInject, into what is nowFNS_CustomParTools. The owner dissolvedFNS_Navbarentirely, so "stays a navbar citizen" had no citizenship left to keep:parent_hierarchysplit intoFNS_iopBrowserandFNS_ParentHierarchy, and with those gone the package was ~80 ops of scaffolding. The../panenavobjection turned out to be moot and always had been:panenavis TouchDesigner's OWN pane-bar element, so the reference cannot resolve at the MASTER and warns there for as long as it ships.Corrected 2026-09-09, same day: I first read that warning as a dead feature, because at the master
panel1andnull1carry 0 channels. That is the wrong place to measure a per-pane widget. NavbarRegistry makes COPIES into each pane bar, wherepanenavIS a sibling: measured in/ui/panes/panebar/pane1/nbitem_HijackDragdrop,panel1resolves topane1/panenav/pathwith 67 channels, andnull1carries the same 67. The feature works. Clearing the reference at the master -- which I did, to silence the warning -- broke it in every bar, and it has been restored. The master's warning is the correct, permanent state for this class of widget; do not "fix" it. -
FNS_Toolbar → midiMapper: RESOLVED (2026-08-13). The blocker wasallowCooking = Falseon midiMapper — a live panel widget cannot move into a COMP that does not cook. The owner re-enabled cooking, which took option 3, andwidgets/button_midi_learnmoved into midiMapper carrying its own ToolbarRegistry host. Its calls are now local (parent().), the entry re-registered from the new path keeping order 28, and the toolbar still reports 37 widgets. No core→tool edges remain.midiMapper also joined the fleet properly: it was the only tool with no ConfigRegistry host (it could not have one while cook-disabled), so its settings never roamed. Stamped via
StampHost, canonicalmidiMapper.Fixed in passing:
panelexec4calledop.FNS_MULTIMIDI.Resetall(), butResetallis a pulse PAR, not a method — it raisedtdAttributeError. Now.par.Resetall.pulse(). Invisible until now because the button could not reach a non-cooking tool at all.
2026-08-23:
tools_uiwas retired. Its tabs areFNS_HubRegistrycontributions inFNS_Hub(registration, not a sweep -- see HubContract.md, HubFollowups.md §1); theUitabpars and the manifest'stools_uisurface are gone. The rendering lesson below still holds for panel children; a Select COMP does render a root-level panel that is not displayed anywhere else (the hub relies on it), provided the source has a fixed size --fillwith no panel parent collapses to nothing.
The tab panel was the last structural coupling: a static six-name list
(switch_hack DAT + Menunames) naming sibling COMPs, sibling panels
hand-WIRED into tools_ui as panel children, a startup timer working around
a 2023 folderTabs bug, and two tabs (SearchWords, OpColor) that were
unpackaged root glue no .tox carried — dead tabs even on a full install,
and a None.family crash in the switcher on any partial one.
Resolved by a capability sweep, same philosophy as the Refresh par
(§1.2b): a tool contributes a tab by carrying a UI Tab section on its
Registry par page (Uitabsection header +
Uitab/Uitablabel/Uitaborder/Uitabpanel — same page as the stamped
Cf*/Tb* sections, one prefixed section per surface); tools_ui's
build_tabs sweeps depth-1 siblings on startup/create/open and rebuilds
the folderTabs menu. Missing tool = missing tab, zero tabs = an empty-state hint. Tab
order and active tab roam via tools_ui's ConfigRegistry host
(Tabuserorder/Activetab); a tab's ✕ simply flips the tool's Uitab
off, which roams too.
The rendering lesson (hard-won): TD builds panels lazily and ONLY as
real panel children — opviewerCOMP and selectCOMP render a panel that is
already built somewhere, but neither force-builds an offscreen one
(selectCOMP mirrors already-rendering panels; opviewerCOMP force-renders
only non-panel viewers like DATs). So the old wiring was load-bearing, and
the sweep now reproduces it: root-panel tabs get WIRED
(tools_ui.output → tool.input) by Rebuild() itself; non-panel content
gets a local shim child inside tools_ui (opviewer for DATs, parameterCOMP
for params:<pagescope> tabs). Interior panel COMPs are rejected with a
log line — only the tool root can be a panel tab.
The orphan glue died: root SearchWords (opviewer onto
FNS_OpMenu/OpSearchWords — now FNS_OpMenu's own ./OpSearchWords tab),
root OpColor + openOp1 (a par-window springboard at ColorUI — now
params:Families Colors Search on ColorUI, a strictly better tab: the
full palette editor inline). Node-viewer redirects (opviewer par) on the
four root-panel tab tools were cleared — they pointed at mapping-table
DATs and would have hijacked any viewer-based rendering.
build_manifest.py derives a tools_ui surface from the presence of the
Uitab par (capability, not toggle state). Effect on §1.1's graph: the
tools_ui → GlobalOutSelect guard stays capability-based, openOp1 → ColorUI is gone with its comp, and tab membership is no longer an edge
of any kind. Pkgversions bumped: tools_ui 1.1.0 (rework), the six tab
tools patch (+UI Tab pars, redirects cleared).
Six active hotkeys, and only ONE touches a tool:
| Shortcut | Action |
|---|---|
ctrl+0 / cmd+0 |
op.FNS_RPLS.par.Reset.pulse() — the only tool reference |
shift+alt+q |
…selectedChildren[0].openParameters() |
ctrl+alt+q |
ui.panes.current.owner.openParameters() |
shift+alt+w |
ui.openCOMPEditor(selectedChildren[0]) |
ctrl+alt+w |
ui.openCOMPEditor(owner) |
ctrl+shift+f |
show TD's own palette tab, focus the search field |
Move keyboardin_resetpls + its callback into ResetPLS1 (where the call
becomes local — parent().par.Reset.pulse(), no global shortcut at all) and
MY_HOTKEYS becomes a dependency-free "TD conveniences" package. HotkeyManager
already discovers per-tool hotkeys, so no new mechanism is needed. This also
deletes the audit's only try: … except: pass.
Done (2026-08-23). Both tool-touching hotkeys have moved out:
ctrl+0 to ResetPLS1/keyboardin_reset, and ctrl+shift+f to
TDX_SearchPalette/keyboardin_searchhotkey — which also removed MY_HOTKEYS'
second, undocumented tool reference (it hardcoded the SearchPalette field
path). MY_HOTKEYS is now four keyboardins that call nothing but
openParameters() / openCOMPEditor(); its only remaining op.FNS_ reference
is its stamped ConfigRegistry host, which is infrastructure, not a tool
dependency.
The move also demonstrates the customization shape worth copying: the combo
lives on the TOOL's own Shortcuts par as an app.osName OS-switch
expression, with defaultExpr/defaultMode authored so Par.reset() restores
it; the keyboardin follows that par by a plain expression, which HotkeyManager
deliberately ignores — so the tool shows up as exactly one rebindable row.
- Core package (always installed): registries + RegistryBase +
/sysbootstrap + shared UI shells (toolbar/navbar/mainmenu hosts). - One package per tool (or per small coherent group): self-registers on load via its stamped contributions.
- Design rule worth committing to early: tools depend only on core, never on each other. Then the manifest needs no dependency solver, the configurator needs no constraint logic, and partial installs can never half-break. Cheapest architectural rule available while the redesign is still in flight. (If cross-tool deps must exist, they become real pip dependencies — see §3 — and uv resolves them; but every such edge makes the configurator and the failure modes worse.)
- Corollary (decided 2026-08-12): registry MASTERS live in core; tools ship scrubbed hosts. Host cloning is not just a dev convenience — the globals' healing tick re-asserts clone exprs in USER projects too, so cloning is the core→fleet update rail: updating core rolls every in-project host (even ones inside tool toxes the user never updated) forward to the new master, while host Registration par VALUES survive. The catch it papers over: clone sync is NOT version-aware — with a newer master anywhere but core, an old in-project master would structurally DOWNGRADE a newer tool's host (the /sys global arbitrates by Version; clones don't). Masters-in-core makes "the in-project master" and "the newest registry version" the same thing by construction, so the downgrade case cannot arise. Revisit (version- aware _healHostClones) only if mixed-age installs ship before the core/tool split does.
Headless TD or a live session driven via Envoy walks the tool list,
exports each COMP (ExportPortableTox, same artifact as today's
modules/release/ output — see UvPackagingResearch §6 wrinkle), computes
hash + version, and writes a manifest.json: name, description,
category, icon, version, sha256, deps, artifact URL. The existing UPDATER
tool should consume the same manifest for updates — one catalog, two
consumers.
Revised 2026-08-13 — see §4.2. Rail 3 (pip/uv) is dead; delivery is a bucket plus native
.exe/.dmginstallers. Rails 1 and 2 stand, and both consume the same manifest.
- Installer COMP — a single small
.tox, no launcher required. Reads a selection (JSON) + manifest and loads core then tools. Built (packaging/dist/FNS_Installer.tox, ~4 KB), currently installing from local artifacts; the bucket fetch is the remaining piece and should reuse the vendoredUPDATER/fileDownloaderrather than hand-rolled HTTP. - TDXLU sidecar — the launcher utility bus already exposes
load_toxwithpersist,parent,externaltox,toxfile_module(seeTDXLUUtilityExt._handleCmdLine, actionload_tox). A "store" panel in TDXLU renders the manifest with checkboxes and pushesload_toxcommands into live sessions;persistsurvives restarts. Best UX: already installed, already knows which TD sessions are alive, and browsers can't speak raw TCP to the bus anyway. Still open. pip/uv skeleton— dead, see §3.
A static site (GitHub Pages) over the same manifest.json: pick
features, dependencies auto-check, output one of:
- a downloadable
selection.json+ installer-COMP bundle; - a client-side-assembled zip of the chosen toxes;
- a deep link (
tdxlpp://install?tools=...) handled by TDXLU, which performs the install over its bus — website as storefront, launcher as installer; - a
pip install fns-tool-a fns-tool-bline (§3 route).
A browser POSTing directly to a Web Server DAT on 127.0.0.1 is
possible but the fiddliest option (CORS, port discovery) — noted, not
recommended.
Dead as of 2026-08-13. Distribution is buckets + manifests, with native
.exe/.dmginstallers as the bootstrap (see §4.2). pip was only ever a delivery rail, and it argued mostly against itself even then. Kept for the reasoning, not as a plan.
Refines UvPackagingResearch with the selection mechanism. If pip is the rail, feature selection must live in pip's world or the resolver contributes nothing:
- Each feature is a tiny marker package (
fns-tool-quickop, …): no real code, just metadata — tox artifact URL, version, sha256, and pip deps onfns-tools-core(+ any cross-tool deps, if allowed). Extras form also works:fns-tools[quickop,swapops]. - The bootstrap COMP enumerates installed
fns-tool-*distributions and loads their toxes in dependency order. This is the "skeleton on pip, bootstrapper collects toxes" model. - Pin, never "latest": package version N points at an immutable
artifact (GitHub Release asset / versioned bucket key) with its hash
in the metadata. Mutable
latest/paths → unreproducible installs, uncorrelatable bug reports. - Embed vs fetch: if toxes are pinned per package version anyway,
embedding them in the wheel as package data (tdp-MVP's model,
UvPackagingResearch §2.1) is simpler and atomic —
pip installis the collection step, offline installs work, no runtime HTTP in TD. Remote-fetch only earns its complexity when payloads are large or binaries must update without republishing packages.
(From the same discussion; UvPackagingResearch is neutral on this.)
- It doesn't install anything into TD — the bootstrap COMP and all the hard work (core/tool split, audit, manifest) exist regardless; pip only replaces the download step.
- Scope mismatch:
TDPyEnvManagerContextis per-project; the toolkit is session-level UI tooling wanted in every project. (Unverified whether a shared/global env mode exists — check docs before leaning on it.) - Two sources of truth: pip's ledger is the venv, the user's reality is the network. Dissolves only under fully ephemeral loading (toxes loaded fresh every startup, never saved into the .toe) — a strong commitment made mostly to accommodate the transport.
- Audience friction: TD users drag toxes; "set up a Python environment first" is an adoption tax. TD 2025.31310+ only.
- Lifecycle: partially mitigated — the env manager Helper runs during
TD core startup before any COMP cooks (UvPackagingResearch §4.1),
so
sys.pathis ready before the bootstrap COMP inits. Failure modes still surface as a silently missing toolkit rather than an installer error.
Verdict from the discussion: with TDXLPP existing, pip mostly duplicates the delivery layer while adding per-project ceremony. Without TDXLPP it would rate considerably higher. The steel-man is the ephemeral-bootstrap model — one universal bootstrap tox that never changes, pip as the sync mechanism behind it.
Correction worth recording: TDPyEnvManager manages pip packages/venvs — it does not "install toxes." Any tox materialization is our own bootstrap's job, whichever rail delivers the bytes.
Dependency audit + core/tool boundary definition— DONE, §1.1.Manifest + per-tool tox export automation— DONE,packaging/.Installer consuming manifest + selection JSON— DONE as a script (packaging/install.py). The droppable COMP wrapper is still open.TDXLU store panel over the— DONE (2026-08-19), launcher-side (TDXLPPload_toxbusdocs/fns-integration.md): an FNS Tools tab in TDX Launcher Ultra renders this manifest by category, stocks the palette store (sha256-verified), writes aselection.jsonand drivesFNS_Installerin a running session via new companion verbs (fns_install/fns_status, utility ≥ 0.9.0) — plus a live configurator over the ConfigRegistry settings server (fns_settings_url+/api/state///api/setproxy), an offline editor overFNStools_config.json, a per-component palette shelf (standalone drops), and the §2.4 paste rail as fallback. Nothing on this repo's side had to change — the bucket, store, installer and settings-server contracts were consumed as published.Static configurator— DONE,packaging/configurator/.- Optional: pip marker-package rail on top (§3), sharing the same artifacts and manifest. Open.
packaging/build_manifest.py derives manifest.json from the live project
packaging/catalog.json the only hand-written data: category + description
packaging/manifest.json 41 packages, 6 core, artifacts + hashes
packaging/install.py Plan() / Install() over a selection.json
packaging/configurator/ the picker; also emits a single-file build
packaging/dist/ 41 exported .tox (6.5 MB, gitignored)
Pick-and-choose works end to end today, with no web presence: open
configurator/configurator-standalone.html, pick, download
selection.json, then exec(open('packaging/install.py').read()) and
Install('path/to/selection.json').
Verified: all 41 artifacts export, and all 41 install into a scratch
container (11,066 ops) with zero failures and op counts matching their live
originals. Install tests MUST use a cooking-disabled container — a live copy
of a registry master will otherwise try to promote itself to the /sys
global and destroy the running one.
Dependencies are derived, not declared. A package's requires is
exactly the core packages owning the registries it hosts, because masters
live in core and tools ship stamped hosts (§2.1). Every tool requires
FNS_Config; a tool with a toolbar button also requires FNS_Toolbar.
Nothing hand-maintains this, so it cannot drift from reality.
Two findings the packaging work surfaced, both pre-existing:
-
ExternalTablescould not be exported at all — its ownpre_releasehook resolvedpar.Root(a sibling reference) against the staged copy in/sys/quiet, where siblings do not exist. Fixed. -
The manifest carries a
portabilityfield, because Embody's absolute-path warnings scroll past during export and a package whose files point at THIS machine arrives subtly broken. Checked against the artifacts rather than assumed: the export strips the ROOT comp'sexternaltox, butfilepars and NESTEDexternaltoxsurvive, so the scan skips the former and reports the latter.The one that matters:
OpTemplatesships expectingOPTemplates1.toxto already exist in the installing user's palette (<palette>/FNSTools/OpTemplates/OPTemplates1.tox). On a fresh machine that file is absent and the template library comes up empty. Six of its render templates also pin TD's ownSamples/Geoby version. The other palette references (ExprHotStrings, FNS_HotkeyManager, FNS_OpMenu, ResetPLS1) are per-user data files those tools recreate, so they are benign.
Status: built and verified. The rework described at the end of this section is implemented in
UPDATER/ExtUpdater.py. The rest of the section is kept as the record of what was replaced and why.
How updates worked before (UPDATER/ExtUpdater.py): one version for the
whole toolkit — Gittag on the root COMP, currently v2.11.2 — polled
against the latest GitHub tag. Update() snapshots every tool's settings
through op.FNS_CONFIGREGISTRY.SaveAll(), downloads ONE tox, and calls
TDF.replaceOp(parent.FNS, newComp): the entire
FunctionStore_tools_2025 COMP is swapped wholesale. Settings survive
because each tool's ConfigRegistry host re-loads its own section after the
replacement. Docked ops are undocked and restored around the swap.
So there is no per-tool update mechanism at all — one artifact, one version, all or nothing.
This is incompatible with subset installs. A user who picks five tools
and later updates gets replaceOp'd with whatever the release tox
contains — the whole toolkit back, their selection erased. Per-package
installs without per-package updates is half a system.
Is UPDATER therefore core? In effect yes: it is the only update path.
It stayed tool only until the mechanism was decided — with the rework
landed it is now core (CORE in build_manifest.py), because the one
package that can fetch updates is the one a user must not be able to
accidentally decline.
This section originally read "hashes drive updates, not version numbers". That was wrong, and the correction is the more useful record.
The reasoning was: per-package versions do not exist (38 of 39 empty,
build is a counter), but the manifest already carries a sha256 per
artifact, so let the hash answer "has this changed since the bytes I
installed?" — exact, zero maintenance, no way to drift.
It cannot answer that, because .tox export is not reproducible.
Exporting one untouched component three times:
| Export | Bytes | sha256 |
|---|---|---|
| a | 66198 | 061982f7… |
| b | 66190 | 8c41e46b… |
| dist | 66150 | 116ae877… |
They diverge at byte offset 9 — the container header, before any content. So a hash comparison marks every package updated on every release: publish a drop that touched two tools and all 39 installs re-download. The property the design was chosen for never existed.
Two replacements were considered and rejected:
vc_data/ theVc*pars — the table already on 38 of 39 components. Rejected: it belongs to Private Investigator, is written by tooling outside this repo, and nothing in packaging governs it. (Also thin: one package had a real version, one had no table, andbuilddid not move across two project saves — so even "save counter" was wrong.)- A TDN content fingerprint — genuinely stable (two exports of one
component differ by a single line,
exported_at, out of 2280). Rejected: TDN is an external package, and identity cannot depend on one.
What we do instead: Pkgversion, a custom parameter on every package,
governed by us — the FunctionStore_tools_2023.tox Gittag idea made
per-package. The manifest publishes it; the updater compares it against
the same parameter read live off the installed component.
Reading it live is what makes the embedded case work at all: a package
loaded into a .toe has no file to hash and no artifact to consult, but
it still declares what it is. It also means no side record can drift out
of truth — the component is the truth.
So: sha256 verifies downloads (its real job), Pkgversion decides
updates, and the release label names the drop. The cost is that
bumping is manual and forgetting is silent, which is why publish.py
refuses a new release that bumps nothing.
Supersedes every GitHub-based flow in this document (§2.3 rail 3, §3's pip rail, and the old "GitHub keeps the tag" wording). The owner's direction, 2026-08-13:
in the future we will only serve an .exe and .dmg installer so don't worry about github-based update workflows. simply think in buckets and manifests from now on
So there are exactly two moving parts:
- A bucket holding
manifest.jsonand the per-release artifacts. It is the single source of truth for what exists and what the bytes are. - Native installers (
.exe/.dmg) as the bootstrap that gets a first copy onto a machine. Everything after that — picking, installing into a project, updating — is manifest-driven.
Consequences, all of them simplifications:
- The release label is ours, not git's. It lives in
packaging/release.json(with achannel, sobetais just another bucket prefix) and is stamped into the manifest and every artifact URL. The root COMP'sGittagpar survives only as a fallback. UPDATER/github_remoteandPollLatestTag()become legacy. Update checks stop being "poll the newest git tag and compare strings" and become "fetch<base_url>/manifest.json, compare artifact hashes". TheGittagstring comparison inOnPolledLatestTag— including its major-version gate — has no role in that.UPDATERbecomes core once reworked: reading the manifest and replacing changed packages is infrastructure, not an optional tool. (It staystoolin the manifest only until that rework lands.)- Distribution size stops mattering. 39 artifacts is a directory listing in a bucket, not 39 release assets.
packaging/publish.py stages the exact bucket tree and re-hashes every
staged file against the manifest before reporting success — publishing
bytes that disagree with the hashes an installer will check is worse than
not publishing:
<release>/manifest.json immutable snapshot
<release>/<Package>.tox immutable artifacts
<release>/FNS_Installer.tox bare installer (root already exists)
<release>/FunctionStore_tools_2025.tox one-drop bootstrap: the toolkit
root carrying installer + UPDATER
manifest.json ROLLING pointer to the newest release
Releases are pinned: every artifact URL inside a manifest carries its
release, so a manifest always resolves to the bytes it was built from. The
rolling root copy exists only to answer "what is current?" once. No
mutable latest/<Package>.tox (§3).
The palette is a store; projects embed self-contained copies. Two deliberately separate motions:
- Refresh the store — fetch the bucket manifest, download changed
artifacts into
<palette>/FNSTools/store/. Machine-wide, project-independent. - Update this project from the store — compare the open project's
embedded packages against the store and
replaceOponly what differs. Explicit, per project, never automatic.
Two records, and they are not interchangeable: the store's own
manifest.json (what the palette holds), and an installed table DAT
inside the toolkit root COMP. That table was originally the identity
source; since the Versioning reversal above it is an audit trail only
(release, when, the sha of the artifact fetched). Identity is the
component's own Pkgversion, read live.
Rejected: per-package palette toxes with externaltox bindings (updates
land machine-wide, but shipped packages stop being self-contained and
every project mutates under the user), and in-project replaceOp alone
(clean artifacts, but every project becomes its own frozen fork).
The accepted cost is that a project can sit behind the store, so the UI must say so plainly rather than pretend everything is current.
Never hash the live COMP to decide staleness — a .tox re-saved
inside a project no longer hashes to what was published. (This instinct
was right and the conclusion drawn from it was not: the fix is not to
compare a recorded hash, it is to not compare hashes at all. See the
Versioning reversal above.)
With buckets-and-manifests settled there is no longer a choice to make. Selection-aware whole-replace and reinstall-only both existed to work around "one artifact, one version"; the bucket removes that constraint.
UPDATER, reworked:
- Fetch
<base_url>/manifest.json(rolling) via the vendoredUPDATER/fileDownloader— a TDFileDownloader wrapping a Web Client DAT, with callbacks, progress UI, auth and a concurrency cap. Do not hand-roll HTTP;requestsblocks the frame. - For each package present, compare the
Pkgversionit declares against the version the manifest publishes. Newer = update available. Nothing else is consulted — not hashes, notbuild, notGittag. - Download only those artifacts (pinned URLs), verify each hash after download — that is what hashes are for — then apply per package.
- Settings survive as they do today: each tool's ConfigRegistry host reloads its own section after replacement.
One detail worth getting right the first time: a package the user never installed must stay uninstalled — an update pass is not an install pass.
UPDATER.par.Filename (FunctionStore_tools_2023.tox on a 2025 toolkit)
was stale naming, not a broken download. Under the store model there is no
monolith artifact at all, so the parameter is gone.
Three pulses on UPDATER, one for each motion, plus Baseurl /
Storefolder / Showprogress / Status:
| Pulse | Cost | What it does |
|---|---|---|
| Refresh Store | whole store | manifest + every artifact whose bytes differ → <palette>/FNSTools/store/ |
| Check for Updates | one small JSON | manifest only, then compare — answering "anything new?" must not cost 6 MB |
| Update This Project | only what differs | fetches just the packages this project needs, then replaces them |
Compare() is the whole decision in one place, and reports five states:
update, current, untracked (in the project with no install record —
shown, never auto-updated), missing, gone. The project's side of the
comparison is an installed table DAT in the toolkit root, written per
package as it lands by BOTH rails (InstallerExt.RecordInstalled and the
updater), which is also what makes an interrupted pass safe to re-run.
Baseurl accepts a bucket URL, a file:// URL, or a plain directory, and
artifacts resolve relative to it rather than to the manifest's own
base_url — same string against the real bucket, but it is what lets a
mirror, a local packaging/publish/ tree or a localhost server serve the
whole flow. Both rails were verified end to end before the bucket exists.
Three traps paid for, all in the vendored TDFileDownloader:
- A request issued from inside the Web Client DAT's own callback is
silently dropped. The file lands, the next GET never goes out — and the
downloader's own
queueNext()re-issues from exactly there, so its internal queue cannot be relied on either. Every stage after a download is deferred one frame (_later) and the queue is driven here. - A stale
stateDictentry poisons every later request for that file. It keys on url+location; an entry left inGET/WAITmakesDownload()return the stale state instead of fetching. Each job starts withAbortAll(). - A connection that never opens produces no callback at all — no
success, no abort, just a request sitting in
GETforever. Hence the stall watchdog: 45 s without progress fails the pass with a message naming the stuck files.
An install can put its package .tox files in one of three places, chosen
on the installer (Package Files), and the updater does not track the
choice — it follows whatever binding each package actually has, so there
is no mode flag to drift out of sync:
| Mode | Files | Update path | Trade |
|---|---|---|---|
embedded (default) |
inside the .toe |
replaceOp from the store |
one file to move; nothing to lose track of |
shared |
bound to the palette store | rewrite + reload (usually just reload) | one copy per machine — a store refresh reaches every project sharing it |
project |
copied to the project's own folder | rewrite + reload | each project owns its files, so one can hold a modified package without touching any other |
Updating a bound package is a file write plus a reload, not COMP
surgery — no copy/destroy of an extension-bearing COMP, no docked-op
juggling, and the change is a file the user can see and version-control.
Verified by rewriting a bound .tox with different bytes and confirming
the live COMP reloaded to match. A COMP reloaded this way does not report
its new state in the call that fired the pulse, so the pass records what to
check and settles it on the next tick.
Settings are safe by construction, not by care: they live in
<palette>/FNSTools/config/FNStools_config.json, never in the .tox,
and each tool's ConfigRegistry host re-registers on reload with
autoload, which re-applies its section. SaveAll() still runs before any
pass.
shared deliberately reintroduces the machine-wide coupling §4a rejected
as a default — a store refresh changes bytes every sharing project will
pick up on its next reload. It is offered because some users want exactly
that; project is the isolation-preserving choice, and embedded is the
default.
What is still refused (state locked): a COMP whose .tox Embody
authors — tracked rows in externalizations.tsv — because there the file
is generated FROM the live COMP, so writing over it destroys work. Note
this is the second line of defence: the first is that a package with no
install record is never a candidate at all, which is what actually keeps a
dev checkout out of every update pass. Learned the hard way by replacing
the live AutoRes with an artifact and losing its Embody bindings — and
note that pi_suspect is no help as a marker, since it survives into the
shipped artifacts.
The ConfigRegistry settings server is now the toolkit's single in-project
landing ("FNS tools — Console", FNSTools/FNS_ConfigRegistry/ settings_page.html + settings_server_callbacks.py + Ui* methods on the
ext). One page, two tabs plus document actions:
- Settings — the scrollable all-tools sections view (nav = scrollspy
jump list), unchanged
UiState/UiSetcontract. - Install & remove — the FNS_Installer picker, NOT reimplemented: the
settings server serves
/toolsand forwards/manifest.js,/selection,/status,/installstraight toInstallerExt.ServeRequestin-process — one origin, one port, zero duplicated picker logic. Without an installer COMP the tab shows a plain explanation. Verified live (2026-08-22) once the rails became dev-root residents:/toolsserves the picker,/manifest.jscomes backFNS_SERVEDwith the root's children pre-checked,/statusforwards. Unchecking an installed tool = removal, and the tab's banner states the precedence rule: install decides WHAT is in the project; the config layer always re-applies on top, and removed tools keep their settings for the next install. The same sentence now ships in the picker's core note (all three flavors). - Export / Import —
/api/exportsnapshots every registered tool LIVE into a schema-1 config document (correct under project scope, where the file is stale by design);/api/importapplies a document onto installed tools now and merges sections of not-installed tools into the roaming file for their next install (verified end to end, including the deferred merge surviving a later SaveAll). Schema-gated. - Scope —
/api/scopereads/flipsConfigscope.projectflips quietly;globalREQUIRESmode: push|adopt(the page shows the same three-way choice as the in-TD dialog — no popup, an explicit decision).
FNS_Installer and webBrowser are now residents of the live FNSTools
root beside FNS_Updater, and the bootstrap is that root castrated with
the rails kept (BOOTSTRAP_KEEP in build_installer.py) rather than
emptied and re-injected. The reason is the one that motivated castration
in the first place: a build-only copy of anything is a second source that
drifts. EnsureDevRails() builds missing rails and re-embeds the
installer's two source snapshots in place; BuildBootstrap() runs the
same refresh on its staged copy and blanks per-project installer state
(selection, status, server). FNS_Updater stays a build-time injection
from the dist artifact on purpose — the dev copy is an Embody-tracked
master with file bindings.
Tracked like everything else, shipped unbound. FNS_Installer is a
Private Investigator suspect like every other dev-root component (tox
under modules/suspects/FNSTools/, version history in the lister). Two
guards make that safe: build_manifest.RAILS keeps it (and webBrowser)
out of Packages() — rails publish under the manifest's rails, never as
installable packages — and _resetInstallerState cuts the shipped copy's
externaltox binding, because a NESTED binding survives export and would
point a user's bootstrap at a dev-only path. Its DATs are deliberately
NOT Embody-externalized: packaging/InstallerExt.py and
configurator/index.html stay the single sources, re-embedded by
EnsureDevRails().
Root entry points, revised (2026-08-22). Three pulses on the root's
FNSTools page, three roles: Pick Tools → the console's Install &
remove tab (#tools, in the in-TD webBrowser panel when the root has
one; the installer's own picker only while core is not installed yet);
Open Settings → the console's Settings tab, same panel; Installer
Parameters → the manual rail. OpenSettingsUI(tab=, panel=) grew the
two knobs (panel defaults on; a root without the panel opens the system
browser); the console reads its tab from the URL fragment. Correction
recorded: Open Settings first shipped to the system browser on the
unverified assumption that the in-TD panel could not do file dialogs —
the owner tested it, it can. Export now also writes
<config dir>/exports/FNStools_config_<stamp>.json server-side and the
page reports the path, so the export never rests on the browser honouring
a download in the first place.
The source-checkout lock. A resident installer's default target is
the dev root itself, and the picker pre-checks every live child — an
Apply there would remove authored masters. SourceLock(target) in
InstallerExt.py mirrors the updater's _refuseReason layer for layer:
the target is the container build_manifest.py's TOOLKIT exports from
(source checkout + exporting root, both required), or Embody tracks rows
under it. ResolvePlan carries the reason as plan['locked'];
InstallPlan and RemoveTools refuse, the Plan/Install pulses report it,
the served /selection and /install answer REFUSED, and /manifest.js
hands the picker FNS_LOCKED so Apply is disabled up front with the
reason shown. A scratch container elsewhere in the source project stays
installable — that is still how installs are tested. Known gap surfaced
while doing this: the
published v2.11.2 has no rails (no bootstrap was ever built after the
castration rework), so the website's paste-script rail asserts against it
until the next publish.
Owner decision, same day: "the console stopped being a ConfigRegistry
feature the moment it grew an install tab." Extracted on branch
fns-console (brief: briefs/2026-08-22-fns-console.md).
FNS_Console is a RegistryBase registry (ConsoleRegistryExt, shortcut
FNS_CONSOLE, master /FNSTools/FNS_Console, global
/sys/FNS_Registries/FNS_Console). Its surface is the server: the
global owns the ephemeral Web Server DAT (ports 36710-36759, idle-stop),
serves console_page, and routes — /api/* to FNS_CONFIGREGISTRY's
Ui* (paths unchanged on purpose; TDXLPP reads /api/state + /api/set),
/tools + picker URIs to FNS_Installer.ServeRequest, /t/<tab>/ to a
contributed tab. Its hosts are tab contributors: a stamped host's
Registration pars name a page DAT and an optional api DAT
(onConsoleRequest(action, method, body)), and RegisterTab publishes
them; Settings and Install & remove are built in. See
packaging/docs/FNS_Console.md for the contract.
First contributor: ColorUI (same day). Its panel was already a web
page with a title/executeJavaScript bridge, so the tab is the same
webui_html with an HTTP transport chosen at load (SERVED): commands
POST to /t/ColorUI/api/cmd, state polls every 2 s; ExtColorUI grew
ConsoleState/ConsoleCommand (toasts captured into the HTTP answer)
and a 12-line console_api DAT names the actions. Stamped via
StampHost(par_values={...}); DAT pars are bare sibling names. Verified
in the console: tab, framed page "connected", 626 colors, command
round-trip. Lesson: parameter-execute callbacks fire at frame end —
never read a pulse's effect in the same call.
Tab visibility (owner ask, same day). Built-ins sit before a divider,
contributions after; a ⋯ tab manager shows/hides contributions. The
decision is written back to the host's new Displayed par
(compare-before-set) -- the toolbar's pattern: the host par is the durable
record, mirrored onto the tool's Registry page, so it roams through the
config registry and the console keeps no list. Auto-register is relabelled
Expose to Console on this registry: off = the tool runs only its own
local interface. Verified: API and UI round-trips flip the host par and
the bar. Lesson: reinitializing a host fires onDestroyTD and unregisters
it -- re-register explicitly after any host reinit.
Local renderer off while exposed (owner ask, same day). A contributor
whose panel is a Web Render of the same page must not keep a CEF process
and a texture alive for a page the console now serves. Owner's call: the
console handles it, not each tool -- a Local Browser COMP par on the
host names the tool's webBrowser panel, and the host switches its Active
off when it publishes and back on when exposure ends (Expose off, failed
registration, host destroyed via onDestroyTD with the owner invalid).
ColorUI keeps only what is its own: Open UI routes to the console while
exposed, and no JS push / reload-kick at a renderer that is off.
Standalone vs toolkit release — DECIDED: one artifact, the install rail
decides exposure (2026-08-22). A standalone ColorUI drop would have
bootstrapped a console global from its own host, exposed the tab and
killed its panel. Two options were weighed: (A) fork artifacts by a
release profile (root par read by the shared pre_release_common.py,
dist/standalone/ tree) — rejected: two hashes, two publish trees, a
second identity story; (B) ship every artifact with Expose off
(pre_release_common.py scrubs Csautoregister/Autoregister on any
console host) and have InstallerExt.ExposeConsoleHosts flip it on as
the package lands — chosen. Updates never touch it; the flag is the
tool's Registry-page par, persisted by the config registry, so the
user's choice roams. It is the same principle as §2.4's precedence rule:
install decides once, config remembers. Written up for users in
packaging/docs/FNS_Console.md ("Exposure: shipped dormant, enabled by
the install"). General rule recorded there: a host whose exposure takes
a local surface away ships dormant and is enabled by the install rail.
Renderer off unless the viewer is open (owner ask, last one of the
day). Signal verified against the docs: panel value winopen = "1 if
panel is open as a floating window" (display is just the Display par
-- it read 0 with the viewer open). ColorUI: watch_viewer panelexec
on winopen -> SyncLocalBrowser, rule Active = winopen AND not
served by the console. The console hands exposure changes to a tool's
OnConsoleExposure(exposed) when it exists (host _setLocalBrowser
and the destroy hand-back both route through it), so the renderer has
exactly one owner. A viewer shown any way other than a floating window
is outside winopen and stays off -- documented, accepted.
The FNS webBrowser (owner ask, 2026-08-22). The palette webBrowser
became ours: the visibility policy moved INSIDE it (Watchwindow ->
panel value winopen, event-driven; Watchviewer -> the node's Viewer
Active flag, polled once a frame -- the flag has no callback; one
watch_rules.sync() behind both, compare-before-set; both off = Active
is manual), and the Web Render's Source/DAT are mirrored on the
component's custom pars. That second part is what makes it clonable:
clone sync replaces the children, so per-instance configuration must
live on the clone COMP's own parameters. /FNSTools/webBrowser is the
master; ColorUI's panel browser clones it (guarded
op.FNS.op('webBrowser') expression, Source=DAT webui_html, watchers
off because ColorUI's own rule owns Active); the shipped rail is cut
loose from cloning at build time and packaging/webBrowser.tox is
saved from the master. The root-level watcher and the owner's eval/datexec
scaffold were retired once the policy moved in. Verified: ColorUI renders
"connected" through the clone; master follows both the window and the
flag in both directions.
FNS_ConfigRegistry keeps its Ui* API and nothing else of the UI: the
page, callbacks and server lifecycle are gone; OpenSettingsUI(tab, panel)
is a thin forward to op.FNS_CONSOLE.Open so the root pulses and the
launcher keep working. The root pulses prefer the console and fall back to
the forward. FNS_Console is core in the manifest (CORE,
REGISTRY_OWNER) and a PI suspect like its siblings.
Paid for on the way: a TD crash + relaunch reverted /FNSTools to its
last PI-saved tox (02:11) — every live structural change made since
(rails, pulse revision, installer PI row) vanished while files and commits
stayed intact. save_project writes the .toe; the root reloads from
modules/suspects/FNSTools.tox, which only pi.Save(op.FNS) rewrites.
Rule: after structural changes under the root, pi.Add new comps,
pi.Save changed suspects, pi.Save(op.FNS) LAST, then save_project.
- Does TDPyEnvManager offer any shared/global (non-per-project) environment mode? (Affects §3 objection 2.)
-
tdxlpp://protocol-handler registration in TDXLU — feasible on all target OSes? - Ephemeral vs persisted installs as the default:
load_tox persist=Truesemantics vs load-fresh-every-start (affects update story and §3 objection 3). - Package granularity — per-tool, one deliberate bundle (§1.2). MISC and OUTPUT already ship as single COMPs, so they are one package each by construction; no further grouping needed.
- Update mechanism vs subset installs — DONE (§4.2). Per-package,
store + per-project pull, driven by a
Pkgversionparameter we govern. Hashes were tried first and cannot work:.toxexport is not reproducible, so they verify downloads and nothing more. - Descriptions in
packaging/catalog.jsonwere seeded by inspection and need an owner pass. -
OpTemplatesdoes not ship self-contained — itsOPTemplates1child is an external tox in the user palette, so a fresh install gets an empty template library. Either embed the child in the artifact or have the installer fetch it alongside. - Where does the manifest live — GitHub Releases per-version, plus
a rolling
manifest.jsonindex?