Files
shopdb-flask/docs/CONTRACT-STABILITY.md
cproudlock 3324dbd91e
Some checks failed
CI / backend (push) Failing after 9s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 7s
Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint
changed size, and machines moved, so a position now records WHICH DRAWING its
coordinates belong to.

Buildings and levels (ADR-017). Each level owns its blueprint per theme and its
own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the
site. A position whose level is unknown renders "level unknown" and is never
drawn on the default level, because a marker on the wrong floor plan looks
entirely correct while pointing at the wrong place.

Repositioning in bulk: filter by unplaced, needs-review or level, search, place,
confirm. Landmark recalibration solves the transform PER AXIS from landmark
pairs and never from image dimensions - the canvas grew taller without
rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong
everywhere. It defaults to a dry run, reports what would land off the drawing,
snapshots before applying, and clears mapverifiedat because a transform is a
guess awaiting review. Snapshots restore, including the level and the review
state, and a restore snapshots first so an undo is undoable.

Search: gaugelabreference was matched only for measuring tools and
maintenancereference was matched nowhere at all, for any asset type, while
Settings happily offers both identifiers on machines and PCs. A tag an operator
is told to record has to be findable or it is a write-only field. USB devices
and printed items were unreachable from search entirely - neither is an asset,
so the generic asset search could not see them and no searcher existed; they
now match on serial, asset tag, label, bin code and gage-lab tag, honouring
isactive, with Settings toggles and result labels to match.

The retired-application rule was half a rule: GET /api/knowledgebase hid
articles whose topic application is retired while global search still returned
them and printed the retired application as the subject. A filter is only real
if every path that reaches the row applies it.

Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location
gained levelid, and resolve_asset_position returns the levelid belonging to
whichever source supplied the coordinates. The five plugins that write a map
position are re-pinned. The install-list text format gained levelid as a NINTH
field, appended, because the shipped Pascal installer reads fields 0-7 by index.

That installer still compiles in one drawing's dimensions and bundles one
blueprint, so its map is accurate for the default level only; /api/maplevels is
deliberately unauthenticated so it can read both at runtime once rebuilt.
Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps.

Migration 7d33 converts an existing single-map site into one building and one
default level carrying the old map_* settings, then assigns every placed asset
and location to it. Nothing moves on screen. Old settings rows are kept so a
rollback still finds them. Verified end to end on MySQL 5.6 from a
production-shaped database.
2026-08-17 12:55:51 -04:00

11 KiB

Path to contract 1.0

This page is an honest read of how stable the plugin contract is today, for a sister site deciding how much to build on it. It is derived from the ADRs and the live code, not aspiration. The authoritative hook reference is PLUGIN-HOOKS.md; the versioning rules are in ADR-002.

Current version

The plugin contract version is declared in shopdb/__init__.py as __contract_version__, and the current value is in PROJECT-MAP.md, which is generated. It is pre-1.0, which under semver means any 0.x minor bump is allowed to break the contract, and this project has used that latitude - see 0.19.0 below.

The product release version (__version__) is a separate series with its own bump rules; see ADR-007. Do not pin against it for compatibility - pin against __contract_version__.

0.x history

Recorded in the comment block in shopdb/__init__.py:

