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.
- checksums.yaml +7 -0
- data/.beads/.gitignore +77 -0
- data/.beads/README.md +81 -0
- data/.beads/config.yaml +54 -0
- data/.beads/metadata.json +7 -0
- data/.rspec +3 -0
- data/AGENTS.md +91 -0
- data/CHANGELOG.md +294 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +565 -0
- data/Rakefile +13 -0
- data/assets/streamweaver-hero.jpg +0 -0
- data/bench/baselines/ledger.rb +58 -0
- data/bench/baselines/warroom.rb +51 -0
- data/bench/fixtures/ledger.rb +76 -0
- data/bench/fixtures/warroom.rb +60 -0
- data/bench/results/6daf8b8.md +102 -0
- data/bench/results/c8d749b.md +102 -0
- data/bench/results/eb06ab9.md +102 -0
- data/bench/run.rb +151 -0
- data/bench/support.rb +84 -0
- data/copy_this/dto_run_sample_run_7631.html +220 -0
- data/docs/SERVICE_MODE.md +302 -0
- data/docs/architecture/how_streamweaver_works.md +772 -0
- data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
- data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
- data/docs/canvas-ipc-session-summary.md +127 -0
- data/docs/canvas-panel-workflow.md +172 -0
- data/docs/canvas-read.md +147 -0
- data/docs/canvas-roadmap.md +275 -0
- data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
- data/docs/claude-code-companion-skill-spec.md +567 -0
- data/docs/components_reference.md +1223 -0
- data/docs/crud-patterns.md +265 -0
- data/docs/endpoints.md +122 -0
- data/docs/for_llms.md +1 -0
- data/docs/form-patterns.md +165 -0
- data/docs/frontend-only.md +169 -0
- data/docs/html-artifact-audit.md +238 -0
- data/docs/ideas/2025-01-01-as-a-service.md +203 -0
- data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
- data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
- data/docs/ideas/dhh-review-future-refactors.md +142 -0
- data/docs/opal-jamstack.md +47 -0
- data/docs/opal-npm-direction.md +184 -0
- data/docs/opal-spike-findings.md +194 -0
- data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
- data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
- data/docs/plans/2026-01-02-charts-design.md +288 -0
- data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
- data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
- data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
- data/docs/plans/canvas-claude-project-design.md +238 -0
- data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
- data/docs/plans/org-doc-preview-surfaces.md +293 -0
- data/docs/plans/shadcn-polish-plan.md +417 -0
- data/docs/porting-artifacts.md +252 -0
- data/docs/reference/agent-skills-comparison.md +369 -0
- data/docs/reference/travel-state-prd.artifact.html +780 -0
- data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
- data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
- data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
- data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
- data/docs/research/frontend-only-matrix.md +278 -0
- data/docs/research/streamweaver-way-spike-findings.md +529 -0
- data/docs/resource-dsl.md +342 -0
- data/docs/routing.md +302 -0
- data/docs/ruby-ui-comparison.md +319 -0
- data/docs/shared-dsl-fragments.md +356 -0
- data/docs/streamweaver-for-ai-agents.md +401 -0
- data/docs/streamweaver-frontend-vision.md +123 -0
- data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
- data/docs/streamweaver_canvas/save_example.rb +9 -0
- data/docs/templates.md +213 -0
- data/docs/testing.md +155 -0
- data/docs/theming-hooks.md +281 -0
- data/docs/tutorials/the-streamweaver-way.md +882 -0
- data/docs/university/capability-inventory.md +137 -0
- data/docs/university/dependency-survey.md +114 -0
- data/docs/university/design-spec.md +217 -0
- data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
- data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
- data/docs/university/mockups/course-list-dark.png +0 -0
- data/docs/university/mockups/course-list-light.png +0 -0
- data/docs/university/mockups/course_canvas_mockup.rb +741 -0
- data/docs/university/mockups/step-screen-dark.png +0 -0
- data/docs/university/mockups/step-screen-light.png +0 -0
- data/docs/university/roadmap.md +72 -0
- data/docs/university/send-to-coworker.md +77 -0
- data/docs/university/worker-session-mining.md +109 -0
- data/docs/visual/browser-rendering-spike.md +173 -0
- data/docs/visual/builder-visual-plan-analysis.md +378 -0
- data/docs/visual/sw-plan-format-exploration.md +511 -0
- data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
- data/docs/visual-skills/PROGRESS.md +60 -0
- data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
- data/docs/visual-skills/analysis/components.md +317 -0
- data/docs/visual-skills/analysis/overlap.md +360 -0
- data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
- data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
- data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
- data/docs/visual-skills/analysis/visual-explainer.md +964 -0
- data/docs/visual-skills/blog/blog-series-outline.md +39 -0
- data/docs/visual-skills/blog/token-efficiency.md +73 -0
- data/docs/visual-skills/design/architecture.md +1725 -0
- data/docs/visual-skills/design/codex-review.md +340 -0
- data/docs/visual-skills/design/dhh-review.md +209 -0
- data/docs/visual-skills/design/evolution.md +100 -0
- data/docs/visual-skills/design/gemini-review.md +55 -0
- data/docs/visual-skills/design/review-synthesis.md +149 -0
- data/docs/visual-skills/implementation/STATE.md +22 -0
- data/docs/visual-skills/implementation/plan.md +347 -0
- data/docs/visual-skills/implementation/spike-findings.md +237 -0
- data/docs/visual-skills/implementation/tasks.md +702 -0
- data/docs/visual-skills/lessons-learned/process.md +145 -0
- data/examples/README.md +96 -0
- data/examples/advanced/all_components.rb +177 -0
- data/examples/advanced/examples_browser.rb +394 -0
- data/examples/advanced/teachables_browser.rb +261 -0
- data/examples/advanced/theme_tweaker.rb +366 -0
- data/examples/advanced/tutorial.rb +1528 -0
- data/examples/agentic/agentic_form.rb +30 -0
- data/examples/agentic/agentic_form_autoclose.rb +30 -0
- data/examples/agentic/cultivation_tracker.rb +69 -0
- data/examples/basic/hello_world.rb +22 -0
- data/examples/basic/opal_tabs_table.rb +44 -0
- data/examples/basic/todo_list.rb +39 -0
- data/examples/button_loading_test.rb +36 -0
- data/examples/canvas/mermaid_canvas_demo.sh +151 -0
- data/examples/charts/bar_chart_demo.rb +57 -0
- data/examples/charts/line_chart_demo.rb +73 -0
- data/examples/charts/pie_area_demo.rb +87 -0
- data/examples/charts/stacked_bar_chart_demo.rb +69 -0
- data/examples/claude_code/README.md +85 -0
- data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
- data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
- data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
- data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
- data/examples/claude_code/codebreaker/README.md +213 -0
- data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
- data/examples/claude_code/tutorial/README.md +102 -0
- data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
- data/examples/claude_code/verification_flow/README.md +126 -0
- data/examples/components/annotated_code_demo.rb +54 -0
- data/examples/components/callout_demo.rb +33 -0
- data/examples/components/checkbox_group_demo.rb +46 -0
- data/examples/components/design_review.css +468 -0
- data/examples/components/design_review_demo.rb +44 -0
- data/examples/components/design_review_dsl.rb +300 -0
- data/examples/components/diff_block_demo.rb +112 -0
- data/examples/components/events_demo.rb +184 -0
- data/examples/components/form_demo.rb +62 -0
- data/examples/components/lesson_demo.rb +53 -0
- data/examples/components/markdown_demo.rb +110 -0
- data/examples/components/mermaid_demo.rb +166 -0
- data/examples/components/pareto_set.rb +64 -0
- data/examples/components/prd_demo.rb +21 -0
- data/examples/components/prd_dsl.rb +385 -0
- data/examples/components/quiz_demo.rb +47 -0
- data/examples/components/run_viewer_demo.rb +100 -0
- data/examples/components/score_and_collapsible_demo.rb +82 -0
- data/examples/components/select_stale_value_smoke_test.rb +39 -0
- data/examples/components/table_demo.rb +247 -0
- data/examples/components/timer_with_state_demo.rb +58 -0
- data/examples/components/todo_due_dates.rb +63 -0
- data/examples/components/uat_gaps_demo.rb +112 -0
- data/examples/dashboard/feed_simulator.rb +60 -0
- data/examples/dashboard/live_dashboard.rb +80 -0
- data/examples/dashboard_components.rb +108 -0
- data/examples/deferred_fragments_demo.rb +62 -0
- data/examples/generate_more_spike/README.md +63 -0
- data/examples/generate_more_spike/app.rb +441 -0
- data/examples/git_health.sh +401 -0
- data/examples/layout/layout_components_demo.rb +174 -0
- data/examples/layout/modal_demo.rb +215 -0
- data/examples/layout/navigation_demo.rb +227 -0
- data/examples/layout/route_tabs_demo.rb +52 -0
- data/examples/layout/routing_demo.rb +52 -0
- data/examples/layout/scroll_box_demo.rb +154 -0
- data/examples/lazy_fragments_demo.rb +151 -0
- data/examples/my_todos/README.md +29 -0
- data/examples/my_todos/my_todos.rb +312 -0
- data/examples/my_todos/store.rb +86 -0
- data/examples/opal/reactive_demo.rb +62 -0
- data/examples/opal/scenarios/s1_counter.rb +15 -0
- data/examples/opal/scenarios/s2_search_filter.rb +17 -0
- data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
- data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
- data/examples/opal/scenarios/s5_watch.rb +39 -0
- data/examples/opal/scenarios/s6_on_start.rb +20 -0
- data/examples/opal/scenarios/s7_wizard.rb +33 -0
- data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
- data/examples/opal/scenarios/s9_dashboard.rb +27 -0
- data/examples/operations_dashboard_demo.rb +167 -0
- data/examples/panel_demo.sh +447 -0
- data/examples/parity/assets/tyrion_slice.css +86 -0
- data/examples/parity/rivet_people_slice.rb +182 -0
- data/examples/parity/tyrion_components.css +777 -0
- data/examples/parity/tyrion_warroom_components.rb +414 -0
- data/examples/parity/tyrion_warroom_slice.rb +549 -0
- data/examples/puma_dev/README.md +124 -0
- data/examples/puma_dev/config.ru +34 -0
- data/examples/puma_dev/standalone_app.rb +34 -0
- data/examples/scaffolding/blog.rb +49 -0
- data/examples/scaffolding/utf_lite.rb +105 -0
- data/examples/styling/feedback_demo.rb +229 -0
- data/examples/styling/style_showcase.rb +172 -0
- data/examples/styling/theme_demo.rb +508 -0
- data/examples/timer_health_checker.rb +111 -0
- data/examples/timer_showcase.rb +131 -0
- data/examples/tutorials/render_markdown.rb +112 -0
- data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
- data/examples/tutorials/tutorial_content.rb +1082 -0
- data/examples/visual_skills/design_deck_demo.rb +314 -0
- data/examples/visual_skills/explainer_demo.rb +499 -0
- data/exe/streamweaver +9 -0
- data/gsd/ROADMAP-1.0.md +167 -0
- data/gsd/STATE.md +13 -0
- data/gsd/research/market-positioning-research.md +139 -0
- data/gsd/research/production-patterns-research.md +270 -0
- data/gsd/research/repo-audit-1.0.md +274 -0
- data/lib/stream_weaver/action_token.rb +58 -0
- data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
- data/lib/stream_weaver/adapter/base.rb +591 -0
- data/lib/stream_weaver/adapter/opal.rb +334 -0
- data/lib/stream_weaver/adapter/static.rb +1118 -0
- data/lib/stream_weaver/admin.rb +176 -0
- data/lib/stream_weaver/app.rb +1821 -0
- data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
- data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
- data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
- data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
- data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
- data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
- data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
- data/lib/stream_weaver/canvas/bridge.rb +239 -0
- data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
- data/lib/stream_weaver/canvas/client.rb +298 -0
- data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
- data/lib/stream_weaver/canvas/doc_store.rb +252 -0
- data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
- data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
- data/lib/stream_weaver/canvas/gist_store.rb +135 -0
- data/lib/stream_weaver/canvas/helpers.rb +109 -0
- data/lib/stream_weaver/canvas/history.rb +90 -0
- data/lib/stream_weaver/canvas/protocol.rb +86 -0
- data/lib/stream_weaver/canvas/reader.rb +853 -0
- data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
- data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
- data/lib/stream_weaver/canvas/session.rb +132 -0
- data/lib/stream_weaver/cli.rb +3235 -0
- data/lib/stream_weaver/component_assets.rb +70 -0
- data/lib/stream_weaver/component_registry.rb +67 -0
- data/lib/stream_weaver/component_renderer.rb +48 -0
- data/lib/stream_weaver/components/annotated_code.rb +53 -0
- data/lib/stream_weaver/components/api_endpoint.rb +42 -0
- data/lib/stream_weaver/components/callout.rb +59 -0
- data/lib/stream_weaver/components/chart.rb +84 -0
- data/lib/stream_weaver/components/code_block.rb +77 -0
- data/lib/stream_weaver/components/comparison.rb +39 -0
- data/lib/stream_weaver/components/decision.rb +38 -0
- data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
- data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
- data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
- data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
- data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
- data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
- data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
- data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
- data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
- data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
- data/lib/stream_weaver/components/diff_block.rb +125 -0
- data/lib/stream_weaver/components/doc_header.rb +57 -0
- data/lib/stream_weaver/components/image_block.rb +70 -0
- data/lib/stream_weaver/components/implementation_map.rb +33 -0
- data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
- data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
- data/lib/stream_weaver/components/mermaid.rb +79 -0
- data/lib/stream_weaver/components/pipeline.rb +63 -0
- data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
- data/lib/stream_weaver/components/slide_container.rb +141 -0
- data/lib/stream_weaver/components/timeline_event.rb +58 -0
- data/lib/stream_weaver/components/wireframe.rb +29 -0
- data/lib/stream_weaver/components/wireframe_block.rb +34 -0
- data/lib/stream_weaver/components.rb +2711 -0
- data/lib/stream_weaver/css.rb +230 -0
- data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
- data/lib/stream_weaver/display_dsl.rb +1004 -0
- data/lib/stream_weaver/export/html_exporter.rb +478 -0
- data/lib/stream_weaver/feed.rb +34 -0
- data/lib/stream_weaver/feed_builder.rb +31 -0
- data/lib/stream_weaver/fonts.rb +33 -0
- data/lib/stream_weaver/interaction_runner.rb +487 -0
- data/lib/stream_weaver/iterm.rb +460 -0
- data/lib/stream_weaver/layout_registry.rb +92 -0
- data/lib/stream_weaver/opal/bridge.rb +52 -0
- data/lib/stream_weaver/opal/builder.rb +193 -0
- data/lib/stream_weaver/opal/env.rb +29 -0
- data/lib/stream_weaver/opal/reactive_state.rb +75 -0
- data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
- data/lib/stream_weaver/opal/renderer.rb +77 -0
- data/lib/stream_weaver/opal/runtime.rb +250 -0
- data/lib/stream_weaver/opal/shell.rb +145 -0
- data/lib/stream_weaver/opal/string_bridge.rb +50 -0
- data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
- data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
- data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
- data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
- data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
- data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
- data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
- data/lib/stream_weaver/opal_entry.rb +136 -0
- data/lib/stream_weaver/org/inline.rb +95 -0
- data/lib/stream_weaver/org/reader.rb +561 -0
- data/lib/stream_weaver/org/recording_context.rb +86 -0
- data/lib/stream_weaver/org/source_splitter.rb +63 -0
- data/lib/stream_weaver/org/writer.rb +300 -0
- data/lib/stream_weaver/page_shell.rb +530 -0
- data/lib/stream_weaver/portfile.rb +79 -0
- data/lib/stream_weaver/pushable.rb +42 -0
- data/lib/stream_weaver/resource/default_views.rb +111 -0
- data/lib/stream_weaver/resource/field_input.rb +25 -0
- data/lib/stream_weaver/resource/state_keys.rb +16 -0
- data/lib/stream_weaver/resource/store.rb +18 -0
- data/lib/stream_weaver/resource.rb +104 -0
- data/lib/stream_weaver/server.rb +1471 -0
- data/lib/stream_weaver/service.rb +1240 -0
- data/lib/stream_weaver/service_client.rb +104 -0
- data/lib/stream_weaver/session_store.rb +186 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
- data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
- data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
- data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
- data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
- data/lib/stream_weaver/streamer.rb +63 -0
- data/lib/stream_weaver/templates/choices.rb +149 -0
- data/lib/stream_weaver/templates/code.rb +157 -0
- data/lib/stream_weaver/templates/confirm.rb +110 -0
- data/lib/stream_weaver/templates/diff.rb +195 -0
- data/lib/stream_weaver/templates/info.rb +142 -0
- data/lib/stream_weaver/templates/table.rb +169 -0
- data/lib/stream_weaver/templates/wizard.rb +271 -0
- data/lib/stream_weaver/theme/auto_mode.rb +116 -0
- data/lib/stream_weaver/theme/presets.rb +555 -0
- data/lib/stream_weaver/theme.rb +639 -0
- data/lib/stream_weaver/university/canvas.rb +924 -0
- data/lib/stream_weaver/university/course.rb +699 -0
- data/lib/stream_weaver/university/demos/counter.rb +33 -0
- data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
- data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
- data/lib/stream_weaver/university/demos.rb +50 -0
- data/lib/stream_weaver/university/listener.rb +467 -0
- data/lib/stream_weaver/university/progress.rb +201 -0
- data/lib/stream_weaver/university/runner.rb +138 -0
- data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
- data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
- data/lib/stream_weaver/utils.rb +31 -0
- data/lib/stream_weaver/version.rb +5 -0
- data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
- data/lib/stream_weaver/views.rb +3940 -0
- data/lib/stream_weaver.rb +122 -0
- data/llms.txt +1269 -0
- data/sig/stream_weaver.rbs +4 -0
- metadata +598 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# RubyUI / shadcn Comparison & Adoption Plan
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-02-14
|
|
4
|
+
**Context:** Evaluating RubyUI (shadcn/ui port for Phlex) to improve StreamWeaver's component polish.
|
|
5
|
+
**Repos:** `~/work/ruby_ui` (gem), `~/work/ruby_ui_web` (docs site)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Component Gap Analysis
|
|
10
|
+
|
|
11
|
+
### High-Value Components StreamWeaver Is Missing
|
|
12
|
+
|
|
13
|
+
| RubyUI Component | What It Does | Priority |
|
|
14
|
+
|-----------------|-------------|----------|
|
|
15
|
+
| **Calendar** | Date picker calendar | HIGH |
|
|
16
|
+
| **Clipboard** | Copy-to-clipboard with visual feedback | HIGH |
|
|
17
|
+
| **Codeblock** | Syntax-highlighted code blocks | HIGH |
|
|
18
|
+
| **Combobox** | Searchable select/autocomplete | HIGH |
|
|
19
|
+
| **Command** | Command palette (⌘K style) | HIGH |
|
|
20
|
+
| **Sheet** | Slide-in panel (left/right/top/bottom) | HIGH |
|
|
21
|
+
| **Skeleton** | Loading placeholder animation | HIGH |
|
|
22
|
+
| **AlertDialog** | Modal confirmation with overlay | HIGH |
|
|
23
|
+
| **HoverCard** | Hover preview popover | Medium |
|
|
24
|
+
| **Popover** | Floating positioned content | Medium |
|
|
25
|
+
| **Switch** | Toggle switch | Medium |
|
|
26
|
+
| **ContextMenu** | Right-click menus | Medium |
|
|
27
|
+
| **ShortcutKey** | Keyboard shortcut badge display | Medium |
|
|
28
|
+
| **MaskedInput** | Input with format mask (phone, date) | Medium |
|
|
29
|
+
| **Avatar** | User image with fallback initials | Medium |
|
|
30
|
+
| **Typography** | Styled heading/paragraph components | Medium |
|
|
31
|
+
| **Carousel** | Content slider | Low |
|
|
32
|
+
| **AspectRatio** | Maintain aspect ratios | Low |
|
|
33
|
+
|
|
34
|
+
### StreamWeaver Has, RubyUI Doesn't
|
|
35
|
+
|
|
36
|
+
| StreamWeaver Component | Notes |
|
|
37
|
+
|----------------------|-------|
|
|
38
|
+
| **Charts** (Bar, Line, Pie, Stacked) | RubyUI has a basic Chart component |
|
|
39
|
+
| **AppShell** | Full app layout with sidebar + main |
|
|
40
|
+
| **ExpandableCard** | Click-to-expand card |
|
|
41
|
+
| **CodeEditor** | Editable code with syntax highlighting |
|
|
42
|
+
| **StatusDot / PulseIndicator** | Real-time status indicators |
|
|
43
|
+
| **StatDisplay** | Metric display with label |
|
|
44
|
+
| **TagButtons** | Tag/filter buttons |
|
|
45
|
+
| **Columns/Column** | Column layout DSL |
|
|
46
|
+
| **VStack/HStack/Grid** | Layout primitives |
|
|
47
|
+
| **Toast** | Notification toasts |
|
|
48
|
+
| **Spinner** | Loading spinner |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Theming Comparison
|
|
53
|
+
|
|
54
|
+
### RubyUI (shadcn/ui pattern)
|
|
55
|
+
|
|
56
|
+
**How it works:**
|
|
57
|
+
- CSS custom properties on `:root` with semantic names
|
|
58
|
+
- `.dark` class on `<html>` overrides all variables
|
|
59
|
+
- Tailwind's `@theme inline` maps CSS vars → utility classes
|
|
60
|
+
- Components use Tailwind classes: `bg-primary text-primary-foreground`
|
|
61
|
+
- `TailwindMerge` gem resolves class conflicts
|
|
62
|
+
- `localStorage` persists theme preference
|
|
63
|
+
- Stimulus controller toggles `.dark` class
|
|
64
|
+
|
|
65
|
+
**Color tokens (the full set):**
|
|
66
|
+
```css
|
|
67
|
+
:root {
|
|
68
|
+
--background /* Page background */
|
|
69
|
+
--foreground /* Default text */
|
|
70
|
+
--card /* Card background */
|
|
71
|
+
--card-foreground /* Card text */
|
|
72
|
+
--popover /* Popover/dropdown background */
|
|
73
|
+
--popover-foreground
|
|
74
|
+
--primary /* Primary action color */
|
|
75
|
+
--primary-foreground
|
|
76
|
+
--secondary /* Secondary elements */
|
|
77
|
+
--secondary-foreground
|
|
78
|
+
--muted /* Muted backgrounds */
|
|
79
|
+
--muted-foreground /* Muted text */
|
|
80
|
+
--accent /* Hover/active states */
|
|
81
|
+
--accent-foreground
|
|
82
|
+
--destructive /* Danger/delete actions */
|
|
83
|
+
--destructive-foreground
|
|
84
|
+
--border /* Default border */
|
|
85
|
+
--input /* Input border */
|
|
86
|
+
--ring /* Focus ring */
|
|
87
|
+
--chart-1 through --chart-5
|
|
88
|
+
--radius /* Base border radius */
|
|
89
|
+
--sidebar, --sidebar-foreground, --sidebar-primary, etc.
|
|
90
|
+
--warning, --warning-foreground /* RubyUI additions */
|
|
91
|
+
--success, --success-foreground
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dark {
|
|
95
|
+
/* Same variables, different values */
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Color format:** oklch (perceptually uniform, modern)
|
|
100
|
+
|
|
101
|
+
**Key CSS:**
|
|
102
|
+
```css
|
|
103
|
+
@custom-variant dark (&:is(.dark *));
|
|
104
|
+
|
|
105
|
+
@theme inline {
|
|
106
|
+
--color-primary: var(--primary);
|
|
107
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
108
|
+
/* ... maps CSS vars to Tailwind color names */
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@layer base {
|
|
112
|
+
* { @apply border-border outline-ring/50; }
|
|
113
|
+
body { @apply bg-background text-foreground; }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### StreamWeaver (current)
|
|
118
|
+
|
|
119
|
+
**How it works:**
|
|
120
|
+
- CSS custom properties with `--sw-` prefix
|
|
121
|
+
- Ruby `Theme` class with `VARIABLE_SCHEMA` mapping
|
|
122
|
+
- Multiple named themes (not just light/dark)
|
|
123
|
+
- `register_theme` API for custom themes
|
|
124
|
+
- Components use inline styles referencing CSS vars
|
|
125
|
+
- No Tailwind — custom CSS classes
|
|
126
|
+
|
|
127
|
+
**Variable categories:**
|
|
128
|
+
- Typography: `--sw-font-family`, `--sw-font-size-base`, etc.
|
|
129
|
+
- Colors: `--sw-color-primary`, `--sw-color-bg`, `--sw-color-text`, etc.
|
|
130
|
+
- Spacing: `--sw-spacing-xs` through `--sw-spacing-2xl`
|
|
131
|
+
- Radius: `--sw-radius-sm` through `--sw-radius-xl`
|
|
132
|
+
- Shadows: `--sw-shadow-sm` through `--sw-shadow-xl`
|
|
133
|
+
|
|
134
|
+
**Strengths over RubyUI:**
|
|
135
|
+
- More granular control (spacing, shadows, radius as variables)
|
|
136
|
+
- Named theme system (not just light/dark)
|
|
137
|
+
- Ruby-native theme registration
|
|
138
|
+
|
|
139
|
+
**Weaknesses:**
|
|
140
|
+
- Components don't benefit from Tailwind utility classes
|
|
141
|
+
- More CSS to maintain per component
|
|
142
|
+
- No automatic dark mode
|
|
143
|
+
- Visual polish not at shadcn level
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Why RubyUI/shadcn Looks Better
|
|
148
|
+
|
|
149
|
+
1. **Consistent design tokens** — every component pulls from the same small set of semantic colors
|
|
150
|
+
2. **TailwindMerge** — prevents class conflicts, always clean output
|
|
151
|
+
3. **oklch colors** — perceptually uniform, look great at all lightness levels
|
|
152
|
+
4. **Transitions/animations** — `tw-animate-css` provides smooth enter/exit
|
|
153
|
+
5. **Focus states** — consistent `focus-visible:ring-1 focus-visible:ring-ring` everywhere
|
|
154
|
+
6. **Disabled states** — `disabled:pointer-events-none disabled:opacity-50` pattern
|
|
155
|
+
7. **Accessibility** — `aria-disabled` support built into base classes
|
|
156
|
+
8. **Minimal but precise spacing** — every component has exactly the right padding/margins
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Adoption Strategy
|
|
161
|
+
|
|
162
|
+
### Option A: Add shadcn CSS Variable Layer (Recommended)
|
|
163
|
+
|
|
164
|
+
Add the shadcn semantic tokens as an **additional layer** on top of existing `--sw-` variables. Map them so existing themes still work.
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
/* Add to StreamWeaver's base CSS */
|
|
168
|
+
:root {
|
|
169
|
+
/* Map shadcn tokens to existing SW variables */
|
|
170
|
+
--primary: var(--sw-color-primary);
|
|
171
|
+
--primary-foreground: #fff;
|
|
172
|
+
--background: var(--sw-color-bg);
|
|
173
|
+
--foreground: var(--sw-color-text);
|
|
174
|
+
--card: var(--sw-color-bg-card);
|
|
175
|
+
--card-foreground: var(--sw-color-text);
|
|
176
|
+
--muted: var(--sw-color-bg-elevated);
|
|
177
|
+
--muted-foreground: var(--sw-color-text-muted);
|
|
178
|
+
--border: var(--sw-color-border);
|
|
179
|
+
--input: var(--sw-color-border);
|
|
180
|
+
--ring: var(--sw-color-border-focus);
|
|
181
|
+
--destructive: #ef4444;
|
|
182
|
+
--destructive-foreground: #fff;
|
|
183
|
+
--accent: var(--sw-color-accent);
|
|
184
|
+
--accent-foreground: var(--sw-color-text);
|
|
185
|
+
--secondary: var(--sw-color-secondary);
|
|
186
|
+
--secondary-foreground: var(--sw-color-text);
|
|
187
|
+
--radius: var(--sw-radius-md);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.dark {
|
|
191
|
+
/* Dark overrides — pull from dark theme values */
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Pros:** Non-breaking, incremental, existing themes keep working
|
|
196
|
+
**Cons:** Two variable systems in parallel temporarily
|
|
197
|
+
|
|
198
|
+
### Option B: Adopt Tailwind in the AlpineJS Adapter
|
|
199
|
+
|
|
200
|
+
StreamWeaver's adapter pattern means we can emit Tailwind classes from components without changing the DSL:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Current: adapter emits custom CSS classes
|
|
204
|
+
def render_button(view, component, state)
|
|
205
|
+
view.button(class: "sw-button sw-button--primary") { ... }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# New: adapter emits Tailwind + shadcn classes
|
|
209
|
+
def render_button(view, component, state)
|
|
210
|
+
view.button(class: "inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2 text-sm font-medium") { ... }
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Pros:** Full shadcn polish, consistent with RubyUI
|
|
215
|
+
**Cons:** Requires Tailwind CSS in the output, bigger change
|
|
216
|
+
|
|
217
|
+
### Option C: Port RubyUI Components Directly
|
|
218
|
+
|
|
219
|
+
Since both use Phlex, RubyUI components can be adapted for StreamWeaver:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# RubyUI's Button (Phlex component)
|
|
223
|
+
module RubyUI
|
|
224
|
+
class Button < Base
|
|
225
|
+
def view_template(&)
|
|
226
|
+
button(**attrs, &)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# StreamWeaver equivalent would be in the adapter layer
|
|
232
|
+
# The DSL stays the same: `button "Click me", variant: :primary`
|
|
233
|
+
# But the rendered output matches RubyUI's polish
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Recommended Path
|
|
237
|
+
|
|
238
|
+
1. **Phase 1:** Add shadcn CSS variable layer (Option A) — immediate dark mode + consistent tokens
|
|
239
|
+
2. **Phase 2:** Update adapter to emit shadcn-style classes for existing components — visual polish
|
|
240
|
+
3. **Phase 3:** Port high-value missing components (Calendar, Command, Sheet, Skeleton, Combobox)
|
|
241
|
+
4. **Phase 4:** Consider full Tailwind adoption for new components
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Implementation: Phase 1 — shadcn Variable Layer + Dark Mode
|
|
246
|
+
|
|
247
|
+
### 1. Create `shadcn_compat.css`
|
|
248
|
+
|
|
249
|
+
A single CSS file that maps StreamWeaver themes to shadcn tokens and adds dark mode support:
|
|
250
|
+
|
|
251
|
+
```css
|
|
252
|
+
/* lib/stream_weaver/assets/shadcn_compat.css */
|
|
253
|
+
|
|
254
|
+
:root {
|
|
255
|
+
/* Semantic tokens from shadcn */
|
|
256
|
+
--background: var(--sw-color-bg, #ffffff);
|
|
257
|
+
--foreground: var(--sw-color-text, #0a0a0a);
|
|
258
|
+
--card: var(--sw-color-bg-card, #ffffff);
|
|
259
|
+
--card-foreground: var(--sw-color-text, #0a0a0a);
|
|
260
|
+
--primary: var(--sw-color-primary, #171717);
|
|
261
|
+
--primary-foreground: #fafafa;
|
|
262
|
+
--secondary: var(--sw-color-secondary, #f5f5f5);
|
|
263
|
+
--secondary-foreground: #171717;
|
|
264
|
+
--muted: var(--sw-color-bg-elevated, #f5f5f5);
|
|
265
|
+
--muted-foreground: var(--sw-color-text-muted, #737373);
|
|
266
|
+
--accent: var(--sw-color-accent, #f5f5f5);
|
|
267
|
+
--accent-foreground: #171717;
|
|
268
|
+
--destructive: #ef4444;
|
|
269
|
+
--destructive-foreground: #ffffff;
|
|
270
|
+
--border: var(--sw-color-border, #e5e5e5);
|
|
271
|
+
--input: var(--sw-color-border, #e5e5e5);
|
|
272
|
+
--ring: var(--sw-color-primary, #171717);
|
|
273
|
+
--radius: var(--sw-radius-md, 0.5rem);
|
|
274
|
+
--warning: #f59e0b;
|
|
275
|
+
--warning-foreground: #ffffff;
|
|
276
|
+
--success: #22c55e;
|
|
277
|
+
--success-foreground: #ffffff;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.dark {
|
|
281
|
+
--background: #0a0a0a;
|
|
282
|
+
--foreground: #fafafa;
|
|
283
|
+
--card: #171717;
|
|
284
|
+
--card-foreground: #fafafa;
|
|
285
|
+
--primary: #fafafa;
|
|
286
|
+
--primary-foreground: #171717;
|
|
287
|
+
--secondary: #262626;
|
|
288
|
+
--secondary-foreground: #fafafa;
|
|
289
|
+
--muted: #262626;
|
|
290
|
+
--muted-foreground: #a3a3a3;
|
|
291
|
+
--accent: #262626;
|
|
292
|
+
--accent-foreground: #fafafa;
|
|
293
|
+
--destructive: #dc2626;
|
|
294
|
+
--destructive-foreground: #fafafa;
|
|
295
|
+
--border: rgba(255,255,255,0.1);
|
|
296
|
+
--input: rgba(255,255,255,0.15);
|
|
297
|
+
--ring: #737373;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 2. Add Dark Mode Toggle
|
|
302
|
+
|
|
303
|
+
Update `ThemeSwitcher` component to support `.dark` class toggle with localStorage persistence.
|
|
304
|
+
|
|
305
|
+
### 3. Update Component CSS
|
|
306
|
+
|
|
307
|
+
Gradually migrate component styles from hardcoded colors to `var(--primary)`, `var(--background)`, etc.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## References
|
|
312
|
+
|
|
313
|
+
- RubyUI docs: https://rubyui.com
|
|
314
|
+
- RubyUI gem source: `~/work/ruby_ui/`
|
|
315
|
+
- RubyUI web source: `~/work/ruby_ui_web/`
|
|
316
|
+
- shadcn/ui (React original): https://ui.shadcn.com
|
|
317
|
+
- Tailwind CSS v4: https://tailwindcss.com
|
|
318
|
+
- StreamWeaver theme system: `~/work/rstreamlit/stream_weaver/lib/stream_weaver/theme.rb`
|
|
319
|
+
- StreamWeaver components: `~/work/rstreamlit/stream_weaver/lib/stream_weaver/components.rb`
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Shared DSL Fragments
|
|
2
|
+
|
|
3
|
+
How two or more StreamWeaver docs share ONE canonical copy of a table, a rule
|
|
4
|
+
list, or a set of numbers, so the documents cannot drift apart.
|
|
5
|
+
|
|
6
|
+
This is a pattern doc, not a feature doc -- nothing in `lib/` implements it
|
|
7
|
+
today. It is built entirely out of `instance_eval`, which is already how every
|
|
8
|
+
doc body is evaluated. See the "Proposal" section at the end for what a
|
|
9
|
+
first-class helper would look like.
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
Multi-audience documents are the normal case, not the exception. The same
|
|
14
|
+
decision gets written up twice: once for the people who have to approve it and
|
|
15
|
+
once for the people who have to build it. Both write-ups quote the same rules
|
|
16
|
+
table and the same options table. The moment one of them is edited, the two
|
|
17
|
+
documents disagree, and nobody finds out until a meeting where two people are
|
|
18
|
+
reading different numbers off two different pages.
|
|
19
|
+
|
|
20
|
+
Forrest's 10th Rule applies to documents as much as to code. Two hand-maintained
|
|
21
|
+
copies of one table are a slow, buggy, half-assed version of one table.
|
|
22
|
+
|
|
23
|
+
## When to use it
|
|
24
|
+
|
|
25
|
+
- Two or more docs for **different audiences** that must agree on the same
|
|
26
|
+
tables, rules, prices, or estimates. A decision memo and its engineering
|
|
27
|
+
companion is the canonical shape.
|
|
28
|
+
- A doc plus a longer implementer reference where a summary table is the
|
|
29
|
+
contract between them.
|
|
30
|
+
- Any number that gets restated. If a figure appears in two files and a human
|
|
31
|
+
has to keep them equal, it belongs in a fragment.
|
|
32
|
+
|
|
33
|
+
Do NOT use it for prose. Prose written for engineers should not be the prose
|
|
34
|
+
shown to the product owner -- that difference is the whole point of having two
|
|
35
|
+
documents. Share the **facts** (tables, rule lists, numbers) and let each doc
|
|
36
|
+
write its own framing around them.
|
|
37
|
+
|
|
38
|
+
## The pattern
|
|
39
|
+
|
|
40
|
+
Three parts.
|
|
41
|
+
|
|
42
|
+
**1. The fragment** -- a plain `.rb` file, conventionally under a `shared/`
|
|
43
|
+
directory next to the docs. It defines methods that EMIT DSL and nothing else.
|
|
44
|
+
It is never rendered on its own.
|
|
45
|
+
|
|
46
|
+
**2. The consumer** -- each doc body loads the fragment with `instance_eval` at
|
|
47
|
+
the top, then calls the fragment's methods wherever the shared content belongs.
|
|
48
|
+
Because `instance_eval` on a string with a `def` in it defines a **singleton
|
|
49
|
+
method on the receiver**, and the receiver here is the `App` the doc body is
|
|
50
|
+
itself being evaluated against, those methods can call `table`, `md`, `callout`
|
|
51
|
+
and every other DSL verb exactly as if they had been written inline.
|
|
52
|
+
|
|
53
|
+
**3. Delivery** -- `streamweaver export` and `canvas-read` resolve the fragment
|
|
54
|
+
themselves; `canvas-push` and `org-export` need a hand. See "Delivery" below.
|
|
55
|
+
|
|
56
|
+
## The consumer snippet
|
|
57
|
+
|
|
58
|
+
Copy this into the top of each doc body. Replace the fragment path and the
|
|
59
|
+
guard method name.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# streamweaver-doc: v1
|
|
63
|
+
#
|
|
64
|
+
# The shared tables come from ONE canonical file so this doc and its companion
|
|
65
|
+
# cannot drift. `streamweaver export` and `canvas-read` pass this file's path
|
|
66
|
+
# to instance_eval, so __dir__ is this file's own directory there. `org-export`
|
|
67
|
+
# and the canvas-push bridge eval the text with NO filename, so __dir__ is nil:
|
|
68
|
+
# org-export runs in your shell (cwd or SW_DOC_DIR resolves it), and canvas-push
|
|
69
|
+
# needs the fragment concatenated ahead of the body -- see the push command in
|
|
70
|
+
# docs/shared-dsl-fragments.md.
|
|
71
|
+
unless respond_to?(:pm_base_rules_table)
|
|
72
|
+
_root = __dir__ || ENV["SW_DOC_DIR"] || Dir.pwd
|
|
73
|
+
_frag = File.expand_path("shared/pm_discount_shared.rb", _root)
|
|
74
|
+
raise "shared DSL fragment not found: #{_frag}" unless File.file?(_frag)
|
|
75
|
+
instance_eval(File.read(_frag), _frag)
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Three things earn their place here and nothing else does:
|
|
80
|
+
|
|
81
|
+
- `__dir__` covers export and canvas-read, which is most of the time.
|
|
82
|
+
- `ENV["SW_DOC_DIR"] || Dir.pwd` covers `org-export`, which runs in the same
|
|
83
|
+
process as your shell.
|
|
84
|
+
- `unless respond_to?(...)` makes the concatenated canvas-push form a no-op
|
|
85
|
+
rather than a second load. It is also what lets the resolver fail loudly
|
|
86
|
+
instead of silently: if nothing defined the methods and nothing on disk
|
|
87
|
+
matches, you get a path in the error message.
|
|
88
|
+
|
|
89
|
+
`DisplayDSL` and `App` define no `method_missing` (grep confirms), so
|
|
90
|
+
`respond_to?` is an honest test.
|
|
91
|
+
|
|
92
|
+
**Do not hardcode an absolute home-directory path as a fallback.** It works on
|
|
93
|
+
one machine and silently breaks the doc for anyone else, including a CI export.
|
|
94
|
+
Use the concatenation form for canvas-push instead.
|
|
95
|
+
|
|
96
|
+
## Why the snippet looks like that: how StreamWeaver evaluates a doc
|
|
97
|
+
|
|
98
|
+
Every mode is `instance_eval` of the doc text against a fresh `App` (or, for
|
|
99
|
+
org, against `Org::RecordingContext`, which includes the same `DisplayDSL`).
|
|
100
|
+
They differ only in whether a **filename** is passed to `instance_eval`, and
|
|
101
|
+
that is what decides whether `__dir__` works.
|
|
102
|
+
|
|
103
|
+
| Mode | Eval site | Filename passed | `__dir__` inside the doc | Same process as your shell |
|
|
104
|
+
|---|---|---|---|---|
|
|
105
|
+
| `streamweaver export` | `lib/stream_weaver/export/html_exporter.rb:104` | yes | the doc's directory | yes |
|
|
106
|
+
| `canvas-read` | `lib/stream_weaver/canvas/reader.rb:732` | yes | the doc's directory | yes |
|
|
107
|
+
| `streamweaver org-export` | `lib/stream_weaver/org/writer.rb:35` | no | `nil` | yes |
|
|
108
|
+
| `canvas-push` (bridge) | `lib/stream_weaver/canvas/bridge.rb:205` | no | `nil` | **no** |
|
|
109
|
+
| standalone `app ... do instance_eval(File.read(p), p) end` | your own wrapper | yes (you pass it) | the doc's directory | yes |
|
|
110
|
+
|
|
111
|
+
Verified empirically on Ruby 3.3.5: with a filename, `__dir__` is that file's
|
|
112
|
+
directory (and stays **relative** if you passed a relative path, which is fine
|
|
113
|
+
-- `File.expand_path` resolves it against the unchanged cwd). With no filename,
|
|
114
|
+
`__FILE__` is `"(eval at .../writer.rb:35)"` and `__dir__` returns `nil` rather
|
|
115
|
+
than raising. Guard with `||`, not with `rescue`.
|
|
116
|
+
|
|
117
|
+
The canvas-push row is the one that constrains everything. `canvas-push` reads
|
|
118
|
+
the DSL from **stdin** (`lib/stream_weaver/cli.rb:1177`) and ships it as plain
|
|
119
|
+
text over a socket to a long-lived bridge process. The bridge's cwd, its
|
|
120
|
+
environment, and its idea of "the current project" are not yours. It does
|
|
121
|
+
receive a `source_dir` -- the pushing side's git root
|
|
122
|
+
(`lib/stream_weaver/cli.rb:1185`, `lib/stream_weaver/canvas/protocol.rb:51`) --
|
|
123
|
+
but nothing plumbs that into the eval, and it is the repo root anyway, not the
|
|
124
|
+
doc's directory.
|
|
125
|
+
|
|
126
|
+
The gem already conceded this point once: `CLI.prepend_stylesheets`
|
|
127
|
+
(`lib/stream_weaver/cli.rb:1215-1228`) reads `--stylesheet` files on the
|
|
128
|
+
**pushing** side and inlines their content into the pushed text, with a comment
|
|
129
|
+
saying it does so "precisely because canvas-push has no reliable notion of 'the
|
|
130
|
+
DSL's own directory' once it's plain text." Fragments have the same problem and
|
|
131
|
+
take the same answer.
|
|
132
|
+
|
|
133
|
+
## Delivery
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd <repo>/docs/streamweaver_canvas
|
|
137
|
+
|
|
138
|
+
# Export to standalone HTML -- __dir__ resolves the fragment
|
|
139
|
+
streamweaver export pm-discount-decision-memo.rb -o /tmp/memo.html
|
|
140
|
+
|
|
141
|
+
# Browse -- canvas-read passes the path, __dir__ resolves the fragment
|
|
142
|
+
streamweaver canvas-read
|
|
143
|
+
|
|
144
|
+
# Org sibling -- __dir__ is nil, so run it from the doc's directory,
|
|
145
|
+
# or set SW_DOC_DIR to that directory from anywhere
|
|
146
|
+
streamweaver org-export pm-discount-decision-memo.rb
|
|
147
|
+
SW_DOC_DIR=$PWD streamweaver org-export /abs/path/pm-discount-decision-memo.rb
|
|
148
|
+
|
|
149
|
+
# Push to a canvas -- concatenate the fragment ahead of the body.
|
|
150
|
+
# The `unless respond_to?` guard makes the doc's own loader a no-op.
|
|
151
|
+
cat shared/pm_discount_shared.rb pm-discount-decision-memo.rb \
|
|
152
|
+
| streamweaver canvas-push pm-memo
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
All five forms were run end to end against a throwaway doc before this was
|
|
156
|
+
written; the fragment's table renders in each.
|
|
157
|
+
|
|
158
|
+
## Fragment authoring conventions
|
|
159
|
+
|
|
160
|
+
- **One method per shared artifact**, named with a doc-family prefix:
|
|
161
|
+
`pm_base_rules_table`, `pm_policy_table`. The prefix is not decoration --
|
|
162
|
+
these become singleton methods on the `App`, sharing a namespace with every
|
|
163
|
+
DSL verb, so a fragment method called `summary` or `rules` is a collision
|
|
164
|
+
waiting to happen.
|
|
165
|
+
- **Emit DSL only.** The method body is `table(...)`, `md(...)`, `callout(...)
|
|
166
|
+
do ... end`. Nothing else.
|
|
167
|
+
- **No side effects, no output, no `require`.** The file is read and eval'd,
|
|
168
|
+
possibly several times per process, in contexts that are not a normal Ruby
|
|
169
|
+
load path.
|
|
170
|
+
- **No headers.** No `doc_header`, no `doc_section_header`, no `sidebar_toc`.
|
|
171
|
+
Section structure belongs to each doc, because each audience gets a different
|
|
172
|
+
outline. The fragment supplies content that slots into whatever section the
|
|
173
|
+
consumer chose.
|
|
174
|
+
- **No `# streamweaver-doc: v1` marker.** That marker means "this is a
|
|
175
|
+
renderable doc body." A fragment is not one, and marking it invites tooling
|
|
176
|
+
to try to render it.
|
|
177
|
+
- **A header comment naming the consumers.** The next person to edit a number
|
|
178
|
+
in the fragment needs to know, in the file itself, which documents that
|
|
179
|
+
number is about to change.
|
|
180
|
+
|
|
181
|
+
## Org mode
|
|
182
|
+
|
|
183
|
+
**Are fragment-sourced tables inlined in an org export?** Yes. `Org::Writer`
|
|
184
|
+
does not read your `.rb` source and translate it statement by statement -- it
|
|
185
|
+
`instance_eval`s the DSL text against `Org::RecordingContext`
|
|
186
|
+
(`lib/stream_weaver/org/writer.rb:35`) and then walks the resulting component
|
|
187
|
+
tree. By the time the writer sees anything, the fragment has already run and
|
|
188
|
+
its `Components::Table` objects are indistinguishable from tables written
|
|
189
|
+
inline. The generated `.org` is a fully expanded **snapshot**: drift-free at the
|
|
190
|
+
moment of export, and NOT re-linked to the fragment afterwards. Re-run
|
|
191
|
+
`org-export` after editing the fragment.
|
|
192
|
+
|
|
193
|
+
**Does the org round-trip path support `#+INCLUDE:`?** No, and nothing else
|
|
194
|
+
either. `Org::Reader.to_dsl` takes a **String**, never a path
|
|
195
|
+
(`lib/stream_weaver/org/reader.rb:41`), and all three of its callers hand it
|
|
196
|
+
text they have already read: `CLI.org_render`
|
|
197
|
+
(`lib/stream_weaver/cli.rb:1500`), `Canvas::Reader.render_doc`
|
|
198
|
+
(`lib/stream_weaver/canvas/reader.rb:731`), and the browser extension's
|
|
199
|
+
`sandbox.js`. There is no filesystem access anywhere in the parse. An
|
|
200
|
+
`#+INCLUDE:` line is not recognized as a keyword either -- `PREAMBLE_RE`
|
|
201
|
+
(`lib/stream_weaver/org/reader.rb:38`) matches only `#+STREAMWEAVER_DSL:` and
|
|
202
|
+
`#+TITLE:` -- so it falls through into the surrounding prose and round-trips
|
|
203
|
+
into an `md` block as visible literal text. Confirmed by running `org-render`
|
|
204
|
+
over an org doc containing one.
|
|
205
|
+
|
|
206
|
+
What it would take (proposal only, not implemented):
|
|
207
|
+
|
|
208
|
+
1. Thread a base directory through `Org::Reader.to_dsl(text, base_dir:)` and
|
|
209
|
+
`Reader#initialize`, and pass it from `CLI.org_render` (which knows
|
|
210
|
+
`org_path`) and `Canvas::Reader.render_doc` (which already has `path:`).
|
|
211
|
+
2. Add an expansion pre-pass before `#chunks` that splices the referenced
|
|
212
|
+
file's lines in place of each `#+INCLUDE:` line, with a depth or cycle
|
|
213
|
+
guard.
|
|
214
|
+
3. Accept that the **extension** path cannot do it. `sandbox.js` parses org in
|
|
215
|
+
the browser with no filesystem, so an org doc using `#+INCLUDE:` would
|
|
216
|
+
render one way through the CLI and another way in the extension. That
|
|
217
|
+
divergence, not the parsing work, is the real cost.
|
|
218
|
+
|
|
219
|
+
Given that org files are generated snapshots today, the honest recommendation
|
|
220
|
+
is: author in Ruby, share fragments in Ruby, and let org be an export format.
|
|
221
|
+
|
|
222
|
+
**Why does a hand-typed `#+begin_quote` card/callout marker raise "malformed
|
|
223
|
+
card header"?** The marker line must contain ONLY the bolded title (`*title*`,
|
|
224
|
+
or `*[badge] title*`, plus an optional trailing `/(meta)/`) -- nothing else on
|
|
225
|
+
that line. Both `callout_marker_match` (`lib/stream_weaver/org/reader.rb:504-509`)
|
|
226
|
+
and `emit_card`'s own regex (`lib/stream_weaver/org/reader.rb:417-423`) are
|
|
227
|
+
fully line-anchored (`\A...\z`), so a natural "bolded lead-in phrase, then
|
|
228
|
+
continue the sentence" instinct --
|
|
229
|
+
|
|
230
|
+
```org
|
|
231
|
+
#+begin_quote
|
|
232
|
+
*ℹ️ Append-only.* This doc isn't version-tracked, so this is the history
|
|
233
|
+
mechanism...
|
|
234
|
+
#+end_quote
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
-- fails BOTH the callout match and the card fallback (even for a marker that
|
|
238
|
+
starts with a reserved callout emoji), so you get the generic "malformed card
|
|
239
|
+
header" error rather than anything pointing at the real cause. Body prose has
|
|
240
|
+
to start on the next line instead:
|
|
241
|
+
|
|
242
|
+
```org
|
|
243
|
+
#+begin_quote
|
|
244
|
+
*ℹ️ Append-only*
|
|
245
|
+
This doc isn't version-tracked, so this is the history mechanism...
|
|
246
|
+
#+end_quote
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Same class of bug as the numbered-list-across-separate-`md()`-calls gotcha in
|
|
250
|
+
the visual-companion skill: looks fine to a human, breaks a strict line-based
|
|
251
|
+
parser. Confirmed live 2026-08-27 against a hand-typed org doc outside this
|
|
252
|
+
repo -- hand-typing `.org` at all runs against the "author in Ruby" advice
|
|
253
|
+
just above, but the parser should fail with a clearer message regardless.
|
|
254
|
+
|
|
255
|
+
## Caveat: the org writer's verbatim passthrough degrades
|
|
256
|
+
|
|
257
|
+
`Org::Writer` has an escape hatch for components it does not know how to
|
|
258
|
+
represent: it emits the component's **original source text** inside a
|
|
259
|
+
`#+begin_src ruby :streamweaver-raw t` block. That hatch depends on a 1:1
|
|
260
|
+
correspondence between top-level statements in the DSL text and components in
|
|
261
|
+
the tree, and it is switched off wholesale when the counts disagree --
|
|
262
|
+
`return {} unless statements.length == components.length`
|
|
263
|
+
(`lib/stream_weaver/org/writer.rb:206`).
|
|
264
|
+
|
|
265
|
+
A fragment loader is three top-level statements that produce zero components,
|
|
266
|
+
so the counts always disagree. Measured on a doc containing one unrecognized
|
|
267
|
+
component (`header1`):
|
|
268
|
+
|
|
269
|
+
| Doc | Coverage |
|
|
270
|
+
|---|---|
|
|
271
|
+
| without a fragment loader | `passthrough_verbatim: 1, passthrough_lossy: 0`, org contains `header1 "Unrecognized Thing"` |
|
|
272
|
+
| with a fragment loader | `passthrough_verbatim: 0, passthrough_lossy: 1`, org contains `# unrecognized component: StreamWeaver::Components::Header` |
|
|
273
|
+
|
|
274
|
+
Recognized components (`md`, `table`, `callout`, `card`, `comparison`,
|
|
275
|
+
`mermaid`, `code_block`, plus `doc_header`/`doc_section_header`/`sidebar_toc`)
|
|
276
|
+
are entirely unaffected -- which is most doc-theme docs, including both worked
|
|
277
|
+
examples below. The rule of thumb: a fragment-loading doc should stick to the
|
|
278
|
+
recognized vocabulary if its org sibling matters.
|
|
279
|
+
|
|
280
|
+
`NO_OP_STATEMENT_RE` (`lib/stream_weaver/org/writer.rb:30`) already exists to
|
|
281
|
+
excuse exactly this kind of component-less statement, but it only matches
|
|
282
|
+
`use_theme` / `use_layout`. It is not extensible from a doc.
|
|
283
|
+
|
|
284
|
+
## Proposal: a first-class `dsl_fragment` helper
|
|
285
|
+
|
|
286
|
+
Not implemented. This is a sketch for Forrest to accept or reject.
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
doc_section_header "01", "Agreed Base", id: "agreed-base"
|
|
290
|
+
dsl_fragment "shared/pm_discount_shared.rb"
|
|
291
|
+
pm_base_rules_table
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Where it lives:** `StreamWeaver::DisplayDSL`
|
|
295
|
+
(`lib/stream_weaver/display_dsl.rb`). That is the one module included by all
|
|
296
|
+
three evaluation contexts -- `App` (`lib/stream_weaver/app.rb:10`),
|
|
297
|
+
`Org::RecordingContext` (`lib/stream_weaver/org/recording_context.rb:28`), and
|
|
298
|
+
therefore the canvas/bridge mini-`App` too. Anywhere else and it exists in some
|
|
299
|
+
modes and not others.
|
|
300
|
+
|
|
301
|
+
**Why it cannot just call `__dir__`:** inside a method defined in the gem,
|
|
302
|
+
`__dir__` is the gem's own directory. The helper must resolve against a base
|
|
303
|
+
directory supplied by whoever did the eval. So the proposal is really two
|
|
304
|
+
things: a `doc_base_dir` accessor on the DSL, and a `dsl_fragment` method that
|
|
305
|
+
resolves against it (falling back to `Dir.pwd`, and raising with the attempted
|
|
306
|
+
path when the file is missing).
|
|
307
|
+
|
|
308
|
+
Cases it must cover, one per row of the evaluation table above:
|
|
309
|
+
|
|
310
|
+
| Mode | What sets `doc_base_dir` | Change needed |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| `streamweaver export` | `HtmlExporter.from_dsl` already computes `File.dirname(File.expand_path(path))` for its own `base_dir:` | pass the same value to the app before `instance_eval` (`html_exporter.rb:104-106`) |
|
|
313
|
+
| `canvas-read` | `render_doc` already takes `path:` | set it on `mini_app` before eval (`canvas/reader.rb:732`) |
|
|
314
|
+
| `org-export` | `CLI.org_export` knows `rb_path` but throws it away | thread it into `Org::Writer.from_dsl(text, base_dir:)` and on to `RecordingContext` (`cli.rb:1479`, `org/writer.rb:35`) |
|
|
315
|
+
| standalone wrapper | the author knows the path | the existing `instance_eval(File.read(p), p)` idiom, plus an explicit setter |
|
|
316
|
+
| `canvas-push` | **nothing can** -- stdin has no path, and the bridge is a different process | see below |
|
|
317
|
+
|
|
318
|
+
**canvas-push is the load-bearing case.** Two candidate answers:
|
|
319
|
+
|
|
320
|
+
- *Textual pre-resolution on the pushing side*, following the exact precedent
|
|
321
|
+
of `CLI.prepend_stylesheets` (`cli.rb:1215-1228`): give `canvas-push` an
|
|
322
|
+
optional `--file PATH` (or a path argument in place of stdin), and have the
|
|
323
|
+
CLI expand each `dsl_fragment "..."` line into the fragment's literal text
|
|
324
|
+
before the push. The bridge then never needs a base dir, and the pushed text
|
|
325
|
+
is self-contained -- which also fixes the history snapshot
|
|
326
|
+
(`CLI.record_push_history`), currently saving a doc that cannot re-render
|
|
327
|
+
because its fragment reference is unresolvable.
|
|
328
|
+
- *Plumb `session.source_dir` through.* The bridge already has it
|
|
329
|
+
(`canvas/session.rb:11`, `canvas/bridge.rb:136`). But it is the git root, not
|
|
330
|
+
the doc's directory, so fragment paths would have to be repo-root-relative --
|
|
331
|
+
a second, inconsistent resolution rule. Not recommended.
|
|
332
|
+
|
|
333
|
+
The first option is the better one and it subsumes the concatenation workaround
|
|
334
|
+
this doc currently recommends.
|
|
335
|
+
|
|
336
|
+
**One more change comes with it:** add `dsl_fragment` to
|
|
337
|
+
`Org::Writer::NO_OP_STATEMENT_RE` (`org/writer.rb:30`), so a fragment-loading
|
|
338
|
+
doc keeps verbatim raw passthrough instead of degrading to lossy. That fix is
|
|
339
|
+
only available to a first-class helper -- a hand-rolled loader cannot buy it --
|
|
340
|
+
and on its own it is a decent argument for building the helper.
|
|
341
|
+
|
|
342
|
+
## Worked example
|
|
343
|
+
|
|
344
|
+
Live in the `billing_engine` repo, two audiences, one canonical pair of tables.
|
|
345
|
+
Paths are relative to that repo's root:
|
|
346
|
+
|
|
347
|
+
- Fragment: `docs/streamweaver_canvas/shared/pm_discount_shared.rb` -- defines
|
|
348
|
+
`pm_base_rules_table` and `pm_policy_table`.
|
|
349
|
+
- Product/decision audience:
|
|
350
|
+
`docs/streamweaver_canvas/pm-discount-decision-memo.rb`
|
|
351
|
+
- Engineering audience:
|
|
352
|
+
`docs/streamweaver_canvas/pm-discount-engineering-brief.rb`
|
|
353
|
+
|
|
354
|
+
The seven base rules and the three renewal policies appear in both documents,
|
|
355
|
+
framed completely differently around them, and exist in exactly one place on
|
|
356
|
+
disk.
|