Skip to content

schemas: DeviceService contract v0.1 — the southbound device plane (W8.7) - #215

Merged
mdheller merged 2 commits into
mainfrom
feat/device-service-contract
Jul 30, 2026
Merged

schemas: DeviceService contract v0.1 — the southbound device plane (W8.7)#215
mdheller merged 2 commits into
mainfrom
feat/device-service-contract

Conversation

@mdheller

Copy link
Copy Markdown
Contributor

The gap

The estate has no southbound device abstraction anywhere. The health twin ingests batch corpora; the smart home has nothing; a Δt<50ms twin sync is aspirational for the simple reason that no sensor stream exists to be late.

EdgeX Foundry's lesson is the fix, and it is a lesson about shape, not about adopting EdgeX: ONE southbound interface, N protocol drivers. A driver speaks a protocol; it does not invent a vocabulary. The moment each driver defines its own event shape, every consumer downstream grows a per-driver special case and the "device plane" is a directory of adapters rather than a plane.

Two types

SchemaWhat it isURN
DeviceProfileWhat a device is: protocol, typed protocol binding, and the exact metric set it produces with units, value types, operating ranges and protocol-native source addresses.urn:srcos:device-profile:
DeviceReadingOne observation: metric, value, unit, quality, observedAt — on the MPCC ConversationEvent envelope, byte-identical, parity machine-enforced.urn:srcos:device-reading:

The normative invariant: a reading is attributable or it is nothing

An unattributable reading is the silent-wrong we keep paying for.

deviceRef, deviceProfileRef, profileDigest, metric, sourceAddress and unit are all required — there is no optional path to an anonymous value. tools/validate_device_service_examples.py resolves every one across the example set: the profile exists, the digest matches the recomputed declaration, the metric is declared, the unit and source address agree with it, the value is of the declared type and inside the declared range, and provenanceLinks independently name both the device and the profile — so the attribution survives being read by something that is not this validator.

Digest-pinning: a range cannot be widened after the fact

A reading fails the declared range, so somebody widens the range. DeviceProfile.definitionDigest (sha256 over the {deviceClass, protocol, metrics} projection as written) closes that: widening produces a new digest and orphans the readings it was meant to legalise instead of silently admitting them. Same construct as UpdateHealthProbe (W9.2), applied to metrology — and recomputed, never read back. A digest that is merely stored is an assertion about pinning; a digest that is recomputed is pinning.

Quality is closed, absence is typed, simulation is visible

  • Five values: ok / degraded / stale / substituted / unavailable. Normatively stale and substituted are not ok, and substituted is not measured — conflating them is how a smart-home twin comes to believe a default is a measurement, and then how a health twin does.
  • unavailable is schema-bound (if/then/else) to a null value plus a NullAbsenceRecord ref. The device plane reuses the existing 12-kind MPCC absence taxonomy rather than inventing a device-local one.
  • protocol: "virtual" is a first-class labelled member of the taxonomy; the validator enforces synthetic:simulated-device in both directions. This is the KnowledgeNuggetmodel-generated rule applied to sensors: generated data that is indistinguishable downstream is worse than no data, because it is believed.

Read-only at v0.1

access is closed to "read". Commanding a device is a world-changing effect and must travel EffectRequestEffectDecision. Admitting actuation through a widened enum would hand every protocol driver unreviewed physical authority over a citizen's home.

Overlap decided, not ignored

ExistingDecision
DeviceIdentityGoverns admission/attestation/trust for a SourceOS operator workstation; says nothing about metrology. DeviceProfile.identityRef binds to it. A negative vector forbids attributing a reading to a urn:srcos:device-identity: URN.
TelemetryEventAn AgentSession diagnostic log event — no unit, range, quality or device. Untouched.
NullAbsenceRecordReused verbatim. No device-local absence vocabulary exists.
ConversationEventThe envelope authority.
Fog plane (Topic/Offer/WorkOrder/…)No overlap despite the shared word — that is the FogCompute marketplace; this is sensor fog.

Verification

make validate exit 0 (whole repo, incl. the new target)
make validate-device-service-examples exit 0 ok: true
scripts/check_duplicate_schema_ids.py exit 0 316 unique $id
ajv compile (CI step) both schemas ok
CI example loop 215 passed, 82 skipped, 0 failed
  • 21 negative vectors in fixtures/device-service/conformance.json — every one derived from a complete canonical example and broken in exactly one way, so a failure is attributable to the named defect. Verified all 21 fail, none passes.
  • The validator was mutation-tested: 8 deliberate defects (widen a range without re-digesting; unit drift; source-channel drift; a simulated reading losing its labels; a value out of range; an edited envelope description; provenance that no longer names the device; receivedAt before observedAt) each turn it red. A checker that cannot go red is theatre.

Deliberate gaps (§11 of the spec note)

Reference implementation: device-service in SocioProphet/prophet-platform (separate PR).

