plushie 0.0.1 → 0.6.0

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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +22 -0
  3. data/AGENTS.md +595 -0
  4. data/CHANGELOG.md +209 -0
  5. data/CLAUDE.md +1 -0
  6. data/CONTRIBUTING.md +115 -0
  7. data/Gemfile +15 -0
  8. data/{LICENSE → LICENSE.txt} +6 -6
  9. data/README.md +163 -5
  10. data/Rakefile +21 -0
  11. data/Steepfile +52 -0
  12. data/docs/README.md +56 -0
  13. data/docs/guides/01-introduction.md +266 -0
  14. data/docs/guides/02-getting-started.md +472 -0
  15. data/docs/guides/03-your-first-app.md +516 -0
  16. data/docs/guides/04-the-development-loop.md +499 -0
  17. data/docs/guides/05-events.md +548 -0
  18. data/docs/guides/06-lists-and-inputs.md +605 -0
  19. data/docs/guides/07-layout.md +530 -0
  20. data/docs/guides/08-styling.md +599 -0
  21. data/docs/guides/09-animation.md +516 -0
  22. data/docs/guides/10-subscriptions.md +470 -0
  23. data/docs/guides/11-async-and-effects.md +531 -0
  24. data/docs/guides/12-canvas.md +583 -0
  25. data/docs/guides/13-custom-widgets.md +705 -0
  26. data/docs/guides/14-state-management.md +600 -0
  27. data/docs/guides/15-testing.md +649 -0
  28. data/docs/guides/16-shared-state.md +567 -0
  29. data/docs/guides/17-packaging.md +549 -0
  30. data/docs/reference/accessibility.md +420 -0
  31. data/docs/reference/animation.md +482 -0
  32. data/docs/reference/app-lifecycle.md +504 -0
  33. data/docs/reference/built-in-widgets.md +702 -0
  34. data/docs/reference/canvas.md +589 -0
  35. data/docs/reference/commands.md +376 -0
  36. data/docs/reference/composition-patterns.md +816 -0
  37. data/docs/reference/configuration.md +302 -0
  38. data/docs/reference/custom-types.md +203 -0
  39. data/docs/reference/custom-widgets.md +681 -0
  40. data/docs/reference/dsl.md +500 -0
  41. data/docs/reference/events.md +596 -0
  42. data/docs/reference/native-extension.md +636 -0
  43. data/docs/reference/rake-tasks.md +440 -0
  44. data/docs/reference/scoped-ids.md +336 -0
  45. data/docs/reference/subscriptions.md +248 -0
  46. data/docs/reference/testing.md +448 -0
  47. data/docs/reference/themes-and-styling.md +511 -0
  48. data/docs/reference/versioning.md +174 -0
  49. data/docs/reference/windows-and-layout.md +692 -0
  50. data/docs/reference/wire-protocol.md +328 -0
  51. data/docs/stewardship/README.md +109 -0
  52. data/docs/stewardship/concurrency-shape.md +208 -0
  53. data/docs/stewardship/dsl-discipline.md +204 -0
  54. data/docs/stewardship/elm-invariants.md +196 -0
  55. data/docs/stewardship/goals-and-non-goals.md +95 -0
  56. data/docs/stewardship/performance-bar.md +157 -0
  57. data/docs/stewardship/posture.md +118 -0
  58. data/docs/stewardship/resilience.md +159 -0
  59. data/docs/stewardship/roadmap/README.md +20 -0
  60. data/docs/stewardship/simplicity.md +182 -0
  61. data/docs/stewardship/test-discipline.md +154 -0
  62. data/docs/stewardship/triage.md +153 -0
  63. data/docs/stewardship/trust-model.md +112 -0
  64. data/examples/README.md +163 -0
  65. data/examples/async_fetch.rb +71 -0
  66. data/examples/clock.rb +43 -0
  67. data/examples/color_picker.rb +96 -0
  68. data/examples/counter.rb +34 -0
  69. data/examples/notes.rb +219 -0
  70. data/examples/rate_plushie.rb +288 -0
  71. data/examples/shortcuts.rb +79 -0
  72. data/examples/todo.rb +102 -0
  73. data/examples/widgets/color_picker_widget.rb +360 -0
  74. data/examples/widgets/star_rating.rb +153 -0
  75. data/examples/widgets/theme_toggle.rb +141 -0
  76. data/lib/plushie/animation/sequence.rb +58 -0
  77. data/lib/plushie/animation/spring.rb +88 -0
  78. data/lib/plushie/animation/transition.rb +86 -0
  79. data/lib/plushie/animation/tween.rb +264 -0
  80. data/lib/plushie/animation.rb +30 -0
  81. data/lib/plushie/app.rb +82 -0
  82. data/lib/plushie/binary.rb +317 -0
  83. data/lib/plushie/bounded_queue.rb +32 -0
  84. data/lib/plushie/bridge.rb +277 -0
  85. data/lib/plushie/canvas/shape/canvas_image.rb +29 -0
  86. data/lib/plushie/canvas/shape/canvas_svg.rb +25 -0
  87. data/lib/plushie/canvas/shape/canvas_text.rb +31 -0
  88. data/lib/plushie/canvas/shape/circle.rb +31 -0
  89. data/lib/plushie/canvas/shape/clip.rb +22 -0
  90. data/lib/plushie/canvas/shape/dash.rb +26 -0
  91. data/lib/plushie/canvas/shape/drag_bounds.rb +31 -0
  92. data/lib/plushie/canvas/shape/group.rb +80 -0
  93. data/lib/plushie/canvas/shape/hit_rect.rb +25 -0
  94. data/lib/plushie/canvas/shape/line.rb +30 -0
  95. data/lib/plushie/canvas/shape/linear_gradient.rb +26 -0
  96. data/lib/plushie/canvas/shape/path.rb +31 -0
  97. data/lib/plushie/canvas/shape/rect.rb +36 -0
  98. data/lib/plushie/canvas/shape/shape_style.rb +30 -0
  99. data/lib/plushie/canvas/shape/stroke.rb +31 -0
  100. data/lib/plushie/canvas/shape/transform.rb +62 -0
  101. data/lib/plushie/canvas/shape.rb +183 -0
  102. data/lib/plushie/canvas_widget.rb +508 -0
  103. data/lib/plushie/cargo_plushie.rb +121 -0
  104. data/lib/plushie/command/image.rb +78 -0
  105. data/lib/plushie/command/scroll.rb +51 -0
  106. data/lib/plushie/command/text.rb +55 -0
  107. data/lib/plushie/command/window.rb +146 -0
  108. data/lib/plushie/command/window_query.rb +54 -0
  109. data/lib/plushie/command.rb +345 -0
  110. data/lib/plushie/connection.rb +383 -0
  111. data/lib/plushie/data.rb +106 -0
  112. data/lib/plushie/dev_server.rb +113 -0
  113. data/lib/plushie/dsl/buildable.rb +48 -0
  114. data/lib/plushie/effect.rb +170 -0
  115. data/lib/plushie/encode.rb +71 -0
  116. data/lib/plushie/event/diagnostic.rb +221 -0
  117. data/lib/plushie/event/specs.rb +211 -0
  118. data/lib/plushie/event.rb +413 -0
  119. data/lib/plushie/key_modifiers.rb +99 -0
  120. data/lib/plushie/model.rb +34 -0
  121. data/lib/plushie/node.rb +48 -0
  122. data/lib/plushie/protocol/decode.rb +1053 -0
  123. data/lib/plushie/protocol/encode.rb +460 -0
  124. data/lib/plushie/protocol/keys.rb +295 -0
  125. data/lib/plushie/protocol/parsers.rb +66 -0
  126. data/lib/plushie/protocol.rb +23 -0
  127. data/lib/plushie/rake.rb +261 -0
  128. data/lib/plushie/renderer_env.rb +122 -0
  129. data/lib/plushie/renderer_exit.rb +41 -0
  130. data/lib/plushie/route.rb +86 -0
  131. data/lib/plushie/runtime/commands.rb +327 -0
  132. data/lib/plushie/runtime/subscriptions.rb +151 -0
  133. data/lib/plushie/runtime/windows.rb +178 -0
  134. data/lib/plushie/runtime.rb +1306 -0
  135. data/lib/plushie/selection.rb +135 -0
  136. data/lib/plushie/state.rb +136 -0
  137. data/lib/plushie/subscription.rb +279 -0
  138. data/lib/plushie/test/case.rb +62 -0
  139. data/lib/plushie/test/helpers.rb +374 -0
  140. data/lib/plushie/test/rspec.rb +78 -0
  141. data/lib/plushie/test/script/runner.rb +109 -0
  142. data/lib/plushie/test/script.rb +129 -0
  143. data/lib/plushie/test/session.rb +694 -0
  144. data/lib/plushie/test/session_pool.rb +195 -0
  145. data/lib/plushie/test/snapshot.rb +117 -0
  146. data/lib/plushie/test.rb +72 -0
  147. data/lib/plushie/thread_pool.rb +83 -0
  148. data/lib/plushie/timer_scheduler.rb +103 -0
  149. data/lib/plushie/transport/framing.rb +112 -0
  150. data/lib/plushie/transport/tcp_adapter.rb +75 -0
  151. data/lib/plushie/tree/diff.rb +208 -0
  152. data/lib/plushie/tree/search.rb +98 -0
  153. data/lib/plushie/tree.rb +806 -0
  154. data/lib/plushie/type/a11y.rb +102 -0
  155. data/lib/plushie/type/alignment.rb +29 -0
  156. data/lib/plushie/type/anchor.rb +22 -0
  157. data/lib/plushie/type/border.rb +104 -0
  158. data/lib/plushie/type/color.rb +175 -0
  159. data/lib/plushie/type/content_fit.rb +22 -0
  160. data/lib/plushie/type/direction.rb +23 -0
  161. data/lib/plushie/type/filter_method.rb +22 -0
  162. data/lib/plushie/type/font.rb +83 -0
  163. data/lib/plushie/type/gradient.rb +78 -0
  164. data/lib/plushie/type/length.rb +41 -0
  165. data/lib/plushie/type/line_height.rb +42 -0
  166. data/lib/plushie/type/padding.rb +98 -0
  167. data/lib/plushie/type/position.rb +22 -0
  168. data/lib/plushie/type/shadow.rb +79 -0
  169. data/lib/plushie/type/shaping.rb +22 -0
  170. data/lib/plushie/type/style_map.rb +116 -0
  171. data/lib/plushie/type/theme.rb +120 -0
  172. data/lib/plushie/type/wrapping.rb +22 -0
  173. data/lib/plushie/ui.rb +970 -0
  174. data/lib/plushie/undo.rb +208 -0
  175. data/lib/plushie/version.rb +12 -0
  176. data/lib/plushie/widget/build.rb +95 -0
  177. data/lib/plushie/widget/button.rb +18 -0
  178. data/lib/plushie/widget/canvas.rb +43 -0
  179. data/lib/plushie/widget/checkbox.rb +37 -0
  180. data/lib/plushie/widget/column.rb +16 -0
  181. data/lib/plushie/widget/combo_box.rb +41 -0
  182. data/lib/plushie/widget/container.rb +35 -0
  183. data/lib/plushie/widget/floating.rb +24 -0
  184. data/lib/plushie/widget/grid.rb +28 -0
  185. data/lib/plushie/widget/image.rb +37 -0
  186. data/lib/plushie/widget/keyed_column.rb +24 -0
  187. data/lib/plushie/widget/markdown.rb +32 -0
  188. data/lib/plushie/widget/native_build.rb +333 -0
  189. data/lib/plushie/widget/overlay.rb +28 -0
  190. data/lib/plushie/widget/pane_grid.rb +30 -0
  191. data/lib/plushie/widget/pick_list.rb +40 -0
  192. data/lib/plushie/widget/pin.rb +23 -0
  193. data/lib/plushie/widget/pointer_area.rb +38 -0
  194. data/lib/plushie/widget/progress_bar.rb +29 -0
  195. data/lib/plushie/widget/qr_code.rb +29 -0
  196. data/lib/plushie/widget/radio.rb +36 -0
  197. data/lib/plushie/widget/responsive.rb +21 -0
  198. data/lib/plushie/widget/rich_text.rb +90 -0
  199. data/lib/plushie/widget/row.rb +16 -0
  200. data/lib/plushie/widget/rule.rb +24 -0
  201. data/lib/plushie/widget/scrollable.rb +34 -0
  202. data/lib/plushie/widget/sensor.rb +22 -0
  203. data/lib/plushie/widget/slider.rb +35 -0
  204. data/lib/plushie/widget/space.rb +20 -0
  205. data/lib/plushie/widget/stack.rb +23 -0
  206. data/lib/plushie/widget/svg.rb +32 -0
  207. data/lib/plushie/widget/table.rb +84 -0
  208. data/lib/plushie/widget/text.rb +18 -0
  209. data/lib/plushie/widget/text_editor.rb +42 -0
  210. data/lib/plushie/widget/text_input.rb +25 -0
  211. data/lib/plushie/widget/themer.rb +21 -0
  212. data/lib/plushie/widget/toggler.rb +35 -0
  213. data/lib/plushie/widget/tooltip.rb +29 -0
  214. data/lib/plushie/widget/vertical_slider.rb +33 -0
  215. data/lib/plushie/widget/window.rb +24 -0
  216. data/lib/plushie/widget.rb +760 -0
  217. data/lib/plushie/widget_set.rb +82 -0
  218. data/lib/plushie.rb +263 -5
  219. data/sig/base64.rbs +6 -0
  220. data/sig/msgpack.rbs +13 -0
  221. data/sig/plushie/animation.rbs +79 -0
  222. data/sig/plushie/app.rbs +29 -0
  223. data/sig/plushie/binary.rbs +21 -0
  224. data/sig/plushie/bounded_queue.rbs +13 -0
  225. data/sig/plushie/bridge.rbs +53 -0
  226. data/sig/plushie/canvas/shape.rbs +304 -0
  227. data/sig/plushie/canvas_widget.rbs +55 -0
  228. data/sig/plushie/cargo_plushie.rbs +11 -0
  229. data/sig/plushie/command/image.rbs +11 -0
  230. data/sig/plushie/command/scroll.rbs +10 -0
  231. data/sig/plushie/command/text.rbs +11 -0
  232. data/sig/plushie/command/window.rbs +29 -0
  233. data/sig/plushie/command/window_query.rbs +14 -0
  234. data/sig/plushie/command.rbs +101 -0
  235. data/sig/plushie/connection.rbs +63 -0
  236. data/sig/plushie/data.rbs +13 -0
  237. data/sig/plushie/dev_server.rbs +15 -0
  238. data/sig/plushie/dsl/buildable.rbs +13 -0
  239. data/sig/plushie/effect.rbs +31 -0
  240. data/sig/plushie/encode.rbs +10 -0
  241. data/sig/plushie/event.rbs +254 -0
  242. data/sig/plushie/key_modifiers.rbs +31 -0
  243. data/sig/plushie/model.rbs +11 -0
  244. data/sig/plushie/node.rbs +19 -0
  245. data/sig/plushie/protocol.rbs +134 -0
  246. data/sig/plushie/renderer_env.rbs +11 -0
  247. data/sig/plushie/route.rbs +19 -0
  248. data/sig/plushie/runtime/windows.rbs +32 -0
  249. data/sig/plushie/runtime.rbs +208 -0
  250. data/sig/plushie/selection.rbs +23 -0
  251. data/sig/plushie/state.rbs +23 -0
  252. data/sig/plushie/subscription.rbs +39 -0
  253. data/sig/plushie/thread_pool.rbs +19 -0
  254. data/sig/plushie/timer_scheduler.rbs +19 -0
  255. data/sig/plushie/transport/framing.rbs +13 -0
  256. data/sig/plushie/tree/diff.rbs +13 -0
  257. data/sig/plushie/tree/search.rbs +12 -0
  258. data/sig/plushie/tree.rbs +52 -0
  259. data/sig/plushie/type/a11y.rbs +44 -0
  260. data/sig/plushie/type/font.rbs +23 -0
  261. data/sig/plushie/type/gradient.rbs +9 -0
  262. data/sig/plushie/type/line_height.rbs +10 -0
  263. data/sig/plushie/ui.rbs +81 -0
  264. data/sig/plushie/undo.rbs +43 -0
  265. data/sig/plushie/widget/build.rbs +18 -0
  266. data/sig/plushie/widget_dsl.rbs +47 -0
  267. data/sig/plushie/widget_set.rbs +5 -0
  268. data/sig/plushie.rbs +29 -0
  269. metadata +322 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b62b68b40590aee927eee8df2cf4e0f94d3459b9ddbeb2ac15e13c5c73098e96
