stream_weaver 0.3.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 (381) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +77 -0
  3. data/.beads/README.md +81 -0
  4. data/.beads/config.yaml +54 -0
  5. data/.beads/metadata.json +7 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +91 -0
  8. data/CHANGELOG.md +294 -0
  9. data/CLAUDE.md +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +565 -0
  12. data/Rakefile +13 -0
  13. data/assets/streamweaver-hero.jpg +0 -0
  14. data/bench/baselines/ledger.rb +58 -0
  15. data/bench/baselines/warroom.rb +51 -0
  16. data/bench/fixtures/ledger.rb +76 -0
  17. data/bench/fixtures/warroom.rb +60 -0
  18. data/bench/results/6daf8b8.md +102 -0
  19. data/bench/results/c8d749b.md +102 -0
  20. data/bench/results/eb06ab9.md +102 -0
  21. data/bench/run.rb +151 -0
  22. data/bench/support.rb +84 -0
  23. data/copy_this/dto_run_sample_run_7631.html +220 -0
  24. data/docs/SERVICE_MODE.md +302 -0
  25. data/docs/architecture/how_streamweaver_works.md +772 -0
  26. data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
  27. data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
  28. data/docs/canvas-ipc-session-summary.md +127 -0
  29. data/docs/canvas-panel-workflow.md +172 -0
  30. data/docs/canvas-read.md +147 -0
  31. data/docs/canvas-roadmap.md +275 -0
  32. data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
  33. data/docs/claude-code-companion-skill-spec.md +567 -0
  34. data/docs/components_reference.md +1223 -0
  35. data/docs/crud-patterns.md +265 -0
  36. data/docs/endpoints.md +122 -0
  37. data/docs/for_llms.md +1 -0
  38. data/docs/form-patterns.md +165 -0
  39. data/docs/frontend-only.md +169 -0
  40. data/docs/html-artifact-audit.md +238 -0
  41. data/docs/ideas/2025-01-01-as-a-service.md +203 -0
  42. data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
  43. data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
  44. data/docs/ideas/dhh-review-future-refactors.md +142 -0
  45. data/docs/opal-jamstack.md +47 -0
  46. data/docs/opal-npm-direction.md +184 -0
  47. data/docs/opal-spike-findings.md +194 -0
  48. data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
  49. data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
  50. data/docs/plans/2026-01-02-charts-design.md +288 -0
  51. data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
  52. data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
  53. data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
  54. data/docs/plans/canvas-claude-project-design.md +238 -0
  55. data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
  56. data/docs/plans/org-doc-preview-surfaces.md +293 -0
  57. data/docs/plans/shadcn-polish-plan.md +417 -0
  58. data/docs/porting-artifacts.md +252 -0
  59. data/docs/reference/agent-skills-comparison.md +369 -0
  60. data/docs/reference/travel-state-prd.artifact.html +780 -0
  61. data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
  62. data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
  63. data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
  64. data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
  65. data/docs/research/frontend-only-matrix.md +278 -0
  66. data/docs/research/streamweaver-way-spike-findings.md +529 -0
  67. data/docs/resource-dsl.md +342 -0
  68. data/docs/routing.md +302 -0
  69. data/docs/ruby-ui-comparison.md +319 -0
  70. data/docs/shared-dsl-fragments.md +356 -0
  71. data/docs/streamweaver-for-ai-agents.md +401 -0
  72. data/docs/streamweaver-frontend-vision.md +123 -0
  73. data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
  74. data/docs/streamweaver_canvas/save_example.rb +9 -0
  75. data/docs/templates.md +213 -0
  76. data/docs/testing.md +155 -0
  77. data/docs/theming-hooks.md +281 -0
  78. data/docs/tutorials/the-streamweaver-way.md +882 -0
  79. data/docs/university/capability-inventory.md +137 -0
  80. data/docs/university/dependency-survey.md +114 -0
  81. data/docs/university/design-spec.md +217 -0
  82. data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
  83. data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
  84. data/docs/university/mockups/course-list-dark.png +0 -0
  85. data/docs/university/mockups/course-list-light.png +0 -0
  86. data/docs/university/mockups/course_canvas_mockup.rb +741 -0
  87. data/docs/university/mockups/step-screen-dark.png +0 -0
  88. data/docs/university/mockups/step-screen-light.png +0 -0
  89. data/docs/university/roadmap.md +72 -0
  90. data/docs/university/send-to-coworker.md +77 -0
  91. data/docs/university/worker-session-mining.md +109 -0
  92. data/docs/visual/browser-rendering-spike.md +173 -0
  93. data/docs/visual/builder-visual-plan-analysis.md +378 -0
  94. data/docs/visual/sw-plan-format-exploration.md +511 -0
  95. data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
  96. data/docs/visual-skills/PROGRESS.md +60 -0
  97. data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
  98. data/docs/visual-skills/analysis/components.md +317 -0
  99. data/docs/visual-skills/analysis/overlap.md +360 -0
  100. data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
  101. data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
  102. data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
  103. data/docs/visual-skills/analysis/visual-explainer.md +964 -0
  104. data/docs/visual-skills/blog/blog-series-outline.md +39 -0
  105. data/docs/visual-skills/blog/token-efficiency.md +73 -0
  106. data/docs/visual-skills/design/architecture.md +1725 -0
  107. data/docs/visual-skills/design/codex-review.md +340 -0
  108. data/docs/visual-skills/design/dhh-review.md +209 -0
  109. data/docs/visual-skills/design/evolution.md +100 -0
  110. data/docs/visual-skills/design/gemini-review.md +55 -0
  111. data/docs/visual-skills/design/review-synthesis.md +149 -0
  112. data/docs/visual-skills/implementation/STATE.md +22 -0
  113. data/docs/visual-skills/implementation/plan.md +347 -0
  114. data/docs/visual-skills/implementation/spike-findings.md +237 -0
  115. data/docs/visual-skills/implementation/tasks.md +702 -0
  116. data/docs/visual-skills/lessons-learned/process.md +145 -0
  117. data/examples/README.md +96 -0
  118. data/examples/advanced/all_components.rb +177 -0
  119. data/examples/advanced/examples_browser.rb +394 -0
  120. data/examples/advanced/teachables_browser.rb +261 -0
  121. data/examples/advanced/theme_tweaker.rb +366 -0
  122. data/examples/advanced/tutorial.rb +1528 -0
  123. data/examples/agentic/agentic_form.rb +30 -0
  124. data/examples/agentic/agentic_form_autoclose.rb +30 -0
  125. data/examples/agentic/cultivation_tracker.rb +69 -0
  126. data/examples/basic/hello_world.rb +22 -0
  127. data/examples/basic/opal_tabs_table.rb +44 -0
  128. data/examples/basic/todo_list.rb +39 -0
  129. data/examples/button_loading_test.rb +36 -0
  130. data/examples/canvas/mermaid_canvas_demo.sh +151 -0
  131. data/examples/charts/bar_chart_demo.rb +57 -0
  132. data/examples/charts/line_chart_demo.rb +73 -0
  133. data/examples/charts/pie_area_demo.rb +87 -0
  134. data/examples/charts/stacked_bar_chart_demo.rb +69 -0
  135. data/examples/claude_code/README.md +85 -0
  136. data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
  137. data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
  138. data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
  139. data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
  140. data/examples/claude_code/codebreaker/README.md +213 -0
  141. data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
  142. data/examples/claude_code/tutorial/README.md +102 -0
  143. data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
  144. data/examples/claude_code/verification_flow/README.md +126 -0
  145. data/examples/components/annotated_code_demo.rb +54 -0
  146. data/examples/components/callout_demo.rb +33 -0
  147. data/examples/components/checkbox_group_demo.rb +46 -0
  148. data/examples/components/design_review.css +468 -0
  149. data/examples/components/design_review_demo.rb +44 -0
  150. data/examples/components/design_review_dsl.rb +300 -0
  151. data/examples/components/diff_block_demo.rb +112 -0
  152. data/examples/components/events_demo.rb +184 -0
  153. data/examples/components/form_demo.rb +62 -0
  154. data/examples/components/lesson_demo.rb +53 -0
  155. data/examples/components/markdown_demo.rb +110 -0
  156. data/examples/components/mermaid_demo.rb +166 -0
  157. data/examples/components/pareto_set.rb +64 -0
  158. data/examples/components/prd_demo.rb +21 -0
  159. data/examples/components/prd_dsl.rb +385 -0
  160. data/examples/components/quiz_demo.rb +47 -0
  161. data/examples/components/run_viewer_demo.rb +100 -0
  162. data/examples/components/score_and_collapsible_demo.rb +82 -0
  163. data/examples/components/select_stale_value_smoke_test.rb +39 -0
  164. data/examples/components/table_demo.rb +247 -0
  165. data/examples/components/timer_with_state_demo.rb +58 -0
  166. data/examples/components/todo_due_dates.rb +63 -0
  167. data/examples/components/uat_gaps_demo.rb +112 -0
  168. data/examples/dashboard/feed_simulator.rb +60 -0
  169. data/examples/dashboard/live_dashboard.rb +80 -0
  170. data/examples/dashboard_components.rb +108 -0
  171. data/examples/deferred_fragments_demo.rb +62 -0
  172. data/examples/generate_more_spike/README.md +63 -0
  173. data/examples/generate_more_spike/app.rb +441 -0
  174. data/examples/git_health.sh +401 -0
  175. data/examples/layout/layout_components_demo.rb +174 -0
  176. data/examples/layout/modal_demo.rb +215 -0
  177. data/examples/layout/navigation_demo.rb +227 -0
  178. data/examples/layout/route_tabs_demo.rb +52 -0
  179. data/examples/layout/routing_demo.rb +52 -0
  180. data/examples/layout/scroll_box_demo.rb +154 -0
  181. data/examples/lazy_fragments_demo.rb +151 -0
  182. data/examples/my_todos/README.md +29 -0
  183. data/examples/my_todos/my_todos.rb +312 -0
  184. data/examples/my_todos/store.rb +86 -0
  185. data/examples/opal/reactive_demo.rb +62 -0
  186. data/examples/opal/scenarios/s1_counter.rb +15 -0
  187. data/examples/opal/scenarios/s2_search_filter.rb +17 -0
  188. data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
  189. data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
  190. data/examples/opal/scenarios/s5_watch.rb +39 -0
  191. data/examples/opal/scenarios/s6_on_start.rb +20 -0
  192. data/examples/opal/scenarios/s7_wizard.rb +33 -0
  193. data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
  194. data/examples/opal/scenarios/s9_dashboard.rb +27 -0
  195. data/examples/operations_dashboard_demo.rb +167 -0
  196. data/examples/panel_demo.sh +447 -0
  197. data/examples/parity/assets/tyrion_slice.css +86 -0
  198. data/examples/parity/rivet_people_slice.rb +182 -0
  199. data/examples/parity/tyrion_components.css +777 -0
  200. data/examples/parity/tyrion_warroom_components.rb +414 -0
  201. data/examples/parity/tyrion_warroom_slice.rb +549 -0
  202. data/examples/puma_dev/README.md +124 -0
  203. data/examples/puma_dev/config.ru +34 -0
  204. data/examples/puma_dev/standalone_app.rb +34 -0
  205. data/examples/scaffolding/blog.rb +49 -0
  206. data/examples/scaffolding/utf_lite.rb +105 -0
  207. data/examples/styling/feedback_demo.rb +229 -0
  208. data/examples/styling/style_showcase.rb +172 -0
  209. data/examples/styling/theme_demo.rb +508 -0
  210. data/examples/timer_health_checker.rb +111 -0
  211. data/examples/timer_showcase.rb +131 -0
  212. data/examples/tutorials/render_markdown.rb +112 -0
  213. data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
  214. data/examples/tutorials/tutorial_content.rb +1082 -0
  215. data/examples/visual_skills/design_deck_demo.rb +314 -0
  216. data/examples/visual_skills/explainer_demo.rb +499 -0
  217. data/exe/streamweaver +9 -0
  218. data/gsd/ROADMAP-1.0.md +167 -0
  219. data/gsd/STATE.md +13 -0
  220. data/gsd/research/market-positioning-research.md +139 -0
  221. data/gsd/research/production-patterns-research.md +270 -0
  222. data/gsd/research/repo-audit-1.0.md +274 -0
  223. data/lib/stream_weaver/action_token.rb +58 -0
  224. data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
  225. data/lib/stream_weaver/adapter/base.rb +591 -0
  226. data/lib/stream_weaver/adapter/opal.rb +334 -0
  227. data/lib/stream_weaver/adapter/static.rb +1118 -0
  228. data/lib/stream_weaver/admin.rb +176 -0
  229. data/lib/stream_weaver/app.rb +1821 -0
  230. data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
  231. data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
  232. data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
  233. data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
  234. data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
  235. data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
  236. data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
  237. data/lib/stream_weaver/canvas/bridge.rb +239 -0
  238. data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
  239. data/lib/stream_weaver/canvas/client.rb +298 -0
  240. data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
  241. data/lib/stream_weaver/canvas/doc_store.rb +252 -0
  242. data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
  243. data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
  244. data/lib/stream_weaver/canvas/gist_store.rb +135 -0
  245. data/lib/stream_weaver/canvas/helpers.rb +109 -0
  246. data/lib/stream_weaver/canvas/history.rb +90 -0
  247. data/lib/stream_weaver/canvas/protocol.rb +86 -0
  248. data/lib/stream_weaver/canvas/reader.rb +853 -0
  249. data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
  250. data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
  251. data/lib/stream_weaver/canvas/session.rb +132 -0
  252. data/lib/stream_weaver/cli.rb +3235 -0
  253. data/lib/stream_weaver/component_assets.rb +70 -0
  254. data/lib/stream_weaver/component_registry.rb +67 -0
  255. data/lib/stream_weaver/component_renderer.rb +48 -0
  256. data/lib/stream_weaver/components/annotated_code.rb +53 -0
  257. data/lib/stream_weaver/components/api_endpoint.rb +42 -0
  258. data/lib/stream_weaver/components/callout.rb +59 -0
  259. data/lib/stream_weaver/components/chart.rb +84 -0
  260. data/lib/stream_weaver/components/code_block.rb +77 -0
  261. data/lib/stream_weaver/components/comparison.rb +39 -0
  262. data/lib/stream_weaver/components/decision.rb +38 -0
  263. data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
  264. data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
  265. data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
  266. data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
  267. data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
  268. data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
  269. data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
  270. data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
  271. data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
  272. data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
  273. data/lib/stream_weaver/components/diff_block.rb +125 -0
  274. data/lib/stream_weaver/components/doc_header.rb +57 -0
  275. data/lib/stream_weaver/components/image_block.rb +70 -0
  276. data/lib/stream_weaver/components/implementation_map.rb +33 -0
  277. data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
  278. data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
  279. data/lib/stream_weaver/components/mermaid.rb +79 -0
  280. data/lib/stream_weaver/components/pipeline.rb +63 -0
  281. data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
  282. data/lib/stream_weaver/components/slide_container.rb +141 -0
  283. data/lib/stream_weaver/components/timeline_event.rb +58 -0
  284. data/lib/stream_weaver/components/wireframe.rb +29 -0
  285. data/lib/stream_weaver/components/wireframe_block.rb +34 -0
  286. data/lib/stream_weaver/components.rb +2711 -0
  287. data/lib/stream_weaver/css.rb +230 -0
  288. data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
  289. data/lib/stream_weaver/display_dsl.rb +1004 -0
  290. data/lib/stream_weaver/export/html_exporter.rb +478 -0
  291. data/lib/stream_weaver/feed.rb +34 -0
  292. data/lib/stream_weaver/feed_builder.rb +31 -0
  293. data/lib/stream_weaver/fonts.rb +33 -0
  294. data/lib/stream_weaver/interaction_runner.rb +487 -0
  295. data/lib/stream_weaver/iterm.rb +460 -0
  296. data/lib/stream_weaver/layout_registry.rb +92 -0
  297. data/lib/stream_weaver/opal/bridge.rb +52 -0
  298. data/lib/stream_weaver/opal/builder.rb +193 -0
  299. data/lib/stream_weaver/opal/env.rb +29 -0
  300. data/lib/stream_weaver/opal/reactive_state.rb +75 -0
  301. data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
  302. data/lib/stream_weaver/opal/renderer.rb +77 -0
  303. data/lib/stream_weaver/opal/runtime.rb +250 -0
  304. data/lib/stream_weaver/opal/shell.rb +145 -0
  305. data/lib/stream_weaver/opal/string_bridge.rb +50 -0
  306. data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
  307. data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
  308. data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
  309. data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
  310. data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
  311. data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
  312. data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
  313. data/lib/stream_weaver/opal_entry.rb +136 -0
  314. data/lib/stream_weaver/org/inline.rb +95 -0
  315. data/lib/stream_weaver/org/reader.rb +561 -0
  316. data/lib/stream_weaver/org/recording_context.rb +86 -0
  317. data/lib/stream_weaver/org/source_splitter.rb +63 -0
  318. data/lib/stream_weaver/org/writer.rb +300 -0
  319. data/lib/stream_weaver/page_shell.rb +530 -0
  320. data/lib/stream_weaver/portfile.rb +79 -0
  321. data/lib/stream_weaver/pushable.rb +42 -0
  322. data/lib/stream_weaver/resource/default_views.rb +111 -0
  323. data/lib/stream_weaver/resource/field_input.rb +25 -0
  324. data/lib/stream_weaver/resource/state_keys.rb +16 -0
  325. data/lib/stream_weaver/resource/store.rb +18 -0
  326. data/lib/stream_weaver/resource.rb +104 -0
  327. data/lib/stream_weaver/server.rb +1471 -0
  328. data/lib/stream_weaver/service.rb +1240 -0
  329. data/lib/stream_weaver/service_client.rb +104 -0
  330. data/lib/stream_weaver/session_store.rb +186 -0
  331. data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
  332. data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
  333. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
  334. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
  335. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
  336. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
  337. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
  338. data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
  339. data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
  340. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
  341. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
  342. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
  343. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
  344. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
  345. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
  346. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
  347. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
  348. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
  349. data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
  350. data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
  351. data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
  352. data/lib/stream_weaver/streamer.rb +63 -0
  353. data/lib/stream_weaver/templates/choices.rb +149 -0
  354. data/lib/stream_weaver/templates/code.rb +157 -0
  355. data/lib/stream_weaver/templates/confirm.rb +110 -0
  356. data/lib/stream_weaver/templates/diff.rb +195 -0
  357. data/lib/stream_weaver/templates/info.rb +142 -0
  358. data/lib/stream_weaver/templates/table.rb +169 -0
  359. data/lib/stream_weaver/templates/wizard.rb +271 -0
  360. data/lib/stream_weaver/theme/auto_mode.rb +116 -0
  361. data/lib/stream_weaver/theme/presets.rb +555 -0
  362. data/lib/stream_weaver/theme.rb +639 -0
  363. data/lib/stream_weaver/university/canvas.rb +924 -0
  364. data/lib/stream_weaver/university/course.rb +699 -0
  365. data/lib/stream_weaver/university/demos/counter.rb +33 -0
  366. data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
  367. data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
  368. data/lib/stream_weaver/university/demos.rb +50 -0
  369. data/lib/stream_weaver/university/listener.rb +467 -0
  370. data/lib/stream_weaver/university/progress.rb +201 -0
  371. data/lib/stream_weaver/university/runner.rb +138 -0
  372. data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
  373. data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
  374. data/lib/stream_weaver/utils.rb +31 -0
  375. data/lib/stream_weaver/version.rb +5 -0
  376. data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
  377. data/lib/stream_weaver/views.rb +3940 -0
  378. data/lib/stream_weaver.rb +122 -0
  379. data/llms.txt +1269 -0
  380. data/sig/stream_weaver.rbs +4 -0
  381. metadata +598 -0