Version Change Kind
0.3.0 shopdb.api expanded to the full plugin import surface (db, cache, model bases, core models, response + pagination helpers, employee_connection) so plugins stop importing internal core paths additive (minor)
0.4.0 Removed the never-implemented get_searchable_fields hook (search is a core concern over the asset model) and wired get_dashboard_widgets to a real consumer (/api/dashboard/widgets) pre-1.0 contract reduction
0.6.0 Added the get_reports hook, consumed by GET /api/reports to merge plugin report cards into the Reports hub additive optional hook (minor)
0.7.0 Added the four ADR-010 frontend-contribution hooks (get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation), consumed by the GET /api/pluginui/* endpoints additive optional hooks (minor)
0.9.0 Exposed the dualpath pair-resolution helpers on shopdb.api for the machines plugin additive surface (minor)
0.10.0 Added the get_permissions hook so plugins declare their own RBAC permissions; the catalog is resolved dynamically from core + enabled plugins additive optional hook (minor)
0.11.0 Added service_token_authorized(scope) to shopdb.api so a plugin's unattended endpoints (e.g. the GE-Enforce fetch API) can authorize a scoped managed service token without importing core token internals additive surface (minor)
0.12.0 Added the mailer helpers (send_email, send_alert) to shopdb.api additive surface (minor)
0.13.0 Added the User model to the shopdb.api surface additive surface (minor)
0.14.0 Added send_webhook to the shopdb.api surface additive surface (minor)
0.15.0 Added authorized_service_token / the SupportTeam model to the shopdb.api surface additive surface (minor)
0.16.0 Added the get_settings_defaults hook so a plugin declares the Setting rows it owns; the framework seeds them at install, at enable, and on flask plugin upgrade-all, files a first-time write under the declared category, and honours public: True for pages that render before login additive optional hook (minor)
0.17.0 Added DashboardDefault to the shopdb.api surface, so a plugin can resolve a display without reaching into core additive surface (minor)
0.18.0 Added DISPLAY_ROLES, DISPLAY_ROLE_PATHS and normalize_display_role, and finally exported DashboardDefault, which 0.17.0 imported but left out of __all__. The role vocabulary became the kiosk's own - Dashboard, Lobby, 3DPrintRoom - so a plugin holding its own copy of that map reads core's instead of drifting from it additive surface (minor)
0.19.0 BREAKING. get_dashboard_widgets returns DATA AND SHAPE, not a component name. The old shape (name + component + size) named a Vue component per widget, which cannot survive a lean build - a plugin's component may never be staged into the frontend bundle (ADR-013) - and five plugins were declaring widgets that pointed at components nobody had written. A card now declares id / title / endpoint / render / severity / permission / empty / position; see PLUGIN-HOOKS.md. Also added send_upload so a plugin serving user-supplied bytes gets the headers that keep an SVG from running as script contract change (minor, pre-1.0)
0.20.0 The Asset model gained levelid (which floor plan its mapx/mapy are pixels of) and mapverifiedat; Location gained levelid too, and resolve_asset_position now returns the levelid belonging to whichever source supplied the coordinates. A plugin that writes a map position MUST write the level with it - a position without one renders as "level unknown" rather than being drawn on the default drawing, because a marker on the wrong floor plan looks correct (ADR-017) additive surface (minor)

The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0, 0.11.0, 0.16.0, 0.18.0 and 0.19.0; 0.12.0 through 0.15.0 and 0.17.0 are recorded in this table only.

0.19.0 is the one to read before pinning. It is the only entry in this series that took something away, and it shipped before it was recorded here - which is precisely the reason to pin tight rather than to trust that a minor bump is safe. Earlier points (0.1.x / 0.2.x) predate that recorded rationale; PluginMeta's fallback core_version default of >=0.2.0,<1.0.0 is the only remaining trace of the 0.2 baseline.

Settled surface

These are unlikely to break before 1.0. A plugin can depend on them with reasonable confidence; a breaking change to any would be a major bump and would land with a new or amended ADR.

Surface What it is
meta / manifest schema PluginMeta fields and manifest.json (ADR-002 single source of truth)
get_blueprint Flask Blueprint registration at the manifest api_prefix
get_models SQLAlchemy model classes the plugin owns
init_app Custom init after blueprint + models are known
get_cli_commands Click commands added to the Flask CLI
get_services Named service classes, consumed by plugin_manager.get_service
Lifecycle hooks on_install, on_uninstall, on_enable, on_disable
get_navigation_items Sidebar menu entries
get_dashboard_widgets Dashboard widgets, consumed by /api/dashboard/widgets
get_reports Report cards, consumed by /api/reports (added 0.6.0)
get_permissions Plugin RBAC permissions, merged into the catalog for roles and token scopes (added 0.10.0)
Frontend-contribution hooks get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation, consumed by /api/pluginui/* (added 0.7.0, ADR-010)
Collector pair get_collector_schema + apply_collector_payload per ADR-006
Settings helpers get_setting / set_setting, namespaced to the plugin
get_provisioning_note Setup-wizard transparency note for extra tables
get_config_schema Setup-wizard config field declarations
shopdb.api import surface The only core module plugins may import (plus shopdb.plugins.base); adding a name is minor, removing one is major

The asset model itself - Asset, AssetType, AssetStatus, AssetRelationship, and the shared reference models - is the platform contract locked in ADR-001.

Expected churn before 1.0

Known-unstable areas. Building on these means expecting rework.

Area Status Reference
Frontend renderers (residual) The four data-only hooks and their /api/pluginui/* consumers are settled (0.7.0). The generic core renderers are landing incrementally: the settings-cards rail/landing renderer ships with 0.7.0; the asset-panel, map-overlay, and search-presentation renderers are wired opt-in per the ADR adoption plan. Real component-backed panels (bespoke charts, custom overlays) remain deferred to Option C (build-time glob discovery). ADR-010 (ACCEPTED)
Per-plugin migrations Brand new. The per-plugin Alembic engine exists and every bundled plugin now carries a chain, but the pattern has one release of production mileage, not years. ADR-008 (2026-07-10)
Pip distribution Deferred to v2. External plugins install by clone / submodule / symlink; there is no entry-point discovery and no automatic update path yet. ADR-003

Bump rules

From ADR-002, applied to __contract_version__:

Bump Trigger
major Breaking change to the BasePlugin ABC, the PluginMeta schema, or any model in the platform contract (Asset, AssetType, AssetStatus, AssetRelationship, Vendor, Location, BusinessUnit, Model, OperatingSystem). Removing a name from shopdb.api is major.
minor Additive change: a new optional hook, a new field on a contract model with a default, a new name added to shopdb.api.
patch Bug fix with no change to the contract surface.

Deprecation policy

ADR-002 defines the bump classification above but is silent on any deprecation window or notice period for pre-1.0 removals. In practice a removal is simply a major (or, pre-1.0, a breaking minor) bump: the hook or name is gone, and the loader fails loud in dev or excludes the mismatched plugin in prod (see the load-time table in PLUGIN-EXTERNAL-REPO.md). Because the ADR says nothing about a grace period, this document proposes none; the honest guidance for sister sites is the mitigation that already exists - pin a tight core_version range and re-test before widening it.

Criteria for declaring 1.0

Provisional. This is the maintainer's working list, not a committed checklist, and it will move. ADR-002's own open question ("When does the framework declare 1.0.0?") ties 1.0 to the Machine retirement from ADR-001 and the framework being "ready for sister sites"; the items below expand that intent.

  • Frontend hook contract defined via its own ADR (asset-detail panels, map markers, search results), closing the biggest churn item above.
  • Machine / legacy-model retirement complete per ADR-001, so the asset model is the only contract.
  • At least two external plugins running in production at a second site - the bar ADR-003 already sets before pip distribution and sister-site readiness are considered justified.
  • The per-plugin migration pattern (ADR-008) proven across real upgrades, not just fresh installs.
  • No contract bump needed for N consecutive product releases (N to be fixed when the list is firmed up), showing the surface has actually settled.

Until then: pre-1.0, pin tight, re-test on every framework bump.