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,542 @@
1
+ # StreamWeaver for TUI: Adapting the DSL to Charm
2
+
3
+ **Date:** January 16, 2026
4
+ **Status:** Exploration / Feasibility Analysis
5
+
6
+ ## Executive Summary
7
+
8
+ StreamWeaver's reactive DSL could be adapted to target Charm's TUI libraries (Bubbletea, Lipgloss, Bubbles) instead of web browsers. The core insight—**your Ruby block re-executes on every interaction**—translates naturally to Bubbletea's Elm Architecture. This document explores feasibility, architectural alignment, and a potential implementation path.
9
+
10
+ ## The Opportunity
11
+
12
+ With Marco Roth's Charm Ruby gems now available, Ruby developers can build "glamorous" terminal UIs. But Charm follows the Elm Architecture pattern, requiring explicit model/update/view separation:
13
+
14
+ ```ruby
15
+ # Current Charm/Bubbletea approach
16
+ class Counter
17
+ include Bubbletea::Model
18
+
19
+ def initialize
20
+ @count = 0
21
+ end
22
+
23
+ def update(msg)
24
+ case msg
25
+ when Bubbletea::KeyMsg
26
+ @count += 1 if msg.key == "+"
27
+ @count -= 1 if msg.key == "-"
28
+ end
29
+ [self, nil]
30
+ end
31
+
32
+ def view
33
+ "Count: #{@count}\n\nPress + or - to change, q to quit"
34
+ end
35
+ end
36
+
37
+ Bubbletea::Program.new(Counter.new).run
38
+ ```
39
+
40
+ StreamWeaver's approach is more declarative—state changes and UI definition live together:
41
+
42
+ ```ruby
43
+ # StreamWeaver's reactive approach
44
+ app "Counter" do
45
+ state[:count] ||= 0
46
+
47
+ text "Count: #{state[:count]}"
48
+
49
+ button "+" do |s|
50
+ s[:count] += 1
51
+ end
52
+ end.run!
53
+ ```
54
+
55
+ **Question: Can we bring StreamWeaver's token-efficient DSL to terminal UIs?**
56
+
57
+ ## Architectural Alignment Analysis
58
+
59
+ ### What Aligns Well
60
+
61
+ | StreamWeaver Concept | Charm Equivalent | Compatibility |
62
+ |---------------------|------------------|---------------|
63
+ | `state[:key]` hash | Model instance variables | Direct mapping |
64
+ | Block re-execution | `view()` method called after `update()` | Same pattern |
65
+ | Component rendering | Lipgloss styled strings | Conceptual match |
66
+ | Button callbacks | KeyMsg/MouseMsg handling | Needs translation layer |
67
+ | Layout (vstack, hstack) | Lipgloss.join_vertical/horizontal | Direct equivalent |
68
+
69
+ ### Key Differences
70
+
71
+ | Aspect | StreamWeaver (Web) | Charm (TUI) |
72
+ |--------|-------------------|-------------|
73
+ | Event model | HTTP requests via HTMX | Keyboard/mouse messages |
74
+ | Rendering | HTML via Phlex | ANSI strings via Lipgloss |
75
+ | Components | Form elements (text_field, select) | Bubbles components (TextInput, List) |
76
+ | State persistence | Session cookies | In-memory only |
77
+ | Interaction | Click, type, submit | Keypress, mouse click |
78
+
79
+ ## Proposed Architecture: CharmWeaver
80
+
81
+ A new rendering backend for StreamWeaver's DSL that targets Charm instead of web browsers.
82
+
83
+ ### Core Abstraction
84
+
85
+ ```ruby
86
+ # Same DSL, different target
87
+ tui "Counter" do
88
+ state[:count] ||= 0
89
+
90
+ text "Count: #{state[:count]}"
91
+
92
+ # TUI-specific: key bindings instead of buttons
93
+ on_key "+" do |s|
94
+ s[:count] += 1
95
+ end
96
+
97
+ on_key "-" do |s|
98
+ s[:count] -= 1
99
+ end
100
+ end.run!
101
+ ```
102
+
103
+ ### Implementation Strategy
104
+
105
+ **Phase 1: Proof of Concept**
106
+ - Create `CharmWeaver::App` that wraps Bubbletea::Model
107
+ - Implement basic components: `text`, `header`, `vstack`, `hstack`
108
+ - Map `state[:key]` to model instance variables
109
+ - Block re-execution on each `view()` call
110
+
111
+ ```ruby
112
+ module CharmWeaver
113
+ class App
114
+ include Bubbletea::Model
115
+
116
+ def initialize(title, &block)
117
+ @title = title
118
+ @block = block
119
+ @state = {}
120
+ @components = []
121
+ @key_handlers = {}
122
+ end
123
+
124
+ def init
125
+ [self, nil]
126
+ end
127
+
128
+ def update(msg)
129
+ case msg
130
+ when Bubbletea::KeyMsg
131
+ return [self, Bubbletea.quit] if msg.key == "q"
132
+
133
+ if handler = @key_handlers[msg.key]
134
+ handler.call(@state)
135
+ end
136
+ end
137
+ [self, nil]
138
+ end
139
+
140
+ def view
141
+ @components.clear
142
+ @key_handlers.clear
143
+
144
+ # Re-execute block (StreamWeaver's core insight)
145
+ instance_eval(&@block)
146
+
147
+ render_components
148
+ end
149
+
150
+ private
151
+
152
+ def state
153
+ @state
154
+ end
155
+
156
+ def text(content)
157
+ @components << { type: :text, content: content }
158
+ end
159
+
160
+ def on_key(key, &handler)
161
+ @key_handlers[key] = handler
162
+ end
163
+
164
+ def render_components
165
+ @components.map do |c|
166
+ case c[:type]
167
+ when :text
168
+ c[:content]
169
+ end
170
+ end.join("\n")
171
+ end
172
+ end
173
+ end
174
+
175
+ def tui(title, &block)
176
+ CharmWeaver::App.new(title, &block)
177
+ end
178
+ ```
179
+
180
+ **Phase 2: Component Library**
181
+
182
+ Map StreamWeaver components to Charm equivalents:
183
+
184
+ | StreamWeaver | CharmWeaver | Implementation |
185
+ |--------------|-------------|----------------|
186
+ | `text` | `text` | Lipgloss styled string |
187
+ | `header` | `header` | Lipgloss bold/colored |
188
+ | `text_field :name` | `text_input :name` | Bubbles::TextInput |
189
+ | `text_area :bio` | `text_area :bio` | Bubbles::TextArea |
190
+ | `select :choice, [...]` | `list :choice, [...]` | Bubbles::List |
191
+ | `checkbox :agree` | `toggle :agree` | Custom toggle component |
192
+ | `button "Click"` | `on_key "enter"` | Key binding |
193
+ | `vstack` | `vstack` | Lipgloss.join_vertical |
194
+ | `hstack` | `hstack` | Lipgloss.join_horizontal |
195
+ | `card` | `box` | Lipgloss border + padding |
196
+ | `alert` | `styled_box` | Lipgloss with color variant |
197
+
198
+ **Phase 3: Focus Management**
199
+
200
+ TUIs need focus handling for input components:
201
+
202
+ ```ruby
203
+ tui "Form" do
204
+ state[:focus] ||= :name
205
+
206
+ text_input :name, focused: state[:focus] == :name
207
+ text_input :email, focused: state[:focus] == :email
208
+
209
+ on_key "tab" do |s|
210
+ s[:focus] = s[:focus] == :name ? :email : :name
211
+ end
212
+
213
+ on_key "enter" do |s|
214
+ # Submit
215
+ end
216
+ end.run!
217
+ ```
218
+
219
+ **Phase 4: Agentic Mode**
220
+
221
+ StreamWeaver's `run_once!` maps to TUI forms:
222
+
223
+ ```ruby
224
+ result = tui "Quick Input" do
225
+ text "Enter your name:"
226
+ text_input :name
227
+
228
+ on_key "enter" do |s|
229
+ s[:_submit] = true
230
+ end
231
+ end.run_once!
232
+
233
+ puts result[:name]
234
+ ```
235
+
236
+ ## Component Mapping Deep Dive
237
+
238
+ ### Text & Display
239
+
240
+ ```ruby
241
+ # StreamWeaver (web)
242
+ text "Hello, #{name}"
243
+ md "**Bold** text"
244
+ header "Section"
245
+
246
+ # CharmWeaver (TUI)
247
+ text "Hello, #{name}" # Plain text
248
+ text "**Bold** text", style: :markdown # Glamour rendering
249
+ header "Section" # Lipgloss bold + color
250
+ ```
251
+
252
+ ### Input Components
253
+
254
+ ```ruby
255
+ # StreamWeaver (web)
256
+ text_field :name, placeholder: "Name"
257
+
258
+ # CharmWeaver (TUI) - wraps Bubbles::TextInput
259
+ text_input :name, placeholder: "Name"
260
+ ```
261
+
262
+ The key difference: TUI inputs require focus management and cursor handling, which Bubbles handles internally.
263
+
264
+ ### Layout
265
+
266
+ ```ruby
267
+ # StreamWeaver (web)
268
+ columns widths: ['30%', '70%'] do
269
+ column { sidebar_content }
270
+ column { main_content }
271
+ end
272
+
273
+ # CharmWeaver (TUI) - character-based widths
274
+ columns widths: [20, 60] do
275
+ column { sidebar_content }
276
+ column { main_content }
277
+ end
278
+ ```
279
+
280
+ ### Styling
281
+
282
+ ```ruby
283
+ # New: Lipgloss-based styling DSL
284
+ tui "Styled" do
285
+ style :title,
286
+ bold: true,
287
+ foreground: "#FF6B6B",
288
+ margin_bottom: 1
289
+
290
+ text "Welcome", style: :title
291
+ end.run!
292
+ ```
293
+
294
+ ## Challenges & Solutions
295
+
296
+ ### Challenge 1: Event Model Difference
297
+
298
+ **Web:** Events trigger HTTP requests, server re-renders entire UI
299
+ **TUI:** Events are keyboard/mouse messages, model updates locally
300
+
301
+ **Solution:** CharmWeaver's `update()` method triggers block re-execution, mimicking StreamWeaver's behavior. The pattern is the same—just the transport differs.
302
+
303
+ ### Challenge 2: Component Complexity
304
+
305
+ Bubbles components (TextInput, List) maintain internal state (cursor position, scroll offset). StreamWeaver components are stateless—state lives in the hash.
306
+
307
+ **Solution:** CharmWeaver manages Bubbles component instances internally, syncing their values to `state[:key]` after each update cycle.
308
+
309
+ ```ruby
310
+ class CharmWeaver::App
311
+ def text_input(key, **opts)
312
+ @bubble_components[key] ||= Bubbles::TextInput.new(**opts)
313
+ @components << { type: :text_input, key: key, component: @bubble_components[key] }
314
+ end
315
+
316
+ def update(msg)
317
+ # Forward messages to focused component
318
+ if focused = @bubble_components[@focused_key]
319
+ focused, cmd = focused.update(msg)
320
+ @state[@focused_key] = focused.value
321
+ end
322
+ [self, cmd]
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### Challenge 3: Mouse Support
328
+
329
+ StreamWeaver buttons are clickable. TUI "buttons" are typically key-bound.
330
+
331
+ **Solution:** Use Bubblezone for mouse region tracking:
332
+
333
+ ```ruby
334
+ tui "Clickable" do
335
+ button "Save" do |s|
336
+ s[:saved] = true
337
+ end
338
+ # Renders as clickable zone + key hint: [Save] (Enter)
339
+ end.run!
340
+ ```
341
+
342
+ ### Challenge 4: Forms & Submission
343
+
344
+ StreamWeaver's `run_once!` returns when a form is "submitted." TUIs need explicit submission handling.
345
+
346
+ **Solution:** Define submission triggers:
347
+
348
+ ```ruby
349
+ result = tui "Form" do
350
+ text_input :name
351
+ text_input :email
352
+
353
+ # Explicit submit key
354
+ submit_on "ctrl+s"
355
+ # Or automatic when all fields filled
356
+ submit_when { state[:name] && state[:email] }
357
+ end.run_once!
358
+ ```
359
+
360
+ ## API Design: Full Example
361
+
362
+ ```ruby
363
+ require 'charm_weaver'
364
+
365
+ tui "Task Manager",
366
+ theme: :dracula, # Lipgloss color scheme
367
+ border: :rounded # App border style
368
+ do
369
+ header "My Tasks"
370
+
371
+ # Input with auto-focus
372
+ text_input :new_task, placeholder: "Add task..."
373
+
374
+ on_key "enter" do |s|
375
+ next if s[:new_task].to_s.empty?
376
+ s[:tasks] ||= []
377
+ s[:tasks] << { name: s[:new_task], done: false }
378
+ s[:new_task] = ""
379
+ end
380
+
381
+ divider
382
+
383
+ # List with selection
384
+ state[:tasks]&.each_with_index do |task, i|
385
+ selected = state[:selected] == i
386
+
387
+ hstack do
388
+ text selected ? ">" : " "
389
+ text task[:done] ? "[x]" : "[ ]"
390
+ text task[:name], style: task[:done] ? :dim : :normal
391
+ end
392
+ end
393
+
394
+ # Key bindings
395
+ on_key "j" do |s|
396
+ s[:selected] = [(s[:selected] || 0) + 1, (s[:tasks]&.size || 1) - 1].min
397
+ end
398
+
399
+ on_key "k" do |s|
400
+ s[:selected] = [(s[:selected] || 0) - 1, 0].max
401
+ end
402
+
403
+ on_key "space" do |s|
404
+ if s[:tasks] && s[:selected]
405
+ s[:tasks][s[:selected]][:done] ^= true
406
+ end
407
+ end
408
+
409
+ on_key "d" do |s|
410
+ s[:tasks]&.delete_at(s[:selected]) if s[:selected]
411
+ end
412
+
413
+ # Footer
414
+ divider
415
+ text "j/k: navigate | space: toggle | d: delete | q: quit", style: :help
416
+ end.run!
417
+ ```
418
+
419
+ ## Token Efficiency Comparison
420
+
421
+ The goal: same expressiveness, fewer tokens for LLM-driven development.
422
+
423
+ **Raw Bubbletea (58 lines):**
424
+ ```ruby
425
+ class TaskManager
426
+ include Bubbletea::Model
427
+
428
+ def initialize
429
+ @tasks = []
430
+ @new_task = Bubbles::TextInput.new(placeholder: "Add task...")
431
+ @selected = 0
432
+ @focused = :input
433
+ end
434
+
435
+ def init
436
+ [self, Bubbles::TextInput.focus]
437
+ end
438
+
439
+ def update(msg)
440
+ case msg
441
+ when Bubbletea::KeyMsg
442
+ case msg.key
443
+ when "enter"
444
+ unless @new_task.value.empty?
445
+ @tasks << { name: @new_task.value, done: false }
446
+ @new_task.set_value("")
447
+ end
448
+ when "j"
449
+ @selected = [@selected + 1, @tasks.size - 1].min
450
+ when "k"
451
+ @selected = [@selected - 1, 0].max
452
+ when " "
453
+ @tasks[@selected][:done] ^= true if @tasks[@selected]
454
+ when "d"
455
+ @tasks.delete_at(@selected)
456
+ when "q"
457
+ return [self, Bubbletea.quit]
458
+ else
459
+ @new_task, cmd = @new_task.update(msg)
460
+ return [self, cmd]
461
+ end
462
+ end
463
+ [self, nil]
464
+ end
465
+
466
+ def view
467
+ lines = ["My Tasks", "", @new_task.view, ""]
468
+ @tasks.each_with_index do |t, i|
469
+ prefix = i == @selected ? ">" : " "
470
+ check = t[:done] ? "[x]" : "[ ]"
471
+ lines << "#{prefix} #{check} #{t[:name]}"
472
+ end
473
+ lines << ""
474
+ lines << "j/k: navigate | space: toggle | d: delete | q: quit"
475
+ lines.join("\n")
476
+ end
477
+ end
478
+
479
+ Bubbletea::Program.new(TaskManager.new).run
480
+ ```
481
+
482
+ **CharmWeaver DSL (35 lines):** ~40% fewer tokens, same functionality.
483
+
484
+ ## Implementation Roadmap
485
+
486
+ ### Phase 1: Core (Week 1-2)
487
+ - [ ] `CharmWeaver::App` base class wrapping Bubbletea::Model
488
+ - [ ] Basic components: `text`, `header`, `divider`, `vstack`, `hstack`
489
+ - [ ] State hash integration
490
+ - [ ] `on_key` handler registration
491
+ - [ ] Block re-execution in `view()`
492
+
493
+ ### Phase 2: Input Components (Week 3-4)
494
+ - [ ] `text_input` wrapping Bubbles::TextInput
495
+ - [ ] `text_area` wrapping Bubbles::TextArea
496
+ - [ ] Focus management system
497
+ - [ ] Tab navigation between inputs
498
+
499
+ ### Phase 3: Selection Components (Week 5-6)
500
+ - [ ] `list` wrapping Bubbles::List
501
+ - [ ] `table` wrapping Bubbles::Table
502
+ - [ ] Selection state synchronization
503
+
504
+ ### Phase 4: Styling & Polish (Week 7-8)
505
+ - [ ] Theme support via Lipgloss
506
+ - [ ] `style` DSL for custom styles
507
+ - [ ] `box` component with borders
508
+ - [ ] `alert` with color variants
509
+
510
+ ### Phase 5: Advanced Features (Week 9-10)
511
+ - [ ] Mouse support via Bubblezone
512
+ - [ ] `run_once!` agentic mode
513
+ - [ ] Animation via Harmonica
514
+ - [ ] Charts via NTCharts
515
+
516
+ ## Risks & Mitigations
517
+
518
+ | Risk | Likelihood | Impact | Mitigation |
519
+ |------|------------|--------|------------|
520
+ | Bubbles component state sync complexity | High | Medium | Start with simple components, add complex ones incrementally |
521
+ | Focus management edge cases | Medium | High | Study Huh? form library implementation |
522
+ | Performance with large state | Low | Medium | Optimize re-render diffing if needed |
523
+ | Charm gem API changes | Medium | Medium | Pin gem versions, contribute upstream |
524
+
525
+ ## Success Criteria
526
+
527
+ 1. **Token efficiency:** CharmWeaver DSL uses 30-50% fewer tokens than raw Bubbletea for equivalent functionality
528
+ 2. **Familiar API:** StreamWeaver users can transfer knowledge directly
529
+ 3. **Full component coverage:** All common TUI patterns expressible
530
+ 4. **Production quality:** Suitable for real CLI tools, not just demos
531
+
532
+ ## Conclusion
533
+
534
+ Adapting StreamWeaver's DSL to Charm is **highly feasible**. The reactive model (block re-execution on state change) maps directly to Bubbletea's Elm Architecture. The main work is:
535
+
536
+ 1. Wrapping Bubbles components to sync with `state[:key]`
537
+ 2. Implementing focus management
538
+ 3. Translating web concepts (buttons → key bindings)
539
+
540
+ The payoff: Ruby developers get a token-efficient, declarative DSL for beautiful TUIs, consistent with StreamWeaver's web approach. Same mental model, different output target.
541
+
542
+ **Recommendation:** Proceed with Phase 1 proof of concept to validate the component wrapping approach before committing to full implementation.
@@ -0,0 +1,153 @@
1
+ # Static Doc Shelf — free multi-doc export for canvas docs
2
+
3
+ **Status:** captured, not scheduled. Brainstormed 2026-08-27; no implementation.
4
+ **Trigger to pick this up:** when a doc corpus needs to reach a reader who
5
+ should not have to run StreamWeaver, use GitHub, or hold an account.
6
+
7
+ ## The gap, in one line
8
+
9
+ `streamweaver export` gives you one self-contained HTML file. `canvas-read`
10
+ gives you a browsable *shelf* of many docs — but only as a local Sinatra app.
11
+ **There is no static shelf.** That is the whole missing piece.
12
+
13
+ ## Why this is small
14
+
15
+ Both halves already exist and are reused verbatim:
16
+
17
+ - `StreamWeaver::Export::HtmlExporter.from_dsl_file(path, theme:, layout:)` —
18
+ per-doc rendering, already handles `.rb`, mermaid, images, CDN collection.
19
+ - `StreamWeaver::Canvas::Reader::FileList.build(args, ...)` — input resolution,
20
+ already globs `.rb` and `.org`, already does multi-root discovery.
21
+ - `StreamWeaver::Org::Reader` — `.org` → DSL, already parses `#+TITLE:`
22
+ (`lib/stream_weaver/org/reader.rb:62`).
23
+
24
+ No new rendering engine. No Opal component-parity dependency.
25
+
26
+ ## Framing decisions (settled during the brainstorm)
27
+
28
+ | Question | Decision |
29
+ |---|---|
30
+ | What is the durable asset? | The extraction pipeline, the doc format, and the curated corpus. **Not** the delivery. |
31
+ | So how much delivery do we build? | As little as possible. Delivery is plumbing; over-building it means building the thing that is not the moat. |
32
+ | What do recipients do? | **Read only.** Published shelf. No accounts, no writes, no server. Tracking stays on the author's side. |
33
+ | Build strategy | Approach A (a real `shelf` command), shipped C-first (walk + export + index usable before search exists). |
34
+
35
+ ### Approaches considered and rejected
36
+
37
+ - **One Opal app that is the shelf.** Single bundle, instant nav, snappy — but
38
+ every doc in the corpus must first clear Opal component parity (tables,
39
+ mermaid, images), and `stream_weaver-5n9w` shows doc output there still has
40
+ rough edges. Rejected: it gates a content corpus on framework work.
41
+ - **Bash loop + hand-written index.** `for f in docs/*.rb; do streamweaver
42
+ export "$f"; done` plus a small index generator. Genuinely available today
43
+ and worth doing if the need is urgent and one-off. Differs from the real
44
+ thing by exactly two features: search and shared nav.
45
+
46
+ ## Design § 1 — Doc identity and the manifest
47
+
48
+ This is the part that is expensive to change later, and the reason to design it
49
+ now rather than improvise it under time pressure.
50
+
51
+ **Stable, content-derived slugs — never ordinal.** The reader addresses docs by
52
+ position (`?file=0`); `FileList` carries `files`, `history_roots`, and `labels`
53
+ but no slug or id. A link to "the third doc in the directory" breaks the moment
54
+ a doc is added. Precedence for the shelf:
55
+
56
+ 1. explicit `#+SLUG:` in the org file
57
+ 2. slugified `#+TITLE:`
58
+ 3. filename basename
59
+
60
+ Output is `dist/<slug>.html`, stable across rebuilds and reordering.
61
+
62
+ **`dist/shelf.json` — one manifest, three consumers.** An array of
63
+ `{slug, title, source, headings[], tags[], links_out[], links_in[]}`:
64
+
65
+ 1. feeds client-side search (no separate `search.json`)
66
+ 2. is what an **agent** reads to find what is in the corpus without crawling HTML
67
+ 3. is the substrate a future wiki computes related-content over
68
+
69
+ The builder walks every file anyway, so this is close to free.
70
+
71
+ **Tags from org.** Teach `Org::Reader` to read `#+FILETAGS:` into the manifest —
72
+ a few lines, and it means docs can be categorized in the authoring format
73
+ starting immediately, so a later wiki inherits real data instead of an empty
74
+ corpus.
75
+
76
+ ### Wiki-readiness (deliberate seam)
77
+
78
+ An org-based Karpathy-style wiki over the doc corpus is anticipated but **out of
79
+ scope here** — no related-content rendering, no category pages, no graph view.
80
+ What this design *does* commit to, so the wiki is cheap when it arrives:
81
+
82
+ - `[[slug]]` resolves to `<a href="slug.html">` when the slug is in the
83
+ manifest; stays literal text when it is not. ~10 lines.
84
+ - Forward links **and backlinks** are recorded in `shelf.json`.
85
+
86
+ The payoff is timing: links can be written into org docs from day one, so the
87
+ corpus arrives at the wiki already connected rather than needing a linking pass
88
+ over the whole shelf. Related: a desire to extend UKF to support org mode.
89
+
90
+ ## Design § 2 — The build pipeline
91
+
92
+ **Command:** `streamweaver shelf <paths...> [-o dist/] [--title "..."] [--offline]`
93
+
94
+ Same argument shape as `canvas-read`, using **the same `FileList.build`** — so
95
+ whatever `canvas-read` shows, `shelf` builds, and the two cannot drift into
96
+ disagreeing about what is in the corpus.
97
+
98
+ **Three passes, because links force it:**
99
+
100
+ 1. **Metadata** — walk every file, extract `{slug, title, tags, source}`. No
101
+ rendering. Exists because `[[slug]]` cannot resolve until all slugs are known.
102
+ 2. **Render** — per doc, `HtmlExporter.from_dsl_file(path, theme: :default,
103
+ layout: :fluid)` (identical defaults to `streamweaver export`, so a shelf
104
+ page and a single-file export of the same doc are byte-comparable), then
105
+ resolve `[[slug]]` and record edges.
106
+ 3. **Write** — `dist/<slug>.html`, `dist/index.html`, `dist/shelf.json` with
107
+ backlinks now that all edges are known.
108
+
109
+ **Units:**
110
+
111
+ | Unit | Job | Knows nothing about |
112
+ |---|---|---|
113
+ | `Shelf::Manifest` | slug/title/tag extraction, doc list | HTML |
114
+ | `Shelf::LinkResolver` | `[[slug]]` → anchor, emits edges | files, rendering |
115
+ | `Shelf::IndexPage` | builds the index | the filesystem |
116
+ | `Shelf::Builder` | orchestrates passes, writes output | how any of the above work |
117
+
118
+ **The index page is itself a StreamWeaver DSL doc**, rendered through the same
119
+ `HtmlExporter`. No second templating system, no second styling path — the index
120
+ inherits the theme and is authored in the DSL.
121
+
122
+ **Where a shelf differs from N single files** (decided, not open):
123
+
124
+ - **Images copy to `dist/assets/` with paths rewritten, not base64.**
125
+ `--inline-images` suits one emailed file; across a 50-doc shelf it duplicates
126
+ every shared image into every referencing page. The flag stays for the
127
+ single-file path.
128
+ - **Mermaid is written once to `dist/assets/` and shared.** `--offline`
129
+ currently inlines the whole library per document — fine once, absurd fifty
130
+ times. One copy, every page points at it, still works with no network.
131
+
132
+ ## Slices
133
+
134
+ 1. **Walk + per-doc export + generated index.** Usable immediately; no search.
135
+ 2. **`shelf.json` + client-side search + shared nav strip.**
136
+ 3. **`[[slug]]` resolution + backlinks.** (Can fold into 1 if links are wanted
137
+ from the very first corpus.)
138
+
139
+ ## Open questions deferred to build time
140
+
141
+ - **Hosting/privacy.** GitHub Pages is free only for public repos; a corpus
142
+ intended for a named set of students is not obviously public. Alternatives
143
+ worth pricing then: Netlify or Cloudflare Pages (both serve from a private
144
+ repo on free tiers).
145
+ - Whether readers want search at all, categorization by category/position, or
146
+ links back to source-video timestamps. Unknown until a real reader uses it —
147
+ do not guess, ship slice 1 and watch.
148
+
149
+ ## Related
150
+
151
+ - `docs/opal-jamstack.md` — the static Opal path and its Phase 1 scope limits
152
+ - `stream_weaver-5n9w` — opal-build standalone HTML `sidebar_toc` scroll-spy gap
153
+ - `stream_weaver-mdc` — canvas docs epic (discovery, export, cleanup, fidelity)