vmx 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. vmx-2.6.0/.gitignore +120 -0
  2. vmx-2.6.0/CHANGELOG.md +392 -0
  3. vmx-2.6.0/PKG-INFO +271 -0
  4. vmx-2.6.0/README.md +232 -0
  5. vmx-2.6.0/RELEASING.md +133 -0
  6. vmx-2.6.0/pyproject.toml +98 -0
  7. vmx-2.6.0/scripts/smoke_test.py +65 -0
  8. vmx-2.6.0/src/vmx/__about__.py +10 -0
  9. vmx-2.6.0/src/vmx/__init__.py +289 -0
  10. vmx-2.6.0/src/vmx/aggregates/__init__.py +60 -0
  11. vmx-2.6.0/src/vmx/aggregates/aggregate_vm.py +653 -0
  12. vmx-2.6.0/src/vmx/aggregates/builders.py +451 -0
  13. vmx-2.6.0/src/vmx/builders/__init__.py +14 -0
  14. vmx-2.6.0/src/vmx/builders/_validation.py +31 -0
  15. vmx-2.6.0/src/vmx/builders/exceptions.py +20 -0
  16. vmx-2.6.0/src/vmx/capabilities/__init__.py +56 -0
  17. vmx-2.6.0/src/vmx/capabilities/crud.py +40 -0
  18. vmx-2.6.0/src/vmx/capabilities/current_crud.py +21 -0
  19. vmx-2.6.0/src/vmx/capabilities/dialog.py +29 -0
  20. vmx-2.6.0/src/vmx/capabilities/expandable_state.py +68 -0
  21. vmx-2.6.0/src/vmx/capabilities/expansion.py +33 -0
  22. vmx-2.6.0/src/vmx/capabilities/filter.py +28 -0
  23. vmx-2.6.0/src/vmx/capabilities/lifecycle_capabilities.py +32 -0
  24. vmx-2.6.0/src/vmx/capabilities/management.py +16 -0
  25. vmx-2.6.0/src/vmx/capabilities/pageable.py +103 -0
  26. vmx-2.6.0/src/vmx/capabilities/search.py +21 -0
  27. vmx-2.6.0/src/vmx/capabilities/searchable_state.py +103 -0
  28. vmx-2.6.0/src/vmx/capabilities/selection.py +29 -0
  29. vmx-2.6.0/src/vmx/collections/__init__.py +34 -0
  30. vmx-2.6.0/src/vmx/collections/batch.py +45 -0
  31. vmx-2.6.0/src/vmx/collections/collection_changed.py +21 -0
  32. vmx-2.6.0/src/vmx/collections/observable_dictionary.py +288 -0
  33. vmx-2.6.0/src/vmx/collections/observable_list.py +225 -0
  34. vmx-2.6.0/src/vmx/collections/paged_composition.py +240 -0
  35. vmx-2.6.0/src/vmx/collections/serviced_observable_collection.py +136 -0
  36. vmx-2.6.0/src/vmx/commands/__init__.py +48 -0
  37. vmx-2.6.0/src/vmx/commands/composite_command.py +55 -0
  38. vmx-2.6.0/src/vmx/commands/confirmation_decorator_command.py +62 -0
  39. vmx-2.6.0/src/vmx/commands/decorator_command.py +67 -0
  40. vmx-2.6.0/src/vmx/commands/fluent.py +109 -0
  41. vmx-2.6.0/src/vmx/commands/modeled_crud_commands.py +88 -0
  42. vmx-2.6.0/src/vmx/commands/protocols.py +57 -0
  43. vmx-2.6.0/src/vmx/commands/relay_command.py +278 -0
  44. vmx-2.6.0/src/vmx/components/__init__.py +44 -0
  45. vmx-2.6.0/src/vmx/components/base.py +457 -0
  46. vmx-2.6.0/src/vmx/components/builders.py +346 -0
  47. vmx-2.6.0/src/vmx/components/component_vm.py +156 -0
  48. vmx-2.6.0/src/vmx/components/protocols.py +136 -0
  49. vmx-2.6.0/src/vmx/components/readonly_component_vm.py +77 -0
  50. vmx-2.6.0/src/vmx/composites/__init__.py +29 -0
  51. vmx-2.6.0/src/vmx/composites/builders.py +265 -0
  52. vmx-2.6.0/src/vmx/composites/composite_vm.py +517 -0
  53. vmx-2.6.0/src/vmx/composites/protocols.py +58 -0
  54. vmx-2.6.0/src/vmx/dialogs/__init__.py +18 -0
  55. vmx-2.6.0/src/vmx/dialogs/dialog_service.py +96 -0
  56. vmx-2.6.0/src/vmx/dialogs/null_dialog_service.py +56 -0
  57. vmx-2.6.0/src/vmx/forms/__init__.py +15 -0
  58. vmx-2.6.0/src/vmx/forms/builders.py +107 -0
  59. vmx-2.6.0/src/vmx/forms/form_vm.py +229 -0
  60. vmx-2.6.0/src/vmx/forwarding/__init__.py +14 -0
  61. vmx-2.6.0/src/vmx/forwarding/component.py +153 -0
  62. vmx-2.6.0/src/vmx/forwarding/composite.py +214 -0
  63. vmx-2.6.0/src/vmx/groups/__init__.py +20 -0
  64. vmx-2.6.0/src/vmx/groups/builders.py +118 -0
  65. vmx-2.6.0/src/vmx/groups/group_vm.py +305 -0
  66. vmx-2.6.0/src/vmx/hierarchical/__init__.py +11 -0
  67. vmx-2.6.0/src/vmx/hierarchical/builders.py +157 -0
  68. vmx-2.6.0/src/vmx/hierarchical/hierarchical_vm.py +298 -0
  69. vmx-2.6.0/src/vmx/lifecycle/__init__.py +24 -0
  70. vmx-2.6.0/src/vmx/lifecycle/_data/__init__.py +1 -0
  71. vmx-2.6.0/src/vmx/lifecycle/exceptions.py +27 -0
  72. vmx-2.6.0/src/vmx/lifecycle/status.py +31 -0
  73. vmx-2.6.0/src/vmx/lifecycle/transition_validator.py +116 -0
  74. vmx-2.6.0/src/vmx/localization/__init__.py +12 -0
  75. vmx-2.6.0/src/vmx/localization/localizer.py +15 -0
  76. vmx-2.6.0/src/vmx/localization/null_localizer.py +16 -0
  77. vmx-2.6.0/src/vmx/messages/__init__.py +44 -0
  78. vmx-2.6.0/src/vmx/messages/collection_changed.py +111 -0
  79. vmx-2.6.0/src/vmx/messages/construction_status_changed.py +44 -0
  80. vmx-2.6.0/src/vmx/messages/form_reverted.py +34 -0
  81. vmx-2.6.0/src/vmx/messages/property_changed.py +45 -0
  82. vmx-2.6.0/src/vmx/messages/property_value_changed.py +60 -0
  83. vmx-2.6.0/src/vmx/messages/protocols.py +55 -0
  84. vmx-2.6.0/src/vmx/messages/tree_structure_changed.py +68 -0
  85. vmx-2.6.0/src/vmx/notifications/__init__.py +33 -0
  86. vmx-2.6.0/src/vmx/notifications/confirm_helper.py +25 -0
  87. vmx-2.6.0/src/vmx/notifications/confirmation_vm.py +92 -0
  88. vmx-2.6.0/src/vmx/notifications/notification.py +23 -0
  89. vmx-2.6.0/src/vmx/notifications/notification_hub.py +137 -0
  90. vmx-2.6.0/src/vmx/notifications/notification_reaction.py +13 -0
  91. vmx-2.6.0/src/vmx/notifications/notification_type.py +13 -0
  92. vmx-2.6.0/src/vmx/notifications/notification_vm.py +186 -0
  93. vmx-2.6.0/src/vmx/notifications/null_notification_hub.py +40 -0
  94. vmx-2.6.0/src/vmx/properties/__init__.py +25 -0
  95. vmx-2.6.0/src/vmx/properties/derived.py +222 -0
  96. vmx-2.6.0/src/vmx/py.typed +0 -0
  97. vmx-2.6.0/src/vmx/services/__init__.py +29 -0
  98. vmx-2.6.0/src/vmx/services/dispatcher.py +83 -0
  99. vmx-2.6.0/src/vmx/services/message_hub.py +82 -0
  100. vmx-2.6.0/src/vmx/services/null_dispatcher.py +32 -0
  101. vmx-2.6.0/src/vmx/services/null_message_hub.py +77 -0
  102. vmx-2.6.0/src/vmx/tree/__init__.py +10 -0
  103. vmx-2.6.0/src/vmx/tree/walk.py +65 -0
  104. vmx-2.6.0/tests/__init__.py +0 -0
  105. vmx-2.6.0/tests/conformance/README.md +7 -0
  106. vmx-2.6.0/tests/conformance/__init__.py +0 -0
  107. vmx-2.6.0/tests/conformance/fixtures/__init__.py +0 -0
  108. vmx-2.6.0/tests/conformance/fixtures/loader.py +26 -0
  109. vmx-2.6.0/tests/conformance/test_aggregate_vm.py +379 -0
  110. vmx-2.6.0/tests/conformance/test_builders.py +228 -0
  111. vmx-2.6.0/tests/conformance/test_cap_021_filterable.py +57 -0
  112. vmx-2.6.0/tests/conformance/test_cap_022_pageable.py +171 -0
  113. vmx-2.6.0/tests/conformance/test_capabilities.py +580 -0
  114. vmx-2.6.0/tests/conformance/test_cmd_008_to_011_fluent_commands.py +150 -0
  115. vmx-2.6.0/tests/conformance/test_col_001_to_004_serviced.py +167 -0
  116. vmx-2.6.0/tests/conformance/test_col_005_to_009_observable_list.py +203 -0
  117. vmx-2.6.0/tests/conformance/test_col_010_to_015_observable_dictionary.py +296 -0
  118. vmx-2.6.0/tests/conformance/test_col_016_to_021_paged_composition.py +242 -0
  119. vmx-2.6.0/tests/conformance/test_command_decorators.py +219 -0
  120. vmx-2.6.0/tests/conformance/test_commands.py +144 -0
  121. vmx-2.6.0/tests/conformance/test_component_vm.py +429 -0
  122. vmx-2.6.0/tests/conformance/test_composite_vm.py +648 -0
  123. vmx-2.6.0/tests/conformance/test_derived_properties.py +345 -0
  124. vmx-2.6.0/tests/conformance/test_dia_001_to_008_dialog_service.py +369 -0
  125. vmx-2.6.0/tests/conformance/test_expand_collapse.py +128 -0
  126. vmx-2.6.0/tests/conformance/test_form_001_to_010_form_vm.py +324 -0
  127. vmx-2.6.0/tests/conformance/test_forwarding.py +219 -0
  128. vmx-2.6.0/tests/conformance/test_group_vm.py +228 -0
  129. vmx-2.6.0/tests/conformance/test_hier_001_to_014_hierarchical_vm.py +542 -0
  130. vmx-2.6.0/tests/conformance/test_hier_015_to_017_hierarchical_vm_builder.py +137 -0
  131. vmx-2.6.0/tests/conformance/test_hier_018_reparent_guard.py +66 -0
  132. vmx-2.6.0/tests/conformance/test_hub.py +228 -0
  133. vmx-2.6.0/tests/conformance/test_lifecycle.py +247 -0
  134. vmx-2.6.0/tests/conformance/test_localization.py +57 -0
  135. vmx-2.6.0/tests/conformance/test_modeled_crud.py +139 -0
  136. vmx-2.6.0/tests/conformance/test_notifications.py +453 -0
  137. vmx-2.6.0/tests/conformance/test_null_services.py +97 -0
  138. vmx-2.6.0/tests/conformance/test_property_change.py +72 -0
  139. vmx-2.6.0/tests/conformance/test_search_filter.py +158 -0
  140. vmx-2.6.0/tests/conformance/test_threading.py +208 -0
  141. vmx-2.6.0/tests/conformance/test_tree_utilities.py +133 -0
  142. vmx-2.6.0/tests/conftest.py +1 -0
  143. vmx-2.6.0/tests/unit/__init__.py +0 -0
  144. vmx-2.6.0/tests/unit/aggregates/__init__.py +0 -0
  145. vmx-2.6.0/tests/unit/aggregates/test_aggregate_vm.py +768 -0
  146. vmx-2.6.0/tests/unit/capabilities/__init__.py +0 -0
  147. vmx-2.6.0/tests/unit/capabilities/test_derived_properties.py +31 -0
  148. vmx-2.6.0/tests/unit/capabilities/test_expand_collapse.py +29 -0
  149. vmx-2.6.0/tests/unit/capabilities/test_search_filter.py +75 -0
  150. vmx-2.6.0/tests/unit/collections/__init__.py +0 -0
  151. vmx-2.6.0/tests/unit/collections/test_observable_dictionary.py +400 -0
  152. vmx-2.6.0/tests/unit/collections/test_observable_list.py +536 -0
  153. vmx-2.6.0/tests/unit/collections/test_paged_composition.py +297 -0
  154. vmx-2.6.0/tests/unit/collections/test_serviced_observable_collection.py +221 -0
  155. vmx-2.6.0/tests/unit/commands/__init__.py +0 -0
  156. vmx-2.6.0/tests/unit/commands/test_decorator_command.py +50 -0
  157. vmx-2.6.0/tests/unit/commands/test_modeled_crud_commands.py +85 -0
  158. vmx-2.6.0/tests/unit/commands/test_relay_command.py +223 -0
  159. vmx-2.6.0/tests/unit/components/__init__.py +0 -0
  160. vmx-2.6.0/tests/unit/components/test_component_vm.py +519 -0
  161. vmx-2.6.0/tests/unit/components/test_readonly_component_vm.py +152 -0
  162. vmx-2.6.0/tests/unit/composites/__init__.py +0 -0
  163. vmx-2.6.0/tests/unit/composites/test_composite_vm.py +676 -0
  164. vmx-2.6.0/tests/unit/composites/test_modeled_composite_vm.py +224 -0
  165. vmx-2.6.0/tests/unit/dialogs/__init__.py +0 -0
  166. vmx-2.6.0/tests/unit/dialogs/test_null_dialog_service.py +217 -0
  167. vmx-2.6.0/tests/unit/forms/__init__.py +0 -0
  168. vmx-2.6.0/tests/unit/forms/test_form_vm.py +311 -0
  169. vmx-2.6.0/tests/unit/forwarding/__init__.py +0 -0
  170. vmx-2.6.0/tests/unit/forwarding/test_forwarding.py +333 -0
  171. vmx-2.6.0/tests/unit/groups/__init__.py +0 -0
  172. vmx-2.6.0/tests/unit/groups/test_group_vm.py +551 -0
  173. vmx-2.6.0/tests/unit/helpers/__init__.py +0 -0
  174. vmx-2.6.0/tests/unit/helpers/test_dispatcher.py +32 -0
  175. vmx-2.6.0/tests/unit/hierarchical/__init__.py +0 -0
  176. vmx-2.6.0/tests/unit/hierarchical/test_hierarchical_vm.py +303 -0
  177. vmx-2.6.0/tests/unit/lifecycle/__init__.py +0 -0
  178. vmx-2.6.0/tests/unit/lifecycle/test_exceptions.py +34 -0
  179. vmx-2.6.0/tests/unit/lifecycle/test_status.py +40 -0
  180. vmx-2.6.0/tests/unit/lifecycle/test_transition_validator.py +68 -0
  181. vmx-2.6.0/tests/unit/messages/__init__.py +0 -0
  182. vmx-2.6.0/tests/unit/messages/test_messages.py +112 -0
  183. vmx-2.6.0/tests/unit/messages/test_property_value_changed.py +104 -0
  184. vmx-2.6.0/tests/unit/notifications/__init__.py +0 -0
  185. vmx-2.6.0/tests/unit/notifications/test_confirmation_vm.py +184 -0
  186. vmx-2.6.0/tests/unit/notifications/test_notification_vm.py +206 -0
  187. vmx-2.6.0/tests/unit/services/__init__.py +0 -0
  188. vmx-2.6.0/tests/unit/services/test_message_hub.py +99 -0
  189. vmx-2.6.0/tests/unit/services/test_null_services_typing.py +96 -0
  190. vmx-2.6.0/tests/unit/services/test_rx_dispatcher.py +58 -0
  191. vmx-2.6.0/tests/unit/test_smoke.py +28 -0
