feat: key the builder on device id, and give extend() an inverse - #80
Merged
Conversation
#74: DeviceTreeBuilder keyed its bookkeeping on DeviceSpec object identity. A producer deriving its spec set from a manifest re-derives equal-but-distinct objects on every pass, so identity keying made each pass a new device. The alternative was an unstated obligation: hold a device_id -> DeviceSpec map for the process lifetime and never re-derive, which is exactly what a declarative API exists to avoid. Now keyed on the resolved device id. add(), remove(), extend(), device_for() and homie_properties() all answer for any spec naming the same device. Deferred specs stay keyed by identity, having no id yet by definition. One semantic decided explicitly: add() is idempotent on the DEVICE, not on the declaration. A differing capability set on an already-built id returns the existing device unchanged rather than applying the difference, because add() silently mutating a live tree is not what its name suggests. extend() is how a built device grows. The test that asserted the old contract (device_for on an equal spec returns None) is rewritten to assert the new one rather than deleted, so the change of contract is visible in the diff. #78: remove_capabilities(), the inverse of extend(). A capability that becomes relevant at runtime can stop being relevant, and its node otherwise stayed advertised with retained topics behind it. Device.delete_node already clears those and re-announces; the gap was the bookkeeping, since reaching around the builder left model_keys and created_groups describing properties that no longer existed and a later remove() working from that stale record. Bookkeeping now carries the capability, so a node's share of it is identifiable. Closes #74 Closes #78 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dcj
force-pushed
the
feat/id-keyed-idempotence-and-node-removal
branch
from
August 21, 2026 02:29
a2c9ad7 to
ae1154d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #74, closes #78. Stacked on #79, which should merge first.
#74 — keyed on the resolved device id
DeviceTreeBuilderkeyed its bookkeeping onDeviceSpecobject identity. A producer deriving its spec set from a manifest re-derives equal-but-distinct objects on every pass, so each pass made a new device. The alternative was an obligation the API never stated: hold adevice_id -> DeviceSpecmap for the process lifetime and never re-derive, which is precisely what a declarative API exists to avoid.add(),remove(),extend(),device_for()andhomie_properties()now all answer for any spec naming the same device. Deferred specs stay keyed by identity, having no id yet by definition.One semantic decided explicitly:
add()is idempotent on the device, not on the declaration. A differing capability set on an already-built id returns the existing device unchanged rather than applying the difference, becauseadd()silently mutating a live tree is not what its name suggests.extend()is how a built device grows.test_a_differing_spec_on_a_built_id_returns_the_existing_devicepins that.The test that asserted the old contract is rewritten rather than deleted, so the change is visible in the diff:
#78 —
remove_capabilities()The inverse of
extend(). A capability that becomes relevant at runtime can stop being relevant, and its node otherwise stayed advertised in$descriptionwith retained topics behind it.Device.delete_node()already clears those and re-announces, so what this closes is the bookkeeping: reaching around the builder to call it leftmodel_keysandcreated_groupsdescribing properties that no longer existed, and a laterremove()working from that stale record. Bookkeeping now carries the capability, so a node's share of it is identifiable.Idempotent like
extend(), and there's a test for the full lifecycle the issue describes: relevant, then not, then relevant again.Verification
700 tests pass (692 before, 8 new plus 3 replacing the rewritten one). ruff and markdownlint clean.
Release note
This plus #79 makes 0.23.0, one release for all seven issues rather than a patch now and a minor later — each version bump costs a Yocto image rebuild downstream, so batching them is the point.
🤖 Generated with Claude Code