Internal boundaries: dependency inventory ========================================= This is an architectural assessment of mpylab at commit ``9d09c9a`` (2026-10-18), before any boundary refactoring. It proposes internal ownership within the existing repository. It does not introduce packages, move modules, change import paths, or define new supported APIs. Scope and method ---------------- All 229 Python source files under ``src/mpylab`` were inspected through an AST import inventory. Imports inside functions and relative imports were included; 14 files in ``device/legacy``, ``tools/legacy`` and the forwarding module ``device/driver.py`` were distinguished from the other 215 files. Generated files, including ``_version.py`` and ``limits_ui.py``, remain in the inventory. Module counts describe locations, not effort or architectural quality. An edge means that one module explicitly imports another, including function imports. Repeated imports between the same pair count once in the figures below. Attribute imports were resolved to a source module when possible. Imports do not prove that a branch executes, or that an imported name is a supported public API. Package initialization, re-exports, configuration-driven loading and pickle restoration require additional inspection. In particular, the absence of a static edge does not imply runtime independence. Tracked Python sources in TEMField, Conducted Emission, PExplorer, Shielding Effectiveness, both active configuration repositories and MpyConfig were also searched. Application/configuration sources, tests and archived/migrated configurations were distinguished. No configuration script or driver was executed. No measurement procedure or formula was assessed. Current locations and responsibilities -------------------------------------- .. list-table:: Source inventory :header-rows: 1 :widths: 25 10 65 * - Location - Files - Present responsibility * - ``tools`` - 53 - Quantities, configuration, graph, numerics, compatibility exports and CLI tools * - ``device`` excluding legacy - 89 - Contracts, transports, drivers, virtual devices, UI helpers and windows * - ``env`` excluding domain subpackages - 19 - Measurement lifecycle, EUT monitoring, UI contracts, Qt adapter and runners * - ``env/tem`` - 18 - TEM workflows, evaluation, history, reports and interactive review * - ``env/msc`` - 5 - MSC workflows, protected acquisition and threshold search * - ``env/univers`` - 3 - Amplifier-test workflow and name maps * - ``limits`` - 22 - Limit models, profiles, registry and Qt explorer * - ``gui`` - 4 - Qt-free identity/availability, branding, macOS helper and launcher * - Package root / legacy - 2 / 14 - Version/package initialization / historical compatibility Main findings ------------- 1. **TEM and MSC already have a useful separation.** No explicit imports from either domain into the other, or into ``env/univers``, were found. Both use the shared measurement layer and tools. There are five distinct module-pair edges from TEM to shared ``env`` modules and three from MSC. There are 23 from TEM to tools, 13 from MSC and six from the amplifier workflow. No non-domain source module explicitly imports these domain modules. External applications and configuration loading still use them. 2. **Directory names do not describe Qt ownership.** Twenty-six non-legacy source modules directly import PySide6, Shiboken or a Matplotlib Qt backend. They span ``device``, ``env``, ``env/tem``, ``gui`` and ``limits``. Six of these are concrete drivers with function-local GUI launchers, not drivers requiring Qt for normal imports. Conversely, ``gui/__init__.py`` contains Qt-free metadata and availability checks. ``device/ui_quantity_display.py``, ``ui_driver_lifecycle.py`` and ``receiver_scan_worker.py`` are also Qt-free; the ``ui_`` prefix alone is not a reason to move a module. 3. **TEM has an explicit UI dependency during interactive review.** ``TEMCell.py`` locally imports ``emission_sweep_ui._require_display`` and ``review_sweep``. The latter module imports Qt only inside functions, so a normal TEM import remains Qt-free. A stricter domain boundary would pass review/display services through an adapter, while keeping acquisition, review decisions, autosave and resume state owned by the workflow. 4. **The limit calculation layer is largely separate from its explorer.** ``limit.py`` uses NumPy and the registry dynamically loads named profiles. Three FCC profile modules use canonical conversions from ``tools.uconv``; four profiles have local example imports from ``tools.spacing``. The Qt explorer additionally imports ``device.ui_frequency`` and shared branding. TEM prescreening imports ``limits.registry.resolve_limit``. This is a sensible domain-to-calculation dependency, not a dependency on the explorer. 5. **The explorer has a small explicit cycle.** ``gui.launchers.limit_explorer_main`` locally imports ``limits.limits_ui_main.main``. That ``main`` imports the private ``gui.launchers._limit_parser``. This is the sole multi-module strongly connected component in the explicitly resolved non-legacy import graph. Both edges are deferred: it is not evidence of an import-time crash. A neutral parser module or an argument-parsing boundary can remove it. 6. **Tools mix foundation and orchestration.** ``tools.config_inspect`` imports ``env.config_view`` and ``env.script_cli``; ``tools.nport_check`` imports ``device.nport``; ``tools.visa_probe`` imports private ``communication_gpib._resource_manager``. These are application/validation tools rather than evidence that all numerical helpers depend on devices. A blanket rule forbidding every ``tools -> env/device`` edge would be misleading. Separate responsibilities logically before enforcing an import policy. 7. **Compatibility exports are heavily used.** Fifty-nine distinct non-legacy modules import ``tools.util``; 40 import ``tools.uconv`` and 31 import ``tools.configuration``. ``util`` re-exports existing implementations from focused modules such as ``logging_utils``, ``text`` and ``sequences``. Prefer those implementation modules for new internal imports, while retaining the existing facade for consumers. ``uconv`` is already the canonical conversion backend and should not be duplicated in domains or limits. Observed consumer contracts --------------------------- These are used import paths, not a proposal to make every imported name public. All paths below are relative to the corresponding repository. .. list-table:: Representative production consumers :header-rows: 1 :widths: 23 42 35 * - Consumer - Evidence - Boundary implication * - TEMField ``7f12792`` - ``src/temfield_mpylab/TestSusceptibility.py`` imports ``dBm2W`` from ``env.univers.AmplifierTest`` - A conversion should come from canonical ``tools.uconv``, without importing an amplifier workflow. * - Shielding Effectiveness ``8b000c3`` - ``report.py`` and ``report_logo.py`` import helpers/errors from ``env.tem.report_profile`` - Shared report metadata currently lives under TEM; a neutral home needs compatible defaults and old-path exports. * - Conducted Emission ``7f739e5`` - ``ce.py`` and ``limit_dialog.py`` use limit models/profiles/registry; the measurement uses ``MGraph`` and quantity helpers - Preserve calculation APIs independently of the explorer. Audit re-exports such as units from ``aunits`` separately. * - PExplorer ``3cb5af1`` - ``src/pexplorer/pickle_loading.py`` uses ``env.Measure.load_pickle_compat`` - Pickle viewing depends on restoring concrete classes, even when there is no direct TEM/MSC import. * - GTEM configuration ``b473175`` - ``sw_gtem.py`` uses the ``device.driver.DRIVER`` facade; tests exercise ``TEMCell`` and ``MGraph`` - The legacy driver facade is still an active external contract. * - RC configuration ``6e92692`` - ``virtual/build.py`` uses ``env.msc.acquisition.MSC_CONDITION_MAP`` and ``MGraph``; custom sensors subclass virtual drivers - Preserve condition-map semantics, custom-driver loading and virtual driver contracts. MpyConfig ``0be1460`` and the ``migrated`` trees contain historical callers. They were examined separately and are compatibility evidence, not additional active applications to modernize in this step. TEMField also implements its own measurement workflows using ``Measure``; it is not simply a TEMCell GUI. Dependencies beyond ordinary imports ------------------------------------ * ``tools.mgraph.MGraph.CreateDevices`` builds ``mpylab.device.`` names from INI configuration and also loads custom driver files. This is a real graph-to-device dependency absent from the static module-pair counts. * ``env.script_cli`` and ``env.qt_runner`` load user configuration modules. Device windows select driver modules dynamically. Limit discovery/registry also uses dynamic imports. These extension mechanisms need explicit ownership. * ``env.Measure.MpyLabCompatUnpickler`` maps historical ``mpy`` paths, then resolves stored classes. Moving a class or removing an old module can break saved measurements and PExplorer. Preserve old import paths and test real restoration before moving persisted types. No measurement files were opened for this analysis. * ``device.pm_rxatt`` contains a demonstration that loads an external switch script by absolute path. This is demonstration/configuration coupling, not an automatic dependency of every power-meter import. Do not execute it as an import-graph probe. Proposed internal ownership --------------------------- Keep one distribution and the current public import paths. Define logical ownership first; existing directory-wide rules would be too coarse. .. graphviz:: digraph proposed_boundaries { rankdir=LR; node [shape=box]; "Qt adapters / applications" -> "TEM / MSC / amplifier workflows"; "Qt adapters / applications" -> "Limits calculation"; "TEM / MSC / amplifier workflows" -> "Measurement lifecycle / EUT / UI contracts"; "TEM / MSC / amplifier workflows" -> "Graph / configuration"; "TEM / MSC / amplifier workflows" -> "Limits calculation" [label="where needed"]; "Graph / configuration" -> "Device contracts / drivers / transports"; "Measurement lifecycle / EUT / UI contracts" -> "Graph / configuration"; "Device contracts / drivers / transports" -> "Quantities / conversion / numerics"; "Limits calculation" -> "Quantities / conversion / numerics"; } The diagram is a target dependency direction, not a complete current call graph. Qt-free application identity and shared report metadata are supporting services. Measurement UI contracts remain Qt-free. Amplifier protection, RF-off and input limits stay shared safety responsibilities; an amplifier-test workflow does not own all amplifier safety. TEM-specific calculations in ``tools`` may retain their paths until a separately reviewed ownership decision. Recommended next increments --------------------------- 1. Remove the launcher/parser cycle without changing CLI help, version handling or optional-Qt behavior. This is the smallest independently reviewable change. 2. Establish a neutral report-metadata service and canonical imports for shared helpers. Keep compatibility exports and test existing report defaults and application consumers; do not alter report formats while relocating ownership. 3. Put TEM interactive review behind a Qt-free adapter contract. Retain the existing interactive decisions, interruption and persistence semantics. 4. Classify mixed driver launchers, CLI tools and facades explicitly, then add import-boundary checks for the agreed logical areas. Static checks must include local imports and be complemented by Qt-free runtime checks and configuration/pickle compatibility tests. Add these checks in a later step. There is no justification here for splitting repositories or publishing new packages. The concrete follow-up is the launcher/parser boundary; larger moves should follow only after reviewing these findings. First increment: explorer argument boundary ------------------------------------------- The launcher/parser cycle identified in the baseline above has now been removed. ``gui.limit_explorer_cli`` owns the program name and argument parsing and imports neither the launcher, the explorer nor Qt. The console launcher validates arguments once, checks Qt availability, then calls the internal window runner. The existing ``limits.limits_ui_main.main(argv=None)`` validates arguments through the same CLI module and calls that runner. There is no import back to the launcher. The console command, direct module entry point, help/version text and Qt exit code are retained. Console help/version and invalid-argument handling still work without Qt; an actual GUI request explains the ``mpylab[gui]`` extra when absent. This does not extend Qt-free behavior to the old direct GUI module import. Window creation, branding and event-loop ownership are unchanged. No domain, driver, limit calculation or persisted type was moved. Second increment: shared report profiles ---------------------------------------- ``reporting.profile`` now owns profile validation, merging, origin tracking and metadata tables. It has no domain/device/Qt imports. The TEM history writer and the Shielding Effectiveness feature branch use this neutral service. The latter requires mpylab 1.0.33 and must wait for that release before merging/publishing. ``env.tem.report_profile`` remains an explicit compatibility facade with the same exported objects. Profile/error pickle globals retain their historical module path; attributes, defaults, errors, logo policy and TSV output are unchanged. The generic implementation retains the historical fallback title; Shielding continues to supply its own title and PDF-logo opt-in. Third increment: interchangeable sweep review --------------------------------------------- TEMCell and the emission script now use the Qt-free sweep-review adapter contract. Adapters return decisions and evaluation proposals from detached data; the workflow validates and commits metadata/selections. Existing Qt dialogs remain behind an adapter. The terminal adapter saves the same curves as PNGs and explicitly asks for repeat/continue/finish/save-exit and evaluation sets. Auto mode falls back to terminal when Qt/display is absent; unavailable input fails before device creation. EOF/Ctrl-C retains pending data for resume. Adapters remain invocation-local and are not added to measurement pickles. The plot helper is Qt-free; historical private plotting imports remain aliases. See :doc:`../results/tem-emission-data` for configuration and operator steps. Enforced direct-import rules ---------------------------- The quality job now runs the stdlib-only checker:: python tools/check_import_boundaries.py Rules are defined explicitly in ``tools/check_import_boundaries.py``. They cover 49 modules at introduction; the count changes as modules are added. No runtime module is imported or hardware accessed by the checker. .. list-table:: Enforced scope :header-rows: 1 :widths: 25 35 40 * - Rule - Selected sources - Forbidden direct dependencies * - ``report-profile`` - ``reporting.profile`` - ``env``, ``device``, ``gui``, Qt bindings/backends * - ``sweep-contract`` - ``env.ui.sweep_review`` - TEM/MSC/amplifier domains, devices, GUI and concrete review adapters * - ``tem-workflow-ui`` - ``env.tem.TEMCell`` - Qt bindings/backends and concrete Qt frontends, including ``emission_sweep_ui`` * - ``limit-calculation`` - ``limits`` initializer, ``limit``, ``registry``, ``fcc_ism`` and both profile packages - Qt bindings/backends, ``gui``, ``device``, ``env`` and the three explorer modules * - ``limit-cli`` - ``gui.limit_explorer_cli`` - Launcher, limits/explorer, devices, ``env`` and concrete Qt frontends * - ``tem-domain`` / ``msc-domain`` / ``amplifier-domain`` - Respective ``env.tem`` / ``env.msc`` / ``env.univers`` package trees - Either of the other two domain trees Qt targets include PySide2/6, PyQt5/6, Shiboken2/6 and the explicitly listed Matplotlib Qt adapters. Exact source-module selectors do not implicitly include children; source-package selectors include future child modules. Forbidden prefixes match complete name components, so ``temporal`` does not match ``tem``. Imports inside functions, classes, conditional/try blocks and TYPE_CHECKING are checked. Relative imports and ``from parent import child`` are resolved syntactically, without executing Python or looking up installed packages. Violations report file, line, target, rule ID and reason. Exit status is 0 for success, 1 for a boundary violation and 2 for unreadable/invalid selected source or a missing source root. A rule matching no source module also fails with 2, preventing an empty scan from passing silently. ``--source-root`` accepts a package directory named ``mpylab`` for isolated checks. This is not a full dependency or cycle analysis. It does not follow re-exports, parent-package side effects, aliases assigned at runtime, dynamic import calls, or runtime Matplotlib backend selection. Wildcards only expose their named base, not unknown re-exported children. MGraph/custom-driver/configuration/limit loading and pickle globals remain runtime contracts. Compatibility facades and historical module-path strings remain valid; there is no blanket ban on imports from ``tools`` to devices or measurement code and no legacy exception baseline. Rule changes need an explicit scope/reason and corresponding tests/documentation; do not add a suppression simply to hide a new dependency. The installed-core job without Qt and the existing configuration/pickle tests remain required.