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:
graph TD
CLI["CLI layer<br/>cli.py · context.py"]
CHECK["Check engine<br/>commands/check.py · authorship.py · semver.py · report.py"]
DOCS["Docs builder<br/>commands/docs/*"]
SCAFFOLD["Scaffolding<br/>commands/init.py · update.py · field_test.py · mock_test.py"]
LICENSE["Licensing<br/>commands/license.py"]
MANUAL["Manuals<br/>commands/manual.py"]
ASSETS["Packaged assets<br/>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 |
|
Click command group; resolves |
Check engine |
|
The compliance gates behind |
Docs builder |
|
Sphinx configuration and build ( |
Scaffolding |
|
Project generation from the copier template ( |
Licensing and manuals |
|
SPDX header maintenance via the REUSE tool, and Markdown/PDF manual generation for external (non-hub) device packages. |
Packaged assets |
|
Data shipped inside the wheel: the canonical |
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— compliance gates and standard resolutionplesty init— project scaffolding and naming conventionsplesty docs— documentation preview and deploymentplesty license— SPDX headers and authorship currencyplesty manual— external device manualsplesty update— 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 |
|---|---|---|
|
|
Pins the compliance level ( |
|
|
One of |
|
|
Docs site title; defaults to the project name. |
|
|
Documentation source directory; defaults to |
|
|
External TOC file relative to the docs dir; defaults to |
|
|
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=truewithoutCI_PIPELINE_SOURCE=merge_request_event) gate 11 reports itself skipped, because the dedicatedhub-deploy-docsCI job builds the docs instead. Tests exercising the Sphinx path must therefore clear theCIvariable.[Device]/[Analyzer]/[Experiment](d1–d3, a1, e1–e3) — module-type gates, active only when[tool.plesty] module_typeis"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 eightDevicePipelinemock tests, d2 the telemetry return-annotation contract, d3 that the device renders a populateddoc_model(); a1 verifies the fiveAnalyzerPipelinecontract tests; e1 verifies the fiveExperimentPipelinecontract tests, e2 that persistence goes through the unified data layer, e3 thatpython -m <module> --manifestpublishes the run contract and that its configuration schema and the module’s ownconfig/*.yamlsay 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 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 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/<module>/ 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.<module> 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 |
|---|---|
|
CLI framework — command group, options, context passing, error reporting. |
|
Canonical linter and formatter enforced by gate 2 (shipped so the SDK pins the toolchain, not the user). |
|
Static type checking in gate 2. |
|
Test runner and coverage measurement for gates 7, d1, e1. |
|
SPDX header maintenance and REUSE compliance linting (gate 9, |
|
Template engine driving |
|
Documentation build engine (gate 11, |
|
Markdown (MyST) source support for Sphinx. |
|
Generated API reference from the project’s packages. |
|
|
|
The single documentation theme for all modules. |
|
Badges/shields used by the generated device reference pages. |
|
Copy buttons on code blocks. |
|
Mermaid diagrams in docs (including this page). |
|
Local preview server and rebuild-on-change for |
|
PDF rendering for |
|
Static public-API extraction for the gate 8 diff — the analyzed package is never imported. |
|
Version and specifier parsing (gate 8 baselines, |
|
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
— per the
governance DoD,
permanent architectural debt is filed before the shortcut lands.