Skip to content

docs: state two parts of the DeviceTreeBuilder contract the API did not convey - #65

Merged
dcj merged 1 commit into
mainfrom
docs/tree-builder-contract
Aug 21, 2026
Merged

docs: state two parts of the DeviceTreeBuilder contract the API did not convey#65
dcj merged 1 commit into
mainfrom
docs/tree-builder-contract

Conversation

@dcj

@dcj dcj commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Refs #49. Documentation only, no behavior change.

Two parts of DeviceTreeBuilder's contract that the API alone did not convey, both reported by a consumer reconciling an existing multi-device builder against it. Both are places where reading the types could not answer the question.

1. Model ownership

The builder takes a GroupedPropertyDict it never creates, which leaves open whether a producer should adapt its own model to that type or own one of that type.

It is the second: the observable-model pattern doc/building-a-proxy.md prescribes, where acquisition code writes values into the model and publishing is a reactive side effect of the bindings. It accepts rather than creates one so a single model can span a whole tree, and so a producer that already holds one (populated before any Homie tree exists) can hand it over. It is not an adapter seam for a foreign model type.

2. The limit of add()'s ordering

add() orders late-bound ids, and builds an unbuilt parent it was handed. It does not order the construction of the specs themselves.

DeviceSpec is frozen and parent is a direct reference, so a child spec cannot be constructed before its parent spec exists. A caller deriving specs from a declarative source that names parents indirectly (by class, by type, by key) still owns that dependency ordering, one step earlier than before.

The consumer's phrasing is the reason this is worth writing down: add() "reads as though ordering is handled and it is only handled for ids, not for parent resolution."

Verification

658 tests pass, ruff check / ruff format --check clean, markdownlint clean on CHANGELOG.md and doc/building-a-proxy.md.

🤖 Generated with Claude Code

…ot convey

Both reported by a consumer reconciling an existing multi-device builder
against it, and both were places the types alone could not answer the
question.

1. Model ownership. DeviceTreeBuilder takes a GroupedPropertyDict it
   never creates, which leaves open whether a producer should ADAPT its
   own model to that type or OWN one of that type. It is the second: the
   observable-model pattern building-a-proxy.md prescribes, where
   acquisition code writes into the model and publishing is a reactive
   side effect of the bindings. It accepts rather than creates so one
   model can span a whole tree, and so a producer already holding one can
   hand it over.

2. The limit of add()'s ordering. add() orders late-bound ids and builds
   an unbuilt parent it was handed, but DeviceSpec is frozen and parent
   is a direct reference, so a child spec cannot be constructed before
   its parent spec exists. A caller deriving specs from a declarative
   source that names parents indirectly (by class, by type, by key) still
   owns that dependency ordering. add() reads as though ordering is
   handled generally; it is handled for ids.

Documentation only. No behavior change.

Refs #49

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dcj
dcj merged commit 06e9c3f into main Aug 21, 2026
5 checks passed
@dcj
dcj deleted the docs/tree-builder-contract branch August 21, 2026 01:08
dcj added a commit that referenced this pull request Aug 21, 2026
…n) (#71)

Bumps __version__ to 0.22.0 (single source of truth), promotes Unreleased
to [0.22.0], and adds the tag link definition.

  #64  node_id callable, so one device can carry two instances of a
       capability
  #65  DeviceTreeBuilder contract: model ownership, and the limit of
       add()'s ordering
  #66  the builder replaced properties in a model it does not own,
       discarding live values, callbacks and entity_setters. A producer
       that wired inbound control lost the actuator while $description
       kept advertising settable: true. Data loss in 0.21.0's headline
       feature.
  #67  a child could silently shadow an ancestor's id, or a sibling's,
       putting two devices on the same topics. Plus the feature it was
       filed for: add_root_capabilities(), so a root can carry its own
       capabilities rather than only parenting children.
  #68  extend(), so a device that already exists can grow a capability.
       The previous workaround dropped properties from $description while
       leaving their retained topics on the broker.

Minor rather than patch: three new public methods. #66's fix is the
reason not to sit on it, since 0.21.0 is live with that data loss.

Materialization is now idempotent at three levels (node, property, and
the state transition itself), which is what makes a re-fired incremental
lifecycle a genuine no-op. The third matters most: an empty
state_transition still emits init -> ready, and that edge forces every
controller on the bus to resync.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to 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