Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.
Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.
Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.
Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.
Four things only running it finds:
* Logos::DesignSystem may be linked by exactly ONE image -- both linked it
and the app aborted with "Cannot add multiple registrations for
Logos.Icons"; QML module registration is process-global
* qmltyperegistrar emits no #include for a SOURCES header given as an
absolute path outside the project
* each qt_add_qml_module is its own target and inherits no include dirs
* AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
split breaks
tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
The shell held the host's objects: MainContainer owned MainUIBackend, took a
LogosAPI* and a QtLogosCore*, and connected to backend signals by concrete
type. Nothing stopped it minting identities or reading the token store.
Three headers in app/interfaces/, on the include path both targets share, so
there is exactly one copy and source drift is impossible:
* IShellHost -- 8 operations the shell may perform
* IShellObserver -- 5 notifications the host may deliver
* IShellView -- how the host builds and tears down the shell
Only QObject*, QWidget* and Qt value types cross. MainContainer holds one
IShellHost* and nothing else; QML reaches the backend as an opaque QObject*
via backendObject(), resolved through the metaobject, so no host C++ type has
to be nameable by the shell.
Ownership inverts: Window owns MainUIBackend, ShellHostAdapter and
MainShellView; the shell borrows. Teardown stops being a consequence of
construction order and becomes a stated contract:
1. beginShutdown() unmounts in-process UI plugin widgets WHILE the shell's
tree is intact -- they are docked inside it
2. destroyShell() detaches the observer, then deletes the shell
3. the backend goes, tearing down Package -> UIPlugin -> Core
4. main() destroys the core facade
Every observer forward is null-guarded: PluginLoader dispatches through
QTimer::singleShot(0, ...) and a 30s ViewModuleHost timeout, so a callback can
land after teardown starts, and QPointer cannot help -- IShellObserver is not
a QObject. UIPluginManager's plugin-widget maps become QPointer for the
mirror-image reason: those widgets are docked inside the shell, so its Qt
parent can destroy them without going through unloadUiModule.
IComponent is untouched and still serves the third-party legacy widget
plugins PluginLoader loads.