Skip to content

Device Discovery

Rules for adapters that discover a device's configured topology over the live connection (the config-import / Discover(...) flow).

Part of compliance-rules.

These rules are the review-side enforcement of the authoring contract in patterns/14-device-config-import.md (see especially its Reimport Reconciliation Contract) and the modeling recommendation MODEL-014. They must never contradict those documents — when in doubt, the contract text there is authoritative and these rules only tell the reviewer how to flag deviations from it.

Mental model (repeated here so a finding can cite it): discovery is authoritative by default. The discovered tree is the source of truth — anything the protocol reports overwrites what was stored, and anything the protocol no longer reports is removed. Exactly two things are carved out of that rule because the protocol usually cannot know them: deviceRef bindings the protocol does not expose (preserved) and the device name (preserve-if-present). Every rule below is a consequence of that model.


RULE-042: Enumerable Protocols Should Ship Discovery

Severity: Recommendation

Description: If the protocol can enumerate the device's configured objects — read back which zones/partitions/outputs/readers/doors/modules exist, plus their names and relationships — the adapter should implement discovery rather than requiring the whole device tree to be authored by hand. When the protocol can report the inventory it is the correct source of truth; hand-authoring a full tree is error-prone and does not scale.

This is a recommendation, not a hard failure — some protocols genuinely cannot enumerate. But the choice must be visible: if the protocol cannot enumerate, the adapter must carry a cited limitation note (survey, build log, or the adapter's design-notes document) rather than silently omitting discovery. Silent omission is the violation this rule targets, not the absence of discovery itself.

Detection pattern:

  • Survey / vendor docs describe an enumeration, topology, or "read configuration" call, but the adapter has no static Discover(...) handler and no identity: declarations.
  • The whole descendant tree is authored by hand in adapter-registration.yaml for a device the protocol could report.
  • No documented, cited reason explaining why discovery was not implemented.

How to flag:

  • Protocol can enumerate + no discovery + no cited limitation → Recommendation (improvement): add discovery per patterns/14.
  • Protocol genuinely cannot enumerate → Passed with note only if a cited limitation exists; otherwise Recommendation to record that limitation with a citation.

Consistent with MODEL-014 (same recommendation, from the modeling side).


RULE-043: Undeterminable deviceRef Bindings Must Be Left Untouched

Severity: Critical

Description: When discovery IS implemented, a deviceRef binding the protocol cannot determine (the classic case: which reader serves which door) MUST be left Undefined — the adapter does not assign it and does not clear it. Undefined is the default: a deviceRef the adapter never touches is preserved on re-import, so an operator's hand-wiring survives every re-discovery.

Assigning (Set) or clearing (.None / empty list) a binding the protocol does not actually report is a correctness defect, not a style nit: on every re-discovery it silently overwrites or wipes operator-configured wiring with a guessed or empty value. Because re-discovery is expected to be safe to run repeatedly (e.g. nightly), this corrupts the operator's configuration on a schedule.

Detection pattern:

  • A deviceRef property is assigned in Discover(...) from a value the protocol does not expose (e.g. inferred from list order, address arithmetic, or a hard-coded guess).
  • A deviceRef the protocol cannot report is set to <TargetNode>.None / an empty list "to be safe" or "to reset it".
  • Any writing of a reader↔door (or equivalent operator-wired) binding without a cited protocol field that authoritatively supplies it.

Correct pattern:

// Protocol does NOT report which reader serves which door → leave Undefined (do nothing).
// The framework preserves the operator's stored binding on every re-import.
var door = new DoorDiscoveryNode { Address = 1, Name = panelDoorName };
// door.EntryReader intentionally NOT assigned → Undefined → preserved

Violation example:

// WRONG: protocol never reports the door↔reader link; guessing it here
// overwrites the operator's wiring on every re-discovery.
door.EntryReader = readers[door.Address - 1];          // guessed → Set → overwrites
// WRONG: "resetting" a binding the protocol cannot report wipes operator config.
door.EntryReader = ReaderDiscoveryNode.None;            // Cleared → removes operator wiring

Consistent with the tri-state deviceRef table in patterns/14 → Reimport Reconciliation Contract.


RULE-044: Protocol-Exposed deviceRef Bindings Must Be Emitted

Severity: High

Description: The mirror of RULE-043. A deviceRef binding the protocol does authoritatively expose must be emitted as Set (assign the concrete target node / a list of nodes), or as <TargetNode>.None / an empty list when the protocol authoritatively reports that the binding is empty. Emitting it is what lets PQ track the panel as the operator changes it there.

A protocol-known binding left Undefined (untouched) is a gap: PQ will silently drift from the panel because re-discovery preserves the stale stored value instead of following the panel. Undefined is correct only for bindings the protocol cannot determine (RULE-043) — not for ones it reports.

Detection pattern:

  • The protocol reports a relationship (e.g. a zone's partition membership, a module's parent slot) but the corresponding deviceRef is never assigned in Discover(...).
  • A binding is read from the protocol into a local variable but not written onto the discovery node.

Correct pattern:

// Protocol reports zone→partition membership → emit it as Set so PQ tracks panel changes.
zone.Partitions = discoveredPartitionNodes;            // Set → overwrites stored value
// Protocol authoritatively reports "no partitions" → emit authoritative empty.
zone.Partitions = PartitionDiscoveryNode.None;         // Cleared → removes stored binding

Violation example:

// WRONG: protocol DOES report partition membership, but it is never emitted →
// PQ keeps a stale stored value and drifts from the panel.
var partitions = await protocol.ReadZonePartitions(zone.Address, ct);
// zone.Partitions never assigned → Undefined → stale value preserved

Consistent with the tri-state deviceRef table in patterns/14 → Reimport Reconciliation Contract.


RULE-045: Scalars Must Be Emitted on Every Discovery

Severity: High

Description: Scalar properties (addresses, numbers, reactions, flags) are authoritative on discovery. A scalar omitted from a re-import is removed from the stored device — omission is not preservation. Therefore the adapter must emit the current protocol value for every scalar on every discovery.

Conditional or omitted scalar emission intended to "preserve" a stored value is a defect: it deletes the value instead of keeping it. If a scalar should persist, emit it every time.

Detection pattern:

  • A scalar is assigned only inside an if branch, or skipped when the protocol read is "unchanged", with a comment implying the omission preserves the stored value.
  • A discovery node is built without populating a scalar the type declares, relying on omission to "keep" the previous value.

Correct pattern:

// Always emit the current protocol scalar — omission would DELETE it, not preserve it.
var reader = new ReaderDiscoveryNode
{
    Address = channel.Address,        // identity scalar, always emitted
    Name = channel.Name,
    Reaction = channel.Reaction,      // ordinary scalar, always emitted
};

Violation example:

// WRONG: omitting the scalar to "keep" the stored value actually deletes it on re-import.
if (channel.ReactionChanged)
    reader.Reaction = channel.Reaction;   // else: omitted → removed, not preserved

Consistent with the scalar authority note in patterns/14 → Reimport Reconciliation Contract.


RULE-046: Name Must Always Be Emitted and Never Read Back

Severity: Medium

Description: A device name is preserve-if-present server-side: if the stored name is non-empty (an operator renamed it), re-discovery does not overwrite it; if the stored name is empty, the device adopts the discovered name. The framework decides this — the adapter does not.

Therefore the adapter must always emit the panel's current name and must not read the stored name or branch on it. Discovery that conditionally emits or suppresses the name (e.g. "only set the name if it looks empty") duplicates a decision the framework already owns and risks either clobbering an operator's rename or leaving a device unnamed.

Detection pattern:

  • Discover(...) reads the persisted/stored name (e.g. via a persisted-subtree lookup) to decide whether to emit a name.
  • The name is assigned only inside a conditional, or left unset for some nodes.

Correct pattern:

// Always emit the current protocol name; the framework preserves an operator's rename.
var node = new DoorDiscoveryNode { Address = door.Address, Name = door.Name };

Violation example:

// WRONG: reading the stored name and branching on it duplicates framework logic
// and can clobber an operator's rename.
var stored = await server.GetPersistedSubtree(rootId, ct);
if (IsEmpty(stored, door.Address))
    node.Name = door.Name;                // else: name suppressed

Consistent with the "Name is preserve-if-present" section in patterns/14 → Reimport Reconciliation Contract.


RULE-047: No Manual Dangling-Reference Handling; No Refs to Non-Included Devices

Severity: High

Description: When a device is removed — whether re-discovery dropped it or an operator deleted it — the framework automatically clears references to it held by surviving devices (a single deviceRef is nulled; the device is dropped from any array deviceRef). The adapter must not defensively clear or re-emit references to devices it did not include.

Two consequences the reviewer enforces:

  1. The adapter must never point a deviceRef at a device absent from the discovered tree — it is dangling by construction.
  2. The adapter must not hand-roll dangling-ref cleanup (walking survivors to null out references); that duplicates framework behavior and can mask a real modeling bug.

Detection pattern:

  • A deviceRef is assigned to a node that is not added to the returned AdapterDeviceCollection (or to any child list within it).
  • Hand-written code that iterates discovered nodes to clear/reset references to nodes that were dropped this pass.
  • "Defensive" re-emission of references to devices no longer present.

Correct pattern:

// Only reference nodes that are actually part of the discovered tree.
// Do nothing about vanished devices — the framework prunes references to them.
door.EntryReader = discoveredReaderNode;   // discoveredReaderNode IS added to the collection

Violation example:

// WRONG #1: pointing a deviceRef at a device not included in the discovered tree (dangling).
door.EntryReader = new ReaderDiscoveryNode { Address = 7 };   // never added to the collection

// WRONG #2: hand-rolled dangling-ref cleanup the framework already does.
foreach (var d in discoveredDoors)
    if (!IsStillPresent(d.EntryReader)) d.EntryReader = ReaderDiscoveryNode.None;

Consistent with the "Automatic dangling-reference cleanup" section in patterns/14 → Reimport Reconciliation Contract.


RULE-048: Every Discoverable Type Must Declare a Stable Identity

Severity: High

Description: Reconciliation is identity-keyed: the framework matches a discovered node to a persisted node by (TypeId, identity property values) and preserves the persisted device id. Every discoverable type must therefore declare identity: on a stable scalar vendor identity — an address, channel number, or vendor object id — used consistently across discoveries.

Identity must not be a display name or list order, because those change: an operator rename or a reordered enumeration would break matching, causing every device to be deleted and recreated (losing ids, history, and operator-configured bindings) on the next re-import. A discoverable type with no identity: gets no generated discovery node at all.

Detection pattern:

  • A device type is built in Discover(...) (or is part of the discoverable subtree) but has no identity: in adapter-registration.yaml.
  • identity: references a name / label property, or a property whose value is not stable across discoveries (position index, list order).
  • Identity keyed on a value the protocol does not report consistently.

Correct pattern:

types:
  Reader:
    identity: [address]        # stable scalar vendor identity
    properties:
      address:
        type: integer

Violation example:

# WRONG: name is not stable — an operator rename breaks matching → delete + recreate.
types:
  Reader:
    identity: [name]
    properties:
      name:
        type: string

Consistent with the "keyed on identity" rules in patterns/14 → Reimport Rule and MODEL-014.