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,396 @@
1
+ ---
2
+ name: streamweaver-way
3
+ description: Use when building or changing an interactive StreamWeaver app or feature — inline editing, live search/filtering, per-row buttons, hover cards, infinite scroll, tabs, or any region slow enough to hold up the page. Encodes "The StreamWeaver Way": the conventions (fragments, defer:/lazy:, form_for, interactive-id keying, dev-loud/prod-self-heal) that make app code zero-JavaScript by default. For one-off mockups, diagrams, or dashboards to look at, use streamweaver-visual-companion instead; for long-form editorial documents, use streamweaver-doc-builder.
4
+ ---
5
+
6
+ # The StreamWeaver Way
7
+
8
+ StreamWeaver is an *omakase* layer, not a widget kit: the backend and the frontend were
9
+ designed as one system, so the conventions below are the framework, not style advice.
10
+ Follow them and you write no JavaScript. Fight them and you write a lot.
11
+
12
+ **Read the laws. Drill into a recipe only when you're building that thing.**
13
+
14
+ ---
15
+
16
+ ## The Laws
17
+
18
+ **1. Key by what is unique per position on the page, not by what the content is about.**
19
+ Every interactive element needs a server-dispatchable id, and StreamWeaver derives one
20
+ for you (label + block source location, with `-dup-N` auto-disambiguation on repeats), so
21
+ you rarely pass anything. Auto-derived ids are *position*-stable, not *content*-stable:
22
+ insert or delete an earlier row and every later suffix shifts. Reach for `key:` only when
23
+ identity must outlive reordering, filtering, or deletion — per-row Edit/Delete buttons,
24
+ per-row fragments. Then key it by the thing that is unique *per row on this page* (the
25
+ todo), never by what the row happens to display (its assignee) — two rows sharing an
26
+ assignee is the classic collision, and it fails silently: hovering the second card shows
27
+ the first one's data. Precedence is `id:` > `key:` > auto. `key:` takes stable scalars
28
+ only (String/Symbol/Integer; anything else raises). Never key by positional index.
29
+
30
+ **2. Deferred over hand-rolled timers.** A slow region gets `fragment(..., defer: true)`
31
+ so the shell ships now and the region lands later. Never an `every(...)` poll, never a
32
+ "loading" flag toggled by a second request you wrote yourself. Measured on one 1.5s
33
+ region: initial GET 0.018s instead of 1.5s.
34
+
35
+ **3. Lazy means visible.** `lazy: true` holds the fetch until the fragment is actually
36
+ *visible* — scrolled into view, or CSS flipping an ancestor out of `display: none`.
37
+ Hidden content costs exactly nothing, and it fetches exactly once. That interlock is what
38
+ buys you hover cards and infinite scroll with no scroll handler and no state counter.
39
+
40
+ **4. Dev loud, prod self-heals.** A stale action token gets a 409 + full-container
41
+ re-render that is byte-for-byte identical in production — the user never sees an error.
42
+ In development only, the same response prepends a dismissible `.sw-dev-fallback` overlay
43
+ naming the stale target and the likely cause. This is a deliberate inversion of Hotwire's
44
+ "Content missing": keep the debugging signal, don't charge the user for it. If you see
45
+ that overlay, your wiring is wrong — don't style it away.
46
+
47
+ **5. State your intent in DSL verbs. Zero custom JavaScript is the default, not an
48
+ aspiration.** All four benchmark features below run with no script tag, no inline JS, no
49
+ hand-written Alpine — only DSL verbs and CSS. If you're reaching for JavaScript, you have
50
+ almost certainly missed a verb. Do not touch adapter internals (`lib/stream_weaver/
51
+ adapter/*`) or emit raw `hx-*` attributes from app code; the verbs own those.
52
+
53
+ **6. Trilaws as design filters.** Run every feature through all three:
54
+ *Matt* — is it findable and digestible (progressive disclosure for the long tail)?
55
+ *Forrest* — zero friction plus a real perk (if it can be automatic, it must be)?
56
+ *Gloria* — is the default path the correct path (design for the brain you have, not the
57
+ one that remembers to pass the right option)? Law 1's auto-keying is Gloria's Law in
58
+ code: the default already disambiguates, so the silent-wrong-callback bug can't happen.
59
+
60
+ ---
61
+
62
+ ## Core mechanics (everything below builds on these)
63
+
64
+ ```ruby
65
+ app 'My App' do
66
+ state[:count] ||= 0 # the whole block RE-RUNS on every interaction
67
+ text "Count: #{state[:count]}"
68
+ button('+') { |s| s[:count] += 1 }
69
+ end.run!
70
+ ```
71
+
72
+ **`fragment(name) { ... }`** marks a region that interactions *inside it* swap on their
73
+ own, instead of swapping the whole app container. Everything interactive rendered inside
74
+ a fragment is auto-scoped to it — that scoping is the whole mechanism the recipes use.
75
+
76
+ | Form | Fetch timing | Use for |
77
+ |---|---|---|
78
+ | `fragment(:n) { }` | inline, with the page | scoping a swap to one region |
79
+ | `fragment(:n, defer: true) { }` | right after page load | slow regions off the critical path |
80
+ | `fragment(:n, lazy: true) { }` | first time it is visible | hover cards, infinite scroll, tab panels |
81
+
82
+ `lazy: true` implies `defer: true`. `placeholder:` accepts nothing (small spinner), a
83
+ String (rendered as text), or a Proc (run as DSL). **Give the placeholder height** — the
84
+ observer watches the wrapper, and a zero-area target never intersects.
85
+
86
+ **Three rules that apply to every deferred/lazy fragment:**
87
+
88
+ - The block is skipped on the shell render, so anything it *registers* (actions, timers)
89
+ isn't registered until the fetch. Keep registration outside deferred blocks.
90
+ - A **named**-action button (`button 'X', action: :foo`) inside one does not fire — its
91
+ token is minted outside the session's action manifest. Use a block button
92
+ (`button('X') { |s| ... }`), which dispatches by id and is unaffected.
93
+ - Each fetch re-runs its **ancestors'** blocks. Nest for staging, not to split one
94
+ expensive block into cheaper pieces.
95
+
96
+ **`form_for`** is the record-bound form primitive: seeds fields from a record, infers
97
+ create vs. update from record identity, coerces + validates on submit, calls
98
+ `store.create`/`store.update`, flashes, and PRGs. **Only the fields you declare are read
99
+ from the submitted state** — that allowlist is edit-safety by construction, not by
100
+ remembering to pass the right URL.
101
+
102
+ A **store** is any object answering `all`, `find(id)`, `create(attrs)`, `update(id, attrs)`,
103
+ `destroy(id)`. A plain module over an Array is enough to build all four recipes.
104
+
105
+ ---
106
+
107
+ ## The four benchmark recipes
108
+
109
+ These are the four Turbo Frames features from the learnhotwire.com course, in
110
+ StreamWeaver, with zero custom JavaScript. Recipe 1 defines the store; recipes 2-4 add
111
+ one method each to it, shown inline. Each is complete and runnable.
112
+
113
+ ### 1. Inline editing (a row becomes a form in place)
114
+
115
+ One `fragment` per row, keyed by record id, branching on an `editing_id` in state. The
116
+ Edit button and the form submit both swap **just this row**, because both are rendered
117
+ inside that row's fragment.
118
+
119
+ ```ruby
120
+ require 'stream_weaver'
121
+
122
+ module TodoStore
123
+ @todos = [{ id: '1', title: 'Buy milk', completed: false },
124
+ { id: '2', title: 'Ship it', completed: true }]
125
+ class << self
126
+ def all = @todos
127
+ def find(id) = @todos.find { |t| t[:id] == id.to_s }
128
+ def create(attrs) = (id = (@todos.size + 1).to_s; @todos << { id: id, **attrs }; id)
129
+ def update(id, attrs) = (t = find(id)) ? (t.merge!(attrs); true) : false
130
+ def destroy(id) = (@todos.reject! { |t| t[:id] == id.to_s }; true)
131
+ end
132
+ end
133
+
134
+ # Declare ONLY the editable field. A submit through this form cannot reach
135
+ # :completed -- the allowlist is enforced by construction.
136
+ TITLE_ONLY = [StreamWeaver::Field.new(:title, :string, {})].freeze
137
+
138
+ app 'My Todos' do
139
+ TodoStore.all.each do |todo|
140
+ fragment("todo-#{todo[:id]}") do
141
+ if state[:editing_id].to_s == todo[:id]
142
+ form_for(
143
+ store: TodoStore,
144
+ fields: TITLE_ONLY,
145
+ name: :"todo_#{todo[:id]}_form",
146
+ record: todo,
147
+ on_success: ->(_id) { state[:editing_id] = nil }
148
+ ) { submit_label 'Save' }
149
+ button('Cancel', style: :secondary, key: "cancel-#{todo[:id]}") { |s| s[:editing_id] = nil }
150
+ else
151
+ hstack spacing: :sm do
152
+ text "#{todo[:completed] ? '☑' : '☐'} #{todo[:title]}"
153
+ button('Edit', style: :secondary, key: "edit-#{todo[:id]}") { |s| s[:editing_id] = todo[:id] }
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end.run!
159
+ ```
160
+
161
+ **Gotchas**
162
+
163
+ - Per-row buttons **must** carry `key:` (Law 1). Without it the derived ids are
164
+ position-stable only, and a delete or a filter shifts them under you.
165
+ - `on_success:` is **required** for standalone `form_for` (no `resource` block to fall
166
+ back to for the post-submit transition).
167
+ - Standalone `form_for` derives its flash noun by stripping `_form` from `name:`, so
168
+ `:todo_1_form` flashes `"Todo_1 updated."` — cosmetic, known, unfixed.
169
+ - Inside a `resource` block the shorter form works and the transition is free:
170
+ `form_for :todo, record: todo do submit_label 'Save' end`.
171
+
172
+ ### 2. Scoped live search (submit-as-you-type)
173
+
174
+ `text_field` auto-submits on input — no Stimulus controller, no debounce code. The only
175
+ decision is **where the field lives**, and it is a real trade-off:
176
+
177
+ ```ruby
178
+ # Add to TodoStore. The blank guard is load-bearing -- see gotchas.
179
+ def TodoStore.search(query)
180
+ return all if query.to_s.strip.empty?
181
+ needle = query.to_s.downcase
182
+ all.select { |t| t[:title].downcase.include?(needle) }
183
+ end
184
+
185
+ app 'Search' do
186
+ # B -- field INSIDE the fragment: scoped morph, fewer bytes, focus and caret
187
+ # survive. Cost: the field re-renders itself on every keystroke.
188
+ fragment(:results) do
189
+ text_field :query, placeholder: 'Filter todos…'
190
+ results = TodoStore.search(state[:query])
191
+ text "#{results.length} of #{TodoStore.all.length} todos"
192
+ results.first(8).each { |t| text "• #{t[:title]}" }
193
+ end
194
+ end.run!
195
+ ```
196
+
197
+ **Gotchas**
198
+
199
+ - **`text_field :query, updates: :results` is silently accepted and silently ignored.**
200
+ This is the natural transliteration of Rails' `data: {turbo_frame: :todos}` and it looks
201
+ right; `updates:` is read only by `button` and `clickable`. There is no
202
+ `data-turbo-frame` equivalent for inputs yet.
203
+ - Consequently an input can target only its own enclosing fragment or `#app-container`.
204
+ **A field placed outside the results fragment swaps the whole container** — it works and
205
+ filters correctly, it just costs the whole body every keystroke. Prefer arrangement B
206
+ above unless you specifically need the field to never re-render itself.
207
+ - Guard the blank query in your store (`return all if query.to_s.strip.empty?`) or
208
+ clearing the box makes the whole list vanish.
209
+ - Search is a GET filter, so put the query in the URL via `route_with` if you want it
210
+ shareable.
211
+
212
+ ### 3. Lazy hover cards (CSS reveals it; revealing it is what fetches it)
213
+
214
+ Nothing on the page fetches anything until the pointer arrives. Three lines of CSS plus
215
+ `lazy: true` — the two halves interlock.
216
+
217
+ ```ruby
218
+ # Each todo carries a :user_id; UserStore is any lookup, deliberately slow here
219
+ # so "eager or lazy?" is observable.
220
+ module UserStore
221
+ USERS = { 'u1' => { id: 'u1', name: 'Ada Lovelace', role: 'Engineering' },
222
+ 'u2' => { id: 'u2', name: 'Alan Turing', role: 'Research' } }.freeze
223
+ def self.find(id) = USERS[id]
224
+ end
225
+
226
+ app 'Directory' do
227
+ use_stylesheet <<~CSS
228
+ .hovercard { position: relative; display: inline-block; cursor: help; }
229
+ .hovercard .hovercard-panel { display: none; position: absolute; top: 1.6em; left: 0; z-index: 50; min-width: 15rem; }
230
+ .hovercard:hover .hovercard-panel { display: block; }
231
+ CSS
232
+
233
+ TodoStore.all.each do |todo|
234
+ user = UserStore.find(todo[:user_id])
235
+ div class: 'hovercard' do
236
+ text user[:name]
237
+ div class: 'hovercard-panel' do
238
+ # Keyed by the TODO, not the user -- see the gotcha below.
239
+ fragment(:"hovercard_#{todo[:id]}", lazy: true, placeholder: 'Loading…') do
240
+ card do
241
+ header3 user[:name]
242
+ text user[:role]
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end.run!
249
+ ```
250
+
251
+ **Gotchas**
252
+
253
+ - **Key by the todo, not the user.** Two todos sharing an assignee would emit the same
254
+ fragment name twice; hovering the second shows the first one's card, silently. This is
255
+ Law 1 in its purest form.
256
+ - `display: none` means no fetch, for as long as it lasts — that is the guarantee that
257
+ makes six cards cost zero on page load. Don't hide the panel with `visibility: hidden`
258
+ or `opacity: 0`; those stay visible to the observer and every card fetches eagerly.
259
+ - It fetches **once**. Hover away and back and the landed content stays put.
260
+ - To verify laziness: `document.querySelectorAll('.sw-fragment-lazy').length` counts
261
+ fragments that have *not* fetched yet.
262
+
263
+ ### 4. Russian-doll infinite scroll (nested lazy fragments)
264
+
265
+ **The page the reader is on renders in the shell; only the NEXT page is lazy.** Page N's
266
+ rows go out inline, and its block ends by declaring page N+1 as a lazy fragment whose own
267
+ block recurses. Page N+1 does not exist in the DOM — and never fetches — until page N has
268
+ been scrolled past. That recursion is the entire pagination logic: no scroll handler, no
269
+ page counter in state, no "Load more" button.
270
+
271
+ ```ruby
272
+ # One page's slice -- never everything-so-far. Empty return terminates the chain.
273
+ TodoStore::PER_PAGE = 10
274
+ def TodoStore.page(number) = all.drop(([number.to_i, 1].max - 1) * PER_PAGE).first(PER_PAGE)
275
+
276
+ def scroll_page(number)
277
+ # THIS page renders now, in whatever response we're already writing.
278
+ TodoStore.page(number).each { |t| card { text t[:title] } }
279
+
280
+ # Only the NEXT page is deferred. The guard is load-bearing: without it the
281
+ # chain dead-ends in an empty fragment that fetches nothing.
282
+ nxt = number + 1
283
+ unless TodoStore.page(nxt).empty?
284
+ fragment(:"page_#{nxt}", lazy: true, placeholder: 'Loading…') do
285
+ scroll_page(nxt) # recursion lives INSIDE the lazy fragment
286
+ end
287
+ end
288
+ end
289
+
290
+ app('Feed') { scroll_page(1) }.run!
291
+ ```
292
+
293
+ **Gotchas**
294
+
295
+ - **Do not wrap page 1 in the lazy fragment.** Writing
296
+ `fragment(:"page_#{number}", lazy: true) { rows; recurse }` and calling it with `1` is
297
+ the tempting shape and it is wrong: the block is skipped on the shell render (see the
298
+ three rules above), so the served HTML contains no rows at all — just a placeholder. It
299
+ *looks* like it works only because page 1's placeholder sits in the initial viewport, so
300
+ the observer fires immediately. A lazy fragment already in the viewport fetches at once,
301
+ which is exactly what hides this mistake. Render the current page, lazy the next one —
302
+ that is also what Turbo does, and it is what keeps the shell honest for crawlers, for a
303
+ reader with no JavaScript, and for anyone reading the served HTML.
304
+ - Have the store return **one page's slice** (`all.drop((n - 1) * PER_PAGE).first(PER_PAGE)`),
305
+ never everything-so-far. Each response should be a constant size; if payloads grow
306
+ per page, you're re-sending rows.
307
+ - Each fetch re-runs its ancestors' blocks, so keep per-page work cheap. Nested ids are
308
+ `parent--child`: page 2's fragment is declared beside page 1's rows, so it is
309
+ `sw-frag-page-2`; page 3's is declared inside page 2's block, giving
310
+ `sw-frag-page-2--page-3`.
311
+ - **Inside a tab, the lazy fragment goes INSIDE a `tab` block, never beside one** at the
312
+ top level of a `tabs` block — a non-tab child there shifts every panel index. Route tabs
313
+ (`tabs :view, url: true`) render inactive panels as `display: none`, so a lazy fragment
314
+ in one waits until that tab is opened:
315
+
316
+ ```ruby
317
+ tabs :view, url: true do
318
+ tab('Summary') { text summary_line }
319
+ tab('Revenue') { fragment(:revenue, lazy: true) { revenue_table } }
320
+ end
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Anti-patterns (each one has bitten someone)
326
+
327
+ - **`defer { ... }`** — the bare verb is not the deferred primitive. It now raises and
328
+ names the real one: `fragment(:name, defer: true)`.
329
+ - **`every(...)` to refresh a slow region** — that's Law 2. Use `defer:`.
330
+ - **Named-action buttons inside a deferred/lazy fragment** — they don't fire. Block
331
+ buttons.
332
+ - **Keying by positional index** — `key: i` is exactly the identity that reordering
333
+ destroys. Key by record id.
334
+ - **`text "**bold**"`** — `text` does not render markdown. Use `md`.
335
+ - **`spacer` / `divider`** — don't exist. `div(style: 'height:24px')`.
336
+ - **`<button onclick="location.href=...">`** — bypasses htmx, full reload, loses state.
337
+ Use `link_to` / `nav_item` with a real `href`.
338
+ - **Assuming port 4567** — StreamWeaver finds a free port and prints the real URL. Read
339
+ stdout.
340
+
341
+ **Catch keying bugs before they ship:** `strict_ids` turns a derived-id collision from a
342
+ warning into a hard failure. It raises in development and test, and degrades to a warning
343
+ in production (a live page shouldn't 500 over an id the framework already disambiguated).
344
+
345
+ ```ruby
346
+ StreamWeaver::App.new('My App', strict_ids: true) { ... } # per app
347
+ StreamWeaver.strict_ids = true # global
348
+ SW_STRICT_IDS=1 ruby my_app.rb # env, e.g. in CI
349
+ ```
350
+
351
+ ---
352
+
353
+ ## Going deeper (pointers only — these are not summarized here)
354
+
355
+ **The narrated twin.** `docs/tutorials/the-streamweaver-way.md` — "Build My Todos:
356
+ the StreamWeaver Way" builds these same four features step by step, why-first, each
357
+ step naming the law it obeys, the gotcha it avoids, and the measured number. This
358
+ skill prescribes; the tutorial explains. Point a human at the tutorial. Also runs as
359
+ a `:doc`-theme app: `SW_NO_OPEN=1 ruby examples/tutorials/streamweaver_way_tutorial.rb`.
360
+
361
+ **The worked example set.** `examples/my_todos/` is all four recipes above in one running
362
+ app, annotated with the Rails mechanism each one mirrors. Boot it with
363
+ `SW_NO_OPEN=1 ruby examples/my_todos/my_todos.rb`, and `SW_HOVERCARD_DELAY=1.5` to feel
364
+ the difference lazy makes.
365
+
366
+ **`llms.txt` (aliased as `docs/for_llms.md`)** — the full DSL reference. Sections that
367
+ extend this skill, by name:
368
+
369
+ | Section | What it adds |
370
+ |---|---|
371
+ | Interactive IDs and keying | Full precedence rules, `strict_ids`, `clickable` |
372
+ | Record-Bound Forms (`form_for`) | Every option, in-block DSL, validation semantics |
373
+ | Turbo Frame-Style Inline Editing with `form_for` | The `resource`-based variant of recipe 1 |
374
+ | Deferred Fragments (`fragment ..., defer: true`) | Placeholders, nesting, export behavior |
375
+ | Lazy Fragments (`fragment ..., lazy: true`) | Both guarantees, and all three recipes |
376
+ | Dev Loud, Prod Self-Heal | What the overlay names, and what it can't detect |
377
+ | Resource Scaffolding | `resource` CRUD, named-route helpers |
378
+ | Route Tabs (`tabs url: true`) | Deep-linkable tabs, URL authority ordering |
379
+ | Repo Conventions | The full anti-pattern list |
380
+
381
+ **Reference docs.** `docs/resource-dsl.md` (`resource` + `form_for` full reference),
382
+ `docs/routing.md` (**read Common Pitfalls before any app with more than a couple of
383
+ routes**), `docs/crud-patterns.md`, `docs/components_reference.md`.
384
+
385
+ **Decision docs — read when you want the *why*, or are extending the primitive.**
386
+ `docs/research/streamweaver-way-spike-findings.md` (what broke and how each break was
387
+ closed — the source of every gotcha above),
388
+ `docs/research/2026-08-22-lazy-fragments-trigger-decision.md` (why `intersect once` and
389
+ not `revealed`),
390
+ `docs/research/2026-08-22-learnhotwire-syllabus-coverage.md` (Hotwire feature-by-feature
391
+ coverage: what exists, what's next),
392
+ `docs/streamweaver-frontend-vision.md` (the strategic thesis).
393
+
394
+ **Sibling skills.** `streamweaver-visual-companion` for mockups, diagrams, and dashboards
395
+ you just want to look at; `streamweaver-doc-builder` for long-form editorial documents.
396
+ This skill is for apps people interact with.
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: visual-plan
3
+ description: Use before starting any non-trivial implementation task — renders a live canvas showing the implementation map, open decisions, and wireframes so the user can sign off before code is written
4
+ ---
5
+
6
+ # Visual Plan
7
+
8
+ Pre-flight planning via StreamWeaver canvas. Push a live plan showing what will change and why, flag open decisions, and block for sign-off on anything that needs user input — all before touching any code.
9
+
10
+ ## When to Use
11
+
12
+ Before any task where:
13
+ - Multiple files will change
14
+ - An architecture decision is open (database choice, API shape, component structure)
15
+ - A UI surface is being added or significantly reworked
16
+ - The user said "show me the plan first"
17
+
18
+ Skip for trivial edits (one-line fix, rename, config change) — overhead exceeds value.
19
+
20
+ ## !! DO NOT LAUNCH STANDALONE SERVERS PER QUESTION !!
21
+
22
+ Same rule as the visual companion: **never** run `ruby app.rb` or `streamweaver <file.rb>` for each planning update. Use `canvas-push` to update a single persistent window throughout the session.
23
+
24
+ ## Starting a Plan Session
25
+
26
+ ```bash
27
+ # Open a named canvas panel — opens in iTerm2 split pane, or browser tab elsewhere
28
+ streamweaver panel plan
29
+
30
+ # Push the initial plan content
31
+ streamweaver canvas-push plan <<'RUBY'
32
+ header1 "Implementation Plan"
33
+
34
+ implementation_map(files: [
35
+ { path: "lib/foo/bar.rb", note: "Add #process method for new pipeline step" },
36
+ { path: "lib/foo/pipeline.rb", note: "Wire bar into pipeline chain" },
37
+ { path: "spec/foo/bar_spec.rb", note: "Cover happy path and nil input edge case" }
38
+ ])
39
+
40
+ decision(question: "Should bar be stateless or hold config?") do
41
+ option(id: :stateless, label: "Stateless",
42
+ detail: "Simple — take config as args each call. Easy to test.",
43
+ recommended: true)
44
+ option(id: :stateful, label: "Stateful",
45
+ detail: "Holds config in instance. Needed if config is expensive to build.")
46
+ end
47
+ RUBY
48
+ ```
49
+
50
+ Tell the user: "Check the plan canvas at [url]. Let me know in the terminal if you want to adjust anything."
51
+
52
+ ## The Planning Loop
53
+
54
+ 1. Push the full plan canvas (files + decisions + wireframes)
55
+ 2. Tell the user the canvas URL, end your turn
56
+ 3. User reviews and responds in the terminal
57
+ 4. If a decision needs sign-off: use `canvas-wait` (see below) to block until they click
58
+ 5. Once approved: start implementation; push progress updates to the same canvas
59
+
60
+ **The canvas stays open while you implement.** Push updates as files change.
61
+
62
+ ## DSL Reference
63
+
64
+ ### `implementation_map` — what files change and why
65
+
66
+ ```ruby
67
+ implementation_map(files: [
68
+ { path: "lib/payments/processor.rb", note: "Add retry logic for network errors" },
69
+ { path: "lib/payments/errors.rb", note: "New RetryableError class" },
70
+ { path: "spec/payments/processor_spec.rb", note: "Cover retry behavior" }
71
+ ])
72
+ ```
73
+
74
+ `note:` should explain the why, not the what ("Add retry logic" not "modify method").
75
+
76
+ ### `decision` — open architecture choices
77
+
78
+ ```ruby
79
+ decision(question: "Which caching layer?") do
80
+ option(id: :redis, label: "Redis", detail: "Shared, fast, extra infra dep", recommended: true)
81
+ option(id: :memory, label: "In-memory", detail: "Zero dep, lost on restart")
82
+ option(id: :none, label: "No cache", detail: "Simplest — acceptable if <50ms")
83
+ end
84
+ ```
85
+
86
+ Mark the option you'd choose `recommended: true`.
87
+
88
+ ### `callout(:decision)` — tradeoff flags inline
89
+
90
+ ```ruby
91
+ callout(variant: :decision, title: "Auth approach affects test setup") do
92
+ text "If we use JWT here, the test helpers need to generate tokens — adds ~30 lines to spec/support."
93
+ text "If we use session cookies, existing test helpers work as-is."
94
+ end
95
+ ```
96
+
97
+ Use for tradeoffs that don't need a full option matrix — a constraint, an implicit choice, a flag for the user.
98
+
99
+ ### `callout(:risk)` — things that could go wrong
100
+
101
+ ```ruby
102
+ callout(variant: :risk, title: "Migration is irreversible") do
103
+ text "DROP COLUMN is non-reversible in production without a new migration."
104
+ text "Confirm data is not needed before proceeding."
105
+ end
106
+ ```
107
+
108
+ Use for anything that could bite silently: data loss, breaking changes, non-idempotent operations, external API calls with side effects.
109
+
110
+ ### `wireframe` — UI mockups for new surfaces
111
+
112
+ ```ruby
113
+ wireframe(surface: :browser) do
114
+ <<~HTML
115
+ <div class="wf-card">
116
+ <h2>Invoice #1042</h2>
117
+ <p class="wf-muted">Due 2026-07-01</p>
118
+ <button class="primary">Pay now</button>
119
+ </div>
120
+ HTML
121
+ end
122
+ ```
123
+
124
+ Surfaces: `:browser`, `:desktop`, `:mobile`, `:phone`, `:tablet`, `:popover`, `:card`, `:widget`, `:panel`.
125
+
126
+ Use for UI stories where the layout matters. Skip if the change is purely logic or data.
127
+
128
+ ### Combining components
129
+
130
+ ```ruby
131
+ streamweaver canvas-push plan <<'RUBY'
132
+ header1 "Plan: Add Invoice Detail Page"
133
+
134
+ implementation_map(files: [
135
+ { path: "app/controllers/invoices_controller.rb", note: "Add #show action" },
136
+ { path: "app/views/invoices/show.html.erb", note: "New view" },
137
+ { path: "spec/controllers/invoices_controller_spec.rb", note: "Cover show + 404" }
138
+ ])
139
+
140
+ decision(question: "Render total in controller or view?") do
141
+ option(id: :controller, label: "Controller", detail: "Testable, consistent", recommended: true)
142
+ option(id: :view, label: "View helper", detail: "Less boilerplate if reused")
143
+ end
144
+
145
+ callout(variant: :risk, title: "Invoice data includes PII") do
146
+ text "Confirm auth check on #show — anonymous access must 403, not 404."
147
+ end
148
+
149
+ wireframe(surface: :browser) do
150
+ '<h1>Invoice #1042</h1><p class="wf-muted">$240.00 due</p><button class="primary">Pay</button>'
151
+ end
152
+ RUBY
153
+ ```
154
+
155
+ ## Blocking for Sign-Off with `canvas-wait`
156
+
157
+ When a decision needs explicit user approval before you proceed, add interactive controls and wait:
158
+
159
+ ```bash
160
+ streamweaver canvas-push plan <<'RUBY'
161
+ header2 "Approve to continue"
162
+ radio_group :db_choice, ["PostgreSQL (recommended)", "SQLite", "Needs more discussion"]
163
+ button "Approve and proceed"
164
+ RUBY
165
+
166
+ # Blocks until user clicks the button — prints JSON with their selection
167
+ result=$(streamweaver canvas-wait plan)
168
+ echo "User chose: $result"
169
+ ```
170
+
171
+ `canvas-wait` returns JSON: `{"type":"action","element":"button","value":"Approve and proceed","state":{"db_choice":"PostgreSQL (recommended)"}}`.
172
+
173
+ Use when the decision changes the implementation path, is hard to reverse, or the user should weigh in before code is written. Skip for low-stakes choices — don't block when the user can redirect in the next turn.
174
+
175
+ ## Updating the Canvas During Implementation
176
+
177
+ Once approved, keep the canvas live as work progresses:
178
+
179
+ ```bash
180
+ # After each file is written
181
+ streamweaver canvas-push plan <<'RUBY'
182
+ header1 "Implementation Plan — In Progress"
183
+
184
+ implementation_map(files: [
185
+ { path: "lib/foo/bar.rb", note: "[done]" },
186
+ { path: "lib/foo/pipeline.rb", note: "[wip]" },
187
+ { path: "spec/foo/bar_spec.rb", note: "[pending]" }
188
+ ])
189
+ RUBY
190
+ ```
191
+
192
+ The user sees live status without having to ask. The plan evolves from "what will change" to "what has changed."
193
+
194
+ ## Known Gotchas
195
+
196
+ - **`undefined method` errors on new components** — the canvas bridge may be an orphaned process from a previous session running older code. Fix: `streamweaver canvas-stop && streamweaver panel plan`. The fresh bridge picks up the latest gem code.
197
+ - `spacer` and `divider` don't exist — use `div(style: "height:Npx")`
198
+ - `text` does not render markdown — use `md` for bold/italic/links
199
+ - `decision` block scope: only `option(...)` calls are valid inside it — other DSL methods throw
200
+ - StreamWeaver auto-selects an available port — capture the URL from stdout, don't hardcode 4567
201
+ - Canvas sessions default to `:fluid` (full-width) — good for wide implementation maps; use `--layout=default` for a narrower card