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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Org
|
|
7
|
+
# Splits DSL source text into its top-level statements, each as a
|
|
8
|
+
# literal source-text slice, using Ripper for grammar-accurate
|
|
9
|
+
# boundaries -- correctly distinguishes a modifier-if ("md \"x\" if
|
|
10
|
+
# flag", one statement) from a block-if, and isn't confused by heredoc
|
|
11
|
+
# bodies or nested do..end blocks the way a naive newline/bracket-depth
|
|
12
|
+
# split would be.
|
|
13
|
+
#
|
|
14
|
+
# Used by Writer to recover a genuinely verbatim source span for the
|
|
15
|
+
# raw-passthrough escape hatch (see Writer#raw_passthrough), when the
|
|
16
|
+
# top-level statement <-> component correspondence is unambiguous.
|
|
17
|
+
module SourceSplitter
|
|
18
|
+
# Returns an array of source-text strings, one per top-level
|
|
19
|
+
# statement, in document order. Returns nil if `source` doesn't parse
|
|
20
|
+
# (callers should already have proven it parses -- Writer only calls
|
|
21
|
+
# this after RecordingContext's own instance_eval succeeded -- so this
|
|
22
|
+
# is a defensive fallback, not an expected path).
|
|
23
|
+
def self.top_level_statements(source)
|
|
24
|
+
sexp = Ripper.sexp(source)
|
|
25
|
+
return nil unless sexp
|
|
26
|
+
|
|
27
|
+
# Empty (or whitespace/comment-only) source parses to a single
|
|
28
|
+
# synthetic :void_stmt node with no position of its own.
|
|
29
|
+
stmts = sexp[1]
|
|
30
|
+
return [] if stmts == [[:void_stmt]] || stmts.empty?
|
|
31
|
+
|
|
32
|
+
lines = source.lines
|
|
33
|
+
starts = stmts.map { |s| start_line(s) }
|
|
34
|
+
starts.each_with_index.map do |line, i|
|
|
35
|
+
end_line = i + 1 < starts.length ? starts[i + 1] - 1 : lines.length
|
|
36
|
+
span = lines[(line - 1)...end_line]
|
|
37
|
+
span.pop while span.last&.strip == ""
|
|
38
|
+
span.join.chomp
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Recursively finds the line number of the leftmost terminal token in
|
|
43
|
+
# a Ripper sexp subtree (as returned by Ripper.sexp, which -- unlike
|
|
44
|
+
# bare Ripper::SexpBuilder -- both reliably signals a syntax error
|
|
45
|
+
# (nil) and flattens :program's body into a plain array). Terminal
|
|
46
|
+
# (scanner-event) nodes are 3-element arrays whose last element is a
|
|
47
|
+
# [lineno, column] pair; composite (parser-event) nodes have no
|
|
48
|
+
# position of their own -- the position comes from the earliest
|
|
49
|
+
# terminal nested inside.
|
|
50
|
+
def self.start_line(node, best = nil)
|
|
51
|
+
return best unless node.is_a?(Array)
|
|
52
|
+
|
|
53
|
+
if node.length == 3 && node[2].is_a?(Array) && node[2].length == 2 && node[2].all? { |x| x.is_a?(Integer) }
|
|
54
|
+
pos = node[2][0]
|
|
55
|
+
best = pos if best.nil? || pos < best
|
|
56
|
+
end
|
|
57
|
+
node.each { |n| best = start_line(n, best) }
|
|
58
|
+
best
|
|
59
|
+
end
|
|
60
|
+
private_class_method :start_line
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "recording_context"
|
|
4
|
+
require_relative "inline"
|
|
5
|
+
require_relative "source_splitter"
|
|
6
|
+
|
|
7
|
+
module StreamWeaver
|
|
8
|
+
module Org
|
|
9
|
+
# Converts a StreamWeaver doc-builder DSL body (the text canvas-read
|
|
10
|
+
# instance_evals) into StreamWeaver-flavored org-mode text. See
|
|
11
|
+
# docs/superpowers/specs/2026-08-13-org-doc-format-design.md for the
|
|
12
|
+
# full format spec this implements.
|
|
13
|
+
class Writer
|
|
14
|
+
def self.from_dsl(dsl_text)
|
|
15
|
+
new(dsl_text).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(dsl_text)
|
|
19
|
+
@dsl_text = dsl_text
|
|
20
|
+
@coverage_counts = { total: 0, passthrough_verbatim: 0, passthrough_lossy: 0, omitted: 0 }
|
|
21
|
+
@called = false
|
|
22
|
+
@nesting_depth = 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Top-level DSL statements RecordingContext deliberately no-ops (see
|
|
26
|
+
# its class comment) -- these produce zero components, so they'd
|
|
27
|
+
# otherwise break the 1:1 statement<->component correspondence
|
|
28
|
+
# #build_raw_sources relies on. Every real saved doc has at least one
|
|
29
|
+
# of these prepended by DocStore.dsl_with_metadata.
|
|
30
|
+
NO_OP_STATEMENT_RE = /\Ause_(?:theme|layout)\b/
|
|
31
|
+
|
|
32
|
+
# A saved .org is a static document and cannot hold a live control, so
|
|
33
|
+
# these are DROPPED rather than passed through as dead source (see
|
|
34
|
+
# #omitted_control). Two shapes reach that decision: a real component
|
|
35
|
+
# DisplayDSL knows how to build (button, select), and an
|
|
36
|
+
# UnsupportedCall placeholder for App's form vocabulary, which
|
|
37
|
+
# RecordingContext has no implementation for at all. Both are answered
|
|
38
|
+
# here, in the one place, so "is X a live control?" has a single
|
|
39
|
+
# source of truth.
|
|
40
|
+
OMITTED_COMPONENTS = [Components::Button, Components::Select].freeze
|
|
41
|
+
INTERACTIVE_CALLS = %i[
|
|
42
|
+
form form_for submit cancel submit_label cancel_label validate
|
|
43
|
+
text_field text_area date_field code_editor
|
|
44
|
+
checkbox checkbox_group radio_group tag_buttons chip_group item
|
|
45
|
+
clickable model_selector confirmation_bar
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
def call
|
|
49
|
+
ctx = RecordingContext.new
|
|
50
|
+
ctx.instance_eval(@dsl_text)
|
|
51
|
+
components = ctx.components
|
|
52
|
+
@raw_sources = build_raw_sources(components)
|
|
53
|
+
|
|
54
|
+
toc_sections = components.find { |c| c.is_a?(Components::SidebarToc) }&.sections || []
|
|
55
|
+
toc_by_id = toc_sections.each_with_object({}) { |s, h| h[s[:id]] = s[:label] }
|
|
56
|
+
|
|
57
|
+
header = components.find { |c| c.is_a?(Components::DocHeader) }
|
|
58
|
+
body_components = components.reject { |c| c.is_a?(Components::SidebarToc) || c.is_a?(Components::DocHeader) }
|
|
59
|
+
|
|
60
|
+
out = [preamble(header), sections_and_body(body_components, toc_by_id)].join("\n").rstrip + "\n"
|
|
61
|
+
@called = true
|
|
62
|
+
out
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Accounting for the single conversion pass #call just ran, one bucket
|
|
66
|
+
# per escape hatch: `passthrough_verbatim`/`passthrough_lossy` (raw
|
|
67
|
+
# source kept, styled or not) and `omitted` (interactive controls
|
|
68
|
+
# dropped on purpose -- the .org reader gets a document with the form
|
|
69
|
+
# missing, and the save UI says so). `recognized` is DERIVED from the
|
|
70
|
+
# rest rather than tracked, so the numbers cannot disagree by
|
|
71
|
+
# construction. `total` only reflects body_components (everything
|
|
72
|
+
# sections_and_body walks) -- DocHeader and SidebarToc are metadata,
|
|
73
|
+
# filtered out before that walk starts (see #call), and never
|
|
74
|
+
# contribute to any of these five numbers.
|
|
75
|
+
def coverage
|
|
76
|
+
raise "call #call before #coverage" unless @called
|
|
77
|
+
|
|
78
|
+
escaped = @coverage_counts.values_at(:passthrough_verbatim, :passthrough_lossy, :omitted).sum
|
|
79
|
+
@coverage_counts.merge(recognized: @coverage_counts[:total] - escaped)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def preamble(header)
|
|
85
|
+
return "#+STREAMWEAVER_DSL: 1\n" unless header
|
|
86
|
+
|
|
87
|
+
out = +"#+STREAMWEAVER_DSL: 1\n#+TITLE: #{header.title}\n"
|
|
88
|
+
if header.eyebrow || (header.pills && !header.pills.empty?)
|
|
89
|
+
out << "\n#+begin_quote\n"
|
|
90
|
+
out << "/#{header.eyebrow}/\n" if header.eyebrow
|
|
91
|
+
if header.pills && !header.pills.empty?
|
|
92
|
+
out << header.pills.map { |p| render_pill(p) }.join(" · ") + "\n"
|
|
93
|
+
end
|
|
94
|
+
out << "#+end_quote\n"
|
|
95
|
+
end
|
|
96
|
+
out
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_pill(pill)
|
|
100
|
+
return pill unless pill.is_a?(Hash)
|
|
101
|
+
variant = pill[:variant] || pill["variant"]
|
|
102
|
+
text = pill[:text] || pill["text"]
|
|
103
|
+
variant ? "*[#{variant}] #{text}*" : text
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sections_and_body(components, toc_by_id)
|
|
107
|
+
out = +""
|
|
108
|
+
components.each do |c|
|
|
109
|
+
count(:total)
|
|
110
|
+
out << if c.is_a?(Components::DocSectionHeader)
|
|
111
|
+
section_headline(c, toc_by_id)
|
|
112
|
+
else
|
|
113
|
+
render_component(c)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
out
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def section_headline(section, toc_by_id)
|
|
120
|
+
in_toc = toc_by_id.key?(section.anchor_id)
|
|
121
|
+
depth = in_toc ? "*" : "**"
|
|
122
|
+
headline = "\n#{depth} #{section.number} #{section.title}\n:PROPERTIES:\n:CUSTOM_ID: #{section.anchor_id}\n"
|
|
123
|
+
toc_label = toc_by_id[section.anchor_id]
|
|
124
|
+
headline << ":TOC_LABEL: #{toc_label}\n" if in_toc && toc_label != section.title
|
|
125
|
+
headline << ":END:\n"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# WARNING for future editors: #count (below) only counts at
|
|
129
|
+
# @nesting_depth zero, and only render_quote's recursive call currently
|
|
130
|
+
# increments that depth. If you add ANOTHER path that calls
|
|
131
|
+
# render_component recursively (not via render_quote), wrap it in the
|
|
132
|
+
# same @nesting_depth += 1 / ensure @nesting_depth -= 1 pattern --
|
|
133
|
+
# otherwise a nested escape hatch there is silently counted as
|
|
134
|
+
# top-level, reintroducing the negative-`recognized` bug the guard
|
|
135
|
+
# exists to prevent (see #count for the story).
|
|
136
|
+
def render_component(component)
|
|
137
|
+
case component
|
|
138
|
+
when Components::Markdown
|
|
139
|
+
"\n#{Inline.md_to_org(component.content)}\n"
|
|
140
|
+
when Components::Table
|
|
141
|
+
render_table(component)
|
|
142
|
+
when Components::Callout
|
|
143
|
+
marker = component.title ? "*#{component.icon} #{component.title}*" : "*#{component.icon}*"
|
|
144
|
+
render_quote(marker, component.children)
|
|
145
|
+
when Components::Card
|
|
146
|
+
render_card(component)
|
|
147
|
+
when Components::Comparison
|
|
148
|
+
render_comparison(component)
|
|
149
|
+
when Components::Mermaid
|
|
150
|
+
header = component.zoom ? "mermaid :zoom t" : "mermaid"
|
|
151
|
+
"\n#+begin_src #{header}\n#{component.code.to_s.rstrip}\n#+end_src\n"
|
|
152
|
+
when Components::CodeBlock
|
|
153
|
+
"\n#+begin_src #{component.lang}\n#{component.code.to_s.rstrip}\n#+end_src\n"
|
|
154
|
+
else
|
|
155
|
+
live_control?(component) ? omitted_control(component) : raw_passthrough(component)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def live_control?(component)
|
|
160
|
+
return INTERACTIVE_CALLS.include?(component.name) if component.is_a?(RecordingContext::UnsupportedCall)
|
|
161
|
+
|
|
162
|
+
OMITTED_COMPONENTS.any? { |klass| component.is_a?(klass) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The third escape hatch, for live controls (see OMITTED_COMPONENTS /
|
|
166
|
+
# INTERACTIVE_CALLS). Raw-passthrough would "preserve" them as dead
|
|
167
|
+
# source in a document that has no server behind it; a static org doc
|
|
168
|
+
# simply cannot hold a form. So they are DROPPED from the body, marked
|
|
169
|
+
# by an org keyword line -- invisible in any org viewer and already
|
|
170
|
+
# ignored by Org::Reader's PREAMBLE_RE, so the breadcrumb never
|
|
171
|
+
# reappears as prose on the way back -- and counted separately in
|
|
172
|
+
# #coverage so the Save-as-doc UI can tell the user what the .org left
|
|
173
|
+
# behind. The .rb sibling is untouched by any of this and remains the
|
|
174
|
+
# full-fidelity format.
|
|
175
|
+
def omitted_control(component)
|
|
176
|
+
count(:omitted)
|
|
177
|
+
"\n#+STREAMWEAVER_OMITTED: #{control_name(component)}\n"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def control_name(component)
|
|
181
|
+
return component.name if component.is_a?(RecordingContext::UnsupportedCall)
|
|
182
|
+
|
|
183
|
+
component.class.name.split("::").last.gsub(/(?<!\A)([A-Z])/, '_\1').downcase
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def render_table(table)
|
|
187
|
+
# table.headers/#rows are the raw constructor args, nil for any form
|
|
188
|
+
# other than table(headers:, rows:) -- e.g. table(data:) or the
|
|
189
|
+
# column DSL, which resolve their actual content lazily elsewhere
|
|
190
|
+
# and are out of scope for this format (see the design spec's
|
|
191
|
+
# Tables section). Falling through to the normal empty-array
|
|
192
|
+
# default here would silently emit a garbage 1-empty-column table
|
|
193
|
+
# with all row data gone; raw-passthrough matches the same
|
|
194
|
+
# unrecognized-shape convention used elsewhere in this file.
|
|
195
|
+
return raw_passthrough(table) if table.headers.nil?
|
|
196
|
+
|
|
197
|
+
headers = table.headers
|
|
198
|
+
rows = table.rows || []
|
|
199
|
+
convert = ->(s) { table.markdown ? Inline.md_to_org(s.to_s) : s.to_s }
|
|
200
|
+
widths = headers.each_index.map { |i| ([headers[i]] + rows.map { |r| r[i] }).map { |s| convert.call(s).length }.max }
|
|
201
|
+
out = +"\n"
|
|
202
|
+
out << "#+ATTR_STREAMWEAVER: :markdown nil\n" unless table.markdown
|
|
203
|
+
out << row_line(headers.map { |h| convert.call(h) }, widths)
|
|
204
|
+
out << "|" + widths.map { |w| "-" * (w + 2) }.join("|") + "|\n"
|
|
205
|
+
rows.each { |r| out << row_line(r.map { |c| convert.call(c) }, widths) }
|
|
206
|
+
out
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def row_line(cells, widths)
|
|
210
|
+
"| " + cells.each_with_index.map { |c, i| c.ljust(widths[i]) }.join(" | ") + " |\n"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def render_card(card)
|
|
214
|
+
header = card.children.find { |c| c.is_a?(Components::CardHeader) }
|
|
215
|
+
# A card with no CardHeader (loose children directly under card
|
|
216
|
+
# do...end -- a real pattern elsewhere in this codebase, e.g. admin
|
|
217
|
+
# dashboard stat tiles) has no natural title for the marker-line
|
|
218
|
+
# convention and isn't part of the doc-builder vocabulary this
|
|
219
|
+
# format targets. Fall back to the same raw-passthrough convention
|
|
220
|
+
# render_component's unrecognized-component branch uses, rather
|
|
221
|
+
# than crash or silently drop the content.
|
|
222
|
+
return raw_passthrough(card) unless header
|
|
223
|
+
|
|
224
|
+
body = card.children.find { |c| c.is_a?(Components::CardBody) }
|
|
225
|
+
marker = header.badge ? "*[#{header.badge}] #{header.content}*" : "*#{header.content}*"
|
|
226
|
+
marker << " /(#{header.meta})/" if header.meta
|
|
227
|
+
render_quote(marker, body ? body.children : [])
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Best-effort verbatim-source recovery for the raw-passthrough escape
|
|
231
|
+
# hatch: maps each top-level component to the literal DSL source text
|
|
232
|
+
# of the top-level statement that produced it, ONLY when that
|
|
233
|
+
# correspondence is unambiguous (exactly one top-level statement per
|
|
234
|
+
# top-level component, after filtering out known no-ops -- the common
|
|
235
|
+
# case for the flat DSL body this format targets). Falls back to an
|
|
236
|
+
# empty map (raw_passthrough's comment-only placeholder) rather than
|
|
237
|
+
# guessing when a doc uses top-level control flow (loops,
|
|
238
|
+
# conditionals producing zero-or-many components per statement) that
|
|
239
|
+
# breaks the 1:1 assumption -- silent-but-safe beats attributing the
|
|
240
|
+
# wrong source to a component.
|
|
241
|
+
def build_raw_sources(components)
|
|
242
|
+
statements = SourceSplitter.top_level_statements(@dsl_text)
|
|
243
|
+
return {} unless statements
|
|
244
|
+
|
|
245
|
+
statements = statements.reject { |s| s.strip.match?(NO_OP_STATEMENT_RE) }
|
|
246
|
+
return {} unless statements.length == components.length
|
|
247
|
+
|
|
248
|
+
components.each_with_index.to_h { |c, i| [c.object_id, statements[i]] }
|
|
249
|
+
rescue StandardError
|
|
250
|
+
{}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Coverage only counts TOP-LEVEL statements (see #coverage) -- but the
|
|
254
|
+
# escape hatches are also reachable for a component nested inside an
|
|
255
|
+
# already-recognized container (e.g. an unrecognized type inside a
|
|
256
|
+
# callout/card/comparison body, via render_quote's recursive
|
|
257
|
+
# render_component calls below). Counting one of those would increment
|
|
258
|
+
# passthrough_verbatim/lossy/omitted with no matching #total increment
|
|
259
|
+
# (only sections_and_body's top-level loop increments #total), making
|
|
260
|
+
# recognized go negative. @nesting_depth (incremented/decremented
|
|
261
|
+
# around render_quote's recursive walk) tracks whether we're inside
|
|
262
|
+
# such a nested render, and every hatch counts through here.
|
|
263
|
+
def count(bucket)
|
|
264
|
+
@coverage_counts[bucket] += 1 if @nesting_depth.zero?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def raw_passthrough(component)
|
|
268
|
+
source = @raw_sources[component.object_id]
|
|
269
|
+
count(source ? :passthrough_verbatim : :passthrough_lossy)
|
|
270
|
+
content = source || "# unrecognized component: #{unrecognized_label(component)}"
|
|
271
|
+
"\n#+begin_src ruby :streamweaver-raw t\n#{content.rstrip}\n#+end_src\n"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The placeholder names what the DOC said, not how this class modelled
|
|
275
|
+
# it: for a call RecordingContext had no component for, the struct's
|
|
276
|
+
# own class name ("...::UnsupportedCall") tells the reader nothing,
|
|
277
|
+
# while the call it stands in for (`app_shell`, `bar_chart`) is exactly
|
|
278
|
+
# what was lost.
|
|
279
|
+
def unrecognized_label(component)
|
|
280
|
+
return "#{component.name} (unsupported call)" if component.is_a?(RecordingContext::UnsupportedCall)
|
|
281
|
+
|
|
282
|
+
component.class
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def render_comparison(comparison)
|
|
286
|
+
before = render_quote("*◀ Before — #{comparison.before_label}*", comparison.before_children)
|
|
287
|
+
after = render_quote("*▶ After — #{comparison.after_label}*", comparison.after_children)
|
|
288
|
+
"\n" + before.strip + "\n" + after.strip + "\n"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def render_quote(marker, children)
|
|
292
|
+
@nesting_depth += 1
|
|
293
|
+
body = children.map { |c| render_component(c) }.join.strip
|
|
294
|
+
"\n#+begin_quote\n#{marker}\n#{body}\n#+end_quote\n"
|
|
295
|
+
ensure
|
|
296
|
+
@nesting_depth -= 1
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|