ADR-054: AsyncAPI Emission for the Stream Archetype¶
Status¶
Accepted Date: 2026-08-08
Context¶
The @app.stream decorator gained a DeviceContext and the ability to publish a static retained {prefix}/{name}/state topic in ADR-045 (Stateful Stream Receiver Semantics). As of ADR-045's 2026-08-07 additive amendment (cosalette 0.6.0), a stream's state_model became load-bearing: every published payload is validated through a Pydantic TypeAdapter against the declared model, providing a static, verifiable state-shape contract.
That same amendment deliberately excluded stream (and periodic) registrations from the generated AsyncAPI document, recording three reasons: (1) x-cosalette-archetype is a closed enum {telemetry, command, device} validated by the schema loader — a stream channel had no representable archetype and older cosalette loaders would reject any document containing an unknown value; (2) the AsyncAPI artifact is the target that schema check gates against and that schema ha-discovery derives Home Assistant entities from, so emitting stream channels would silently add HA entities on the next regeneration; (3) it flagged that adding a fourth archetype "is a defensible future change ... but it is a cross-version schema-compatibility decision that needs its own ADR."
The condition named by ADR-045 as the soundness precondition — a static, load-bearing state_model — is now satisfied. This ADR records the decision on whether and how to reverse the stream half of that exclusion.
Current code state (verified against codebase):
- The closed
Literalappears in three sites that must stay in sync:ChannelSchema.archetypeandOperationSchema.archetypeLiterals inpackages/src/cosalette/_schema/__init__.py(~L173, ~L187), and the loader validation set inpackages/src/cosalette/_schema/_loader_helpers.py(~L69,_validate_archetype). - AsyncAPI emission is centralised in
build_app_asyncapi(packages/src/cosalette/_schema/_asyncapi.py~L608–706), which iterates telemetry, command, and device registrations only. schema checkexplicitly subtractsstream_namesfrom its comparison set (packages/src/cosalette/_schema/_cli.py~L223).- HA discovery is generated by
HaDiscoveryGenerator/_infer_component(packages/src/cosalette/_schema/_consumer_gen.py~L78–92, L163–229). @app.periodictasks have no MQTT channel or address by design (ADR-041) and carry nostate_model,payload_model, oreffects; they are categorically out of scope.
Sub-decisions resolved by this ADR:
Schema-compatibility break (Q1): Documents containing x-cosalette-archetype: stream will fail to load on cosalette versions before this change. This is a documented BREAKING schema-compatibility change; the pre-existing loader error path for unknown archetypes is the enforcement gate. Migration path: regenerate the schema artifact with the new version; all consumers must upgrade to a loader that recognises stream.
Home Assistant discovery (Q2): Stream channels are emitted into the AsyncAPI document but are excluded from HaDiscoveryGenerator output by default. Regenerating schema ha-discovery therefore creates no new HA entities for streams. A future opt-in entity mapping is possible but explicitly out of scope here.
Periodic exclusion (Q3): @app.periodic remains categorically excluded from AsyncAPI. It has no MQTT channel or address (ADR-041) and therefore cannot be modelled as a channel operation. The asymmetry between stream (now included) and periodic (still excluded) is intentional and recorded here so that the enum extension is not misread as a signal that periodic will follow.
Channel shape (Q4): A stream is modelled as a send/publish operation on a state channel at {prefix}/{name}/state (or {prefix}/state when root), mirroring the existing telemetry state channel pattern and preserving _device_name_from_archetype behaviour for addresses with three or more segments.
Decision¶
Use a fourth x-cosalette-archetype value (stream) in the generated AsyncAPI document for @app.stream registrations — modelled as a send/publish state channel at {prefix}/{name}/state, accepting the resulting cross-version schema-compatibility break — while excluding stream channels from Home Assistant discovery generation by default and keeping @app.periodic categorically excluded from AsyncAPI, because state_model is now load-bearing and provides the static state-shape contract that makes faithful emission sound.
Decision Drivers¶
- AsyncAPI completeness: the generated document should be a faithful picture of the app's full MQTT surface; stream registrations publish real retained state topics that are currently invisible to schema consumers.
- Soundness precondition: stream
state_modelis now load-bearing (Pydantic TypeAdapter validation on every publish), providing the static state-shape signal that ADR-045 named as the exact condition under which emission would become defensible. - Cross-version schema compatibility: the closed archetype enum is validated by the loader, so adding
streamis a breaking change that must be explicit, documented, and pinned to a version floor — not introduced silently. - Avoid surprising existing deployments: adding channels to AsyncAPI must not silently create new Home Assistant entities when users regenerate
schema ha-discoveryoutput. - Consistency of the archetype model: the closed enum lives in three sites (
ChannelSchema,OperationSchema, and_validate_archetype); periodic has no channel/address and must be treated distinctly from stream, making the asymmetry a first-class documented fact rather than an implementation detail.
Considered Options¶
Option 1: Emit stream as a fourth archetype; accept the schema break; exclude from HA discovery (chosen)¶
Add stream to both archetype Literals and to the _validate_archetype validation set. Emit a send/publish state channel at {prefix}/{name}/state with x-cosalette-archetype: stream for every @app.stream registration. Remove the stream_names subtraction from schema check. Guard HaDiscoveryGenerator to skip stream-archetype channels by default. Keep @app.periodic excluded. This is a single-phase, breaking schema-compatibility change paired with an explicit HA-exclusion guard.
- Advantages: AsyncAPI becomes immediately complete — all MQTT topics with a static state contract are documented.; Emission is sound:
state_modelprovides the static payload schema needed to generate a valid AsyncAPI channel.; No silent Home Assistant entity creation: the HA exclusion guard is ship-together with the emission change.; Single release, clear version floor — no multi-release choreography or flag proliferation.; The archetype model stays a single closed enum; the break is explicit and recorded in this ADR. - Disadvantages: Breaking cross-version schema-compatibility: documents generated with the new version cannot be loaded by older cosalette loaders.; The closed archetype enum now lives in three sites (
ChannelSchema,OperationSchema,_validate_archetype) that must be kept in sync on every future archetype addition.; HA discovery acquires a special-case exclusion branch that must be maintained and tested.
Option 2: Forward-tolerant loader first, then emit later¶
In a first release, make the loader warn-and-skip unknown archetype values instead of hard-failing. Only in a subsequent release, after tolerant loaders are the version floor for all known consumers, add stream to the enum and begin emitting stream channels.
- Advantages: No hard schema-compatibility break: existing consumers experience a warning, not a parse failure.; Smooth multi-release migration path with a defined transition period.
- Disadvantages: Multi-release choreography: the feature is delayed by at least one full release cycle.; Increased complexity: the loader must be maintained in both strict and tolerant modes during the transition.; The user explicitly rejected this approach in favour of accepting the break cleanly.
Option 3: Version-gated / opt-in emission¶
Emit stream channels into AsyncAPI only when an explicit flag is set (e.g. a CLI option or a config setting). The default document remains unchanged; users opt in to stream emission.
- Advantages: No schema-compatibility break for users who do not opt in.; Existing HA discovery pipelines are unaffected by default.
- Disadvantages: AsyncAPI remains incomplete by default, contradicting the goal of a faithful default document.; Adds a config surface and two code paths to maintain.; The flag itself requires documentation, testing, and deprecation handling if later removed.
Option 4: Keep the status quo (stream stays out of AsyncAPI)¶
Do nothing. Maintain the current exclusion of stream (and periodic) registrations from the AsyncAPI document indefinitely.
- Advantages: Zero implementation risk and no schema-compatibility break.; No changes to HA discovery or the loader.
- Disadvantages: AsyncAPI remains an incomplete picture of the app's MQTT surface — streams with a static
state_modelare invisible to schema consumers.; The ADR-045 exclusion was explicitly framed as temporary pending this decision; leaving it permanent contradicts the recorded intent.; The soundness precondition (state_modelis load-bearing) has been met; continued exclusion is no longer justified on those grounds.
Decision Matrix¶
| Criterion | Emit stream as a fourth archetype; accept the schema break; exclude from HA discovery | Forward-tolerant loader first, then emit later | Version-gated / opt-in emission | Keep the status quo (stream stays out of AsyncAPI) |
|---|---|---|---|---|
| AsyncAPI completeness | 5 | 3 | 2 | 1 |
| Schema-compat safety | 2 | 5 | 4 | 5 |
| HA-discovery safety | 4 | 4 | 4 | 5 |
| Implementation simplicity | 4 | 2 | 2 | 5 |
| Faithful-by-default | 5 | 3 | 2 | 1 |
Scale: 1 (poor) to 5 (excellent)
Consequences¶
Positive¶
- AsyncAPI becomes a complete and faithful picture of the app's MQTT surface: all registrations with a static state contract — telemetry, command, device, and now stream — are documented in a single generated artifact.
- Stream state channels are documented under an enforced
state_modelcontract, so schema consumers have both the channel address and the payload schema in one place. - No silent Home Assistant entity creation: the HA exclusion guard is shipped together with the emission change, so
schema ha-discoveryregeneration produces identical output for existing deployments. - The archetype model remains a single closed enum; the deliberate asymmetry between stream (emitted) and periodic (excluded) is explicitly recorded and motivated in this ADR.
Negative¶
- Breaking cross-version schema-compatibility: AsyncAPI documents generated with the new version contain
x-cosalette-archetype: streamand will fail to load on older cosalette loaders. Users must regenerate schema artifacts and upgrade all consumers. This is the documented BREAKING change accepted by this decision. - The closed archetype enum now spans three synchronisation sites (
ChannelSchema.archetype,OperationSchema.archetype, and_validate_archetype); future archetype additions must update all three or risk a silent mismatch. - HA discovery gains a special-case exclusion branch for the
streamarchetype that must be maintained and covered by tests. - If streams should ever map to Home Assistant entities (e.g. via
sensororbinary_sensor), a follow-on ADR and implementation are needed — that scope is explicitly excluded here.
2026-08-08