# 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 the reference site, several Telesis part markers serve one operation number:
0613, 0615 and PRTMK01 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
