# title
Lattice — Topology & Capability Document

# description
An OpenAPI-style, vendor-publishable document describing a site's device topology (a graph of nodes connected by typed directed relationships) and the read/control capabilities each node offers, with concrete data-driven bindings and ranked access paths. A producer (a cloud integration or the gateway) emits a conformant doc; any consumer ingests it generically with no per-provider code. Scale-free: the same constructs describe a cell, a device, a site, or a city.

# properties.topologyVersion.description
Version of THIS specification the doc conforms to (semver).

# properties.docVersion.description
Monotonic instance version of THIS document. Bumps on any change, including capability `ref` (re)assignment. The data-plane protobuf carries `doc_version`; a consumer decodes telemetry/encodes control against the doc of the matching docVersion (re-reading the retained topology on an unknown version).

# properties.id.description
Stable identifier for this document / site / fragment.

# properties.scope.description
'fragment' = one producer's slice (the consumer merges fragments by node identity); 'site' = an already-merged graph.

# properties.deviceTypes.description
Reusable device-type descriptors (the components/$ref equivalent): a fingerprint plus capability/binding templates, referenced by node.deviceType.

# $defs.producer.properties.provider.description
Provider/transport identity backing this producer's access paths.

# $defs.node.properties.id.description
Stable identity (e.g. a serial). THE merge key across producers — the same id from two producers is one node with both their access paths.

# $defs.node.properties.deviceType.description
Optional key referencing a deviceTypes[] descriptor.

# $defs.node.properties.accessPaths.description
Ways this producer can reach the node. The consumer merges + ranks all producers' access paths to the same id.

# $defs.nodeKind.description
Domain-agnostic node class. Vendor/domain extensions use a namespace prefix, e.g. 'x-acme:thing'. 'structural' = a pure aggregator/virtual node (e.g. a plant or a city) with no telemetry of its own.

# $defs.attributes.description
Free-form node attributes referenced by routing, grouping and constraints.

# $defs.geo.description
Optional geospatial location — needed at building/district/city scale.

# $defs.accessPath.description
A way to reach this node's capabilities via a provider/transport.

# $defs.accessPath.properties.preference.description
Higher = preferred; the consumer fails over to the next-highest on unavailability.

# $defs.aggregate.description
When (and with what priority) this node serves as the aggregate read/control point for its children, instead of routing to each child. Replaces hardcoded primary-selection with data.

# $defs.aggregate.properties.minChildren.description
Qualifies only if the configured child-count over `over` >= this. e.g. EMS=0 (always primary), Gateway=2 (only when it fronts >1 unit).

# $defs.aggregate.properties.priority.description
Higher wins when several aggregators qualify (e.g. EMS > Gateway).

# $defs.capabilityOffer.description
A capability this node offers on a given access path, with read and/or control binding(s), aggregation semantics and constraints. The same capability may appear multiple times on one node (one per access path) — the consumer ranks them by access-path preference (this is how 'control via gateway OR cloud, with fallback' is expressed).

# $defs.capabilityOffer.properties.ref.description
Stable small integer (the data-plane `cap_ref`) identifying this (node, capability) on telemetry/control. Keyed by node+capability — multiple access-path offers for the same capability SHARE one ref (the consumer picks the access path). Stable within a docVersion.

# $defs.capabilityOffer.properties.accessPath.description
id of the node's accessPath that serves this offer.

# $defs.capabilityOffer.properties.groupBy.description
Attribute name partitioning a multi-element read into per-group results (e.g. 'phase' for grid voltage).

# $defs.capabilityOffer.properties.schedule.description
true if the control payload is a time-indexed plan (windows/profile), not a scalar.

# $defs.capabilityName.description
Domain-agnostic capability name. Vendor/domain extensions use a namespace prefix (e.g. 'x-acme:foo'). A capability name MUST NOT name a vendor or device. Core set in examples.

# $defs.binding.description
The concrete 'how to read/write this on this access path': protocol, address, op, encoding and a value transform. An adapter executes it; the model needs no per-model code. This is what replaces hardcoded register maps.

# $defs.binding.properties.address.description
Register number/range, API endpoint/field, OCPP action, etc.

# $defs.binding.properties.count.description
# elements/registers — vector reads (per-unit SOC) or slot start+end.

# $defs.transform.description
Named, parameterised value transform (raw <-> engineering units). 'kind' values beyond the core set are vendor/adapter extensions (namespaced). Vendor-specificity is allowed HERE (and in bindings/deviceType keys) — never in capability names.

# $defs.reducer.description
How multiple read sources (or vector elements) combine. 'representative' = redundant value, read one. 'spread' = max-min (e.g. cell imbalance).

# $defs.distribution.description
How one control intent maps onto multiple targets.

# $defs.relationType.description
Directed edge, read subject->object, with an implied named inverse.

# $defs.constraints.description
Bounds/valid-set/interlocks resolution must respect. May be static or a derived expression.

# $defs.constraints.properties.max.description
A number, or a derived expression like 'min(capacity/2, rated)'.

# $defs.constraints.properties.scope.description
site = e.g. max grid import, G98/G99 export limit.

# $defs.deviceType.description
Reusable descriptor: a fingerprint (recognition rules — the discovery schema as data) plus capability/binding templates and a default aggregate. Instances reference it via node.deviceType. Keyed by (make, model, firmware); a firmware change is a new descriptor version, not a per-node rewrite.

# $defs.fingerprint.description
How to recognise a device as this type (used by discovered producers, e.g. the gateway).

# $defs.fingerprint.properties.dtcHi.description
Device-type-code high byte, e.g. '0x80'.

# $defs.fingerprint.properties.probe.description
e.g. an input register that must be > 0.