vmx-2.6.0/.gitignore ADDED
@@ -0,0 +1,120 @@
1
+ # ─── macOS ────────────────────────────────────────────────────────────
2
+ .DS_Store
3
+
4
+ # ─── IDE / editor ─────────────────────────────────────────────────────
5
+ .idea/
6
+ .vscode/
7
+ *.swp
8
+ *.swo
9
+ *~
10
+
11
+ # ─── Claude Code session state ─────────────────────────────────────────
12
+ .claude/
13
+ CLAUDE.md
14
+ docs/superpowers/
15
+
16
+ # ─── Python ───────────────────────────────────────────────────────────
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+ *.so
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ sdist/
29
+ wheels/
30
+ share/python-wheels/
31
+ *.egg-info/
32
+ *.egg
33
+ MANIFEST
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+ htmlcov/
37
+ .tox/
38
+ .nox/
39
+ .coverage
40
+ .coverage.*
41
+ .cache
42
+ coverage.xml
43
+ *.cover
44
+ .pytest_cache/
45
+ .mypy_cache/
46
+ .dmypy.json
47
+ .ruff_cache/
48
+ .hypothesis/
49
+ .env
50
+ .venv
51
+ env/
52
+ venv/
53
+ ENV/
54
+
55
+ # uv
56
+ # uv.lock is not committed in this repo. The vmx library (langs/python/) is a
57
+ # library, not an application, so consumers always re-resolve. The examples
58
+ # under examples/python/ are tiny demo apps — re-resolving is fine for them too.
59
+ uv.lock
60
+ uv.lock.bak
61
+
62
+ # ─── .NET / C# ────────────────────────────────────────────────────────
63
+ bin/
64
+ obj/
65
+ *.user
66
+ *.suo
67
+ *.userosscache
68
+ *.sln.docstates
69
+ TestResults/
70
+ [Bb]uild[Ll]og.*
71
+ *.[Cc]ache
72
+ project.lock.json
73
+ project.fragment.lock.json
74
+ artifacts/
75
+ .vs/
76
+
77
+ # NuGet
78
+ *.nupkg
79
+ *.snupkg
80
+ .nuget/
81
+
82
+ # ─── Swift ────────────────────────────────────────────────────────────
83
+ # SwiftPM build state (transient — regenerated by `swift build` / `swift test`).
84
+ .build/
85
+ # SwiftPM resolved dependency graph. The repo's only Swift target has no
86
+ # external dependencies today, so Package.resolved would be empty; if deps
87
+ # are added later, remove this line to start committing the lockfile.
88
+ Package.resolved
89
+ # Xcode-derived files (xcuserdata, breakpoints, schemes/xcuserdata). Project
90
+ # itself (.xcodeproj/) is Package-managed and would not normally exist.
91
+ xcuserdata/
92
+ *.xcscmblueprint
93
+ *.xccheckout
94
+
95
+ # ─── Node / TypeScript ────────────────────────────────────────────────
96
+ node_modules/
97
+ *.log
98
+ npm-debug.log*
99
+ yarn-debug.log*
100
+ yarn-error.log*
101
+ .npm/
102
+ .pnpm-store/
103
+ # The library lockfile (langs/typescript/package-lock.json) and the two
104
+ # flagship example lockfiles (examples/typescript/console/hello-vmx,
105
+ # examples/typescript/react/notes-showcase) ARE tracked. Any future ad-hoc
106
+ # example projects placed directly under examples/typescript/<dir>/ are
107
+ # excluded by the glob below (single-* doesn't recurse, so the two committed
108
+ # examples one level deeper are unaffected).
109
+ examples/typescript/*/package-lock.json
110
+
111
+ # ─── Build / coverage artifacts ───────────────────────────────────────
112
+ coverage/
113
+ *.coverage
114
+ *.lcov
115
+ *.cobertura.xml
116
+
117
+ # ─── Docs builds ──────────────────────────────────────────────────────
118
+ docs/_build/
119
+ docs/site/
120
+ site/
vmx-2.6.0/CHANGELOG.md ADDED
@@ -0,0 +1,392 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Python flavor are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [2.6.0] — 2026-06-13
10
+
11
+ Implements `spec-v2.6.0`. Adds two declarative selection hooks to the
12
+ composite builders, plus four ADRs capturing absorption-audit decisions.
13
+
14
+ ### Added
15
+
16
+ - `CompositeVMBuilder[VM].current(selector)` — declarative initial-current
17
+ selector (ADR-0042, COMP-025).
18
+ - `CompositeVMBuilder[VM].on_current_changed(callback)` — synchronous
19
+ post-change selection callback (ADR-0042, COMP-026).
20
+ - Same hooks on the modeled `CompositeVMOfBuilder[M, VM]`.
21
+
22
+ ### Documentation
23
+
24
+ - ADR-0039 — `INotifyPropertyChanging` not supported (teaching).
25
+ - ADR-0040 — `IProperty[T]` reactive backing-field not adopted (teaching).
26
+ - ADR-0041 — Single disposable lifecycle, no two-tier bags (teaching).
27
+ - ADR-0042 — `CompositeVMBuilder.current` + `on_current_changed` (behavior change).
28
+
29
+ ## [2.5.0] — 2026-06-10
30
+
31
+ Implements `spec-v2.5.0` (ADR-0037).
32
+
33
+ ### Fixed
34
+
35
+ - `FormVM.dispose()` is idempotent — a second call raised reactivex
36
+ `DisposedException` (rxjs no-ops, C# guards).
37
+ - `CompositeVM.clear()` routes through the current-selection setter; the old
38
+ current child no longer keeps `is_current == True` with no notification.
39
+ - `PagedComposition` subscribes `on_item_replaced`; `replace()` on the
40
+ current page refreshes `items`.
41
+ - `ObservableList.clear()` emits `PropertyChanged("Count")` after `Reset`
42
+ when the count changed (spec/21 §3.3).
43
+ - `GroupVM` construct/destruct iterate a snapshot so a child lifecycle hook
44
+ that mutates the group cannot skip siblings.
45
+ - A background construct/destruct racing `dispose()` could resurrect the
46
+ VM and publish post-dispose status messages; `DISPOSED` is now terminal
47
+ in `_set_status` and the scheduled work (spec/02 invariant 3).
48
+ - `FormVM.approve_async` no longer raises `DisposedException` when
49
+ `dispose()` runs during the persister await (mirrors the C# guard).
50
+ - `NotificationHub.dispose()` tolerates waiters whose event loop is
51
+ already closed instead of raising and skipping the remaining waiters.
52
+ - `ObservableList.remove_at`/`replace` normalize negative indexes before
53
+ emitting, so the event payload carries the spec-mandated
54
+ index-before-removal instead of a raw `-1` (spec/21 §3.2; TS/C# raise
55
+ on negative indexes by design). Out-of-range negatives raise
56
+ `IndexError` instead of wrapping to a valid index.
57
+ - `FormVM`'s deny path is a no-op after `dispose()` (previously it
58
+ reverted the model and re-published hub messages on a disposed form;
59
+ same guard added in C# and TS). `approve_async()` on a disposed form
60
+ is likewise a full no-op — the persister is no longer invoked.
61
+ - `ObservableList.insert` emits the actual insertion index: in-range
62
+ negatives normalize and out-of-range indexes clamp per stdlib
63
+ `list.insert` semantics, instead of the raw argument leaking into the
64
+ `ItemAdded` payload (spec/21 §3.2). The same normalization applies to
65
+ `ServicedObservableCollection.insert`, `CompositeVM.insert`, and
66
+ `GroupVM.insert` (catalogued vs the C#/TS fail-fast contracts in
67
+ ADR-0009).
68
+ - `FormVM`'s `on_approved` now emits the value that was actually
69
+ persisted rather than the live model (parity with C#'s captured
70
+ payload under a racing `set_model`).
71
+ - `NotificationHub` emits pending snapshots inside the lock (ordering +
72
+ dispose-race discipline, mirroring C#).
73
+ - Post-2.4.0 maintenance backfill: `AggregateVM1..6` dispose ordering
74
+ (LIFE-013) and aggregates walk/dispose drift.
75
+ - `FormVM.builder()` raised `TypeError` on every call (subscripted
76
+ instantiation of a frozen+slots dataclass); it was the only builder
77
+ entrypoint no test had ever exercised.
78
+ - `SearchableState.can_search()` returned `False` when the first item was
79
+ a legal `None` value (sentinel conflation; C#/TS were unaffected).
80
+ - `ConfirmationDecoratorCommand`'s fire-and-forget done-callback raised
81
+ `CancelledError` into the event loop when the task was cancelled.
82
+ - `fluent.confirm()` now types its callback as
83
+ `Callable[[], Awaitable[bool]]`, matching the constructor contract it
84
+ forwards to (a sync callback previously passed mypy and failed at
85
+ `await`).
86
+
87
+ ### Added
88
+
89
+ - `FORM-014` conformance coverage: a disposed `FormVM` is inert — approve
90
+ never invokes the persister, deny does not revert (ADR-0038; pins the
91
+ guards shipped earlier in this release).
92
+
93
+ - `HierarchicalVM.reparent_child` rejects self- and ancestor-reparenting
94
+ with `ValueError` instead of silently corrupting the tree (HIER-018).
95
+ - `NotificationHub.dispose()` — resolves in-flight waiters with `PENDING`,
96
+ completes `pending`, refuses new enqueues, idempotent (NOTIF-017).
97
+ - The `Dispatcher` protocol is exported from the top-level `vmx` package
98
+ (parity with TS `IDispatcher` / C# `IDispatcher`).
99
+ - Idempotent `dispose()` on `DecoratorCommand` and
100
+ `ConfirmationDecoratorCommand` (teardown symmetry with the C#
101
+ IDisposable surface; the decorators own no subscriptions).
102
+
103
+ ## [2.4.0] — 2026-06-02
104
+
105
+ Implements spec v2.4.0 — umbrella publication-readiness + Swift flavor
106
+ sibling + example-app theming scenario contract + test-coverage backfill
107
+ (ADR-0036). Purely additive at the surface level; no behaviour changes
108
+ to existing Python APIs.
109
+
110
+ ### Added
111
+
112
+ - **ThemeVM scenario contract** (example apps): the v2.4.0
113
+ `spec-v2.4.0` cycle defines a normative shape for example-app
114
+ theming (`ThemeModel` + `ThemeVM : ComponentVMOf[ThemeModel]` +
115
+ per-framework `ThemeAdapter`) built from the existing core
116
+ primitives (`ComponentVMOf[M]`, `DerivedProperty[T]`, `RelayCommand`,
117
+ `MessageHub`). No new core types are introduced; the contract is
118
+ implemented by the Textual Notes-Showcase flagship under
119
+ `examples/python/textual/notes_showcase/`. See
120
+ `spec/proposals/2026-06-02-theme-vm-scenario.md` + ADR-0036 §2.C.
121
+
122
+ ### Fixed
123
+
124
+ - **Aggregate parametric test coverage backfill.** The
125
+ `AggregateVM1..6` test suite was expanded with parametric
126
+ per-arity cases for construction, destruction, modeled-hint
127
+ propagation, and dispose-cascade ordering — bringing aggregate-family
128
+ line coverage to **100%** across all six arities. Existing
129
+ Notes-Showcase edge cases (filter / search / paging interaction,
130
+ capability-aware action-bar gating) gained dedicated tests in the
131
+ same pass. No production code changed; tests only.
132
+
133
+ ### Conformance
134
+
135
+ - 5 new IDs (`THEME-001..005`); running total goes from 227 to **232**.
136
+ The Python flavor implements `THEME-001..005` as part of the Textual
137
+ Notes-Showcase flagship's conformance suite (the contract is
138
+ scenario-level, not a core library addition).
139
+
140
+ ### Min spec version
141
+
142
+ - 2.4.0 (previously 2.3.0).
143
+
144
+ ## [2.3.0] — 2026-05-31
145
+
146
+ Implements spec v2.3.0 — builder pattern audit follow-through (ADR-0035).
147
+ Purely additive at the surface level. One behaviour change brings
148
+ `CompositeVMBuilder` and `GroupVMBuilder` into compliance with the
149
+ existing spec §3 contract (see Fixed below); callers that were relying
150
+ on the previously-lazy validation were already buggy.
151
+
152
+ ### Added
153
+
154
+ - **`FormVMBuilder`** (`vmx.forms`) — fluent immutable builder for
155
+ `FormVM` with `.initial(...)` and `.persister(...)` required, and
156
+ optional `.hub(...)`, `.strict(bool)`, `.snapshotter(...)`. Validates
157
+ at `build()`. Conformance: `FORM-011..013`. See ADR-0035 §FV1/FV2.
158
+ - **`HierarchicalVMBuilder`** (`vmx.hierarchical`) — fluent immutable
159
+ builder for `HierarchicalVM` with `.model(...)`,
160
+ `.children_factory(...)`, `.services(hub, dispatcher)` required, and
161
+ optional `.name(...)`, `.hint(...)`, `.eager_children(bool)`. Adds
162
+ `.with_default_services()` Wither for opt-in implicit defaults.
163
+ Validates at `build()`. Conformance: `HIER-015..017`. See ADR-0035
164
+ §H1/H2/H3.
165
+ - **`with_null_services()`** Wither extension on `ComponentVMBuilder`
166
+ (and friends) — chainable convenience that wires
167
+ `NULL_MESSAGE_HUB` + `NULL_DISPATCHER` in one call, for parity with
168
+ the C# `WithNullServices()` extension. See ADR-0035 §SV1.
169
+ - **Typed-arity DerivedProperty factories** — `DerivedProperty.from_one`
170
+ through `DerivedProperty.from_five` with per-source type inference;
171
+ `DerivedProperty.from_many` retained as alias of the existing
172
+ `from_sources(...)` for arbitrary-N consumers. See ADR-0035 §DP2.
173
+
174
+ ### Fixed
175
+
176
+ - `CompositeVMBuilder.build()` and `GroupVMBuilder.build()` now raise
177
+ `BuilderValidationError` when `children` is unset, matching the
178
+ spec/10 §3 contract and the TypeScript flavor's existing behaviour.
179
+ Previously the Python flavor silently accepted a missing `children`
180
+ factory and raised later at `on_construct`. See ADR-0035 §CP1/GR2.
181
+
182
+ ### Conformance
183
+
184
+ - 7 new IDs (`BLD-005`, `FORM-011..013`, `HIER-015..017`); running total
185
+ goes from 220 to 227.
186
+
187
+ ### Min spec version
188
+
189
+ - 2.3.0 (previously 2.2.0).
190
+
191
+ ## [2.2.0] — 2026-05-30
192
+
193
+ ### Added
194
+
195
+ - `AggregateVM6` — sixth-arity heterogeneous aggregate.
196
+ Conformance: `AGG-006`. See ADR-0034.
197
+
198
+ ### Conformance
199
+
200
+ - 1 new ID (`AGG-006`); running total: 220.
201
+
202
+ ### Min spec version
203
+
204
+ - 2.2.0 (previously 2.1.0).
205
+
206
+ ## [2.1.0] — 2026-05-28
207
+
208
+ Implements spec v2.1.0. Purely additive — no breaking changes from v2.0.x.
209
+
210
+ ### Added
211
+
212
+ - **`HierarchicalVM`** (`vmx.hierarchical`) — first-class recursive tree VM with
213
+ lazy/eager child loading, depth-first construction, materialized path,
214
+ parent-change and structural-change hub messages. `TreeStructureChangedMessage`
215
+ new type. (ADR-0028; HIER-001..014)
216
+ - **`DialogService`** + **`NullDialogService`** (`vmx.dialogs`) — host-side
217
+ contract for modal interactions (file pick, confirm prompt, severity-tagged
218
+ notify) distinct from `INotificationHub`. (ADR-0029; DIA-001..008)
219
+ - **`FormVM`** (`vmx.forms`) — snapshot/revert edit lifecycle (ORM-agnostic).
220
+ `deny_command`, `approve_command`, `on_approved` event, optional strict mode.
221
+ `FormRevertedMessage` new type. (ADR-0030; FORM-001..010)
222
+ - **`NotificationVM`** + **`ConfirmationVM`** (`vmx.notifications`) — render-side
223
+ VMs with auto-dismiss (60s/300s default), opacity decay, dismiss/approve/reject
224
+ commands. (ADR-0031; NOTIF-011..016)
225
+ - **`ServicedObservableCollection`** (`vmx.collections`) — observable collection
226
+ with hub publication. (ADR-0024; COL-001..004)
227
+ - **`ObservableList`** (`vmx.collections`) — granular per-mutation events
228
+ (item_added/removed/replaced/reset) with batch suppression. (ADR-0026;
229
+ COL-005..009, COL-023)
230
+ - **`ObservableDictionary`** (`vmx.collections`) — composite-key observable
231
+ dictionary with observable keys1/keys2 views and hub publication. (ADR-0025;
232
+ COL-010..015, COL-022)
233
+ - **`PagedComposition`** (`vmx.collections`) — paging decorator over any
234
+ composition implementing `Pageable`. (ADR-0023; COL-016..021)
235
+ - **`Filterable`** + **`Pageable`** (`vmx.capabilities`) — two new capability
236
+ protocols. (ADR-0022, ADR-0023; CAP-021, CAP-022)
237
+ - **Fluent command helpers** (`vmx.commands`) — `confirm(…)`, `precede_with`,
238
+ `succeed_with`, `wrap_with` extension helpers over commands. (ADR-0027;
239
+ CMD-008..011)
240
+ - **`property_value_changed_messages_for`** helper (`vmx.messages`) —
241
+ function that filters `PropertyChangedMessage` events for a given
242
+ sender + property name and returns an observable stream of the
243
+ property's value snapshots. (ADR-0032; informative)
244
+ - **Conformance**: 67 new IDs (total 219).
245
+
246
+ ### Fixed
247
+
248
+ - `CompositeVM.__setitem__` now clears `current` to None when the
249
+ replaced slot held the current selection, mirroring `_remove_at`.
250
+ Previously `_current` would silently dangle on the removed child.
251
+ - `AggregateVM1.._on_construct` now disposes the previous slot
252
+ instance before invoking the factory on Reconstruct, so the old
253
+ VM's hub subscriptions and command Subjects are released instead of
254
+ lingering until the hub itself is disposed. (Parity with the C# fix.)
255
+ - `NotificationHub.resolve()` now schedules `future.set_result` via
256
+ `loop.call_soon_threadsafe`, making `resolve()` safe to call from a
257
+ thread other than the future's owning event loop (`asyncio.Future`
258
+ itself is not thread-safe).
259
+ - `CompositeCommand.dispose()` no longer iterates a permanently-empty
260
+ `_subscriptions` list (dead state). The merged `can_execute_changed`
261
+ observable is lazy; subscribers' own disposables tear down the
262
+ merged chain when they unsubscribe.
263
+ - `SearchableState.search_term` setter no longer pushes the new value
264
+ through the debounce/recompute pipeline when it equals the current
265
+ value (spec wording: "emission on a new value").
266
+ - `SearchableState.can_search` now uses `next(iter(...), None) is not None`
267
+ instead of `any(True for _ in ...)`, materialising one element
268
+ instead of the entire iterable.
269
+ - `DecoratorCommand.execute` now wraps the inner `execute` call in
270
+ try/finally so the `post_execute` callback always runs — a "busy"
271
+ flag set in `pre_execute` no longer gets stuck when the inner
272
+ command raises.
273
+
274
+ ### Changed
275
+
276
+ - `DerivedProperty`, `SearchableState`, and `ExpandableState` `dispose()`
277
+ methods now call `.dispose()` after `.on_completed()` on each Subject,
278
+ matching the project-wide pattern in `MessageHub` and `RelayCommand`.
279
+ - `properties.derived._apply` uses `cast()` instead of
280
+ `assert isinstance(values, tuple)` so the runtime guard is not
281
+ stripped by `python -O`.
282
+
283
+ ## [2.0.0] — 2026-05-25
284
+
285
+ Implements spec v2.0.0 — capability micro-interfaces, derived properties,
286
+ search/filter, expand/collapse, modeled-CRUD commands, null-object services,
287
+ opt-in notifications sub-package, and a localization hook.
288
+
289
+ ### Added
290
+ - **Capabilities** (`vmx.capabilities`): 20 opt-in micro-interfaces —
291
+ `ISelectable`, `IDeselectable`, `ISelectionTogglable`, `IExpandable`,
292
+ `ICollapsible`, `IExpansionTogglable`, `ISearchable`, `IClosable`,
293
+ `IApprovable`, `ICancelable`, `INewCreatable`, `IDeletable`,
294
+ `IUpdatable`, `ISavable`, `ICurrentDeletable`, `ICurrentUpdatable`,
295
+ `IManagable`, `IConstructable`, `IDestructable`, `IReconstructable`
296
+ (see `src/vmx/capabilities/`).
297
+ - **Helpers** (`vmx.capabilities`): `SearchableState[TItem]` (debounced
298
+ filter), `ExpandableState` (expand/collapse + observable change).
299
+ - **Derived properties** (`vmx.properties`): `DerivedProperty[TValue]` +
300
+ `from_sources(*sources, transform)` factory for N-source computed values
301
+ with `distinct_until_changed` + optional write-back.
302
+ - **Commands**: `ConfirmationDecoratorCommand` + the abstract
303
+ `DecoratorCommand` base, `make_confirm` helper,
304
+ `ModeledCrudCommands[M, VM]` for the CRUD trio
305
+ (create / update_current / delete_current) on modeled composites.
306
+ - **Null-object services** (per ADR-0017): `NullMessageHub`, `NullDispatcher`,
307
+ `NullLocalizer`, plus `NullNotificationHub` (in the notifications package).
308
+ - **Localization** (`vmx.localization`): `ILocalizer` Protocol and
309
+ `NullLocalizer` (identity translator) — the only opinionated localizer
310
+ shipped in core.
311
+ - **Notifications sub-package** (`vmx.notifications`, opt-in): `Notification`,
312
+ `NotificationType`, `NotificationReaction`, `INotificationHub` +
313
+ `NotificationHub` reference impl + `NullNotificationHub`.
314
+ - **Tree utilities**: `walk_expanded(root)` — variant of `walk` that only
315
+ descends into expanded composites (uses the new `IExpandable` capability).
316
+ - **Conformance**: 77 new IDs (`CAP-NNN`, `DPROP-NNN`, `NOTIF-NNN`,
317
+ `LOC-NNN`, `COMP-014..024`, `GRP-007..010`) — total now 152 IDs.
318
+
319
+ ### Internal
320
+ - `vmx.builders._validation.require_field` / `require_services` return
321
+ narrowed values for tighter mypy --strict downstream typing.
322
+ - Dispose paths across `Modeled*` / `Searchable*` / `Expandable*` /
323
+ `Derived*` are guarded with `_disposed` for idempotence.
324
+
325
+ ### Notes
326
+ - The legacy aliases `RelayCommandOfT` / `RelayCommandOfTBuilder` and
327
+ `AggregateVMBuilder1..5` continue to ship in v2.0.0; their removal has
328
+ been deferred to **vmx v3.0.0** (next major). See ADR-0009.
329
+
330
+ ## [1.2.0] — 2026-05-23
331
+
332
+ ### Added
333
+ - `RelayCommandOf` and `RelayCommandOfBuilder` are now the canonical names for
334
+ the parameterised command + builder pair, matching the TypeScript flavor's
335
+ `RelayCommandOf` / `RelayCommandOfBuilder`.
336
+ - `AggregateVM1Builder` through `AggregateVM5Builder` are now the canonical
337
+ builder names for the aggregate VMs, matching the TypeScript flavor's
338
+ `AggregateVMNBuilder` shape.
339
+
340
+ ### Deprecated
341
+ - `RelayCommandOfT` and `RelayCommandOfTBuilder` remain as identity aliases for
342
+ backward compatibility. Removal deferred to **vmx v3.0.0** (was originally
343
+ targeted for v2.0.0; see v2.0.0 Notes and ADR-0009).
344
+ - `AggregateVMBuilder1` through `AggregateVMBuilder5` remain as identity aliases
345
+ for backward compatibility. Removal deferred to **vmx v3.0.0** (was originally
346
+ targeted for v2.0.0; see v2.0.0 Notes and ADR-0009).
347
+
348
+ ### Internal
349
+ - Per-suppression rationale comments added at every `# type: ignore` in
350
+ `vmx.forwarding.composite` and `vmx.components.builders` (10 + 2 sites).
351
+ - `vmx.builders._validation` now declares parameters as `object | None` instead
352
+ of `Any`, with a module docstring explaining why a Hub/Dispatcher Protocol is
353
+ intentionally not used.
354
+ - `vmx.components.base` empty B027-silenced override hooks now carry an inline
355
+ reason in their `noqa` comment.
356
+
357
+ ## [1.1.0] — 2026-05-23
358
+
359
+ ### Added
360
+ - Implements spec-v1.1.0 on top of the v1.0.0 surface.
361
+ - `CompositeVM` / `CompositeVMOf` / `GroupVM`: new `.auto_construct_on_add(bool)` builder option. When `True`, a child added after the container reaches `Constructed` is automatically constructed before the `CollectionChanged(Add)` event fires.
362
+ - `CompositeVM` / `CompositeVMOf` / `GroupVM`: new `batch_update()` method returns a context manager / disposable that suppresses per-mutation `CollectionChanged` events. The outermost handle disposal emits a single `CollectionChanged(Reset)` event iff any mutations occurred during the batch.
363
+ - New `vmx.tree` module with `walk(root)` (DFS pre-order generator) and `find(root, predicate)` (short-circuiting first-match).
364
+ - New conformance IDs: COMP-012, COMP-013, GRP-005, GRP-006, UTIL-001, UTIL-002, UTIL-003 (75/75 catalog coverage).
365
+ - Top-level `vmx.collections` module hosting the canonical `CollectionChangedEvent` (unified across composites and groups).
366
+ - Top-level `vmx` re-exports for the most-used types (`from vmx import ComponentVMOf, MessageHub, RxDispatcher, walk, find`).
367
+
368
+ ### Fixed
369
+ - `GroupVM.dispose()` now cascades depth-first, matching the spec's LIFE-013 contract and the C# behavior.
370
+ - `CompositeVM` factory children now emit `CollectionChanged(Add)` events (previously silent), matching C#.
371
+ - Removed a stale "scaffolding state / Phase 3" docstring from `vmx/__init__.py`.
372
+
373
+ ## [1.0.0] — 2026-05-23
374
+
375
+ ### Added
376
+ - Full implementation of spec-v1.0.0:
377
+ - Lifecycle: `ConstructionStatus` IntEnum + `StatusTransitionError` + JSON-fixture-backed transition validator
378
+ - Messages: `Message`/`TypedMessage` Protocols + `PropertyChangedMessage` + `ConstructionStatusChangedMessage` frozen dataclasses
379
+ - Services: `MessageHub` (Subject-backed hot stream with per-subscription exception isolation) + `Dispatcher` Protocol + `RxDispatcher` (with `immediate()` and `asyncio(loop)` factories)
380
+ - Commands: `RelayCommand` + `RelayCommandOfT[T]` with reactive triggers and immutable frozen-dataclass builders; Execute is gated on can_execute
381
+ - Components: `ComponentVM`, `ComponentVMOf[M]`, `ReadonlyComponentVMOf[M]` with full lifecycle, modeled hint, 5 built-in commands, async variants
382
+ - Composites: `CompositeVM[VM]` + `CompositeVMOf[M, VM]` with selection contract, MutableSequence + Observable[CollectionChangedEvent], async-selection dispatch
383
+ - Groups: `GroupVM[VM]` (children-as-peers; no Current; retains SelectCommand/DeselectCommand)
384
+ - Aggregates: `AggregateVM1`..`AggregateVM5` fixed-arity tuples
385
+ - Forwarding: `ForwardingComponentVM[M]` + `ForwardingCompositeVM[VM]` decorators
386
+ - Background option dispatches construct/destruct on `Dispatcher.background` scheduler
387
+ - 68 conformance tests covering LIFE-001..013, HUB-001..007, PROP-001..004, CMD-001..007, CVM-001..006, COMP-001..011, GRP-001..004, AGG-001..005, FWD-001..003, BLD-001..004, THR-001..004 — all pass.
388
+ - 376+ unit tests across all modules — all pass.
389
+ - Python 3.10–3.13 supported.
390
+ - `mypy --strict` clean across the entire `src/vmx/` tree.
391
+ - Examples: `examples/python/hello_vmx/` (console) and `examples/python/tk_todo_app/` (tkinter MVVM).
392
+ - Getting-started tutorial at `docs/getting-started/python.md`.