Files
shopdb-flask/docs/ASSET-COMPOSITION.md
cproudlock bf75cdc43b
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
docs: keep real fleet hostnames out of a page that publishes to the wiki
The check-shared-machines example carried two actual prod hostnames. docs/ is
excluded from the code bundle and goes to the GitHub wiki, so an example is the
wrong place for live fleet identifiers. Replaced with placeholders.
2026-08-11 12:10:04 -04:00

155 lines
6.4 KiB
Markdown

# Composition: several devices under one parent
When more than one physical device answers to a single identifier, model each
device as its own asset and file it under a parent. This page is the pattern:
when to reach for it, what you get for free, and how to make the collector do
it without writing code.
The mechanism is not new - `partof` and its propagation rails are the ADR-001
platform contract. What is new is the recipe.
## The problem it solves
A machine number is supposed to identify one thing. Sometimes it does not.
At West Jefferson, several Telesis part markers serve one operation number:
0613, 0615 and WJPRT each have more than one. Their configurations differ, most
often by COM port. Treating the operation as the device collapsed them into a
single record, and the damage was quiet:
- Their backups overwrote each other, so the history read as one device
flip-flopping between configurations that were really two devices.
- No question about an individual device could be asked at all - how many there
are, which port one is on, which one failed.
- Every PC driving one contested the single `controls` link to the operation,
so whichever reported last appeared to own it.
None of that announced itself. It surfaced as duplicate backup rows, weeks
later.
## When to use it
Reach for composition when ALL of these hold:
- Several physical devices share one identifier, and that is CORRECT - not a
numbering mistake. Two PCs carrying the same machine number by accident is a
fault to fix on the PC, not a model to build. `flask relationships
check-shared-machines` tells the two apart.
- The devices are individually interesting: they fail, get replaced, carry
their own configuration or calibration.
- Something already identifies each device. One device per PC is the easy case,
because the PC names it.
Do NOT reach for it when the parent is simply a location. A room holding six
printers wants a Location, not a parent asset.
## What you get
Filing a device `partof` a parent buys behaviour already built:
- **Control propagates.** `controls` propagates through `partof` (ADR-001, and
seeded by `flask seed reference-data`). A PC controlling a device therefore
controls its parent by inheritance, so the PC does NOT need - and must not
have - a direct link to the parent. That is what stops several devices
contesting a link only one can hold.
- **Position inherits.** Map-position resolution walks `partof` first, so a
device with no coordinates of its own shows at its parent's position.
- **History separates.** Anything keyed on the asset - backup revisions,
relationships, notes, audit - is per device instead of merged.
## Making the collector do it
A PC that drives a subordinate device declares it in
`plugins/computers/pctypemap.py`:
```python
SUBORDINATE_DEVICE_MAP = {
'gea-shopfloor-partmarker': {
'assettype': 'machine', # core AssetType for the device
'typename': 'Part Marker', # type within that vocabulary
'description': 'Telesis part marker',
'suffix': 'PARTMARKER', # device asset number = <PC>-<suffix>
'partof': True, # file under the reported machine number
'label': 'collector:partmarker',
},
}
```
`partof: True` is the switch that matters. It files the device under the
operation the PC reports AND stops that PC claiming the operation directly.
Leave it False for a device that does not share an identifier - a CMM is a
subordinate device too, but no two CMMs answer to one number, so it links to
its PC and nothing more.
`label` is the relationship origin marker. Only rows carrying it are archived
by a collector push, so links made by hand are never touched. **Do not change
an existing label**: those exact strings are in production databases.
A site adds or retargets an entry without a code change, per ADR-015:
```
Setting: subordinatedevice_<pctype> category: pctypemapping
Value: {"assettype": "machine", "typename": "Marking Laser",
"suffix": "LASER", "partof": true, "label": "collector:partmarker"}
```
A malformed override falls back to the built-in default rather than failing the
collector push - a bad setting must not stop a bay reporting its inventory.
## Making a backup kind follow the device
A backup posted by a PC that drives a device belongs to the DEVICE, not the
parent. `plugins/backups/services/registry.py` resolves this through
`markerforsource`: it finds the PC by the reported `sourcehostname`, follows the
PC's active device link, and files against what it finds. It falls back to the
machine number whenever the device cannot be resolved - no hostname on the
payload, a lean build without the computers plugin, or a PC that has not
reported yet - because filing under the parent beats rejecting a backup.
Two things that matter if you add a kind:
- **`sourcehostname` is load-bearing**, not informational. Without it a backup
files against the parent and merges two devices' histories.
- **A revision chain is (asset, kind, source hostname)**, so two devices on one
parent keep separate chains even before they become separate assets.
## Finding the next one
```
flask relationships check-shared-machines
```
Read-only. Lists every machine number claimed by more than one PC and separates
the two cases by whether child assets exist:
```
0615 11 PCs, 11 child asset(s) - modelled
2026 2 PCs, NO child assets - PCONE, PCTWO
```
The first is composition working. The second is two PCs carrying one number -
fix that on the PC.
## The parent is not disposable
Once devices are filed under it, the parent is doing a real job even though it
may look empty: it is the thing that says those devices belong together.
Deactivating it (`isactive = 0`, which is what "delete" does in the UI) breaks
filing, because the resolver requires an ACTIVE parent - every subsequent
report warns and files nothing.
A hard SQL `DELETE` is worse: `backuprevisions.assetid` is `ON DELETE CASCADE`,
so it destroys history still attached to the parent.
If the parent looks wrong in a list - an operation number appearing among
machines - that is a CLASSIFICATION question, not a deletion one.
## See also
- `docs/adr/ADR-001-asset-as-platform-contract.md` - relationship types,
propagation rails, position inheritance
- `docs/adr/ADR-015-site-specific-configuration.md` - why the device map is
settings-overridable
- `docs/COLLECTOR-INTEGRATION.md` - the collector contract and the part-marker
case end to end