4
- data.tar.gz: 5178af8c1b52021bd7046e6381db61538323dfb74d58784db3557610f9da08b4
3
+ metadata.gz: eefba997742401e8ca49f13be903aa36ea00b6da2008a18a6b35935913aef9d4
4
+ data.tar.gz: 9bf004d365977ed0de5ae5423d679f138e7b25662552adafe54e1f6b56fa8d75
5
5
  SHA512:
6
- metadata.gz: d7355f1468b38e58343072335cc1207b412ec891c6088cc3e81a9b45c3c262ef908367470c19f0526a22c5adc51afa37b9f71f96a1016f335c56a5aadd221ca2
7
- data.tar.gz: a2a365e21640dc3d2525b1df4f4124a3ad84267e97d8ae0b77fc1451d66889f72c81e3bcce08fe21c0fcdead6ee0fd12fc77a0a9bf27ea0c9741536d85ccd437
6
+ metadata.gz: ed4381541afdc914022cf542fa7d70124efd6156525a590bc89abd5e7a833c020cbd87624e3205df75d6d5567938921eba145f49dac4e740c4d249393d7c1e62
7
+ data.tar.gz: 8d2e5c47b78276e568b4bafd66f2a6926229c9dc5d16b5de5712d00332207200bbb0c5734432535cb2c3a429b71774ff52e153ab93a6a303c8ece7022ac36fe0
data/.yardopts ADDED
@@ -0,0 +1,22 @@
1
+ --markup markdown
2
+ --output-dir doc
3
+ --protected
4
+ --no-private
5
+ --hide-void-return
6
+ --embed-mixins
7
+ -
8
+ docs/getting-started.md
9
+ docs/tutorial.md
10
+ docs/app-behaviour.md
11
+ docs/layout.md
12
+ docs/events.md
13
+ docs/commands.md
14
+ docs/effects.md
15
+ docs/scoped-ids.md
16
+ docs/theming.md
17
+ docs/testing.md
18
+ docs/running.md
19
+ docs/composition-patterns.md
20
+ docs/accessibility.md
21
+ docs/widgets.md
22
+ docs/dsl-internals.md
data/AGENTS.md ADDED
@@ -0,0 +1,595 @@
1
+ # plushie-ruby
2
+
3
+ This file is not version controlled. Do not reference it in commit
4
+ messages, pull requests, or documentation.
5
+
6
+ Native desktop GUI framework for Ruby, powered by iced. Implements
7
+ the Elm architecture (init/update/view) with commands and subscriptions.
8
+ Communicates with the Rust binary over stdin/stdout using MessagePack
9
+ (default) or JSONL.
10
+
11
+ ## Stewardship
12
+
13
+ Direction, trust posture, goals, and explicit non-goals are captured
14
+ in `docs/stewardship/`. That directory is the authority on what work
15
+ the project takes on and what it declines. The summary below is enough
16
+ for routine work; pull the relevant doc when an axis is in play. Use
17
+ `docs/stewardship/triage.md` as the routing tool when the answer is
18
+ not self-evident.
19
+
20
+ Pre-1.0: no backcompat, right design wins, rename across SDKs is fine.
21
+ Post-1.0: stability obligations begin (Hyrum's Law). plushie-rust =
22
+ protocol authority. plushie-elixir = canonical API-shape reference;
23
+ plushie-ruby follows. Cross-SDK parity audited in sibling
24
+ `plushie-sdk-parity/`. Gem is a library, not an auto-bootstrap
25
+ framework.
26
+
27
+ ### Disciplines (non-negotiable)
28
+
29
+ Tests through real renderer; cross-SDK claims verified by reading
30
+ source on each side; design before code at boundaries (public API,
31
+ DSL surface, wire codec, transport contract); clarity is the bar; no
32
+ half-built features; local cleanup not scope creep; no legacy shims
33
+ pre-1.0.
34
+
35
+ ### Goals
36
+
37
+ Wire codec fidelity on host side; cross-SDK concept parity (semantics
38
+ converge, syntax diverges per language); Elm-architecture purity
39
+ (init/update/view, return-shape validation, commands as pure data,
40
+ pure view, declarative subs); lightweight runtime (no idle work, no
41
+ polling, minimal tree diff via LIS); fault tolerance (renderer crash
42
+ auto-recovers + state re-syncs with bounded backoff, app exception
43
+ reverts to last good model, neither side takes the other down); DSL
44
+ clarity.
45
+
46
+ ### Non-goals (declined, not deprioritized)
47
+
48
+ Backcompat before 1.0; per-Ruby API ergonomics that diverge from
49
+ cross-SDK shape; API stability hardening pre-1.0 (single 1.0 sweep,
50
+ not piecemeal); coverage targets as a metric; mocking renderer for
51
+ speed; micro-optimization at cost of readability; refactoring without
52
+ a forcing function; DSL extensions for hypothetical future widgets;
53
+ heavy metaprogramming where ordinary code would do; fiber/ractor
54
+ runtime architectures; defending against speculative deployment shapes.
55
+
56
+ ### Trust model
57
+
58
+ Asymmetric. Renderer-to-host = closed and typed; host structurally
59
+ protected today (typed event decoding, no opaque-blob path, effect/
60
+ query response correlation by wire ID, no host-side eval, no `to_sym`
61
+ on renderer-supplied strings on hot paths, strict enums via Parsers).
62
+ Host-to-renderer = broad by design (file paths, fonts, images,
63
+ screenshots, effects, `--exec`); bounding it is the capability-manifest
64
+ roadmap in plushie-rust. Wire = byte-stream agnostic; confidentiality
65
+ + integrity delegated to outer transport. Same-access (user attacking
66
+ themselves) out of scope.
67
+
68
+ ### Resilience
69
+
70
+ Things-go-wrong axis, not adversary axis. App exception revert in
71
+ init/update/view (rescue StandardError, not Exception); renderer crash
72
+ auto-recovery via Bridge with exponential backoff + fresh snapshot
73
+ re-sync; bridge heartbeat watchdog catches hung renderers; defensive
74
+ parsing on the wire (reject + structured error); return-shape
75
+ validation in `unwrap_result` raises immediately; bounded queue +
76
+ coalescable events for high-frequency sources; subscription failure
77
+ isolated; thread isolation for async commands. Fail-fast on programming-
78
+ error invariant violations and unrecoverable bridge startup. Degrade
79
+ gracefully on user-facing input. Log suppression after 100 consecutive
80
+ errors. Don't `rescue Exception` (swallows interrupts).
81
+
82
+ ### Performance
83
+
84
+ Lightweight = baseline, not optimization-after-fact. Don't do
85
+ unnecessary work in the first place; cost compounds. Worth doing
86
+ without benchmark (readability preserved/improved): consolidate
87
+ redundant traversals, right data structure, avoid unnecessary `each`/
88
+ `map` passes, move per-frame work that doesn't depend on per-frame
89
+ inputs to the edge. Need benchmark first (readability cost real):
90
+ clever encoding, big-O without realistic N, optimization on idle paths.
91
+ Ruby axes: GVL serializes Ruby across threads (push CPU-bound user
92
+ work to `Command.task`); allocation discipline (`frozen_string_literal`
93
+ everywhere, `Data.define` over Struct, avoid runtime string building
94
+ on hot paths); GC pauses; no `to_sym` on untrusted strings. Numeric
95
+ direction: 16.67ms frame budget at a few hundred to ~1000 nodes; idle
96
+ CPU = no measurable work; tree diff is the load-bearing piece (LIS-
97
+ based child reorder, memo cache, widget view cache).
98
+
99
+ ### Test discipline
100
+
101
+ Integration spine: tests exercise real renderer (default `mock`
102
+ backend = real binary, real wire, real Core, no GPU). Three modes
103
+ (cross-SDK contract): mock (default, fastest), headless (tiny-skia,
104
+ pixels), windowed (headless weston preferred, Xvfb works for X11).
105
+ Pooled mock backend
106
+ multiplexes via `--max-sessions N`. Both Minitest (`Plushie::Test::Case`)
107
+ and RSpec (`Plushie::Test::RSpec`) are first-class. Stubs acceptable
108
+ only for forced crash sim, malformed wire bytes, direct `update` shape
109
+ tests, test infra. Sync via the Session API, never via
110
+ `instance_variable_get` into the runtime. Tests as documentation;
111
+ slow tests = slow code; failing test before fix. Flaky tests are real
112
+ bugs, not tests to retry.
113
+
114
+ ### Simplicity
115
+
116
+ Clarity = constraint, not aspiration. Reader-cost compounds.
117
+ Readability wins ties. Abstraction earns its place: 3 similar lines
118
+ > premature abstraction; 3rd use earns consideration not commitment;
119
+ single-user mixin = costume; "we might need this someday" = reason
120
+ not to extract. Local complexity > global. Cohesion across file >
121
+ brevity of any one file. Mixed-paradigm flavor: pure where possible,
122
+ immutable (`Data.define` records, frozen `with` semantics), pattern
123
+ matching for events, composition over inheritance (single-level
124
+ mixins, no multi-level framework hierarchies), errors as values where
125
+ clean else raise, blocks over higher-order callbacks. Comments answer
126
+ why-not-what. RBS in `sig/` is real type info; drift from
127
+ implementation = bug.
128
+
129
+ ### Elm invariants
130
+
131
+ `init` and `update` return: bare model | `[model, Command::Cmd]` |
132
+ `[model, [Command::Cmd, ...]]`. Anything else raises `ArgumentError`
133
+ from `unwrap_result`. Commands are pure data; runtime executes.
134
+ `view(model)` is pure function of model; top level must be `window(...)`
135
+ node or array of windows (`Tree.normalize` raises otherwise). Subs
136
+ declarative; runtime diffs each cycle (short-circuits when only
137
+ `max_rate` changed). Widget event flow walks scope chain innermost-
138
+ first; handlers return `:ignored`/`:consumed`/`[:update_state, _]`/
139
+ `[:emit, family, data]`. Canvas-internal events auto-consumed if not
140
+ captured. Wire IDs: `window#scope/path/id`; events split into
141
+ `id`/`scope`/`window_id`; `Event.target(event)` reconstructs forward
142
+ path; commands use forward-order path strings.
143
+
144
+ ### DSL discipline
145
+
146
+ Block-based DSL via `include Plushie::App`. Blocks yield in caller's
147
+ binding (no `instance_eval` against user blocks; `self` stays the
148
+ app instance, private helpers work). Thread-local context stack tracks
149
+ parent-child. New DSL form earns its place when: 2+ real users,
150
+ replaces harder-to-read runtime construct, real bug class detectable
151
+ at finalization time, generated code reads as cleanly as hand-written,
152
+ errors point at user's call site. Used: `define_method` for setters,
153
+ `class_eval(&block)` for `Widget.define`, module hooks, frozen
154
+ `Data.define` records. Avoided: `instance_eval` against user blocks
155
+ for the UI DSL, `method_missing` as routing, broad refinements, deep
156
+ inheritance. Generated code is what users read in stack traces; stable
157
+ predictable structure, named methods match expectations, errors name
158
+ DSL context.
159
+
160
+ ### Concurrency shape
161
+
162
+ Three layers: Connection (pipe + framing + writer mutex + reader
163
+ thread), Bridge (restart logic + heartbeat watchdog + exponential
164
+ backoff), Runtime (Elm loop + model + tree + commands). Runtime
165
+ thread is the only thread that touches app state; other threads push
166
+ tagged tuples through a `BoundedQueue`. Dedicated threads for
167
+ `Command.task`/`Command.stream` (cancellable); single `TimerScheduler`
168
+ thread (deadline-based `IO.select`). GVL-aware: CPU-bound user work
169
+ belongs in `Command.task`; runtime doesn't block on I/O. Transport
170
+ adapters (spawn/stdio/iostream) earn their place. SessionPool
171
+ multiplexes mock/headless via `--max-sessions N`. No `Mutex` for app
172
+ state, no concurrent-ruby, no Async reactor, no fibers/ractors, no
173
+ auto-bootstrap.
174
+
175
+ ### Common shapes -> outcomes
176
+
177
+ - "mock the renderer for speed" -> decline
178
+ - "reach into runtime via `instance_variable_get`" -> rewrite to
179
+ Session API
180
+ - "add deprecation warnings / API hardening" -> decline; 1.0 sweep
181
+ - "this is O(n) on a hot path" -> need realistic N
182
+ - "split this large module" -> need forcing function
183
+ - "harden against malicious renderer" -> structurally protected;
184
+ check if proposal loosens that, otherwise misframed
185
+ - "harden against malicious host" -> defer to capability-manifest
186
+ (plushie-rust roadmap)
187
+ - "wire should encrypt / sign" -> outer transport's job
188
+ - "consolidate N redundant traversals" -> do
189
+ - "extract this single-use mixin" -> decline; costume
190
+ - "rescue Exception in this loop" -> no, `rescue StandardError`
191
+ - "let users return `nil` from update" -> no, bare model is no-change
192
+ - "rename field across SDKs" -> route through parity workflow
193
+ - "use `instance_eval` for the block DSL" -> no, breaks user `self`
194
+ - "switch runtime to fibers / ractors" -> stewardship-level question
195
+ - "add `method_missing` routing" -> default no; explicit definition
196
+ - "add a new DSL declaration form" -> run dsl-discipline criteria
197
+
198
+ ## Before committing
199
+
200
+ Run `bundle exec rake`. It mirrors CI: tests, linter, type check.
201
+
202
+ For full preflight (including headless renderer tests against a fresh
203
+ build), run `bundle exec rake plushie:preflight`. When
204
+ `PLUSHIE_RUST_SOURCE_PATH` is set to a plushie-rust checkout, preflight
205
+ runs `cargo build --release -p plushie-renderer` against that workspace
206
+ first and exports `PLUSHIE_BINARY_PATH` so the headless tests use the
207
+ freshly built binary. Without it, the existing binary resolution chain
208
+ runs unchanged.
209
+
210
+ ## Commit hygiene
211
+
212
+ Every commit should be self-contained and functional. Preflight
213
+ should pass at each commit, not just at the tip.
214
+
215
+ Commits after `origin/main` are unpublished and can be freely
216
+ amended, squashed, or reordered to keep the history clean. Run
217
+ `git fetch origin` first to ensure the boundary is current. Use
218
+ `--amend` to fold small fixes into the commit they belong to
219
+ rather than creating "fix the fix" commits. If a later commit
220
+ fixes a bug introduced by an earlier unpublished commit, squash
221
+ them together.
222
+
223
+ Never amend or rebase commits that are already on `origin/main`.
224
+
225
+ ## Commit messages
226
+
227
+ Commit messages should describe what changed and why. Do not include:
228
+ - Counts of any kind (findings, files, tests, items). If the
229
+ content is listed, the reader can count. Counts add noise.
230
+ - Ticket, review, or tracking IDs (R-001, PROJ-123, etc.)
231
+ - References to this file
232
+
233
+ More broadly, think carefully before including counts anywhere
234
+ (code comments, docs, log messages). If the count is derivable
235
+ from the surrounding content, it doesn't add value.
236
+
237
+ ## Writing style
238
+
239
+ Do not use `--` (double dash) as a separator or em-dash substitute
240
+ in prose, docs, comments, or bullet lists. Use a single `-` for
241
+ list item separators and reword sentences to avoid inline dashes
242
+ (use commas, periods, colons, or parentheses instead). `--` should
243
+ only appear as part of CLI flag names (e.g. `--watch`, `--release`).
244
+
245
+ ## Quick reference
246
+
247
+ ```
248
+ bundle exec rake # tests + linter + type check
249
+ bundle exec rake test # tests only
250
+ bundle exec rake standard # linter only
251
+ bundle exec rake steep # type check only
252
+ bundle exec rake yard # generate API docs to doc/
253
+ bundle exec ruby examples/counter.rb # run an example (needs binary)
254
+ ```
255
+
256
+ Test backend selection:
257
+ ```
258
+ bundle exec rake test # mock (default, fastest)
259
+ PLUSHIE_TEST_BACKEND=headless bundle exec rake test # real rendering, no display
260
+ PLUSHIE_TEST_BACKEND=windowed bundle exec rake test # real windows (needs weston or Xvfb)
261
+ ```
262
+
263
+ ## Configuration
264
+
265
+ Environment variables:
266
+ - `PLUSHIE_BINARY_PATH`: path to the renderer binary (overrides all resolution)
267
+ - `PLUSHIE_RUST_SOURCE_PATH`: path to a local plushie-rust checkout.
268
+ When set, `rake plushie:build` runs cargo-plushie out of that
269
+ checkout via `cargo run -p cargo-plushie` (no install required).
270
+ - `PLUSHIE_TEST_BACKEND`: test backend: `mock`, `headless`, `windowed`
271
+ - `PLUSHIE_BIN_FILE`: override binary destination for download/build tasks
272
+ - `PLUSHIE_WASM_DIR`: override WASM output directory for download tasks
273
+
274
+ The native widget build delegates workspace generation and
275
+ `cargo build` to `cargo-plushie`. See `docs/versioning.md` for how
276
+ the installed cargo-plushie version is pinned to
277
+ `Plushie::PLUSHIE_RUST_VERSION`.
278
+
279
+ ## Known gotchas
280
+
281
+ - **`width: "fill"` on intermediate containers.** Iced containers
282
+ default to shrink-wrapping. If a row or column doesn't have
283
+ `width: "fill"`, its children's `width: "fill"` has nothing to
284
+ fill. This is the most common layout bug. Always check parent
285
+ containers when a widget collapses to zero width.
286
+
287
+ - **Event field access: `event.value` not `event.data`.**
288
+ All event payloads use the `value` field. Scalar events
289
+ (input, slider) carry the value directly; structured events
290
+ (pointer, key) carry a symbol-keyed Hash. There is no `data`
291
+ field. Tests with manually constructed events hide type
292
+ mismatches; always test through the renderer with
293
+ `Plushie::Test::Case`.
294
+
295
+ - **Native widget tests need `new_instance()` in Rust.** The
296
+ test framework's session pool multiplexes sessions over one
297
+ renderer process. Native widgets that don't implement
298
+ `new_instance()` cause the pool to hang. Add it to every
299
+ PlushieWidget impl.
300
+
301
+ - **Steep and Widget.define blocks.** Steep cannot analyze the
302
+ `class_eval` block inside `Widget.define` because it resolves
303
+ `self` to the Widget module, not the new class. Widget files
304
+ are excluded from the Steepfile check list. Their RBS
305
+ declarations are retained for downstream type consumers.
306
+
307
+ ## Ruby version
308
+
309
+ Requires Ruby >= 3.2 for `Data.define` and stable pattern matching.
310
+ Developed on Ruby 4.0+.
311
+
312
+ ## Dependencies
313
+
314
+ Required (gemspec):
315
+ - `msgpack`: MessagePack encoding/decoding
316
+ - `logger`: standard Ruby logging (extracted from stdlib in Ruby 4.0)
317
+
318
+ Dev/test (Gemfile only):
319
+ - `minitest`: testing framework
320
+ - `standard`: linting (zero-config RuboCop)
321
+ - `rake`: task runner
322
+
323
+ ## Project layout
324
+
325
+ ```
326
+ lib/
327
+ plushie.rb # top-level: Plushie.run / Plushie.start API
328
+ plushie/
329
+ version.rb # VERSION + PLUSHIE_RUST_VERSION constants
330
+ model.rb # Model.define wrapper (Data.define + #with)
331
+ node.rb # Node: immutable UI tree node (Data.define)
332
+ app.rb # App module (include in user classes)
333
+ ui.rb # block-based DSL (thread-local context stack)
334
+ event.rb # all event Data types (Widget, Key, Ime, etc.)
335
+ event/
336
+ specs.rb # canonical event type catalog (BuiltinSpecs)
337
+ command.rb # Command.Cmd Data type + constructor facade
338
+ command/
339
+ text.rb scroll.rb # command submodules (text, scroll,
340
+ window.rb window_query.rb # window, window_query, image)
341
+ image.rb
342
+ subscription.rb # Subscription.Sub Data type + constructors
343
+ effect.rb # platform effects (file dialogs, clipboard, etc.)
344
+ tree.rb # normalization, delegation to search/diff
345
+ tree/
346
+ search.rb # find, exists?, ids, find_first, find_all
347
+ diff.rb # LIS-based tree diff (patch generation)
348
+ connection.rb # low-level protocol client (pipe management)
349
+ bridge.rb # renderer lifecycle (restart, backoff)
350
+ runtime.rb # Elm update loop, command/subscription engine
351
+ runtime/
352
+ commands.rb # command execution engine
353
+ subscriptions.rb # subscription lifecycle diffing
354
+ protocol.rb # wire protocol facade
355
+ protocol/
356
+ encode.rb # outbound encoding (all message types)
357
+ decode.rb # inbound decoding (all event families)
358
+ keys.rb # named key and physical key wire maps
359
+ parsers.rb # string-to-symbol parsers (mouse, modifiers)
360
+ binary.rb # renderer binary resolution + download
361
+ cargo_plushie.rb # cargo-plushie resolver (source vs PATH)
362
+ thread_pool.rb # simple bounded thread pool for async
363
+ encode.rb # canonical value encoding (fail-fast)
364
+ widget.rb # unified widget system (Widget.define + include)
365
+ widget_set.rb # widget DSL overrides (WidgetSet.create)
366
+ canvas_widget.rb # canvas widget extension system
367
+ renderer_env.rb # filtered subprocess environment (whitelist)
368
+ dsl/
369
+ buildable.rb # Buildable pattern for DSL block types
370
+ type/ # property type modules
371
+ a11y.rb alignment.rb anchor.rb border.rb color.rb
372
+ content_fit.rb direction.rb filter_method.rb font.rb
373
+ gradient.rb length.rb line_height.rb padding.rb position.rb
374
+ shadow.rb shaping.rb style_map.rb theme.rb wrapping.rb
375
+ widget/ # typed builder modules
376
+ build.rb # shared build helpers
377
+ native_build.rb # native widget build: virtual manifest + cargo-plushie shell-out
378
+ button.rb canvas.rb checkbox.rb column.rb combo_box.rb
379
+ container.rb floating.rb grid.rb image.rb keyed_column.rb
380
+ markdown.rb overlay.rb pane_grid.rb pick_list.rb pin.rb
381
+ pointer_area.rb progress_bar.rb qr_code.rb radio.rb
382
+ responsive.rb rich_text.rb row.rb rule.rb scrollable.rb
383
+ sensor.rb slider.rb space.rb stack.rb svg.rb table.rb
384
+ text.rb text_editor.rb text_input.rb themer.rb toggler.rb
385
+ tooltip.rb vertical_slider.rb window.rb
386
+ canvas/
387
+ shape.rb # pure shape builder functions
388
+ shape/ # typed shape structs
389
+ canvas_image.rb canvas_svg.rb canvas_text.rb circle.rb
390
+ clip.rb dash.rb drag_bounds.rb group.rb hit_rect.rb
391
+ line.rb linear_gradient.rb path.rb rect.rb
392
+ shape_style.rb stroke.rb transform.rb
393
+ transport/
394
+ framing.rb # frame encode/decode for raw byte streams
395
+ tcp_adapter.rb # iostream adapter for TCP sockets
396
+ animation.rb # animation system (descriptors + SDK-side tween)
397
+ animation/
398
+ transition.rb # renderer-side timed transition descriptor
399
+ spring.rb # renderer-side spring physics descriptor
400
+ sequence.rb # renderer-side sequential animation chain
401
+ tween.rb # SDK-side interpolation (host-driven)
402
+ route.rb # navigation stack
403
+ selection.rb # single/multi/range selection
404
+ undo.rb # undo/redo with coalescing
405
+ data.rb # DataQuery: filter/search/sort/paginate
406
+ state.rb # path-based state with revisions
407
+ key_modifiers.rb # modifier query predicates
408
+ dev_server.rb # file watcher + hot reload
409
+ rake.rb # Rake task definitions
410
+ test.rb # test framework loader
411
+ test/
412
+ case.rb # Minitest case with setup/teardown
413
+ rspec.rb # RSpec integration helpers
414
+ helpers.rb # test DSL (click/find/assert_text/etc.)
415
+ session.rb # test session (Elm loop + renderer I/O)
416
+ session_pool.rb # shared renderer process, session mux
417
+ snapshot.rb # tree hash + screenshot assertions
418
+ script.rb # .plushie script parser
419
+ script/
420
+ runner.rb # script executor
421
+ examples/
422
+ counter.rb clock.rb todo.rb async_fetch.rb
423
+ notes.rb shortcuts.rb color_picker.rb rate_plushie.rb
424
+ widgets/ # example custom widget extensions
425
+ color_picker_widget.rb star_rating.rb theme_toggle.rb
426
+ ```
427
+
428
+ ## Architecture
429
+
430
+ ### Layered design
431
+
432
+ - **Layer 0: Wire** (`Protocol::Encode`, `Protocol::Decode`).
433
+ Pure encoding/decoding for every message type in protocol.md.
434
+ No I/O; works with any data source.
435
+
436
+ - **Layer 1: Connection** (`Plushie::Connection`).
437
+ Manages the pipe to the renderer binary. Spawns the process,
438
+ handles framing, thread-safe writes via Mutex, reader thread
439
+ pushes decoded messages to a Queue. Usable standalone for
440
+ scripting and custom architectures.
441
+
442
+ - **Layer 2: Runtime** (`Plushie::Runtime`).
443
+ The Elm update loop. init/update/view cycle, tree diffing,
444
+ command execution (async via ThreadPool, timers, widget/window
445
+ ops, effects), subscription lifecycle, error recovery, renderer
446
+ restart (exponential backoff).
447
+
448
+ - **Layer 3: App** (`Plushie::App`).
449
+ `include Plushie::App` gives the UI DSL, default callbacks,
450
+ convenience aliases (Event, Command, Subscription).
451
+
452
+ - **Layer 4: Test** (`Plushie::Test`).
453
+ Session pool manages a shared `plushie --mock --max-sessions N`
454
+ process. Each test gets an isolated session. All three backends
455
+ (mock/headless/windowed) are transparent.
456
+
457
+ ### Concurrency model
458
+
459
+ All state lives in the runtime thread. Events are processed
460
+ sequentially from a `Thread::Queue`. No shared mutable state
461
+ between threads; the Queue is the sole synchronization point.
462
+
463
+ - **Runtime thread**: sequential event processing
464
+ - **Bridge thread**: reads renderer stdout, pushes to queue
465
+ - **Thread pool**: bounded (CPU count) for Command.async work
466
+ - **Timer threads**: for send_after and subscription timers
467
+ - **Write mutex**: Connection#send_message is thread-safe
468
+
469
+ ## Testing
470
+
471
+ All app testing goes through the renderer binary. No Ruby-side
472
+ mocks or stubs. The mock backend is fast enough for TDD.
473
+
474
+ Test flow:
475
+ 1. Session pool starts `plushie --mock --max-sessions N`.
476
+ 2. Test gets a session ID, sends Settings + initial Snapshot.
477
+ 3. `click("#btn")` sends Interact to the renderer.
478
+ 4. Renderer returns synthetic events via interact_response.
479
+ 5. Test feeds events through app.update, re-renders, patches.
480
+ 6. `assert_text("#count", "1")` sends Query and checks result.
481
+
482
+ Headless mode uses interact_step round-trips (renderer injects
483
+ real iced events, waits for snapshot back after each step).
484
+
485
+ ## Non-obvious patterns
486
+
487
+ **Thread-local DSL context.** The block-based UI DSL uses a
488
+ thread-local stack (`UI::Context`) to track parent-child
489
+ relationships. Blocks execute in the caller's binding (no
490
+ `instance_eval`), so `self` stays the app instance and private
491
+ helpers work. Widget calls (e.g. `button(...)`) append to the
492
+ current context as side effects, not return values.
493
+
494
+ **Model.define immutability.** `Model.define` wraps `Data.define`
495
+ and adds `.with()` for partial updates returning new frozen
496
+ instances. Forgetting to reassign the result is a silent no-op.
497
+
498
+ **Encode fail-fast.** `Encode.encode_value` raises `ArgumentError`
499
+ on unknown types immediately, never silently passing through.
500
+ Custom types implement `.to_wire()` for wire serialization.
501
+
502
+ **Return validation.** `update` must return a bare model or
503
+ `[model, command]`. Invalid shapes raise with a helpful message.
504
+
505
+ **Error recovery.** Exceptions in update/view are rescued
506
+ (`StandardError` only), logged, and the previous model is
507
+ preserved. After 100 consecutive errors, log output is suppressed
508
+ entirely until every 1000th error.
509
+ `NoMatchingPatternError` gets a special message suggesting an
510
+ `else` clause.
511
+
512
+ **Bridge restart.** When the renderer crashes, exponential backoff
513
+ restart (100ms to 1600ms, max 5 retries). The Bridge handles
514
+ connection lifecycle; the Runtime owns the resync (re-sends
515
+ settings, fresh snapshot, subscription sync).
516
+
517
+ **Effect tracking.** Effects use a two-way wire ID mapping
518
+ (`@effect_tags`, `@effect_ids`). Responses arrive with a wire ID
519
+ that the runtime maps back to the user's tag for delivery as
520
+ `Event::Effect`.
521
+
522
+ **Subscription diffing.** After each update, `sync_subscriptions`
523
+ diffs the new set against active ones. If only `max_rate` changed
524
+ (keys unchanged), it short-circuits and updates rates without
525
+ re-creating subscriptions. Renderer subscriptions (key press, etc.)
526
+ do not take a tag; the management key is `[type, window_id]`.
527
+
528
+ **Widget system.** One unified system for all widgets. Two entry
529
+ points: `Widget.define(:type) { ... }` for declarative widgets
530
+ (leaf, container, native) and `include Plushie::Widget` for
531
+ behavioral widgets (with view/state/events). Both share the same
532
+ DSL (prop, children, positional, default_a11y, state, event,
533
+ cache_key) and finalization pipeline. Widget.define returns a
534
+ fully-formed class (mirrors Data.define). Classes using `include`
535
+ are lazily finalized on first instantiation. Widget view output
536
+ is rendered during tree normalization. Events flow through the
537
+ scope chain before reaching `app.update()`.
538
+
539
+ **A11y defaults.** Widgets declare `default_a11y role: :button,
540
+ label_from: :label`. During build, `Build.resolve_a11y` produces
541
+ a string-keyed hash (wire-ready format). The `:label_from`
542
+ directive copies the named prop value into the `label` field.
543
+ User-provided a11y overrides win per field.
544
+
545
+ **Event::Specs.** Canonical catalog of all built-in widget event
546
+ types with carrier (:none/:value), field declarations, and type
547
+ hints. Widget events have category predicates: `event.pointer?`,
548
+ `event.keyboard?`, `event.pane?`, `event.focus?`, `event.drag?`.
549
+
550
+ **Canvas widget registry.** Registry keys are scoped IDs directly
551
+ (e.g. `"main#form/rating"`), matching the normalized tree ID
552
+ format. No separate composite key; `widget_key(window, local)`
553
+ just joins with `#`.
554
+
555
+ **Tree submodules.** Search and diff are extracted into
556
+ `Tree::Search` and `Tree::Diff`. Tree delegates to them. Both
557
+ are type-checked by Steep. The LIS-based diff algorithm produces
558
+ minimal patches for reordered children.
559
+
560
+ **Memo caching.** `UI::MemoCache` is a thread-local prev/current
561
+ cache. The runtime seeds it before each render and captures it
562
+ after. Memo nodes (`__memo__`) check the cache by deps; widget
563
+ `cache_key` checks by props+state. Both skip re-rendering on
564
+ cache hit.
565
+
566
+ **Status-based focus tracking.** The renderer emits `status`
567
+ events for interactive widgets. The runtime intercepts these to
568
+ track `@focused_widget_id` and derives `:focused`/`:blurred`
569
+ Widget events from status transitions. No per-widget opt-in
570
+ needed.
571
+
572
+ **Bridge heartbeat.** A watchdog timer detects hung renderers.
573
+ When no message arrives within `heartbeat_interval` (default
574
+ 30s), a synthetic close is pushed to trigger the restart path.
575
+
576
+ ## Reference SDK
577
+
578
+ The plushie-elixir SDK (`../plushie-elixir/`) is the primary
579
+ reference for Ruby due to similar dynamic language conventions.
580
+ Consult it for architecture patterns when adding features.
581
+
582
+ ## Related repositories
583
+
584
+ These are expected as sibling directories (e.g. `../plushie-rust/`):
585
+
586
+ - plushie-rust - Rust workspace (SDK, widget SDK, renderer)
587
+ - plushie-elixir - Elixir SDK (reference implementation)
588
+ - plushie-gleam - Gleam SDK
589
+ - plushie-iced - vendored iced fork
590
+
591
+ ## Protocol reference
592
+
593
+ The wire protocol is defined in `../plushie-rust/docs/protocol.md`.
594
+ That document is the source of truth for all message types, event
595
+ families, and interaction semantics.