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,609 @@
1
+ # frozen_string_literal: true
2
+
3
+ # StreamWeaver University -- step 4's growing-doc appender
4
+ # (story: step-4-growing-doc).
5
+ #
6
+ # Pushes to one canvas session six times in a row, each push carrying
7
+ # everything before it plus one more section, with a visible pause in
8
+ # between so the pane grows into a real document while you watch. Six
9
+ # sections is not decoration: below that the doc theme's sidebar outline has
10
+ # nothing worth showing, and round-5 UAT ended step 5 looking at a doc too
11
+ # short to demonstrate the nav it was supposed to demonstrate.
12
+ #
13
+ # The sections are chosen to show doc features a terminal has no answer for
14
+ # -- a doc header with an outline, a two-column section, syntax-highlighted
15
+ # code, a callout, a rendered diagram, a table -- and every one of them is a
16
+ # component `streamweaver org-export` recognizes natively, so the file the
17
+ # user saves at the end survives the round trip step 5 puts it through.
18
+ #
19
+ # Content-only DSL in the document itself: no buttons (disc-095 --
20
+ # canvas-read's controls grey out and do nothing, so a doc meant to be
21
+ # reviewed there must not lean on any) and no chart shorthands (disc-094 --
22
+ # `streamweaver export` drops Chart.js for those). The one exception is
23
+ # `--picker`, which appends a live form BELOW the document for the co-edit
24
+ # loop; that push is never saved.
25
+ #
26
+ # Ships inside the stream_weaver gem, so a fresh `gem install stream_weaver`
27
+ # already has it on the load path -- no path to type, no home directory to
28
+ # know about:
29
+ #
30
+ # streamweaver panel doc-demo --theme=doc
31
+ # ruby "$(streamweaver university-demo doc)" doc-demo
32
+ #
33
+ # SESSION_NAME (first ARGV, default "doc-demo") must already be open, so
34
+ # there's a pane to watch grow. Flags:
35
+ #
36
+ # --save-as NAME doc name to save under (default "university-doc")
37
+ # --no-save grow only; don't save
38
+ # --extend=a,b append these canned extension sections before saving
39
+ # --add-custom <key> <file> persist a hand-written section's DSL (read
40
+ # from <file>) so it survives every later rebuild, the
41
+ # same way a picked --extend key does; applies and saves
42
+ # in this same invocation
43
+ # --picker append the co-edit picker form and DON'T save
44
+ # --finish re-push the finished document, base + every persisted
45
+ # extend/custom, WITHOUT the picker form, and save --
46
+ # the co-edit loop's own exit: the picker's last-pushed
47
+ # HTML has a submitted form in it (round-10 UAT: nothing
48
+ # else re-pushes after "done" is chosen, so the pane is
49
+ # left showing the canvas-wait adapter's own terminal
50
+ # "Submitted" screen instead of the document). Never
51
+ # replays the six-stage growing animation, even on a
52
+ # "done" picked before anything was ever extended.
53
+ # --reset forget this session's persisted --extend keys AND
54
+ # custom sections, do nothing else
55
+ #
56
+ # Pause between pushes is STREAMWEAVER_GROWING_DOC_PAUSE seconds (default 3;
57
+ # specs override it to run fast).
58
+ #
59
+ # Applied --extend keys persist per session (GrowingDocState, round-7 UAT)
60
+ # so a second invocation -- another --picker round, a resumed course run --
61
+ # rebuilds base + every key applied so far on its own; --extend only ever
62
+ # ADDS to that set, and --reset is the only thing that clears it. A
63
+ # hand-written section from --add-custom persists the same way (round-8
64
+ # UAT): a canned --extend pick already survived a rebuild, but a worker's
65
+ # own free-text section had nowhere to persist and a later --picker round
66
+ # clobbered it back out.
67
+
68
+ require 'stream_weaver/canvas/client'
69
+ require_relative 'growing_doc_state'
70
+
71
+ module StreamWeaver
72
+ module University
73
+ module Scripts
74
+ module GrowingDoc
75
+ # The name step 5 goes looking for. Deterministic on purpose: round-5
76
+ # UAT made the user type a name into the Save dialog and then made
77
+ # step 5 hunt for whatever they typed. The script saves under this
78
+ # name itself; the user's own manual save is a bonus lap, not the
79
+ # dependency.
80
+ DEFAULT_DOC_NAME = 'university-doc'
81
+
82
+ OPENING = <<~RUBY
83
+ doc_header(
84
+ eyebrow: "StreamWeaver University · Step 4",
85
+ title: "A doc that writes itself",
86
+ pills: [{ text: "Live" }, "written by a script, while you watch"]
87
+ )
88
+
89
+ md "A script is writing this document into your pane, one push at a time. Each push carries everything before it plus one new section, so the page grows instead of blinking. Watch the outline on the left fill in as it goes."
90
+ RUBY
91
+
92
+ COMPARE = <<~RUBY
93
+ doc_section_header "01", "Terminal vs canvas", id: "compare"
94
+
95
+ comparison(before_label: "What a terminal can do", after_label: "What this pane adds") do
96
+ before do
97
+ md "- One column of text, redrawn top to bottom\\n- Code and prose look the same\\n- A diagram is ASCII art, or nothing\\n- Scrollback is the only memory"
98
+ end
99
+ after do
100
+ md "- A laid-out document with its own outline\\n- Syntax-highlighted code beside prose\\n- Real diagrams, rendered\\n- One click away from a file you keep"
101
+ end
102
+ end
103
+ RUBY
104
+
105
+ PUSH = <<~RUBY
106
+ doc_section_header "02", "The push itself", id: "push"
107
+
108
+ md "Every section you have watched appear arrived through the same short loop. No server restart, no page reload, no editor open anywhere:"
109
+
110
+ code_block(<<~SNIPPET, lang: "ruby")
111
+ stages.each do |stage|
112
+ body << stage
113
+ Canvas::Client.send_message(
114
+ Canvas::Protocol::Messages.push(session, body, source_dir: nil)
115
+ )
116
+ sleep 3
117
+ end
118
+ SNIPPET
119
+
120
+ callout(variant: :tip, title: "Whole document, every time") do
121
+ md "Each push carries the entire document, not just the new part. That is why the earlier sections are still sitting there -- and why the last push is a complete, self-contained file you can save and reopen."
122
+ end
123
+ RUBY
124
+
125
+ FLOW = <<~RUBY
126
+ doc_section_header "03", "Where it goes next", id: "flow"
127
+
128
+ mermaid <<~DIAGRAM
129
+ graph LR
130
+ A["growing_doc.rb"] -->|"6 pushes"| B["canvas bridge"]
131
+ B --> C["this pane"]
132
+ C -->|"save-doc"| D["docs/streamweaver_canvas/*.rb"]
133
+ D -->|"org-export"| E["a plain .org file"]
134
+ DIAGRAM
135
+
136
+ md "Everything above lives only as long as the canvas bridge does. The save is what turns it into a file -- and that file is what step 5 takes with it."
137
+ RUBY
138
+
139
+ SURVIVES = <<~RUBY
140
+ doc_section_header "04", "What survives the export", id: "survives"
141
+
142
+ md "A canvas doc is only portable if `streamweaver org-export` recognizes every component in it. These are the ones this document is built from, and the reason it is built from exactly these:"
143
+
144
+ table headers: ["Component", "Becomes", "Why it is in this doc"], rows: [
145
+ ["doc_section_header", "A heading", "Becomes the outline, and the outline becomes the nav"],
146
+ ["md", "Body text", "The safe body anywhere, including inside a callout"],
147
+ ["comparison", "Two-column block", "Survives where a raw columns/column pair would not"],
148
+ ["code_block", "#+begin_src", "Keeps its language, so the highlighting comes back"],
149
+ ["callout", "Quote block", "Keeps its variant and its title"],
150
+ ["mermaid", "#+begin_src mermaid", "The diagram source travels; the reader re-renders it"],
151
+ ["table", "An org table", "Round-trips cell for cell -- including this one"]
152
+ ]
153
+
154
+ md "The `headers:`/`rows:` form is deliberate: it is the shape `org-export` can read back. An array of hashes renders identically in the pane and then leaves as an unrecognized block -- the kind of silent loss this section exists to warn you about."
155
+ RUBY
156
+
157
+ READBACK = <<~RUBY
158
+ doc_section_header "05", "Reading it back", id: "readback"
159
+
160
+ md "The saved `.rb` is the document, not a snapshot of it -- full fidelity, and StreamWeaver can re-render or extend it again later exactly as it looked here. `streamweaver org-export <file>` writes a `.org` sibling instead: plain text, human-readable anywhere with nothing to install, and the StreamWeaver Doc Viewer extension (step 5) makes that same file beautiful again without needing StreamWeaver at all."
161
+
162
+ callout(variant: :info, title: "Content-only on purpose") do
163
+ md "There is not one button in this document. Outside the live canvas -- in `canvas-read`, in an export, in the gist -- controls grey out and do nothing. A doc meant to travel is written to be read, not clicked."
164
+ end
165
+ RUBY
166
+
167
+ LOOP = <<~RUBY
168
+ doc_section_header "06", "The loop you are in", id: "loop"
169
+
170
+ md "A script wrote the six sections above without asking you anything. That is the easy half. The interesting half is what happens next: your agent asks you what else the document should say, blocks until you answer, adds it, and saves again."
171
+
172
+ code_block(<<~SNIPPET, lang: "text")
173
+ agent -> pushes doc + a form
174
+ agent -> canvas-wait (blocked, in the background)
175
+ you -> pick a section, or describe one
176
+ agent -> appends it, re-pushes, re-saves
177
+ repeat until you say "done"
178
+ SNIPPET
179
+
180
+ md "That is co-editing a document with an agent, in a pane, with no file open on either side."
181
+ RUBY
182
+
183
+ # Each stage appends to the running document; `toc` is the outline
184
+ # entry that stage introduces (nil for the opening, which has no
185
+ # section of its own). The sidebar is rebuilt from the entries so
186
+ # far on every push, so it never links at an anchor that has not
187
+ # been pushed yet. Six entries is the floor for a sidebar worth
188
+ # showing -- do not drop below it.
189
+ STAGES = [
190
+ { toc: nil, dsl: OPENING },
191
+ { toc: { id: 'compare', label: 'Terminal vs canvas' }, dsl: COMPARE },
192
+ { toc: { id: 'push', label: 'The push itself' }, dsl: PUSH },
193
+ { toc: { id: 'flow', label: 'Where it goes next' }, dsl: FLOW },
194
+ { toc: { id: 'survives', label: 'What survives the export' }, dsl: SURVIVES },
195
+ { toc: { id: 'readback', label: 'Reading it back' }, dsl: READBACK },
196
+ { toc: { id: 'loop', label: 'The loop you are in' }, dsl: LOOP }
197
+ ].freeze
198
+
199
+ # The co-edit loop's menu. Every option is a component the org
200
+ # writer recognizes natively (see the table in section 04) -- a
201
+ # timeline or a kpi_dashboard would look fine in the pane and then
202
+ # fall out of the export as an unrecognized placeholder, which is
203
+ # exactly the trap this course spends step 5 avoiding.
204
+ # `header:` is the rendered `doc_section_header` title, verbatim --
205
+ # kept as its own field (rather than parsed back out of `dsl`) so
206
+ # the picker legend and the OK line can quote it directly with no
207
+ # risk of drifting from what actually lands in the pane. Round-7
208
+ # UAT: the user picked "cheatsheet" and could not find a matching
209
+ # header, because the rendered title ("Commands used so far") never
210
+ # named the key it came from. Every header now BEGINS with its key,
211
+ # capitalized, so "which section did my pick add?" is never a
212
+ # guess -- a spec below pins `header` and `dsl`'s embedded title as
213
+ # identical for every entry, and that the key starts each one.
214
+ EXTENSIONS = {
215
+ 'timeline' => {
216
+ label: 'A release timeline (as an org-safe table)',
217
+ header: 'Timeline — how it got here',
218
+ dsl: <<~RUBY
219
+ doc_section_header "07", "Timeline — how it got here", id: "timeline"
220
+
221
+ table headers: ["Milestone", "What changed"], rows: [
222
+ ["First push", "One session, one DSL string, one pane"],
223
+ ["Growing docs", "Repeated pushes that accumulate instead of replacing"],
224
+ ["Save as doc", "A canvas becomes a git-tracked file"],
225
+ ["org-export", "The file becomes plain text anyone can read"]
226
+ ]
227
+ RUBY
228
+ },
229
+ 'tradeoffs' => {
230
+ label: 'A before/after comparison of the two ways to share a doc',
231
+ header: 'Tradeoffs — two ways to share it',
232
+ dsl: <<~RUBY
233
+ doc_section_header "07", "Tradeoffs — two ways to share it", id: "tradeoffs"
234
+
235
+ comparison(before_label: "Paste the terminal output", after_label: "Send the .org file") do
236
+ before do
237
+ md "- Loses every heading\\n- Code and prose look identical\\n- Diagrams do not survive at all\\n- Unreadable in a week"
238
+ end
239
+ after do
240
+ md "- Keeps the outline and the nav\\n- Code keeps its language\\n- Diagrams re-render for the reader\\n- Opens in any editor, forever"
241
+ end
242
+ end
243
+ RUBY
244
+ },
245
+ 'cheatsheet' => {
246
+ label: 'A cheat-sheet callout of the commands used so far',
247
+ header: 'Cheatsheet — commands used so far',
248
+ dsl: <<~RUBY
249
+ doc_section_header "07", "Cheatsheet — commands used so far", id: "cheatsheet"
250
+
251
+ callout(variant: :tip, title: "The whole course, in six commands") do
252
+ md "- `streamweaver panel <name>` -- open a pane\\n- `streamweaver canvas-push <name>` -- draw into it\\n- `streamweaver canvas-wait <name>` -- block on a human\\n- `streamweaver canvas-list` / `canvas-close` -- housekeeping\\n- `streamweaver org-export <file>` -- make it portable\\n- `streamweaver canvas-read <file>` -- read it back later"
253
+ end
254
+ RUBY
255
+ },
256
+ 'architecture' => {
257
+ label: 'A diagram of where a saved doc can travel next',
258
+ header: 'Architecture — where a saved doc travels',
259
+ dsl: <<~RUBY
260
+ doc_section_header "07", "Architecture — where a saved doc travels", id: "architecture"
261
+
262
+ mermaid <<~DIAGRAM
263
+ graph TD
264
+ F["docs/streamweaver_canvas/university-doc.rb"]
265
+ F -->|"canvas-read"| R["re-rendered, no live session"]
266
+ F -->|"org-export"| O["university-doc.org"]
267
+ O -->|"gh gist create"| G["a gist anyone can open"]
268
+ G -->|"Doc Viewer extension"| V["rendered, with nav and diagrams"]
269
+ DIAGRAM
270
+ RUBY
271
+ }
272
+ }.freeze
273
+
274
+ # Appended BELOW the finished document for the co-edit loop. Never
275
+ # saved -- `--picker` and saving are mutually exclusive, so no
276
+ # control ever lands in the file step 5 carries out.
277
+ #
278
+ # Round-6 UAT bug: the radio choices used to be human labels
279
+ # ("tradeoffs -- A before/after comparison ..."), and `--extend`
280
+ # expected the bare EXTENSIONS key. A worker had to parse one out of
281
+ # the other, got it wrong, and `run!`'s unknown-key branch warned to
282
+ # stderr (easy to miss) and then still printed "Saved: <path>" as if
283
+ # the pick had landed -- the doc was unchanged. The fix is single
284
+ # source of truth: the radio choice IS the `--extend` key, verbatim,
285
+ # so there is nothing left to parse. The legend line explains what
286
+ # each key means, since the key alone is not self-explanatory.
287
+ def self.picker_dsl
288
+ # "key -- what it adds → the exact header it renders as" (round-7
289
+ # UAT: a key alone did not let the user find the section it
290
+ # produced, since the rendered title never said which key it came
291
+ # from -- it always does now, but the legend spells out the
292
+ # mapping up front too).
293
+ legend = (EXTENSIONS.map { |key, ext| "- **#{key}** -- #{ext[:label]} → \"#{ext[:header]}\"" } +
294
+ ["- **done** -- the doc is finished, move on"]).join("\n")
295
+ choices = EXTENSIONS.keys + ['done']
296
+ <<~RUBY
297
+ doc_section_header "--", "Your turn", id: "your-turn"
298
+
299
+ callout(variant: :warning, title: "Your agent is waiting on you") do
300
+ md "This is the same blocking form from step 3, doing a real job. Your agent is sitting inside `streamweaver canvas-wait` and will not move until you answer."
301
+ end
302
+
303
+ md #{legend.inspect}
304
+
305
+ radio_group :section, #{choices.inspect}
306
+
307
+ md "**Or describe a section in your own words.** If you type something here your agent writes that section itself instead of using a canned one."
308
+
309
+ text_field :describe, placeholder: "e.g. a table of every canvas command and what it costs"
310
+
311
+ button "Add it"
312
+ RUBY
313
+ end
314
+
315
+ # One stdout line right before a stage's push, and one right after
316
+ # -- "stage N/7 pushing: <name>" / "stage N/7 pushed: <name>" -- so
317
+ # the worker narrating this run has a live feed to relay between
318
+ # pushes instead of watching a silent twenty-second pause and
319
+ # summarizing it all at the end once it's over. `name` comes from
320
+ # the stage's own `toc` label (the same text that lands in the
321
+ # sidebar) rather than a second, parallel name to keep in sync; the
322
+ # opening stage has no `toc` (see STAGES, above), so it announces as
323
+ # "opening".
324
+ def self.announce_stage(index, verb)
325
+ stage = STAGES[index]
326
+ label = stage[:toc] ? stage[:toc][:label] : 'opening'
327
+ puts "growing_doc: stage #{index + 1}/#{STAGES.length} #{verb}: #{label}"
328
+ end
329
+
330
+ # Applies `keys` to the running `toc`/`body` in place. Prints one
331
+ # unmistakable OK line per key EXTENSIONS recognizes and one FAILED
332
+ # line (to stderr) per key it does not, and returns true only if
333
+ # EVERY key was recognized -- `map { }.all?` rather than a hand-
334
+ # threaded `reduce`, so "did everything succeed" is what the code
335
+ # says, not a fold accumulator a later edit could quietly break.
336
+ # Extracted out of `run!` so it is testable with no canvas bridge
337
+ # involved, and so `run!` can exit non-zero rather than the old
338
+ # silent-warn-and-continue path that printed "Saved: <path>" on a
339
+ # pick that never actually landed (round-6 UAT).
340
+ def self.apply_extensions!(keys, toc, body)
341
+ keys.map { |key| apply_extension!(key, toc, body) }.all?
342
+ end
343
+
344
+ def self.apply_extension!(key, toc, body)
345
+ ext = EXTENSIONS[key]
346
+ unless ext
347
+ warn "growing_doc: FAILED -- no such extension #{key.inspect} " \
348
+ "(have: #{EXTENSIONS.keys.join(', ')})"
349
+ return false
350
+ end
351
+
352
+ # { id:, label: } derived from the key and its header rather than
353
+ # carried as its own EXTENSIONS field -- id always matches the
354
+ # key (see each entry's own doc_section_header call), and label
355
+ # always matches header, so a third copy had nothing to say that
356
+ # wasn't already true by construction, and nothing to keep in
357
+ # sync either.
358
+ toc << { id: key, label: ext[:header] }
359
+ body << ext[:dsl] << "\n"
360
+ # Names the exact rendered header, not just the key (round-7
361
+ # UAT) -- "OK tradeoffs → section 'Tradeoffs — two ways to share
362
+ # it'" is something you can grep for directly.
363
+ puts "growing_doc: OK #{key} → section '#{ext[:header]}'"
364
+ true
365
+ end
366
+
367
+ # A worker-authored, free-text section's own header, for the toc
368
+ # entry it earns -- parsed out of its own `doc_section_header` call
369
+ # the same "title lives with the id" convention EXTENSIONS keeps by
370
+ # hand, because there's nowhere else for a hand-written section to
371
+ # declare either. An id that silently defaulted to the key alone
372
+ # (an earlier version of this) could point the sidebar at an
373
+ # anchor the section itself never declared, if the worker's
374
+ # snippet used a different `id:` -- a dead link in exactly the
375
+ # document step 5 exists to show the sidebar of. Falls back to the
376
+ # key itself, capitalized, with no id override, if the snippet
377
+ # somehow omits the call -- still buildable, just absent from the
378
+ # sidebar's own self-description.
379
+ def self.custom_toc_entry(key, dsl)
380
+ match = dsl.match(/doc_section_header\s+"[^"]*",\s*"([^"]*)"(?:,\s*id:\s*"([^"]*)")?/)
381
+ { id: (match && match[2]) || key, label: (match && match[1]) || key.capitalize }
382
+ end
383
+
384
+ # Applies every persisted custom section (round-8 UAT: a hand-
385
+ # written free-text section had nowhere to persist, so the next
386
+ # --picker rebuild clobbered it back out -- a live run lost a
387
+ # user's own Star Wars chart section exactly this way). `customs`
388
+ # is `{key => dsl}` from GrowingDocState.load_custom; unlike
389
+ # apply_extensions! there is no unknown-key case -- every entry
390
+ # here was already validated (as a snippet file that existed and
391
+ # parsed as Ruby) at the moment it was saved, see run! below.
392
+ def self.apply_custom_sections!(customs, toc, body)
393
+ customs.each do |key, dsl|
394
+ toc << custom_toc_entry(key, dsl)
395
+ body << dsl << "\n"
396
+ end
397
+ end
398
+
399
+ # Persisted --extend keys (round-7 UAT) merged with any passed to
400
+ # THIS invocation, so a second invocation for the same session --
401
+ # another --picker round, a resumed course run -- rebuilds
402
+ # everything applied so far on its own; a fresh --extend only ever
403
+ # ADDS to that set. Extracted, like apply_extensions! before it, so
404
+ # the merge itself is testable with no canvas bridge involved.
405
+ def self.resolve_extend_keys(session_name, argv)
406
+ new_keys = argv.grep(/\A--extend=/) { |a| a.split('=', 2).last }
407
+ .flat_map { |v| v.split(',') }
408
+ .map(&:strip).reject(&:empty?)
409
+ (GrowingDocState.load(session_name) + new_keys).uniq
410
+ end
411
+
412
+ # True for a bare invocation -- no --picker, no --extend -- which
413
+ # means "start the demo over": grow from scratch, forget prior
414
+ # picks, same as clicking Repeat expects. Without this carve-out a
415
+ # later plain re-run would inherit whatever an earlier --picker/
416
+ # --extend round had persisted and silently skip step 4's own
417
+ # payoff, the six-stage growing animation (caught in round-7 review
418
+ # as the fix that broke this exact case).
419
+ # Flags that add content to the document -- a bare re-run with none
420
+ # of these present is what "start the demo over" means.
421
+ CONTENT_FLAGS = %w[--picker --add-custom].freeze
422
+
423
+ def self.fresh_start?(argv)
424
+ # --finish means the OPPOSITE of starting over -- "wrap up with
425
+ # everything accumulated so far" -- so it must never trigger the
426
+ # persisted-state wipe below, even though (like a truly fresh
427
+ # re-run) it can arrive with no --picker/--add-custom/--extend= of
428
+ # its own (round-10 UAT: the "done" case is exactly this shape).
429
+ return false if argv.include?('--finish')
430
+
431
+ (argv & CONTENT_FLAGS).empty? && argv.grep(/\A--extend=/).empty?
432
+ end
433
+
434
+ def self.run!(argv = ARGV)
435
+ session_name = argv.find { |a| !a.start_with?('-') } || 'doc-demo'
436
+
437
+ if argv.include?('--reset')
438
+ GrowingDocState.clear(session_name)
439
+ puts "growing_doc: cleared persisted extensions and custom sections for '#{session_name}'"
440
+ return
441
+ end
442
+
443
+ pause = Float(ENV.fetch('STREAMWEAVER_GROWING_DOC_PAUSE', 3))
444
+ picker = argv.include?('--picker')
445
+ finish = argv.include?('--finish')
446
+ save = !picker && !argv.include?('--no-save')
447
+ doc_name = argv.grep(/\A--save-as=/) { |a| a.split('=', 2).last }.first ||
448
+ (argv.include?('--save-as') ? argv[argv.index('--save-as') + 1] : nil) ||
449
+ DEFAULT_DOC_NAME
450
+ # `--extend key` (space form) would otherwise be silently ignored
451
+ # by resolve_extend_keys's `--extend=` regex below -- exactly the
452
+ # class of bug round-6 fixed (a pick that never landed, printing
453
+ # "Saved:" anyway). Fail loudly instead of guessing.
454
+ abort 'growing_doc: use --extend=key, not --extend key' if argv.include?('--extend')
455
+
456
+ # Round-7 UAT: a worker re-ran --picker without re-passing
457
+ # --extend and clobbered the doc -- every invocation now rebuilds
458
+ # base + every persisted key on its own, and --extend only ever
459
+ # ADDS to that set. fresh_start? is the other half: a PLAIN
460
+ # re-run (Repeat) still means "start over", not "inherit
461
+ # whatever a previous --picker round persisted".
462
+ GrowingDocState.clear(session_name) if fresh_start?(argv)
463
+ extend_keys = resolve_extend_keys(session_name, argv)
464
+
465
+ # --add-custom persists a hand-written section BEFORE this run
466
+ # builds anything, so it's already part of what load_custom
467
+ # returns below -- one persist-and-apply path, not two.
468
+ if (idx = argv.index('--add-custom'))
469
+ custom_key = argv[idx + 1]
470
+ custom_file = argv[idx + 2]
471
+ unless custom_key && custom_file
472
+ abort 'growing_doc: --add-custom needs a key and a snippet file: --add-custom <key> <file>'
473
+ end
474
+ abort "growing_doc: --add-custom snippet not found: #{custom_file}" unless File.exist?(custom_file)
475
+
476
+ custom_dsl = File.read(custom_file)
477
+ # Persisted state is sticky (every rebuild replays it, and the
478
+ # only way out is --reset, which throws away every OTHER
479
+ # persisted pick too) -- a syntax error here must not make it
480
+ # into the file, or the very next --picker round breaks with
481
+ # no working undo.
482
+ require 'ripper'
483
+ abort "growing_doc: --add-custom snippet is not valid Ruby: #{custom_file}" unless Ripper.sexp(custom_dsl)
484
+
485
+ GrowingDocState.save_custom(session_name, custom_key, custom_dsl)
486
+ puts "growing_doc: persisted custom section '#{custom_key}' for '#{session_name}'"
487
+ end
488
+ customs = GrowingDocState.load_custom(session_name)
489
+
490
+ bridge = ::StreamWeaver::Canvas::Client.ensure_bridge_running
491
+ ::StreamWeaver::Canvas::Client.send_message(
492
+ ::StreamWeaver::Canvas::Protocol::Messages.create(session_name, layout: :fluid, theme: :doc)
493
+ )
494
+
495
+ toc = []
496
+ body = +''
497
+
498
+ # Growing the base document is skipped once extensions are in play
499
+ # -- this call's own --extend, OR any persisted from an earlier
500
+ # invocation, OR any persisted custom section: a re-push during
501
+ # the co-edit loop must land in one beat, not re-run the whole
502
+ # six-push show the user already watched. --finish forces this
503
+ # false unconditionally, even with nothing ever extended -- "done"
504
+ # picked on the very first picker offer must still land the
505
+ # finished (here, un-extended) document in one beat, not replay
506
+ # the six-stage animation a second time.
507
+ growing = extend_keys.empty? && customs.empty? && !picker && !finish
508
+ STAGES.each_with_index do |stage, i|
509
+ toc << stage[:toc] if stage[:toc]
510
+ body << stage[:dsl] << "\n"
511
+ next unless growing
512
+
513
+ announce_stage(i, 'pushing')
514
+ push(session_name, document(toc, body))
515
+ announce_stage(i, 'pushed')
516
+ sleep(pause) unless i == STAGES.length - 1
517
+ end
518
+
519
+ extend_ok = apply_extensions!(extend_keys, toc, body)
520
+ # Persist only the keys EXTENSIONS actually recognizes -- an
521
+ # unknown key never applied anything, so it has no business
522
+ # surviving into the NEXT invocation's rebuild.
523
+ GrowingDocState.save(session_name, extend_keys & EXTENSIONS.keys)
524
+ apply_custom_sections!(customs, toc, body)
525
+
526
+ document_body = document(toc, body)
527
+ push(session_name, picker ? "#{document_body}\n#{picker_dsl}" : document_body)
528
+
529
+ if picker
530
+ puts "Pushed the document plus the co-edit picker to '#{session_name}'. " \
531
+ "Now block on it -- in the BACKGROUND, not the foreground: " \
532
+ "streamweaver canvas-wait #{session_name}"
533
+ elsif save
534
+ path = save_doc(bridge, session_name, doc_name)
535
+ puts(save_message(path, doc_name, extend_ok))
536
+ end
537
+
538
+ exit(1) unless extend_ok
539
+ rescue ::StreamWeaver::Canvas::Client::NotRunningError,
540
+ ::StreamWeaver::Canvas::Client::ConnectionError => e
541
+ warn "growing_doc: could not reach the canvas bridge (#{e.message}) -- " \
542
+ "run `streamweaver panel #{session_name} --theme=doc` first"
543
+ end
544
+
545
+ # The line printed after a save -- self-incriminating on purpose
546
+ # when an --extend key failed, so "Saved: <path>" can never again
547
+ # read as unqualified success on its own (round-6 UAT: the old path
548
+ # printed exactly that, on stdout, with the only sign of trouble on
549
+ # stderr where a worker skimming just the save line would miss it).
550
+ def self.save_message(path, doc_name, extend_ok)
551
+ failure_note = extend_ok ? '' : ' -- WITH FAILED EXTENSIONS, see above'
552
+ base = path ? "Saved: #{path}" : "Saved as '#{doc_name}' (the bridge did not report a path)."
553
+ "#{base}#{failure_note}"
554
+ end
555
+
556
+ def self.push(session_name, dsl)
557
+ ::StreamWeaver::Canvas::Client.send_message(
558
+ ::StreamWeaver::Canvas::Protocol::Messages.push(session_name, dsl, source_dir: nil)
559
+ )
560
+ end
561
+
562
+ # The save the user used to have to do by hand. POSTs the session's
563
+ # last-good DSL to the bridge's own `/canvas/:name/save-doc` -- the
564
+ # exact endpoint the floating "Save as doc" button calls, so the
565
+ # script and the button cannot diverge -- and returns the path the
566
+ # bridge reports, which is the path step 5 needs and the one round-5
567
+ # UAT made a human transcribe.
568
+ def self.save_doc(bridge, session_name, doc_name)
569
+ require 'net/http'
570
+ require 'json'
571
+ port = bridge && bridge[:port]
572
+ unless port
573
+ warn 'growing_doc: no bridge port reported; skipping save'
574
+ return nil
575
+ end
576
+
577
+ response = Net::HTTP.start('127.0.0.1', port, open_timeout: 3, read_timeout: 10) do |http|
578
+ http.post(
579
+ "/canvas/#{session_name}/save-doc",
580
+ JSON.generate(name: doc_name, format: 'rb'),
581
+ 'Content-Type' => 'application/json'
582
+ )
583
+ end
584
+ payload = JSON.parse(response.body, symbolize_names: true) rescue {}
585
+ return payload[:path] if payload[:ok]
586
+
587
+ warn "growing_doc: save-doc failed (#{response.code}): #{payload[:error] || response.body}"
588
+ nil
589
+ rescue StandardError => e
590
+ warn "growing_doc: save-doc failed (#{e.class}: #{e.message})"
591
+ nil
592
+ end
593
+
594
+ # One flat DSL body: a sidebar_toc of the sections pushed so far,
595
+ # then everything accumulated. Flat and one-statement-per-component
596
+ # on purpose -- `streamweaver org-export`'s verbatim-source recovery
597
+ # relies on that 1:1 correspondence (lib/stream_weaver/org/writer.rb,
598
+ # #build_raw_sources).
599
+ def self.document(toc, body)
600
+ return body if toc.empty?
601
+
602
+ "sidebar_toc sections: #{toc.inspect}\n\n#{body}"
603
+ end
604
+ end
605
+ end
606
+ end
607
+ end
608
+
609
+ StreamWeaver::University::Scripts::GrowingDoc.run!