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,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ # Raised when a named action's token no longer matches the app's current
5
+ # action definitions (code reload changed the registered action set) or
6
+ # the session's action generation (server restart / session reset).
7
+ # server.rb's dev-mode fallback overlay (dev-loud-failure-overlay) reads
8
+ # #action/#fragment/#cause off the raised instance to name the stale
9
+ # target and likely cause -- kwargs are optional so existing
10
+ # `raise StaleActionDefinition` / `rescue StaleActionDefinition` call
11
+ # sites (and specs) that don't care about the detail keep working.
12
+ class StaleActionDefinition < StandardError
13
+ attr_reader :action, :fragment, :cause
14
+
15
+ def initialize(msg = nil, action: nil, fragment: nil, cause: nil)
16
+ @action = action
17
+ @fragment = fragment
18
+ @cause = cause
19
+ super(msg || "stale action token#{" for action #{action.inspect}" if action}")
20
+ end
21
+ end
22
+
23
+ # Owns the state-to-response pipeline for every interactive request.
24
+ #
25
+ # Updates and events dispatch against descriptors/components retained from the
26
+ # tree sent to the browser, so they need only the post-interaction rebuild.
27
+ # Legacy block buttons and forms still need a discovery rebuild because their
28
+ # executable closures are created while the DSL is evaluated. FAC-P1.3 named
29
+ # actions will remove that discovery rebuild for buttons.
30
+ class InteractionRunner
31
+ # The framework-wide routing params, plus the ones only interaction dispatch
32
+ # puts on the wire.
33
+ ROUTE_PARAMS = (App::ROUTE_OWNED_PARAMS + %w[key form_name _sw_fragment]).freeze
34
+
35
+ def initialize(app:, state:, params:, interaction:, target: nil, adapter:, agentic: false,
36
+ persist:, prepare_state: nil, result_container: nil, auto_close: false,
37
+ action_manifest: nil, generation: "0", persist_manifest: nil,
38
+ response_headers: nil, state_version: 0, persist_state_version: nil)
39
+ @app = app
40
+ @state = state
41
+ @params = params
42
+ @interaction = interaction.to_sym
43
+ @target = target
44
+ @adapter = adapter
45
+ @agentic = agentic
46
+ @persist = persist
47
+ @prepare_state = prepare_state
48
+ @result_container = result_container
49
+ @auto_close = auto_close
50
+ @action_manifest = action_manifest || Set.new
51
+ @generation = generation.to_s
52
+ @persist_manifest = persist_manifest
53
+ @response_headers = response_headers
54
+ @state_version = state_version.to_i
55
+ @persist_state_version = persist_state_version
56
+ end
57
+
58
+ def call
59
+ app.with_render_lock do
60
+ # For named actions (discovery_required? false below), this is still
61
+ # the retained tree from the last rebuild -- i.e. exactly what's live
62
+ # in the browser right now, before this interaction's mutation. Row
63
+ # narrowing (#row_narrowing) diffs a table found in here against the
64
+ # same table after the final rebuild to prove a mutation is row-local.
65
+ components_before = app.components
66
+ before_state = deep_copy(state)
67
+ prepare_state&.call(state)
68
+ prepare_form_state if interaction == :form
69
+ rebuild if discovery_required?
70
+
71
+ unless interaction == :form
72
+ sync_params_to_state
73
+ handle_unchecked_checkboxes
74
+ end
75
+
76
+ dispatch
77
+ persist.call(state)
78
+
79
+ completion_response || begin
80
+ rebuild
81
+ persist_manifest&.call(app.render_state.action_tokens)
82
+ scoped_response(before_state, components_before) || Views::AppContentView.new(app, state, adapter, agentic).call
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :app, :state, :params, :interaction, :target, :adapter, :agentic,
90
+ :persist, :prepare_state, :result_container, :auto_close,
91
+ :action_manifest, :generation, :persist_manifest
92
+
93
+ def response_headers = @response_headers
94
+ def state_version = @state_version
95
+ def persist_state_version = @persist_state_version
96
+
97
+ def deep_copy(value)
98
+ Marshal.load(Marshal.dump(value))
99
+ rescue TypeError
100
+ value.transform_values { |item| item.dup rescue item }
101
+ end
102
+
103
+ # Every rebuild in this request names the fragment the request is scoped to,
104
+ # so a `defer: true` fragment the client is fetching executes its block on
105
+ # this pass instead of rendering its placeholder again (see App#fragment).
106
+ # The name comes off the signed scope token, so it is the server's own
107
+ # claim, not the browser's.
108
+ def rebuild
109
+ app.rebuild_with_state(state, generation: generation, state_version: state_version,
110
+ deferred_target: declared_scope&.fetch(:fragment))
111
+ end
112
+
113
+ def scoped_response(before_state, components_before)
114
+ scope = declared_scope
115
+ if named_action? && !@named_action_authorized
116
+ response_headers&.call("HX-Retarget" => "#app-container")
117
+ return Views::AppContentView.new(app, state, adapter, agentic).call
118
+ end
119
+ if fragment_param_present? && !scope
120
+ response_headers&.call("HX-Retarget" => "#app-container")
121
+ return Views::AppContentView.new(app, state, adapter, agentic).call
122
+ end
123
+ return unless scope
124
+
125
+ if app.route_key && before_state[app.route_key] != state[app.route_key]
126
+ response_headers&.call("HX-Retarget" => "#app-container")
127
+ return Views::AppContentView.new(app, state, adapter, agentic).call
128
+ end
129
+
130
+ fragment = find_fragment(app.components, scope[:fragment])
131
+ unless fragment
132
+ response_headers&.call("HX-Retarget" => "#app-container")
133
+ return Views::AppContentView.new(app, state, adapter, agentic).call
134
+ end
135
+
136
+ extras = scope[:updates].filter_map { |name| find_fragment_by_name(app.components, name) }
137
+ if extras.length != scope[:updates].length
138
+ response_headers&.call("HX-Retarget" => "#app-container")
139
+ return Views::AppContentView.new(app, state, adapter, agentic).call
140
+ end
141
+
142
+ # `primary: :fragment_name` (stream_weaver-78a): a button can name a
143
+ # sibling fragment as the response's actual primary swap target
144
+ # instead of its own enclosing fragment -- e.g. a board's "View"
145
+ # button refreshing a detail pane without resending the whole board.
146
+ # Resolved against the post-rebuild tree exactly like `updates:`; a
147
+ # name that no longer matches any fragment (conditionally removed by
148
+ # this very interaction) falls back to a full-container response
149
+ # rather than silently doing nothing.
150
+ primary_fragment = nil
151
+ if scope[:primary]
152
+ primary_fragment = find_fragment_by_name(app.components, scope[:primary])
153
+ unless primary_fragment
154
+ response_headers&.call("HX-Retarget" => "#app-container")
155
+ return Views::AppContentView.new(app, state, adapter, agentic).call
156
+ end
157
+ end
158
+ primary_content = primary_fragment || fragment
159
+
160
+ # Flash OOB under scoped responses (stream_weaver-m3t). A fragment
161
+ # named `:flash` is StreamWeaver's flash-placement convention
162
+ # (`fragment(:flash) { flash_messages }`); any scoped response that
163
+ # just set a flash message auto-delivers that fragment as an extra OOB
164
+ # swap so authors don't have to remember `updates: [:flash]` on every
165
+ # single action for flash to ever be visible.
166
+ unless (state[:_flash] || {}).empty?
167
+ flash_fragment = find_fragment_by_name(app.components, "flash")
168
+ extras += [flash_fragment] if flash_fragment && !extras.include?(flash_fragment)
169
+ end
170
+
171
+ # A deferred fragment's auto-fetch carries no state patch. Every deferred
172
+ # fragment on a page fires its `load` trigger in the same tick, so all
173
+ # their requests leave with the same `sw_state_version` cookie and would
174
+ # each claim version N+1 -- the client applies the first and reloads on
175
+ # the rest (see the `patch.version !== currentVersion + 1` guard in the
176
+ # adapter's swap hook), which reloads into the same race. A deferred fetch
177
+ # dispatches nothing (see #dispatch's `:update` branch) and returns values
178
+ # the client already holds, so it has no patch to skip.
179
+ unless deferred_fetch?
180
+ version = state_version + 1
181
+ persist_state_version&.call(version)
182
+ patch = state_patch(before_state, state, version)
183
+ end
184
+
185
+ # Row-granular table swaps (stream_weaver-95k). A named action's row
186
+ # mutation can be narrowed whether the table it touched is the primary
187
+ # target fragment *or* one of the declared `updates:` extras -- the
188
+ # common shape is actually the latter (an "Add" button living in a
189
+ # toolbar fragment, with `updates: :the_table_fragment`), so both are
190
+ # analyzed independently and each narrows or falls back on its own.
191
+ # Skipped when `primary:` redirects the response to an unrelated
192
+ # sibling fragment -- narrowing is a size optimization only (never a
193
+ # state-semantics change), so falling back to full fragment content is
194
+ # always correct.
195
+ if named_action? && components_before && !primary_fragment
196
+ row_swap = row_swap_for(components_before, primary_content)
197
+ extra_swaps = extras.map { |extra| row_swap_for(components_before, extra) }
198
+ end
199
+ extra_swaps ||= []
200
+
201
+ if row_swap
202
+ response_headers&.call(primary_swap_headers(row_swap))
203
+ return Views::RowSwapView.new(app, state, adapter, updates: extras, extra_swaps: extra_swaps,
204
+ state_patch: patch, primary: primary_swap_proc(row_swap)).call
205
+ end
206
+
207
+ response_headers&.call("HX-Retarget" => "##{primary_fragment.id}") if primary_fragment
208
+
209
+ Views::FragmentContentView.new(app, state, adapter, primary_content, updates: extras, extra_swaps: extra_swaps,
210
+ state_patch: patch).call
211
+ end
212
+
213
+ # Row-local proof for one fragment: does it contain exactly one table
214
+ # with row identity, and does that table's row-id list before/after this
215
+ # interaction's rebuild prove the mutation touched only one row? Same
216
+ # ordered row keys (edit), pre-set minus exactly the actioned key
217
+ # (delete), or pre-set plus one trailing key (create) -- anything else
218
+ # (a sort, a filter, more than one table, unresolvable row keys) returns
219
+ # nil, and the caller falls back to that fragment's full content, which
220
+ # is always correct (point 4 of the design: narrowing is a size
221
+ # optimization only, never a state-semantics change).
222
+ #
223
+ # @param components_before [Array<Components::Base>] The retained tree from before this
224
+ # interaction's mutation (see #call)
225
+ # @param new_fragment [Components::Fragment] The post-mutation fragment (target or extra)
226
+ # @return [Hash, nil] `{ kind:, table:, idx:, row_dom_id: }` (idx absent for :delete) or nil
227
+ def row_swap_for(components_before, new_fragment)
228
+ old_fragment = find_fragment(components_before, new_fragment.id)
229
+ return nil unless old_fragment
230
+
231
+ old_table = single_row_table(old_fragment.children)
232
+ new_table = single_row_table(new_fragment.children)
233
+ return nil unless old_table && new_table && old_table.dom_id == new_table.dom_id
234
+
235
+ old_ids = old_table.table_options[:row_ids]
236
+ new_ids = new_table.table_options[:row_ids]
237
+ return nil if old_ids.empty? || new_ids.empty? || old_ids.any?(&:nil?) || new_ids.any?(&:nil?)
238
+
239
+ action_key = decoded_action && decoded_action[:k]
240
+ return nil if action_key.nil?
241
+
242
+ if new_ids == old_ids
243
+ idx = old_ids.index(action_key)
244
+ return nil unless idx
245
+ { kind: :edit, table: new_table, idx: idx, row_dom_id: "#{new_table.dom_id}-row-#{old_ids[idx]}" }
246
+ elsif old_ids.length == new_ids.length + 1 && (old_ids - [action_key]) == new_ids
247
+ { kind: :delete, table: old_table, row_dom_id: "#{old_table.dom_id}-row-#{action_key}" }
248
+ elsif new_ids.length == old_ids.length + 1 && new_ids[0...-1] == old_ids
249
+ { kind: :create, table: new_table, idx: new_ids.length - 1 }
250
+ end
251
+ end
252
+
253
+ # The table a fragment's row-mutation could be narrowed against, or nil
254
+ # if the fragment doesn't contain exactly one table with row identity --
255
+ # more than one is ambiguous (which table did the action touch?), so it
256
+ # falls back like everything else unproven (FAC-P2.1's `dom_id` is only
257
+ # assigned to column-DSL tables with row identity in the first place).
258
+ def single_row_table(children)
259
+ tables = collect_tables(children)
260
+ tables.length == 1 ? tables.first : nil
261
+ end
262
+
263
+ def collect_tables(children)
264
+ children.each_with_object([]) do |component, found|
265
+ found << component if component.is_a?(Components::Table) && component.dom_id
266
+ found.concat(collect_tables(component.children)) if component.respond_to?(:children) && component.children
267
+ end
268
+ end
269
+
270
+ def primary_swap_headers(swap)
271
+ case swap[:kind]
272
+ when :edit then { "HX-Retarget" => "##{swap[:row_dom_id]}", "HX-Reswap" => "outerHTML" }
273
+ when :delete then { "HX-Retarget" => "##{swap[:row_dom_id]}", "HX-Reswap" => "delete" }
274
+ when :create then { "HX-Retarget" => "##{swap[:table].dom_id} tbody", "HX-Reswap" => "beforeend" }
275
+ end
276
+ end
277
+
278
+ def primary_swap_proc(swap)
279
+ return nil if swap[:kind] == :delete
280
+
281
+ table = swap[:table]
282
+ idx = swap[:idx]
283
+ ->(view) { adapter.render_table_row(view, table.resolved_rows[idx], idx, table.table_options, state, table.dom_id) }
284
+ end
285
+
286
+ def declared_scope
287
+ payload = named_action? ? decoded_action : decoded_fragment_param
288
+ return unless payload && payload[:f]
289
+ { fragment: payload[:f].to_s, updates: Array(payload[:u]).map(&:to_s), primary: payload[:p]&.to_s }
290
+ end
291
+
292
+ # Memoized: every rebuild now consults the scope too (see #rebuild), and
293
+ # decoding re-verifies the token's signature each time.
294
+ def decoded_fragment_param
295
+ return @decoded_fragment_param if defined?(@decoded_fragment_param)
296
+
297
+ token = params["_sw_fragment"] || params[:_sw_fragment]
298
+ @decoded_fragment_param = begin
299
+ ActionToken.decode(token) if token
300
+ rescue ActionToken::Invalid
301
+ nil
302
+ end
303
+ end
304
+
305
+ # Signed by the adapter when it renders a `defer: true` fragment's own
306
+ # auto-fetch (see Adapter::AlpineJS#htmx_attrs), never by a browser.
307
+ def deferred_fetch?
308
+ !named_action? && !decoded_fragment_param.nil? && !decoded_fragment_param[:d].nil?
309
+ end
310
+
311
+ def fragment_param_present?
312
+ params.key?("_sw_fragment") || params.key?(:_sw_fragment)
313
+ end
314
+
315
+ def state_patch(before_state, after_state, version)
316
+ set = after_state.each_with_object({}) do |(key, value), changed|
317
+ changed[key.to_s] = value unless before_state.key?(key) && before_state[key] == value
318
+ end
319
+ deleted = before_state.keys.reject { |key| after_state.key?(key) }.map(&:to_s)
320
+ { set: set, delete: deleted, version: version }
321
+ end
322
+
323
+ def find_fragment(components, id)
324
+ find_recursive(components) { |component| component.is_a?(Components::Fragment) && component.id == id }
325
+ end
326
+
327
+ def find_fragment_by_name(components, name)
328
+ find_recursive(components) { |component| component.is_a?(Components::Fragment) && component.name.to_s == name.to_s }
329
+ end
330
+
331
+ def discovery_required?
332
+ (interaction == :action && !named_action?) || interaction == :form
333
+ end
334
+
335
+ def sync_params_to_state
336
+ params.each do |key, value|
337
+ next if ROUTE_PARAMS.include?(key.to_s)
338
+
339
+ if value.is_a?(Hash)
340
+ # A scope-nested "live" field (FAC-P3.1 handoff): Rack already
341
+ # parsed the Rails-style `scope[field]=value` param into a nested
342
+ # hash. Merge the one changed field into the scope's sub-hash --
343
+ # never replace it wholesale, since a single live field's keyup
344
+ # only posts that one key, not its siblings.
345
+ scope_name = key.to_sym
346
+ state[scope_name] ||= {}
347
+ value.each { |k, v| state[scope_name][k.to_sym] = coerce_param_value(v, state[scope_name][k.to_sym]) }
348
+ else
349
+ state[key.to_sym] = coerce_param_value(value, state[key.to_sym])
350
+ end
351
+ end
352
+ end
353
+
354
+ def coerce_param_value(value, current_value)
355
+ case
356
+ when value.is_a?(Array) then value
357
+ when value == "on" || value == "true" then true
358
+ when value == "false" then false
359
+ when current_value.is_a?(Array) then Array(value)
360
+ else value
361
+ end
362
+ end
363
+
364
+ def handle_unchecked_checkboxes
365
+ app.render_state.checkbox_keys.each do |scope_name, key|
366
+ if scope_name
367
+ nested = params[scope_name.to_s]
368
+ next if nested.is_a?(Hash) && (nested.key?(key.to_s) || nested.key?(key))
369
+ state[scope_name] ||= {}
370
+ state[scope_name][key] = false
371
+ else
372
+ state[key] = false unless params.key?(key.to_s) || params.key?(key)
373
+ end
374
+ end
375
+ end
376
+
377
+ def prepare_form_state
378
+ values = (params[target.to_s] || params[target.to_sym] || {}).each_with_object({}) do |(key, value), result|
379
+ result[key.to_sym] = coerce_param_value(value, nil)
380
+ end
381
+ state[target.to_sym] = values
382
+ @form_values = values
383
+ end
384
+
385
+ def dispatch
386
+ case interaction
387
+ when :update
388
+ nil
389
+ when :action
390
+ if named_action?
391
+ payload = decoded_action
392
+ return unless payload
393
+ return unless action_manifest.include?(ActionToken.fingerprint(target))
394
+ if payload[:d] != app.action_definition_digest || payload[:g].to_s != generation
395
+ cause = payload[:d] != app.action_definition_digest ? :definition_changed : :generation_changed
396
+ raise StaleActionDefinition.new(action: payload[:a], fragment: payload[:f], cause: cause)
397
+ end
398
+ @named_action_authorized = true
399
+ app.bind_dispatch_state(state)
400
+ app.actions[payload[:a].to_sym]&.call(state, payload[:k])
401
+ else
402
+ find_button(app.components, target.to_s)&.execute(state)
403
+ end
404
+ when :event
405
+ component = find_component(app.components, target.to_sym)
406
+ return unless component
407
+
408
+ value = state[target.to_sym]
409
+ component.execute_on_change(state, value) if component.respond_to?(:execute_on_change)
410
+ component.execute_on_blur(state, value) if component.respond_to?(:execute_on_blur)
411
+ when :form
412
+ find_form(app.components, target.to_sym)&.execute_submit(state, @form_values)
413
+ else
414
+ raise ArgumentError, "unknown interaction: #{interaction.inspect}"
415
+ end
416
+ end
417
+
418
+ def named_action?
419
+ target.to_s.include?(".")
420
+ end
421
+
422
+ def decoded_action
423
+ @decoded_action ||= ActionToken.decode(target)
424
+ rescue ActionToken::Invalid
425
+ nil
426
+ end
427
+
428
+ def completion_response
429
+ return unless agentic && state[:_result] && result_container
430
+
431
+ filtered = {}
432
+ collect_input_keys(app.components).each { |key| filtered[key] = state[key] if state.key?(key) }
433
+ filtered[:_result] = state[:_result]
434
+ result_container[:result] = filtered
435
+ result_container[:ready] = true
436
+ return unless auto_close
437
+
438
+ <<~HTML
439
+ <html><body>
440
+ <h1>Done!</h1>
441
+ <script>setTimeout(function(){ window.close(); }, 500);</script>
442
+ </body></html>
443
+ HTML
444
+ end
445
+
446
+ def find_button(components, id)
447
+ find_recursive(components) do |component|
448
+ (component.is_a?(Components::Button) || component.is_a?(Components::MenuItem)) && component.id == id
449
+ end
450
+ end
451
+
452
+ def find_form(components, name)
453
+ find_recursive(components) do |component|
454
+ component.is_a?(Components::Form) && component.name == name
455
+ end
456
+ end
457
+
458
+ def find_component(components, key)
459
+ find_recursive(components) do |component|
460
+ component.respond_to?(:key) && component.key == key
461
+ end
462
+ end
463
+
464
+ def find_recursive(components, &match)
465
+ components.each do |component|
466
+ return component if match.call(component)
467
+
468
+ if component.respond_to?(:children) && component.children
469
+ found = find_recursive(component.children, &match)
470
+ return found if found
471
+ end
472
+ if component.is_a?(Components::Modal) && component.footer_component&.children
473
+ found = find_recursive(component.footer_component.children, &match)
474
+ return found if found
475
+ end
476
+ end
477
+ nil
478
+ end
479
+
480
+ def collect_input_keys(components)
481
+ components.each_with_object([]) do |component, keys|
482
+ keys << component.key if component.respond_to?(:key) && component.key
483
+ keys.concat(collect_input_keys(component.children)) if component.respond_to?(:children) && component.children
484
+ end
485
+ end
486
+ end
487
+ end