Architectural pass from the skill review ("plugin is the product" boundary).
Core no longer imports plugin models at module load (was a hard import-time
dependency that broke core if the computers plugin was absent/disabled):
- collector.py, applications.py, reports.py: lazy + guarded imports of the
computers plugin models. Endpoints that need install-tracking now return 503
when the plugin is absent instead of failing at import.
Search honors runtime enable/disable:
- search.py: _require_enabled(name) raises ImportError for a disabled plugin,
so each plugin-scoped block skips it (a disabled plugin's rows leave search).
- Replace hardcoded root/rootpassword employee-DB connection in _search_employees
with the shared env-backed employee_connection helper.
Plugin hooks (integrating-plugin-hooks: every hook needs a consumer):
- get_dashboard_widgets: add the consumer GET /api/dashboard/widgets (5 plugins
already implemented the hook; it had none). Skips disabled, isolates in prod.
- get_searchable_fields: REMOVED. Zero plugins implemented it and there was no
consumer; global search is a core concern over the asset model. Contract
reduction, __contract_version__ 0.3.0 -> 0.4.0.
Docs/contract: PLUGIN-HOOKS.md (widgets consumer note, searchable-fields removal,
0.4.0), PLUGIN-QUICKSTART.md, ADR-001 hook list. Tests: widgets endpoint
aggregate + disabled-skip; contract tests for the removed/added hooks.
151 tests pass, naming/style green, app boots all 6 plugins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.5 KiB
ADR-001: Asset model is the platform contract
- Status: ACCEPTED
- Date: 2026-05-08
- Deciders: cproudlock
- Supersedes: none
Context
shopdb-flask is being shaped as a framework that sister GE Aerospace facilities can adopt. The framework defines a stable core; sites install plugins for the asset classes they care about (equipment, computers, printers, measuring tools, network gear, etc.).
The codebase ran two parallel object models:
- Legacy
Machinemodel inshopdb/core/models/machine.py. Original schema inherited from the classic-ASP shopdb. Tables:machines,pctypes,machinetypes. Plugins like printers stored extension data viaPrinterDatakeyed bymachineid. - New
Assetmodel inshopdb/core/models/asset.py. Generic asset abstraction withAssetType,AssetStatus,AssetRelationship. Plugins also exposed asset-based tables keyed byassetid.
For the framework to be adoptable, the platform contract has to be one model, documented, versioned, and stable.
Decision
Asset is the platform contract. Plugin authors target the asset-based API. The Machine model and its dependents (PCType, MachineType, PrinterData keyed by machineid) are legacy and will be retired through a tracked migration.
Platform contract surface
The following are the public, versioned surface. Plugin authors may depend on them. Breaking changes require a major version bump per ADR-002.
Models
Asset(core entity)AssetType(asset classification, registered by plugins)AssetStatus(active / inactive / decommissioned / retired)AssetRelationship+RelationshipType(cross-plugin links)Vendor,Location,LocationType,BusinessUnit,Model,OperatingSystem(shared reference data)
Helpers
AuditLogAPI:audit_log(action, entitytype, entityid, ...)for plugins to record audit entries with consistent schemaSettingAPI:plugin.get_setting(key)andplugin.set_setting(key, value)for plugin-scoped config persisted via the coreSettingmodelresolve_asset_position(asset)for the documented position-resolution algorithm
Import surface (shopdb.api) - expanded in contract_version 0.3.0
shopdb.api is the ONLY core module plugins may import (plus shopdb.plugins.base
for the ABC). Deep imports of shopdb.core.*, shopdb.extensions, or
shopdb.utils.* are contract violations enforced by the
test_plugins_only_import_contract_surface test. The surface re-exports:
- Infrastructure:
db,cache - Model bases:
BaseModel,AuditMixin - Core models:
Asset,AssetType,AssetStatus,Vendor,Model,Communication,CommunicationType,Location,Setting,AuditLog,Application,AppVersion,OperatingSystem - Responses:
success_response,error_response,paginated_response,ErrorCodes - Pagination:
get_pagination_params,paginate_query - Helpers:
audit_log,resolve_asset_position; legacyemployee_connection
Adding a name here is a minor (additive) contract change; removing one is major.
Plugin contract
BasePluginABC and its hooks (navigation, dashboard widgets, collector schema +apply_collector_payload). Note:get_searchable_fieldswas removed in contract 0.4.0 - global search is a core concern over the asset model, not a per-plugin hook.
Excluded from the contract for v1
- Event bus (
get_event_handlersremoved fromBasePlugin). Add later if a real use case appears.
Relationship types (seeded core values)
RelationshipType is seeded with three rows. Plugin authors do not add new types in v1.
| Type | Meaning | Boundary rule |
|---|---|---|
partof |
Composition, siblings, sub-assemblies | Parent dies without it |
controls |
One asset has operational authority over another | PC commands a machine, measuring tool, etc. |
connectedto |
Network or data link without operational authority | Cable, switch port, NAS mount |
AssetRelationship columns
AssetRelationship
- relationshipid PK
- sourceassetid FK to assets
- targetassetid FK to assets
- relationshiptypeid FK to relationshiptypes (one of partof, controls, connectedto)
- label text, free description (e.g., "ethernet PoE", "DNC feed")
- inheritsposition bool, true means resolved-position walk follows this edge
- propagatesthroughid FK to relationshiptypes, nullable, see propagation below
- notes text
- isactive bool
- createddate, modifieddate
Free-text label carries the domain nuance (controls with label "DNC feed" vs controls with label "operator workstation"). Avoids inflating the type list.
Sibling propagation
When a relationship is created or deleted, the framework checks RelationshipType.propagatesthroughid. If set, it finds all assets related to the source via the propagation type and applies the same change.
Default seeding:
| RelationshipType | propagatesthroughid |
|---|---|
partof |
null (the propagation rail itself) |
controls |
partof (controls relationships propagate across siblings) |
connectedto |
null (network paths don't propagate) |
Cycle protection: max walk depth 3, visited-set during traversal.
Position resolution
Resolved map position for any asset follows this priority chain:
- Asset-specific override (
assets.mapx,assets.mapyif non-null) - Walk relationships where
inheritsposition=true, ordered by relationship type priority (partoffirst, thencontrols), recursively resolve the related asset's position with cycle detection - Fall back to the asset's location coords (
locations.mapx,locations.mapyviaassets.locationid) - If none of the above, asset is "unplaced" and rendered in a tray, not on the map
API exposes resolved position with a source indicator: {"mapx": 234, "mapy": 567, "positionsource": "self|related|location|none"}.
Hierarchical locations
locations is extended with parentlocationid (self-FK, nullable). Sites define their own location tree (cells, sub-cells, network closets, meeting rooms, labs).
locationtypes lookup is added with seeded values: section, cell, subcell, meetingroom, lab, office, storage, hallway, networkcloset, building. Sites can extend.
Asset-to-location is the existing assets.locationid FK only (single primary location for v1). Multi-location and transient placement are deferred.
Migration scope (narrowed)
Only one class of legacy data migrates: physical manufacturing equipment with a machinenumber (5-axis mills, lathes, broachers, heat treatment, CMMs, etc.).
Migration filter:
SELECT * FROM legacy.machines
WHERE category = 'Equipment'
AND machinenumber IS NOT NULL
Rows without machinenumber: skipped. Add manually post-migration if any matter.
Migrated rows become assets with assettype='Equipment' plus a row in the equipment plugin's equipment table. Legacy machinetypeid is preserved on the equipment row to enable later reclassification (some "Equipment" rows are actually measuring tools, see ADR-005).
Skipped from migration:
- PCs (rebuilt via PXE collector pipeline, see ADR-006)
- Printers, USB devices, network gear, notifications, KB articles (operational or re-collectable, decision per class deferred until needed)
Consequences
Positive
- Plugin authors at sister sites have a single, documented contract to target
- Cross-plugin features (relationships, search, map) work uniformly
- Three relationship types are easy to learn; free-text label captures nuance
- Sibling propagation handles dual-path machines without code changes per use case
- Position resolution gives users one map without per-plugin map code
Negative
- Migration of equipment data must run cleanly;
migrating-asset-schemaskill owns the procedure - Frontend
views/machines/will be repointed or replaced; effort tracked in Phase 5 - Legacy
core/api/machines.py(641 LOC) becomes deprecated; deletion or shim deferred until equipment is migrated
Neutral
Machine,PCType,MachineType,PrinterDataremain in the schema until the migration completes, behind a deprecation notice. New code does not touch them.
Alternatives considered
- Keep both models indefinitely. Doubles test surface, confuses contract docs, breaks cross-plugin features. Rejected.
- Make
Machinethe contract; retireAsset.Machinehas shop-floor-specific assumptions that don't generalize. Rejected. - Define a higher abstraction above both. Yet another layer.
Assetis already the abstraction. Rejected. - More relationship types (
controls,operates,monitors,dncfeeds,mountedon,connectedvia,inspects,siblingbay). Eight types proved unwieldy. Collapsed to three with free-text labels and per-row inheritance/propagation flags.
Open questions deferred
- Lifecycle relationship type (
replaces,replacedby). Add when first needed. - Multi-location asset placement, transient placement (calibration trip, off-site repair). Add when first needed.
- Map editing UI (drag locations onto floor plan). Phase 6 polish.
- Plugin-extensible relationship types beyond the three seeded. Add when a real cross-plugin case can't be expressed via labels.
References
shopdb/core/models/asset.pyshopdb/core/models/machine.py(legacy, deprecated)shopdb/plugins/base.py- ADR-002 (versioning of the surface)
- ADR-003 (plugin distribution)
- ADR-004 (deployment topology)
- ADR-005 (equipment vs measuring tools split)
- ADR-006 (collector contract pattern)