# Architecture This page is the living architecture document for **plesty-sdk** — the command-line tool behind the `plesty` command. It records the module boundaries, the public contracts other PLESTY repositories rely on, the reasoning behind the major design decisions, the sanctioned dependency list, and the known technical debt. Keep it current: any change that moves a module boundary, alters a contract, or adds a dependency must update this page in the same merge request. ## High-level module view The SDK is a single distribution (`plesty-sdk`) providing one namespace package, `plesty.sdk`. Everything is reached through the `plesty` CLI — the package has no public Python API beyond the entry point (`plesty = "plesty.sdk.cli:app"`). Internally it separates into five areas: ```{mermaid} graph TD CLI["CLI layer
cli.py · context.py"] CHECK["Check engine
commands/check.py · authorship.py · semver.py · report.py"] DOCS["Docs builder
commands/docs/*"] SCAFFOLD["Scaffolding
commands/init.py · update.py · field_test.py · mock_test.py"] LICENSE["Licensing
commands/license.py"] MANUAL["Manuals
commands/manual.py"] ASSETS["Packaged assets
assets/*"] CLI --> CHECK CLI --> DOCS CLI --> SCAFFOLD CLI --> LICENSE CLI --> MANUAL CHECK -->|gate 11, gate d3| DOCS SCAFFOLD --> ASSETS CHECK --> ASSETS DOCS --> ASSETS LICENSE --> CHECK ``` | Area | Modules | Responsibility | |---|---|---| | CLI layer | `cli.py`, `context.py` | Click command group; resolves `--project-dir` into an `ApplicationContext` that lazily parses `pyproject.toml` and derives `DocumentationSettings`. Every command receives this context — no command parses `pyproject.toml` itself. | | Check engine | `commands/check.py`, `authorship.py`, `semver.py`, `report.py` | The compliance gates behind `plesty check`. Each gate is a plain function raising `click.ClickException` (or exiting with the failing tool's code) on violation. `authorship.py` holds the git-history authorship analysis shared with `plesty license author`; `semver.py` the public-API diff against the published release for gate 8; `report.py` the `reports/check.json` document every run writes. | | Docs builder | `commands/docs/` (`serve.py`, `deploy.py`, `sphinx_builder.py`, `generate.py`, `picker.py`) | Sphinx configuration and build (`sphinx_builder.py`), live-reload preview (`serve`), pages-branch deployment with versioned releases (`deploy`), zero-effort device reference-page generation from a live `doc_model()` (`generate.py`), and stamping the version picker into release trees built before it existed (`picker.py`). `cache.py` maintains the git-ignored `.plesty/` build cache used by `serve`. | | Scaffolding | `commands/init.py`, `commands/update.py`, `commands/field_test.py`, `commands/mock_test.py`, `tui.py` | Project generation from the copier template (`plesty init`) and in-place maintenance of existing projects (`plesty update`). `update` reuses `init`'s CI-generation and hook-installation helpers so both commands emit identical artifacts. `field_test.py` and `mock_test.py` generate a device module's field test and mock pipeline test (`plesty init field-test` / `mock-test`); `tui.py` is the dependency-free checkable list the field-test interview uses. | | Licensing and manuals | `commands/license.py`, `commands/manual.py` | SPDX header maintenance via the REUSE tool, and Markdown/PDF manual generation for external (non-hub) device packages. | | Packaged assets | `plesty/sdk/assets/` | Data shipped inside the wheel: the canonical `ruff.toml`, the copier scaffold under `templates/`, the shared Sphinx theme (`hub-theme/`), and the pages-branch CI file (`docs-build-gitlab-ci.yml`). Assets are always accessed through `importlib.resources`, never by path relative to the source tree. | Dependency direction is strictly downward: the CLI layer knows all command modules; command modules know only `context.py`, `authorship.py`/`cache.py` helpers, and assets. The check engine imports the docs builder lazily (inside gate 11) so `plesty check --standard pixel` never pays the Sphinx import cost. Nothing imports `cli.py`. ## Interface contracts ### CLI commands The `plesty` CLI is the SDK's public surface. Each command's options and behaviour are documented on its own page — those pages are the contract, and changes to them are breaking changes for the hub: - [`plesty check`](commands/check.md) — compliance gates and standard resolution - [`plesty init`](commands/init.md) — project scaffolding and naming conventions - [`plesty docs`](commands/docs.md) — documentation preview and deployment - [`plesty license`](commands/license.md) — SPDX headers and authorship currency - [`plesty manual`](commands/manual.md) — external device manuals - [`plesty update`](commands/update.md) — metadata, CI, and hook maintenance All commands honour the global `--project-dir` (`-d`) option; a command must never read state from the process working directory when a project directory is configured. ### The `[tool.plesty]` configuration contract Projects configure the SDK exclusively through `pyproject.toml`: | Key | Section | Meaning | |---|---|---| | `standard` | `[tool.plesty]` | Pins the compliance level (`pixel`/`nebula`/`quantum`). Optional — when absent the level is derived from the release version (see below). Core repos pin `quantum`. | | `module_type` | `[tool.plesty]` | One of `device`, `experiment`, `analyzer`, `core`. Optional; validated by gate 1 when present. Activates the module-type gates and device docs generation. | | `site-name` | `[tool.plesty.docs]` | Docs site title; defaults to the project name. | | `docs-dir` | `[tool.plesty.docs]` | Documentation source directory; defaults to `docs`. | | `toc-file` | `[tool.plesty.docs]` | External TOC file relative to the docs dir; defaults to `toc.yaml`, empty string disables it. | | `api-ignore` | `[tool.plesty.docs]` | Top-level package directories excluded from the generated API reference scan. | There is deliberately no `version_tag` configuration key: the version shown in deployed docs is derived from the installed package metadata by `plesty docs deploy --release`, so it can never drift from the actual release. ### The gate architecture `plesty check` runs a fixed, ordered list of gates. Gates 1–11 run locally; the same engine is re-run by CI (`hub-check` in the `plesty-standard-ci` component) as the authoritative final gate. Gates carry a tier label: - **`[SDK+CI]`** (gates 1–10) — enforced locally and repeated in CI. - **`[SDK]`** (gate 11, docs build) — runs locally and in merge-request pipelines. In a branch pipeline (`CI=true` without `CI_PIPELINE_SOURCE=merge_request_event`) gate 11 reports itself skipped, because the dedicated `hub-deploy-docs` CI job builds the docs instead. Tests exercising the Sphinx path must therefore clear the `CI` variable. - **`[Device]` / `[Analyzer]` / `[Experiment]`** (d1–d3, a1, e1–e3) — module-type gates, active only when `[tool.plesty] module_type` is `"device"`, `"analyzer"`, or `"experiment"` respectively; every other module type gets an explicit "N/A" line, so the gate list is identical everywhere and activation is declarative, never heuristic. Gate d1 verifies the eight `DevicePipeline` mock tests, d2 the telemetry return-annotation contract, d3 that the device renders a populated `doc_model()`; a1 verifies the five `AnalyzerPipeline` contract tests; e1 verifies the five `ExperimentPipeline` contract tests, e2 that persistence goes through the unified data layer, e3 that `python -m --manifest` publishes the run contract and that its configuration schema and the module's own `config/*.yaml` say the same thing. The standard selects how deep the run goes: `pixel` runs gates 1–2, `nebula` gates 1–6 (static, no network), `quantum` everything including the module-type gates. The full per-gate reference lives on the [`plesty check`](commands/check.md) page and is not duplicated here. ### The pre-push hook `plesty init` (and `plesty update --install-hook` for cloned projects) writes `.git/hooks/pre-push` containing a single call: `uv run plesty check`. The hook passes no `--standard`, so enforcement always follows the project's own standard resolution — pinning or releasing a version changes what the hook enforces without touching the hook. Gate 10 needs network access (OSV queries), so pushing requires connectivity under `quantum`. ### Version-derived default standard When `[tool.plesty] standard` is unset, `derive_standard()` maps the release version to a level, so shared prototypes ramp up automatically — pixel before `0.1.0`, nebula before `1.0.0`, quantum from then on. The mapping and its fallbacks are specified once, under [Standard resolution](commands/check.md#standard-resolution) on the `plesty check` page. `plesty check` echoes which rule applied, so the resolution is never silent. ## Design Decisions **Version-derived compliance standard, not a fixed default.** A fixed default forces a choice between blocking day-one prototypes (`quantum`) and under-enforcing mature modules (`pixel`). Deriving the level from the release version encodes the actual policy — rigour should grow with maturity — and makes ramp-up automatic at the moment a project tags `0.1.0` or `1.0.0`. The tradeoff is a strictness change that arrives "by surprise" with a release tag; this is mitigated by echoing the resolution reason on every run and by letting an explicit `[tool.plesty] standard` always win. **Licensing via the REUSE tool, not hand-rolled header checks.** The previous template-based header workflow (`licenseheaders` + `spdx.tmpl`) was our own code to maintain and could silently drift from the SPDX spec. Delegating to `reuse annotate` / `reuse lint` buys a community-maintained implementation of a published specification, machine-verifiable compliance in gate 9, and `--skip-existing` semantics that leave curated per-file contributor headers untouched. The cost is an extra runtime dependency and REUSE's opinionated file layout (`LICENSES/`, `REUSE.toml`), which every repo had to adopt once. **One standard Read-the-Docs theme for every module, dropping pydata for core.** Two themes meant two CSS surfaces, two logo/favicon pipelines, and a version switcher that only existed on one of them — for no reader benefit. Converging on `sphinx-rtd-theme` with the shared `hub-theme` assets gives every PLESTY docs site the same navigation, branding, and zero-warning build behaviour, and halves the surface gate 11 has to keep warning-free. The tradeoff was losing pydata's own version switcher; the theme carries its own instead — a version picker in the sidebar header (`plesty-versions.js`), fed by the deploy-side `versions.json`, which `plesty docs deploy` also stamps into releases built before it existed. **Schema-usage enforcement inside the data-layer gate.** plesty-lib defines the data every module exchanges — a device's configuration and operations, its status and telemetry, an experiment's plan and journal, a stored result — as a fixed set of classes (the *schema models*). A module that wrote its own class of the same name — its own `DeviceStatus`, say — would *shadow* the library's: code in that module would silently use the local copy, which no other module, console or reader understands. Gate 4 therefore scans every Python file under `plesty//` of **every** module type — device, analyzer, experiment and core alike — and fails on a class definition whose name is one of plesty-lib's schema models (`ConfigParameter`, `FuncMeta`, `DeviceStatus`, `TelemetryEvent`, `ResultDocument`, `Plan`, `Journal`, … — the full list is `_PROTECTED_SCHEMA_NAMES` in `commands/check.py`), and on a public function without a return type annotation. Two further checks apply to one module type each: gate d2 (devices) requires `status()` to be annotated `DeviceStatus`, and gate e2 (experiments) requires results to be stored through `save_result` rather than written by hand. The alternative — trusting convention and review — had already produced silent forks of the data contract. Reading the source as a syntax tree, without importing it, is cheap, runs offline in the pre-push hook, and cannot be fooled by a module that fails to import; the tradeoff is that such a check sees only what is written literally (a file mode computed at run time is deliberately left alone), so the gates are a floor, not a proof. **Generated device documentation (gate d3) over hand-written reference pages.** Hand-written parameter and operation tables drift the moment a schema changes. Rendering Usage/Parameters/Functions/Standard-Methods pages from the live `doc_model()` makes drift impossible and reduces the author's obligation to a single QuickStart page, while gate d3 keeps the property machine-checked (the device must construct hardware-free and render a populated model). The tradeoff is that generated prose is uniform rather than tailored, and device classes must keep `__init__` connection-free — an intentional constraint that also benefits testing. **Scaffolding a one-line CI component include, not a copied pipeline.** The generated `.gitlab-ci.yml` includes the `plesty-standard-ci` component with three inputs instead of copying the full job definitions. Pipeline fixes and new gates then reach every module by updating one component in plesty-ci, rather than by patching dozens of generated files that have already diverged. The cost is a coupling to the component's ref and input contract — a rename or ref change in plesty-ci breaks scaffolds (see technical debt below) — which we accept because centralised evolution has proven far cheaper than fleet-wide file surgery. **A generated device carries its own control panel, with Qt in an extra.** The panel is plesty-lib's `DevicePanel`, and it reaches a device only through a four-call interface, `DeviceHandle` — `describe()`, `query()`, `write()`, `call()`. Where the device is decides only which handle the panel is given: in the same process, the device object wrapped in a `LocalHandle`; over the network, a TCP client (`DeviceTCPIPClient`) satisfies the same interface; from a remote `plesty-bench`, the bench agent relays the same four calls. So the one panel drives a device locally or remotely alike. What the scaffold generates is the local case: `--gui` on the module's existing entry point opens the panel on the machine the instrument is plugged into, rather than shipping a separate console or leaving each module to hand-write one. `--gui` and serving are alternatives *within one process*: both open the instrument, and most instruments grant a single session, so `python -m plesty.` does one or the other. Both at once is the remote case — serve the device, and open the panel from a client on the same or another machine. The controls are built from `describe()`, the device's schema serialized as plain data, so a module gets a usable window the moment its schema is right, and a hole in that payload surfaces at the bench instead of only across a network. Operations are filtered by their declared `kind`, which makes the schema the single place that decides what a panel offers, local or remote. A module with a GUI of its own is not overridden by anything: `--gui` is ordinary generated code in the module's `__main__.py` (`open_panel()`), and the module replaces it — with its own `Panel` subclass, several panels in one window, or no panel — by editing that function. Nothing loads a module's panel code from elsewhere. Qt lives in a generated `gui` extra rather than in the generated `dependencies` — a headless bench serves the device and never opens a window — which is why `plesty.lib.ui` is imported inside the panel path and not at module scope. How far Qt reaches is deliberately bounded: the panel contract (`Panel`), the device handle, the worker thread that keeps device calls off the window's thread, and the `describe()` payload import no toolkit; Qt is confined to `plesty.lib.ui.qt` and to the widget each panel builds. Moving to another toolkit, or offering a second kind of interface such as a web page, would therefore mean new widget code and a new shell, while the data a panel renders and the way it reaches a device stay as they are. The tradeoff is that this widget code is Qt today, and a second toolkit would be a second implementation of it rather than a switch. **`plesty update --install-hook` for cloned projects.** Git hooks live in `.git/` and are never cloned, so `plesty init`'s pre-push hook silently vanishes for every contributor after the first. Rather than adopting a hook-manager dependency (pre-commit) or committing wrapper scripts, `plesty update --install-hook` re-creates the identical hook on demand and is cheap to document as a one-time setup step. The tradeoff is that it remains opt-in per clone — CI stays the authoritative backstop for contributors who skip it. ## Allowed dependencies The sanctioned runtime dependencies (from `[project] dependencies` in `pyproject.toml`): | Package | Why it is here | |---|---| | `click` | CLI framework — command group, options, context passing, error reporting. | | `ruff` | Canonical linter and formatter enforced by gate 2 (shipped so the SDK pins the toolchain, not the user). | | `mypy` | Static type checking in gate 2. | | `pytest` / `pytest-cov` | Test runner and coverage measurement for gates 7, d1, e1. | | `reuse` | SPDX header maintenance and REUSE compliance linting (gate 9, `plesty license`). | | `copier` | Template engine driving `plesty init` scaffolds. | | `sphinx` | Documentation build engine (gate 11, `plesty docs`). | | `myst-parser` | Markdown (MyST) source support for Sphinx. | | `sphinx-autoapi` | Generated API reference from the project's packages. | | `sphinx-external-toc` | `toc.yaml`-driven navigation. | | `sphinx-rtd-theme` | The single documentation theme for all modules. | | `sphinx-design` | Badges/shields used by the generated device reference pages. | | `sphinx-copybutton` | Copy buttons on code blocks. | | `sphinxcontrib-mermaid` | Mermaid diagrams in docs (including this page). | | `livereload` / `watchfiles` | Local preview server and rebuild-on-change for `plesty docs serve`. | | `md2pdf` | PDF rendering for `plesty manual generate --with-pdf`. | | `griffe` | Static public-API extraction for the gate 8 diff — the analyzed package is never imported. | | `packaging` | Version and specifier parsing (gate 8 baselines, `--lib-version`, versioned docs deploy). | | `pyside6-essentials` (extra: `gui`) | Optional GUI support; not installed by default. | **Policy:** a new third-party library may only be added if it is actively maintained (released or meaningfully committed to within the last two years), it carries a license compatible with `LGPL-3.0-or-later`, and it is added to this table with a one-line justification in the same merge request. Exact pins (`==`) are forbidden by our own gate 6; use `>=x.y` lower bounds. ## Technical debt Technical debt lives in the issue tracker, not in this document: every debt item is filed as a GitLab issue labeled [`type::debt`](https://gitlab.com/plesty/core/plesty-sdk/-/issues/?label_name%5B%5D=type%3A%3Adebt) — per the [governance DoD](https://gitlab.com/plesty/governance/-/blob/main/CONTRIBUTING.md), permanent architectural debt is filed before the shortcut lands.