@@ -0,0 +1,404 @@
1
+ # Hotwire/Rails vs. StreamWeaver: Concept-by-Concept Gap Map
2
+
3
+ Grounds the "Hotwire-grade omakase layer" proposal. Honest accounting of what Rails/Hotwire
4
+ provides vs. what StreamWeaver ships today, by file and DSL method. Sources: `README.md`,
5
+ `docs/for_llms.md`, `docs/streamweaver-frontend-vision.md`, `docs/routing.md`,
6
+ `docs/resource-dsl.md`, `gsd/analysis/00-analysis-and-plan.md`, `features/route-tabs.feature`,
7
+ `features/now-view-support.feature`, `lib/stream_weaver/server.rb`,
8
+ `lib/stream_weaver/interaction_runner.rb`, `lib/stream_weaver/components.rb`,
9
+ `lib/stream_weaver/views.rb`, `lib/stream_weaver/streamer.rb`, and CHANGELOG.md.
10
+
11
+ StreamWeaver already has an explicit thesis document for this exact question
12
+ (`docs/streamweaver-frontend-vision.md`): a "Hotwire-equivalent stack" built on
13
+ `Ruby DSL + htmx + Alpine.js + Idiomorph` instead of `Turbo + Stimulus`. This map checks that
14
+ thesis against what's actually implemented, concept by concept.
15
+
16
+ ---
17
+
18
+ ## 1. Turbo Drive (full-page nav without reload; history)
19
+
20
+ **Hotwire/Rails**: Every `<a>` and form submission is intercepted, fetched via AJAX, and the
21
+ response body swaps into `<html>` — no full navigation, but the URL bar and browser history
22
+ update as if it were a real nav. Works automatically on all links with zero configuration.
23
+
24
+ **StreamWeaver today**: `hx-boost="true"` at the `<body>` level, documented as the
25
+ "Hotwire-Style Navigation Pattern" in `docs/for_llms.md` (§"Hotwire-Style Navigation Pattern"),
26
+ reference implementation `lib/stream_weaver/views/canvas/reader_layout.erb`. This is opt-in
27
+ per-layout, not automatic for every StreamWeaver app — only canvas-read's layout currently uses
28
+ it. `route_by`/`route_with` (`docs/routing.md`) separately drive `HX-Push-Url` for state-driven
29
+ URL changes on the main app container, which is a different mechanism (POST-then-push, not
30
+ GET-boosted anchor navigation).
31
+
32
+ **Gap assessment: PARTIAL** — the underlying primitive (htmx `hx-boost`) is proven in one
33
+ reference layout, but it is not a framework-level default; a new StreamWeaver app gets full
34
+ POST/morph interaction handling, not Turbo-Drive-style boosted navigation, unless the author
35
+ hand-wires the same `hx-boost`/`hx-select`/`hx-select-oob` pattern documented in the reader
36
+ layout.
37
+
38
+ ---
39
+
40
+ ## 2. Turbo Frames — named region replaced independently (scoped updates)
41
+
42
+ **Hotwire/Rails**: `<turbo-frame id="x">` is a custom element (`alex_turbo_frames_transcript.txt`
43
+ §6:27–7:47 traces this precisely: it's just `customElements.define`, no magic). A frame's
44
+ `connectedCallback` fires the initial fetch if `src` is present; any link/form inside it targets
45
+ it by default. The server returns a full page; Turbo discards everything except the matching
46
+ frame ID.
47
+
48
+ **StreamWeaver today**: `fragment :name do ... end` (`lib/stream_weaver/app.rb:419`,
49
+ `Components::Fragment` in `lib/stream_weaver/components.rb:107`). An action declares
50
+ `updates: :name` or `updates: %i[a b]`; on interaction, `InteractionRunner`
51
+ (`lib/stream_weaver/interaction_runner.rb:170`) renders `Views::FragmentContentView`
52
+ (`lib/stream_weaver/views.rb:3801`) — the primary fragment plus any declared `updates:` regions
53
+ as out-of-band swaps. As of the 2026-07-11 benchmark gate
54
+ (`gsd/analysis/00-analysis-and-plan.md` — "GATE PASSED... 14 PASS, 0 FAIL"), this also has a
55
+ row-granular counterpart (`RowSwapView`, `views.rb:3821`) so a table-row edit returns ~2KB
56
+ instead of the whole fragment.
57
+
58
+ **Mechanism difference worth naming**: a Turbo Frame request re-renders *only that one
59
+ controller action*. A StreamWeaver fragment update still re-runs the **entire DSL block**
60
+ server-side (README: "Fragments keep the normal single full DSL rerun while limiting the HTML
61
+ swapped into the page") — the scoping is in what's sent to the client, not in what executes on
62
+ the server. True fragment-local re-execution is explicitly deferred
63
+ (`gsd/analysis/00-analysis-and-plan.md`, "Deferred items... decision-gated at Phase 5.1").
64
+
65
+ **Gap assessment: PARTIAL** — the client-visible outcome (one region updates, benchmarked
66
+ close to hand-written htmx payload sizes) is achieved and shipped. The server-side execution
67
+ model is architecturally different (whole-rerun-then-slice vs. true per-frame dispatch), which
68
+ is a real cost for apps with expensive per-region computation, and multi-fragment composition
69
+ (nested/nameable nesting like Turbo's `src=`-chained frames) has no equivalent.
70
+
71
+ ---
72
+
73
+ ## 3. Frame ID matching convention (dom_id; "key frame by what's unique per position on page")
74
+
75
+ **Hotwire/Rails**: `dom_id(record, prefix)` generates `prefix_recordtype_id` deterministically.
76
+ The transcript's stated rule (10:56–12:15, reinforced at 39:18): **key the frame by what's
77
+ unique per position on the page, not by what the content is about** — e.g. key a hover-card
78
+ frame by the to-do, not the user, or two cards for the same user collide (this exact bug is
79
+ demonstrated live in the transcript, 38:52–40:24).
80
+
81
+ **StreamWeaver today**: `Fragment#id` (`components.rb:111`) — an author-supplied name, not a
82
+ derived-from-record convention. Loop-rendered interactive components (buttons, in particular)
83
+ historically collided when two loop iterations produced identical label+source-location IDs;
84
+ `CHANGELOG.md` records a shipped **button `id:` option** to disambiguate manually. The larger
85
+ fix — auto-disambiguation by render occurrence, a `key:` option accepting only stable scalars,
86
+ and a `strict_ids: true` mode that becomes the 1.0 default — is scoped as "Phase 0" in
87
+ `gsd/analysis/00-analysis-and-plan.md` and is not confirmed shipped (no `strict_ids` or
88
+ automatic-warn-and-disambiguate logic found in `lib/`).
89
+
90
+ **Gap assessment: MISSING** for the Rails convention itself (there is no `dom_id`-equivalent
91
+ helper deriving a guaranteed-unique ID from a record); **PARTIAL** for the underlying safety
92
+ property — manual `id:`/`key:` opt-out exists, but StreamWeaver's own gap catalog names silent
93
+ loop-ID collision as "the most dangerous bug in the catalog" (`00-analysis-and-plan.md`, Root
94
+ cause 2), i.e. the framework does not yet guarantee what Rails' `dom_id` guarantees by
95
+ construction.
96
+
97
+ ---
98
+
99
+ ## 4. "Content missing" failure semantics — loud, styleable, well-known failure when IDs don't match
100
+
101
+ **Hotwire/Rails**: When a frame's ID doesn't match anything in the response, Turbo renders the
102
+ literal text "Content missing" into the frame and adds a `.turbo-frame` error class — a visible,
103
+ CSS-styleable, well-documented failure (transcript 12:31–14:55: "It's not a crash... it's a
104
+ really quiet failure. There's no exception, no 500... but it's loud in the DOM"). This is a
105
+ deliberate design choice: fail visibly rather than guess.
106
+
107
+ **StreamWeaver today**: the opposite policy, by explicit design. README: "If a target
108
+ disappears or routing changes, StreamWeaver automatically falls back to a full swap." The
109
+ `InteractionRunner`/server.rb path validates a signed action token; on mismatch it raises
110
+ `StaleActionDefinition`, caught in `server.rb:299` to return `status 409` + `HX-Retarget:
111
+ #app-container` + a full re-render — i.e. it self-heals rather than surfacing a stuck "content
112
+ missing" box. `gsd/analysis/00-analysis-and-plan.md` names this explicitly:
113
+ "`HX-Retarget` full-container fallback (correct-by-fallback)."
114
+
115
+ **Gap assessment: N/A by design, not a gap** — this is a deliberate inversion of Hotwire's
116
+ philosophy (fail loud vs. self-heal), consistent with StreamWeaver's whole-rerun architecture
117
+ where a full fallback is always available. Worth flagging to the owner as a design decision to
118
+ make explicit in docs, not an accidental omission: StreamWeaver currently has no dev-mode
119
+ equivalent of "make broken wiring embarrassingly obvious," which the transcript's operator
120
+ (Forrest, 13:11) singles out as the single highest-leverage debugging aid in Turbo Frames.
121
+
122
+ ---
123
+
124
+ ## 5. Eager-loading frames (`src` attribute — render page now, slow panel lands later)
125
+
126
+ **Hotwire/Rails**: `<turbo-frame src="/slow_panel">` fires its own fetch the instant it's
127
+ parsed into the DOM, independent of the rest of the page. The transcript's framing (31:18–33:00):
128
+ "you have a page with three cheap panels and one really expensive query... with a frame you put
129
+ it behind a source, and the page renders it immediately." This is the textbook fix for
130
+ Streamlit-style whole-page-blocks-on-slow-query, which is exactly the failure mode StreamWeaver's
131
+ own gap catalog calls out (`00-analysis-and-plan.md`, Root cause 1: "This is exactly Streamlit's
132
+ scaling wall").
133
+
134
+ **StreamWeaver today**: no equivalent found. Every DSL block — including all fragments —
135
+ executes synchronously as part of one request/response cycle; there is no `src=`-style deferred
136
+ fetch that lets the shell render before a specific region's slow work completes. The closest
137
+ existing primitives are `every(seconds)` timers pushing SSE updates after initial render
138
+ (`docs/for_llms.md` §"Live Streaming with `every` Timers") and the manually-swapped
139
+ `streamer.replace` — both are *post-load* update mechanisms, not *initial-load* deferral. The
140
+ plan doc's own deferred-items list confirms this gap: `button async: true` background-work
141
+ primitive is explicitly "decision-gated at Phase 5.1," not built.
142
+
143
+ **Gap assessment: MISSING** — this is the single largest architectural gap for the "no custom
144
+ JavaScript" complex-app case: a StreamWeaver author with one slow panel today must hand-roll
145
+ `every` + `streamer.replace` (documented as a known workaround pattern, e.g.
146
+ "cultivation_dashboard's 15+ wrapped sections" per the plan doc) to approximate what Turbo gets
147
+ for free with one HTML attribute.
148
+
149
+ ---
150
+
151
+ ## 6. Lazy-loading frames (`loading=lazy`, fires on visibility — hover cards, infinite scroll via nested frames)
152
+
153
+ **Hotwire/Rails**: `loading="lazy"` defers the fetch until the frame becomes visible (not
154
+ scrolled-near, not hovered — actually visible per IntersectionObserver semantics under the
155
+ hood, transcript 33:07–35:02). Combined with pure CSS (`display:none` → `display:block` on
156
+ hover), this gives hover-cards and hover-triggered any-content with zero JS. Nested lazy frames
157
+ ("Russian dolls," transcript 45:xx) give infinite scroll the same way: each page's response
158
+ contains the next page's placeholder frame, already marked lazy.
159
+
160
+ **StreamWeaver today**: `tabs :key, lazy: true` exists (`lib/stream_weaver/app.rb:859-866`,
161
+ renderer in `adapter/alpinejs.rb:2325-2383`) but is **click-triggered POST-then-morph**, not
162
+ visibility-triggered — inactive lazy tab panels render a `<!-- lazy: tab N not rendered -->`
163
+ comment and fetch only when the tab is clicked. This is closer to Rails' `remote: true`
164
+ click-to-load than to Turbo's IntersectionObserver-based `loading=lazy`. Notably,
165
+ `features/route-tabs.feature` (Scenario: `deprecate-lazy-post-morph`) shows this exact mode is
166
+ being **deprecated in favor of a future "lazy route tabs"** replacement — the team already knows
167
+ this primitive needs rework. No hover-card or infinite-scroll pattern (nested-lazy-frame
168
+ equivalent) exists anywhere in `lib/`.
169
+
170
+ **Gap assessment: PARTIAL, and self-acknowledged as needing rework** — the click-lazy primitive
171
+ exists for one component (tabs) and is already flagged for deprecation; visibility-based lazy
172
+ loading and nested-frame infinite scroll have no StreamWeaver equivalent at all.
173
+
174
+ ---
175
+
176
+ ## 7. Turbo Streams — multi-region updates from one response
177
+
178
+ **Hotwire/Rails**: A single response (from a controller action, a broadcast, or a form
179
+ submission) carries `<turbo-stream action="replace/append/prepend/remove" target="...">`
180
+ elements — arbitrary DOM operations to arbitrary named targets, all delivered together.
181
+
182
+ **StreamWeaver today**: two overlapping mechanisms, per `docs/streamweaver-frontend-vision.md`'s
183
+ own comparison table ("Turbo Streams | htmx out-of-band swaps + Alpine | OOB covers the
184
+ partial-update case; Alpine covers the live-update case"):
185
+ 1. **Fragment `updates:` scopes** (see #2) — OOB swaps declared per-action, resolved through the
186
+ same request/response cycle as the primary fragment.
187
+ 2. **`Streamer`** (`lib/stream_weaver/streamer.rb`) — `replace`/`append`/`prepend`/`remove`/
188
+ `add_class`/`remove_class` (`ACTIONS` constant, `streamer.rb:15`), pushed over SSE from
189
+ `every(seconds)` timer blocks (`docs/for_llms.md` §"Live Streaming with `every` Timers").
190
+ This is a near-exact operational match to Turbo Stream actions, just server-timer-driven
191
+ rather than broadcast-from-anywhere.
192
+
193
+ **Gap assessment: HAVE** — the action vocabulary (replace/append/prepend/remove/class toggles)
194
+ and the "multiple regions from one push" capability both exist and are documented with real
195
+ worked examples. The gap is provenance, not capability: Turbo Streams can be emitted from any
196
+ controller action or model callback broadcast; StreamWeaver's `Streamer` is currently reachable
197
+ only from `every` timer blocks and (per `now-view-support.feature`, Scenario
198
+ `session-scoped-broadcast`) has a **known, tracked correctness bug** — pushes broadcast to every
199
+ open SSE connection unfiltered by session, so one browser tab's `every`-driven update can leak
200
+ into a different session's tab. That story is explicitly marked "not a small patch... treat it
201
+ as its own sub-investigation" — i.e. multi-region push works, but not yet safely multi-tenant.
202
+
203
+ ---
204
+
205
+ ## 8. Server-push (broadcasts over websocket/SSE)
206
+
207
+ **Hotwire/Rails**: `Turbo::StreamsChannel.broadcast_*` pushes over ActionCable (WebSocket) from
208
+ anywhere in the Rails stack — model callbacks, background jobs, controller actions — to any
209
+ number of subscribed browsers.
210
+
211
+ **StreamWeaver today**: SSE only (`Streamer`, `lib/stream_weaver/streamer.rb`; client listener
212
+ `adapter/alpinejs.rb:1140-1184`), explicitly a deliberate choice —
213
+ `gsd/analysis/00-analysis-and-plan.md`'s "Deferred items" list states "WebSocket support (SSE
214
+ already exists and covers the dashboard case)." Push is currently triggered only from `every`
215
+ timer blocks inside the app definition, not from arbitrary server-side code (no equivalent of
216
+ "broadcast from a background job" or "broadcast from another process"). The session-scoping bug
217
+ in #7 also applies here directly.
218
+
219
+ **Gap assessment: PARTIAL** — SSE-based push exists and is documented as the intentional,
220
+ lighter-weight substitute for ActionCable, which is reasonable for StreamWeaver's single-process
221
+ model. But it's narrower in origin (timer-triggered only, not broadcast-from-anywhere) and has
222
+ an open, tracked cross-session leak bug that would block relying on it for anything beyond a
223
+ single-user dashboard today.
224
+
225
+ ---
226
+
227
+ ## 9. Stimulus (small JS behaviors with lifecycle, targets, cleanup) vs. Alpine's role in StreamWeaver
228
+
229
+ **Hotwire/Rails**: Stimulus controllers are JS classes with `connect()`/`disconnect()`
230
+ lifecycle hooks, `data-target`/`data-controller` wiring, explicit cleanup on DOM removal (the
231
+ transcript's word-count example, 3:36–5:43, is a canonical minimal Stimulus controller built by
232
+ hand to show what Turbo Frame itself is made of).
233
+
234
+ **StreamWeaver today**: Alpine.js `x-data` declarations inline in server-rendered markup —
235
+ `docs/streamweaver-frontend-vision.md`'s comparison table names this explicitly as the chosen
236
+ substitute ("Stimulus controllers | Alpine `x-data` declarations | Closer to the markup, no
237
+ separate JS file per component"). For genuinely reusable custom behavior beyond what inline
238
+ Alpine expresses well, `StreamWeaver.register_component` / `component_registry.rb` lets an
239
+ author register a real Phlex-backed component as a DSL verb — confirmed shipped and spec'd per
240
+ `gsd/analysis/00-analysis-and-plan.md` ("Component registration — already shipped
241
+ (`StreamWeaver.register_component`, `component_registry.rb`, spec'd); document it prominently
242
+ instead").
243
+
244
+ **Gap assessment: PARTIAL, by design** — this is the one area where StreamWeaver has made an
245
+ explicit architectural bet to *not* replicate Hotwire 1:1 (Alpine's inline-declaration model
246
+ instead of Stimulus's class-based one), and the reasoning is documented (token efficiency for
247
+ LLM-generated views, no separate-file mental overhead). The real gap is that Stimulus's
248
+ lifecycle guarantees (connect/disconnect firing reliably across morphs, listener cleanup
249
+ enforced by the base class) have no equivalent enforcement mechanism in the Alpine convention —
250
+ that discipline is left to whoever writes the `x-data` block, with no framework-level guardrail
251
+ against the leaked-listener class of bug the transcript calls out (3:36) as Stimulus's main
252
+ value.
253
+
254
+ ---
255
+
256
+ ## 10. Form conventions (`form_with model:`, REST resource routing, strong params foot-guns)
257
+
258
+ **Hotwire/Rails**: `form_with model: @record` infers the URL/HTTP verb from the record's
259
+ persistence state; omitting an explicit `url:` on a nested/nonstandard case silently POSTs to
260
+ the generic endpoint and can overwrite unrelated attributes if strong params are too permissive
261
+ (the transcript's demonstrated failure, 17:28–22:00: editing just a title accidentally
262
+ flips `completed` too, because the generic endpoint permitted all three fields).
263
+
264
+ **StreamWeaver today**: two form mechanisms.
265
+ - **`form :name do ... submit "Save" do |values| ... end`** — deferred-submission block, POSTs
266
+ to `/form/:form_name` with Rails-style nested params, documented in `docs/for_llms.md` and
267
+ handled by `InteractionRunner` (`interaction: :form` branch, `server.rb:382-397`).
268
+ - **`resource :post, store: PostStore do field :title, :string; ... end`**
269
+ (`docs/resource-dsl.md`) — the closer Rails analog: `field` declarations act as an explicit
270
+ allowlist (StreamWeaver's version of strong params — only declared fields are read from
271
+ submitted state), auto-generates `new`/`edit` forms bound to a duck-typed store protocol
272
+ (`all`/`find`/`create`/`update`/`destroy`), validated at startup, plus named-route helpers
273
+ (`posts_path`, `post_path(rec)`, etc.). This is explicitly called "shipped, tested (98
274
+ examples green)" in `gsd/analysis/00-analysis-and-plan.md`, described as infrastructure to
275
+ build on, not a gap.
276
+
277
+ **Update (2026-08-22, story `document-form-for`)**: `form_for` — the `form_with`-style
278
+ record-bound form primitive named above as a still-to-do item — shipped 2026-07-10
279
+ (`lib/stream_weaver/app.rb:598`, decision doc `gsd/analysis/decisions/form-for.md`).
280
+ Given a resource name (or explicit `store:`/`fields:`), it seeds fields from a
281
+ `record:`, infers create vs. update from record identity, coerces + validates on
282
+ submit, and PRGs to `show` on success — the same block-yielding record-binding
283
+ ergonomics `form_with(model:)` gives Rails. It's what `resource`'s own default
284
+ `new`/`edit` views call under the hood, and is available directly inside override
285
+ blocks for custom layouts. It was simply undocumented until this story; full
286
+ reference and a worked Turbo-Frame-inline-editing example are now in
287
+ `docs/resource-dsl.md#form_for` and `docs/for_llms.md`.
288
+
289
+ **Gap assessment: HAVE** — `resource`/`field` already delivers the strong-params-equivalent
290
+ safety property Turbo's demo is warning about (a StreamWeaver `resource` form cannot
291
+ accidentally write undeclared fields, by construction), CRUD scaffolding is shipped, and
292
+ `form_for` now closes the block-yielding form-builder ergonomics gap for record-bound
293
+ forms. What remains PARTIAL is `form_with`-equivalent ergonomics for hand-written,
294
+ non-record forms — today's plain `form` block is still closer to Rails' `form_tag` than
295
+ `form_with`.
296
+
297
+ ---
298
+
299
+ ## 11. URL/history semantics (when does URL change; deep-linkable states)
300
+
301
+ **Hotwire/Rails**: Turbo Drive updates the URL/history on every boosted navigation
302
+ automatically; Turbo Frames deliberately do *not* touch the URL unless `data-turbo-action`
303
+ opts in (transcript 15:01, "Gotcha 3: the URL doesn't change... that's usually exactly what we
304
+ want").
305
+
306
+ **StreamWeaver today**: this is one of the most fleshed-out areas. `route_by`/`route_with`
307
+ (`docs/routing.md`) give a bidirectional URL↔state contract: GET seeds state from the path,
308
+ POST responses push `HX-Push-Url` derived from a `builder` lambda. `docs/routing.md`'s "Common
309
+ Pitfalls" section documents two real, previously-hit bug classes in detail (state-merge leakage
310
+ across narrow route branches; non-exhaustive `case`/`when` failing silently in both directions)
311
+ — evidence this has been exercised against real, sizeable apps (~20 branches, 15 tabs), not just
312
+ designed on paper. `features/route-tabs.feature` extends the same contract to `tabs :key,
313
+ url: true` with explicit server-authority rules ("On full GET the URL is authoritative... absent
314
+ param must not inherit session value" — Scenario `server-param-authority`) and documented
315
+ degradation in canvas/websocket mode. The `resource` DSL layers named-route helpers
316
+ (`posts_path`, `post_path(rec)`) on top.
317
+
318
+ **Gap assessment: HAVE** — deep-linkable, bookmarkable, back/forward-correct URL state is a
319
+ real, documented, pitfall-audited feature, arguably StreamWeaver's most Rails-native area today.
320
+ The gap vs. Hotwire is narrow: no automatic URL update on every navigation (StreamWeaver's is
321
+ opt-in per state key/tab, matching Turbo Frame's "don't touch the URL by default" philosophy
322
+ rather than Turbo Drive's "always update" one) — a reasonable design choice, not an oversight.
323
+
324
+ ---
325
+
326
+ ## 12. Scaffolding/generators (`rails g scaffold` → working CRUD)
327
+
328
+ **Hotwire/Rails**: `rails g scaffold Post title:string body:text` generates a model, migration,
329
+ controller with all seven REST actions, and views — a working CRUD app from one command.
330
+
331
+ **StreamWeaver today**: `resource :post, store: PostStore do field :title, :string; ... end`
332
+ (`docs/resource-dsl.md`) is the declarative equivalent — one block replaces "30–50 lines of
333
+ route/state/form boilerplate," generating index/show/new/edit/destroy with deep-linkable URLs,
334
+ route helpers, store validation at startup, and override blocks per action. Two working
335
+ reference apps exist: `examples/scaffolding/blog.rb` (~50 lines, zero-dependency smoke test) and
336
+ `examples/scaffolding/utf_lite.rb` (multi-resource, custom index override). This is a
337
+ **declarative-scaffold-as-code** pattern rather than a **generator-writes-files** pattern — there
338
+ is no `streamweaver new <name>` CLI command that scaffolds files onto disk.
339
+ `docs/streamweaver-frontend-vision.md` names this explicitly under "Scaffolding (long-term)":
340
+ "well downstream — the conventions need to stabilize first," not yet built.
341
+
342
+ **Gap assessment: PARTIAL** — the *outcome* Rails scaffolding produces (working CRUD from
343
+ minimal input) is achieved, arguably more concisely (one `resource` block vs. generated files
344
+ to maintain), and is genuinely shipped/tested. What's missing is the code-generation workflow
345
+ itself — no `streamweaver new` / `streamweaver generate` command exists, and there's no path yet
346
+ for an author who wants generated, then-hand-edited files rather than a live declarative block.
347
+
348
+ ---
349
+
350
+ ## 13. Progressive disclosure for AI agents (skills/guides that teach the framework)
351
+
352
+ **Hotwire/Rails**: no direct analog — Rails' equivalent of "teaching an AI agent the
353
+ framework" is its own extensive documentation, guides, and (increasingly) community-authored
354
+ AGENTS.md/CLAUDE.md files; there's no framework-shipped, tool-agnostic skill mechanism.
355
+
356
+ **StreamWeaver today**: this is a built, first-class part of the framework, and unusually
357
+ mature for a project this size.
358
+ - **`docs/for_llms.md`** (aliased as `llms.txt`) — an LLM-quick-reference doc, deliberately
359
+ structured around common failure modes ("Common Mistakes," "Anti-Patterns That Cause Churn"),
360
+ updated in lockstep with features (e.g. `route-tabs.feature`'s `docs-update` scenario requires
361
+ `for_llms.md` + `llms.txt` updates as an acceptance criterion, not an afterthought).
362
+ - **Two shipped SKILL.md packages**: `streamweaver-visual-companion` (mockups/diagrams/
363
+ brainstorming via canvas-push) and `streamweaver-doc-builder` (long-form `:doc`-theme
364
+ documents), both installable to Claude Code's own path *and* the cross-tool
365
+ `.agents/skills/` alias via `streamweaver setup` / `install-skill` — `docs/for_llms.md` names
366
+ the exact install commands and directs agents to check for these before hand-rolling canvas
367
+ DSL from raw examples.
368
+ - **`docs/reference/agent-skills-comparison.md`** — a researched comparison of how Claude Code,
369
+ Codex, Gemini CLI, and GitHub Copilot each discover/trigger skills (SKILL.md spec via
370
+ agentskills.io, `.agents/skills/` cross-tool alias), which is what informed the multi-tool
371
+ install path above. This is meta-level: StreamWeaver has already researched and built for
372
+ "teach every agent, not just Claude," which is exactly the concern a Hotwire-parity effort
373
+ would need to replicate for its own new surface area.
374
+ - Repo-local skill: `.claude/skills/streamweaver-panel.md` documents the panel/canvas-push/
375
+ canvas-wait workflow directly.
376
+
377
+ **Gap assessment: N/A — this is StreamWeaver's own concept, not a Hotwire one**, and it's a
378
+ genuine, shipped asset: the project already treats "does an AI agent build this correctly on
379
+ first try" as a first-class, tested acceptance criterion (see `docs/routing.md`'s Common
380
+ Pitfalls section, `for_llms.md`'s Common Mistakes, and every `.feature` file's `docs-update`
381
+ scenario). Any Hotwire-parity work should extend this existing discipline (update
382
+ `for_llms.md`/`llms.txt` + relevant skill as part of the feature, not as a follow-up) rather
383
+ than invent a new documentation mechanism.
384
+
385
+ ---
386
+
387
+ ## Summary Table
388
+
389
+ | # | Concept | Assessment |
390
+ |---|---|---|
391
+ | 1 | Turbo Drive | PARTIAL |
392
+ | 2 | Turbo Frames (scoped regions) | PARTIAL |
393
+ | 3 | dom_id / frame-keying convention | MISSING (helper) / PARTIAL (safety) |
394
+ | 4 | "Content missing" loud failure | N/A — deliberate inversion (self-heal instead) |
395
+ | 5 | Eager-loading frames (`src`) | MISSING |
396
+ | 6 | Lazy-loading frames (visibility) | PARTIAL, self-flagged for rework |
397
+ | 7 | Turbo Streams (multi-region push) | HAVE, with a tracked correctness bug |
398
+ | 8 | Server-push (websocket/SSE) | PARTIAL — SSE only, timer-triggered only |
399
+ | 9 | Stimulus vs. Alpine | PARTIAL, by design |
400
+ | 10 | Form conventions / strong params | HAVE (`form_for`, record-bound) / PARTIAL (hand-written non-record forms) |
401
+ | 11 | URL/history semantics | HAVE |
402
+ | 12 | Scaffolding/generators | PARTIAL |
403
+ | 13 | Agent progressive disclosure | N/A (StreamWeaver-only concept) — HAVE, mature |
404
+
@@ -0,0 +1,246 @@
1
+ # Lazy fragments: which htmx trigger, and what `lazy:` means on its own
2
+
3
+ Decision note for the `visibility-lazy-fragments` story (epic `streamweaver-way`).
4
+ Companion to `2026-08-17-hotwire-concept-map.md`, which named deferred/lazy
5
+ fragments as the open gap against Turbo Frames.
6
+
7
+ `fragment(:name, defer: true, lazy: true)` is StreamWeaver's `loading="lazy"`.
8
+ Turbo's rule for that attribute is precise and worth restating, because two of
9
+ the three obvious implementations get it wrong: the frame loads **when it becomes
10
+ visible** — not when it is scrolled near, not when it is hovered, but when it is
11
+ actually visible. A frame inside a `display: none` container never loads. Flip
12
+ that container to `display: block` with no scrolling at all and it loads.
13
+
14
+ ## Decision 1 — `hx-trigger="intersect once"`, not `revealed`
15
+
16
+ htmx offers two candidate triggers. They are not variations on a theme; they are
17
+ built on different primitives and only one of them matches Turbo.
18
+
19
+ Read from htmx 2.0.4 (`https://unpkg.com/htmx.org@2.0.4/dist/htmx.js`), which is
20
+ the exact build `Adapter::AlpineJS#render_cdn_scripts` loads:
21
+
22
+ **`revealed` is scroll-polled geometry.** `addTriggerHandler` (js:2662) calls
23
+ `initScrollHandler()`, which installs `scroll`/`resize` listeners that set a flag,
24
+ plus a 200ms `setInterval` that re-checks every `[hx-trigger*='revealed']`
25
+ element via `maybeReveal` → `isScrolledIntoView` (js:750):
26
+
27
+ ```js
28
+ function isScrolledIntoView(el) {
29
+ const rect = el.getBoundingClientRect()
30
+ return rect.top < window.innerHeight && rect.bottom >= 0
31
+ }
32
+ ```
33
+
34
+ A `display: none` element has no layout box, so `getBoundingClientRect()` returns
35
+ an all-zero rect — and `0 < window.innerHeight && 0 >= 0` is **true**. `revealed`
36
+ therefore considers every hidden element revealed and fetches it immediately, on
37
+ the first `maybeReveal` call at process time. That is a direct contradiction of
38
+ criterion 4 ("a fragment hidden via CSS never triggers a fetch"), and it takes
39
+ the zero-JS hover-card pattern with it: the card would load on page render for
40
+ every host on the page, which is the whole cost the pattern exists to avoid.
41
+ The 200ms polling is a second, smaller mismatch — a hover that flips `display`
42
+ without scrolling never sets the `windowIsScrolling` flag, so nothing re-checks.
43
+
44
+ **`intersect` is an IntersectionObserver.** Same function, js:2666:
45
+
46
+ ```js
47
+ const observer = new IntersectionObserver(function(entries) {
48
+ for (let i = 0; i < entries.length; i++) {
49
+ if (entries[i].isIntersecting) { triggerEvent(elt, 'intersect'); break }
50
+ }
51
+ }, observerOptions)
52
+ observer.observe(asElement(elt))
53
+ ```
54
+
55
+ IntersectionObserver reports no intersection for a target with no layout box, and
56
+ delivers a callback on *any* transition into intersection regardless of cause —
57
+ scrolling, an ancestor's `display` flip, a CSS `:hover` rule, an Alpine `x-show`.
58
+ That is Turbo's rule, expressed in the same browser API Turbo itself uses.
59
+
60
+ **Chosen: `hx-trigger="intersect once"`.** Locked by
61
+ `spec/lazy_fragments_spec.rb` ("waits for intersection instead of firing on
62
+ load"), which also asserts the page contains no `hx-trigger="load"` — the shell
63
+ must ship with no way to fetch a lazy fragment except a visibility event.
64
+
65
+ ### Why `once` is load-bearing
66
+
67
+ The observer keeps observing after it fires. Without `once`, scrolling a
68
+ materialized fragment out of the viewport and back would fire `intersect` again
69
+ and refetch — exactly what criterion 3 forbids. htmx's `once` latch (js:2506) is
70
+ stored per element:
71
+
72
+ ```js
73
+ if (triggerSpec.once) {
74
+ if (elementData.triggeredOnce) { return } else { elementData.triggeredOnce = true }
75
+ }
76
+ ```
77
+
78
+ `elementData` comes from `getInternalData(elt)`, i.e. it is keyed to the DOM node,
79
+ not to the id or the URL. That per-element scope is what makes the next decision
80
+ work.
81
+
82
+ ## Decision 2 — composition with the full-container re-arm
83
+
84
+ `deferred-fragments-src` established that an interaction swapping the whole app
85
+ container re-renders a deferred fragment back to its placeholder and re-fetches.
86
+ The mechanism is the wrapper element: the fetch attributes ride an inner
87
+ `<div id="<frag-id>--deferred">` rather than the fragment container, so the morph
88
+ sees materialized content where the new markup has a wrapper, matches neither by
89
+ id nor positionally, and builds a **fresh element**.
90
+
91
+ A lazy fragment composes with that for free, and correctly:
92
+
93
+ - Fresh element ⇒ fresh `getInternalData` ⇒ `triggeredOnce` is unset ⇒ the
94
+ fragment re-arms rather than staying stuck on its placeholder.
95
+ - Re-armed on `intersect`, not `load` ⇒ it re-arms **lazily**. If the fragment is
96
+ off-screen or CSS-hidden when the re-render lands, it waits, exactly as it did
97
+ on first paint. A plain deferred fragment would have refetched immediately.
98
+
99
+ Locked by "re-arms as a lazy wrapper after a full-container update" and "gives
100
+ the lazy wrapper its own id so the morph replaces rather than reuses it".
101
+
102
+ ## Decision 3 — `lazy: true` implies `defer: true`
103
+
104
+ `fragment(:card, lazy: true)` is legal and means `defer: true, lazy: true`.
105
+ Ruling: **imply, do not raise.**
106
+
107
+ The case for raising was consistency with the adjacent guard — `placeholder:`
108
+ without `defer:` raises `ArgumentError`. But those two options are not alike.
109
+ A placeholder with nothing to place-hold has *no* meaning; the author who wrote it
110
+ has a mistaken model of the API and needs to hear about it. `lazy:` has exactly
111
+ one possible meaning, and it is unambiguous: hold the fetch until visible. There
112
+ is no second reading for an error message to disambiguate, so raising would spend
113
+ an author's round-trip to make them type a word the DSL could have supplied.
114
+ That is friction with nothing on the other side of it (Forrest's Law).
115
+
116
+ It also matches Turbo, where `loading="lazy"` is only meaningful on a frame that
117
+ loads from `src` — the attribute presupposes the deferred load rather than
118
+ combining with it. And the DSL already implies options elsewhere: `area_chart` is
119
+ `line_chart` with `fill: true`, `doughnut_chart` is `pie_chart` with
120
+ `doughnut: true`.
121
+
122
+ Implementation is one line at the top of `App#fragment` (`defer ||= lazy`), placed
123
+ *before* the placeholder guard so `fragment(:card, lazy: true, placeholder: '…')`
124
+ is accepted while `fragment(:card, placeholder: '…')` still raises. All three
125
+ behaviours are specced.
126
+
127
+ ## Design note — lazy route tabs
128
+
129
+ Required by criterion 5. **Design only; no tabs code was changed by this story.**
130
+
131
+ `tabs key, lazy: true` (the POST-morph mode) is deprecated. `App#warn_lazy_tabs_deprecated`
132
+ already names its successor in the warning text: "lazy route tabs will replace
133
+ this mode". This is the primitive that lets that happen.
134
+
135
+ ### The shape
136
+
137
+ Route tabs (`tabs :view, url: true`) render every panel into the DOM as a
138
+ `<div class="sw-tab-panel" x-show="activeTab === N" x-cloak>`. Two independent
139
+ mechanisms make an inactive panel `display: none`: `[x-cloak] { display: none !important; }`
140
+ (adapter/alpinejs.rb:6738) before Alpine boots, and `x-show`'s own inline
141
+ `display: none` afterwards. So an inactive route-tab panel is already, today,
142
+ precisely the CSS-hidden container that a lazy fragment refuses to fetch inside.
143
+
144
+ Adoption is therefore a **DSL pattern, not a renderer feature**:
145
+
146
+ ```ruby
147
+ tabs :view, url: true do
148
+ tab 'Summary' do
149
+ text summary_line # cheap, always rendered
150
+ end
151
+
152
+ tab 'Revenue' do
153
+ fragment :revenue, lazy: true, placeholder: -> { skeleton_rows(8) } do
154
+ revenue_table # 1.5s of work; runs the first time this tab is shown
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ What happens: the shell ships with all three panels present and the Revenue panel
161
+ hidden, so its block never runs. Clicking the Revenue trigger runs Alpine's
162
+ `activeTab = 1` and `swRouteTabs.push(:view, 1)` — client-side only, no server
163
+ round-trip, the URL updates. `x-show` flips the panel to visible; the
164
+ IntersectionObserver on the lazy wrapper fires; one scoped fetch materializes just
165
+ that fragment. Switching back and forward again refetches nothing (`once`, plus
166
+ the wrapper is gone). Back/forward across the tab is client-side too: `@popstate`
167
+ re-reads the index and re-shows an already-materialized panel.
168
+
169
+ ### Why this beats the deprecated mode
170
+
171
+ - **The deprecated mode fetches the whole app container.** Its trigger button
172
+ carries `hx-target="#app-container"` with `hx-vals` writing the new index into
173
+ session state. Lazy route tabs fetch one fragment at its own signed endpoint,
174
+ send no state patch, and leave the tab index where route tabs put it (the URL).
175
+ - **The deprecated mode is dead on a canvas page** because a canvas has no route
176
+ for its POST-morph (`stream_weaver-pkh`, quoted at app.rb:1014). Lazy route tabs
177
+ inherit the deferred-fragment fetch path, so they degrade the same way every
178
+ other deferred fragment does — see the caveat below. That is not yet *working*
179
+ on canvas, but it is one shared gap instead of a second bespoke one.
180
+ - **Panel cost becomes per-panel and opt-in.** `lazy: true` on the tabs group was
181
+ all-or-nothing and skipped evaluation for every inactive tab, which forced the
182
+ two-pass re-render at app.rb:926 when a clamped index changed after the block
183
+ had run. A lazy fragment inside one tab costs nothing to the tabs machinery: the
184
+ panel still renders, its cheap content still evaluates, and only the fragment's
185
+ block is held back.
186
+
187
+ ### Constraint: keep the fragment inside `tab`, never beside it
188
+
189
+ `disc-085`: the index-clamp range bound, `tab`'s positional index, and
190
+ `render_tabs`' `children.each_with_index` all count **all** children of the tabs
191
+ block, not just `Tab` components. A non-tab child at the top level of a `tabs`
192
+ block shifts every panel index after it and lets an in-range-but-wrong index dodge
193
+ the clamp.
194
+
195
+ This design does not touch that, and must not start:
196
+
197
+ ```ruby
198
+ tabs :view, url: true do
199
+ fragment(:sidebar, lazy: true) { … } # WRONG — becomes child 0, shifts every panel
200
+ tab('Summary') { … }
201
+ end
202
+ ```
203
+
204
+ The pattern above puts the fragment **inside** a `tab` block, where it is a child
205
+ of the `Tab` component and invisible to all three counting sites. Whoever
206
+ implements lazy route tabs should carry this as an explicit non-goal: adopting the
207
+ primitive requires no change to `render_tabs`, so it must not become the occasion
208
+ to open the mixed-children counting question. Fixing `disc-085` remains its own
209
+ story, and doing it first would not block this one.
210
+
211
+ ### Open question for the implementer
212
+
213
+ Whether `lazy route tabs` should also become sugar — e.g. `tab 'Revenue', lazy: true`
214
+ wrapping the block in a lazy fragment automatically — or stay the explicit
215
+ two-verb composition above. The explicit form is what this story enables and is
216
+ already zero-JS; sugar would need to answer where the fragment name comes from
217
+ (the label? the index? — an index-derived name reintroduces exactly the
218
+ positional coupling `disc-085` is about) and whether a placeholder can be
219
+ declared per tab. Recommend shipping the composition, using it in `my-todos`, and
220
+ only then deciding whether the sugar earns a name.
221
+
222
+ ## Caveats and known limitations
223
+
224
+ - **Canvas.** `Fragment#render_deferred` posts to `url('/update')` with no
225
+ `websocket_mode?` guard, so a deferred *or* lazy fragment on a live canvas page
226
+ posts to a route the bridge does not serve. Inherited from
227
+ `deferred-fragments-src`, unchanged here; it is an instance of `disc-097` (only
228
+ 4 of 21 `htmx_attrs` call sites consult `websocket_mode?`) and belongs to
229
+ `disc-093`'s backend-less-context matrix.
230
+ - **Named actions inside the fragment.** `disc-100` applies unchanged: a
231
+ `button 'X', action: :foo` minted during a fragment fetch is dead on arrival.
232
+ Use a block button.
233
+ - **Static export.** A lazy fragment's block runs inline in an export, like a
234
+ deferred one — nothing will ever scroll an exported file. Specced.
235
+ - **Placeholder height.** The wrapper is what the observer watches, so a
236
+ placeholder should occupy space. The default spinner does; a placeholder that
237
+ renders nothing gives the observer a zero-area target.
238
+
239
+ ## What the browser pass must still settle
240
+
241
+ Everything above about the *server* is specced (`spec/lazy_fragments_spec.rb`, 20
242
+ examples). Everything about *visibility* is a browser claim and is deliberately
243
+ not asserted in MRI: that IntersectionObserver stays silent for a `display: none`
244
+ target, that a CSS `:hover` reveal fires it, that exactly one network request
245
+ results, and that re-hiding and re-showing produces none. The runbook for those
246
+ checks is in the story's handoff note.