…8.7)
The estate had no southbound device abstraction anywhere. The health twin
ingests batch corpora; the smart home has nothing; a sub-50ms twin sync is
aspirational for the simple reason that no sensor stream exists to be late.
EdgeX Foundry's lesson is the fix, and it is a lesson about shape: ONE
southbound interface, N protocol drivers. A driver speaks a protocol; it does
not invent a vocabulary. The moment each driver defines its own event shape,
every consumer downstream grows a per-driver special case and the "device
plane" is a directory of adapters rather than a plane.
Two types:
DeviceProfile — what a device IS: protocol, typed protocol binding, and the
exact metric set it produces with units, value types, operating ranges and
protocol-native source addresses.
DeviceReading — one observation, carried on the MPCC ConversationEvent
envelope with byte-identical sub-schemas (parity machine-enforced).
Normative invariant: A READING IS ATTRIBUTABLE OR IT IS NOTHING. deviceRef,
deviceProfileRef, profileDigest, metric, sourceAddress and unit are all
required — there is no optional path to an anonymous value. The validator
resolves every one across the example set: the profile exists, the digest
matches the RECOMPUTED declaration, the metric is declared, the unit and
source address agree with it, the value is of the declared type and inside
the declared range, and provenanceLinks independently name both the device
and the profile — so the attribution survives being read by something that
is not this validator.
definitionDigest closes the retroactive-legalisation path: a reading fails
the declared range, so somebody widens the range. With digest-pinning,
widening produces a new digest and ORPHANS the readings it was meant to
legalise instead of silently admitting them. Same construct as
UpdateHealthProbe (W9.2), applied to metrology; recomputed, never read back.
Quality is closed at five values, and unavailable is schema-bound to a null
value plus a NullAbsenceRecord reference — the device plane reuses the
existing 12-kind MPCC absence taxonomy rather than inventing a device-local
one. Normatively, stale and substituted are not ok, and substituted is not
measured: conflating them is how a smart-home twin comes to believe a default
is a measurement, and then how a health twin does.
protocol: "virtual" is a first-class, labelled member of the taxonomy, with
the synthetic:simulated-device label enforced in both directions. This is the
KnowledgeNugget model-generated rule applied to sensors: generated data that
is indistinguishable downstream is worse than no data, because it is believed.
observedAt / receivedAt / wallTime stay three fields because they measure
three things; observedAt to receivedAt is the southbound latency a twin's
sync budget is actually spent on, and collapsing them hides the only number
that would say whether a target is met.
access is closed to "read". Commanding a device is a world-changing effect
and must travel EffectRequest -> EffectDecision; admitting actuation through
a widened enum would hand every protocol driver unreviewed physical authority
over a citizen's home.
Overlap decided rather than ignored: DeviceIdentity governs admission and
trust for an operator workstation and says nothing about metrology
(DeviceProfile.identityRef binds to it; a negative vector forbids attributing
a reading to one), and TelemetryEvent is an AgentSession diagnostic with no
unit, range, quality or device. Neither is duplicated.
Includes a BLE-GATT profile and the virtual profile the reference service
actually runs, measured and typed-absence readings, 21 negative conformance
vectors, a validate-device-service-examples target, and specs/
device-service-contract.md. Reference implementation: apps/device-service in
SocioProphet/prophet-platform.
… check being laxer than the implementation it certifies
Adversarial pass on the contract's own conformance tool found two checks that
could not fail for the documents most likely to be wrong.
1. receivedAt >= observedAt was a STRING compare. `format: date-time` carries no
`pattern`, so schema-valid forms differ in offset and fractional precision.
Two proven counterexamples that the check MISSED:
observedAt 2026-07-29T09:15:00.500Z / receivedAt ...T09:15:00Z
-> 500ms earlier, admitted ('.' 0x2E < 'Z' 0x5A)
observedAt 2026-07-29T20:00:00+00:00 / receivedAt ...T21:00:00+05:00
-> 4h earlier, admitted
Both are now caught; equivalent instants written differently (Z vs +02:00,
lowercase t/z) still pass, so the fix has teeth in both directions. A pair
that cannot be parsed to an absolute instant now fails closed.
2. kkoTypeRef was only checked when BOTH sides carried one, so a reading that
simply omitted it passed — and recorded a green `ontology-agrees`. The
reference implementation (prophet-platform device-service contract.py) keys
the check off the PROFILE's declaration and rejects that document. A
conformance tool laxer than the implementation it certifies is worse than no
tool. Now aligned.
Also: `latency-ordered` no longer reports green on readings that carry no
receivedAt — a check that ran on nothing should not claim it passed.
specs §6 states the instants-not-strings rule normatively so an implementation
cannot reintroduce it and still claim conformance.
Schemas are unchanged and stay byte-identical to the vendored copies in
prophet-platform apps/device-service.
@mdheller
mdhellerforce-pushed the feat/device-service-contract branch from ba84bed to bd1fb43CompareJuly 30, 2026 01:24
@mdheller
mdheller merged commit f2880d3 into mainJul 30, 2026
7 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@mdheller