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,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
|
|
8
|
+
module StreamWeaver
|
|
9
|
+
module Canvas
|
|
10
|
+
# Persistent record of gists StreamWeaver has published on the user's
|
|
11
|
+
# behalf, so a later session can find "the gist for this doc" without
|
|
12
|
+
# keeping any in-memory session state.
|
|
13
|
+
#
|
|
14
|
+
# Unlike DocStore (one file per doc, resolved via a 3-tier git-aware
|
|
15
|
+
# path), GistStore is a single flat JSON file holding every recorded
|
|
16
|
+
# gist, keyed by doc base name. There is no git-root behavior here --
|
|
17
|
+
# gist publication has no repo-relative meaning, so the path is just
|
|
18
|
+
# ENV override or DEFAULT_PATH.
|
|
19
|
+
#
|
|
20
|
+
# This is an index, not a source of truth -- every entry in it can be
|
|
21
|
+
# rebuilt by re-publishing. Two deliberate concessions follow from that:
|
|
22
|
+
# a corrupt or empty file degrades to empty rather than raising
|
|
23
|
+
# (raising would also break the calls you'd use to repair it), and two
|
|
24
|
+
# processes recording at once is last-writer-wins -- the atomic write
|
|
25
|
+
# below buys readers a whole file, not writers a serialized one.
|
|
26
|
+
#
|
|
27
|
+
# Resolved path priority:
|
|
28
|
+
# 1. ENV['STREAMWEAVER_GIST_STORE'] (test/override hook)
|
|
29
|
+
# 2. ~/.streamweaver/canvas/gists.json
|
|
30
|
+
module GistStore
|
|
31
|
+
DEFAULT_PATH = File.expand_path('~/.streamweaver/canvas/gists.json')
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# Resolves the store file path. Read fresh on every call (not memoized)
|
|
36
|
+
# so an ENV override flips behavior mid-process -- specs rely on this,
|
|
37
|
+
# same rationale as DocStore#path.
|
|
38
|
+
def path
|
|
39
|
+
env = ENV['STREAMWEAVER_GIST_STORE']
|
|
40
|
+
return env if env && !env.empty?
|
|
41
|
+
|
|
42
|
+
DEFAULT_PATH
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the full store: name => {"id" =>, "url" =>, "revisions" =>,
|
|
46
|
+
# "created_at" =>, "updated_at" =>}. Empty hash for a missing, empty,
|
|
47
|
+
# or corrupt file -- see the module comment for why this degrades
|
|
48
|
+
# instead of raising.
|
|
49
|
+
def all
|
|
50
|
+
file = path
|
|
51
|
+
return {} unless File.exist?(file)
|
|
52
|
+
|
|
53
|
+
content = File.read(file)
|
|
54
|
+
return {} if content.strip.empty?
|
|
55
|
+
|
|
56
|
+
JSON.parse(content)
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
{}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the entry for `name`, or nil if it has never been recorded.
|
|
62
|
+
def lookup(name)
|
|
63
|
+
all[name.to_s]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Upserts the entry for `name`: id/url/revisions are overwritten,
|
|
67
|
+
# created_at is set once and preserved across later calls, updated_at
|
|
68
|
+
# always advances to now. Returns the resulting entry hash.
|
|
69
|
+
def record(name, id:, url:, revisions:)
|
|
70
|
+
name = name.to_s
|
|
71
|
+
store = all
|
|
72
|
+
now = timestamp
|
|
73
|
+
|
|
74
|
+
entry = {
|
|
75
|
+
'id' => id,
|
|
76
|
+
'url' => url,
|
|
77
|
+
'revisions' => revisions,
|
|
78
|
+
'created_at' => store.dig(name, 'created_at') || now,
|
|
79
|
+
'updated_at' => now
|
|
80
|
+
}
|
|
81
|
+
store[name] = entry
|
|
82
|
+
persist(store)
|
|
83
|
+
entry
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Among recorded names starting with `prefix`, returns the entry with
|
|
87
|
+
# the most recent updated_at -- how a caller with only a doc-name
|
|
88
|
+
# prefix (e.g. a canvas session name) finds "the gist for this
|
|
89
|
+
# session" with no session state of its own. nil if nothing matches.
|
|
90
|
+
def latest_for_prefix(prefix)
|
|
91
|
+
prefix = prefix.to_s
|
|
92
|
+
match = all.select { |name, _| name.start_with?(prefix) }.max_by { |_, entry| entry['updated_at'] }
|
|
93
|
+
match&.last
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Removes the entry for `name` if present. No-op if it was never
|
|
97
|
+
# recorded.
|
|
98
|
+
def forget(name)
|
|
99
|
+
store = all
|
|
100
|
+
persist(store) if store.delete(name.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Millisecond precision, not the 1-second default: two gists published
|
|
104
|
+
# within the same second are ordinary, and latest_for_prefix has to
|
|
105
|
+
# break that tie correctly rather than by hash insertion order.
|
|
106
|
+
def timestamp
|
|
107
|
+
Time.now.utc.iso8601(3)
|
|
108
|
+
end
|
|
109
|
+
private_class_method :timestamp
|
|
110
|
+
|
|
111
|
+
# Writes `store` to `path` atomically: JSON goes to a temp file in the
|
|
112
|
+
# SAME directory (so the rename is on one filesystem, where POSIX
|
|
113
|
+
# guarantees it is atomic) and is then renamed over the target. A plain
|
|
114
|
+
# File.write truncates first, which would let a concurrent reader
|
|
115
|
+
# observe an empty or half-written file; the temp file is removed if
|
|
116
|
+
# the write or rename fails so a failure never leaves stray files
|
|
117
|
+
# behind.
|
|
118
|
+
def persist(store)
|
|
119
|
+
file = path
|
|
120
|
+
dir = File.dirname(file)
|
|
121
|
+
FileUtils.mkdir_p(dir)
|
|
122
|
+
|
|
123
|
+
tmp = File.join(dir, ".#{File.basename(file)}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp")
|
|
124
|
+
begin
|
|
125
|
+
File.write(tmp, JSON.generate(store))
|
|
126
|
+
File.rename(tmp, file)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
FileUtils.rm_f(tmp)
|
|
129
|
+
raise
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
private_class_method :persist
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Canvas
|
|
5
|
+
# High-level helper methods for common canvas patterns.
|
|
6
|
+
# Generates DSL code and parses results for pick, confirm, and form operations.
|
|
7
|
+
module Helpers
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Generate DSL for a single-choice picker
|
|
11
|
+
# @param title [String] The title/prompt
|
|
12
|
+
# @param choices [Array<String>] Available choices
|
|
13
|
+
# @return [String] DSL code
|
|
14
|
+
def pick_dsl(title, choices)
|
|
15
|
+
choices_str = choices.map { |c| "\"#{c}\"" }.join(", ")
|
|
16
|
+
|
|
17
|
+
<<~DSL
|
|
18
|
+
header1 "#{title}"
|
|
19
|
+
radio_group :choice, [#{choices_str}]
|
|
20
|
+
button "Select", id: "btn_submit"
|
|
21
|
+
DSL
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generate DSL for a confirmation dialog
|
|
25
|
+
# @param message [String] The confirmation message
|
|
26
|
+
# @param yes_label [String] Label for confirm button (default: "Confirm")
|
|
27
|
+
# @param no_label [String] Label for cancel button (default: "Cancel")
|
|
28
|
+
# @return [String] DSL code
|
|
29
|
+
def confirm_dsl(message, yes_label: "Confirm", no_label: "Cancel")
|
|
30
|
+
<<~DSL
|
|
31
|
+
header1 "#{message}"
|
|
32
|
+
hstack do
|
|
33
|
+
button "#{no_label}", id: "btn_cancel", style: :secondary
|
|
34
|
+
button "#{yes_label}", id: "btn_confirm"
|
|
35
|
+
end
|
|
36
|
+
DSL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate DSL for a form with specified fields
|
|
40
|
+
# @param title [String] The form title
|
|
41
|
+
# @param fields [Hash] Field definitions { name: { type: :text, ... }, ... }
|
|
42
|
+
# @return [String] DSL code
|
|
43
|
+
def form_dsl(title, fields)
|
|
44
|
+
field_dsl = fields.map do |name, config|
|
|
45
|
+
case config[:type]
|
|
46
|
+
when :text
|
|
47
|
+
placeholder = config[:placeholder] ? ", placeholder: \"#{config[:placeholder]}\"" : ""
|
|
48
|
+
"text_field :#{name}#{placeholder}"
|
|
49
|
+
when :textarea
|
|
50
|
+
placeholder = config[:placeholder] ? ", placeholder: \"#{config[:placeholder]}\"" : ""
|
|
51
|
+
rows = config[:rows] ? ", rows: #{config[:rows]}" : ""
|
|
52
|
+
"text_area :#{name}#{placeholder}#{rows}"
|
|
53
|
+
when :radio
|
|
54
|
+
choices_str = config[:choices].map { |c| "\"#{c}\"" }.join(", ")
|
|
55
|
+
"radio_group :#{name}, [#{choices_str}]"
|
|
56
|
+
when :select
|
|
57
|
+
choices_str = config[:choices].map { |c| "\"#{c}\"" }.join(", ")
|
|
58
|
+
"select :#{name}, [#{choices_str}]"
|
|
59
|
+
when :checkbox
|
|
60
|
+
label = config[:label] || name.to_s.capitalize
|
|
61
|
+
"checkbox :#{name}, \"#{label}\""
|
|
62
|
+
else
|
|
63
|
+
"text_field :#{name}"
|
|
64
|
+
end
|
|
65
|
+
end.join("\n")
|
|
66
|
+
|
|
67
|
+
<<~DSL
|
|
68
|
+
header1 "#{title}"
|
|
69
|
+
#{field_dsl}
|
|
70
|
+
button "Submit", id: "btn_submit"
|
|
71
|
+
DSL
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Parse pick result from event data
|
|
75
|
+
# @param event_data [Hash] Event data from browser
|
|
76
|
+
# @return [String, nil] Selected choice
|
|
77
|
+
def parse_pick_result(event_data)
|
|
78
|
+
# Choice is nested in state from the browser event
|
|
79
|
+
state = event_data[:state] || event_data["state"] || {}
|
|
80
|
+
state[:choice] || state["choice"] || event_data[:choice] || event_data["choice"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parse confirm result from event data
|
|
84
|
+
# @param event_data [Hash] Event data from browser
|
|
85
|
+
# @return [Boolean] true if confirmed, false if cancelled
|
|
86
|
+
def parse_confirm_result(event_data)
|
|
87
|
+
# Button ID is in :button from the browser event
|
|
88
|
+
button = event_data[:button] || event_data["button"] ||
|
|
89
|
+
event_data[:_button] || event_data["_button"] || ""
|
|
90
|
+
button.to_s.include?("confirm")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Parse form result from event data
|
|
94
|
+
# @param event_data [Hash] Event data from browser
|
|
95
|
+
# @param field_names [Array<Symbol>] Expected field names
|
|
96
|
+
# @return [Hash] Form field values
|
|
97
|
+
def parse_form_result(event_data, field_names)
|
|
98
|
+
# Form values are in state from the browser event
|
|
99
|
+
state = event_data[:state] || event_data["state"] || event_data
|
|
100
|
+
result = {}
|
|
101
|
+
field_names.each do |name|
|
|
102
|
+
key = name.to_sym
|
|
103
|
+
result[key] = state[key] || state[name.to_s]
|
|
104
|
+
end
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Canvas
|
|
7
|
+
# Tier 1 (off-repo) history of canvas-push DSL snapshots.
|
|
8
|
+
#
|
|
9
|
+
# Snapshots live at <root>/<session>/<YYYYMMDD_HHMMSS>.rb where <root> is
|
|
10
|
+
# ENV['STREAMWEAVER_HISTORY_ROOT'] || ~/.streamweaver/history. The env-var
|
|
11
|
+
# override exists so specs (and ad-hoc tooling) can redirect writes away
|
|
12
|
+
# from the user's real home directory.
|
|
13
|
+
#
|
|
14
|
+
# Session names are validated against an allowlist (alnum + . _ -, must
|
|
15
|
+
# start with alnum) and an explicit '..'-substring check. Bad names raise
|
|
16
|
+
# ArgumentError -- raising rather than sanitizing keeps the contract
|
|
17
|
+
# explicit and avoids silently storing under an unexpected directory.
|
|
18
|
+
module History
|
|
19
|
+
MAX_AGE_DAYS = 7
|
|
20
|
+
MAX_AGE_SECONDS = MAX_AGE_DAYS * 86_400
|
|
21
|
+
DEFAULT_ROOT = File.expand_path('~/.streamweaver/history')
|
|
22
|
+
VALID_NAME = /\A[A-Za-z0-9][A-Za-z0-9._-]*\z/
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def root
|
|
27
|
+
env = ENV['STREAMWEAVER_HISTORY_ROOT']
|
|
28
|
+
return env if env && !env.empty?
|
|
29
|
+
|
|
30
|
+
DEFAULT_ROOT
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Writes dsl to <root>/<session>/<timestamp>.rb and returns the path.
|
|
34
|
+
# Appends a numeric suffix on collision so back-to-back writes within
|
|
35
|
+
# the same second don't clobber each other. Paths are claimed atomically
|
|
36
|
+
# (File::EXCL), so concurrent callers get distinct files instead of
|
|
37
|
+
# racing between an exist? check and the write.
|
|
38
|
+
def record(session, dsl)
|
|
39
|
+
validate_session!(session)
|
|
40
|
+
dir = File.join(root, session)
|
|
41
|
+
FileUtils.mkdir_p(dir)
|
|
42
|
+
|
|
43
|
+
stamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
44
|
+
write_unique(dir, stamp, dsl)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Deletes snapshot files older than MAX_AGE_DAYS and prunes any
|
|
48
|
+
# session directory left empty afterwards. No-op if root is missing.
|
|
49
|
+
def cleanup
|
|
50
|
+
base = root
|
|
51
|
+
return unless Dir.exist?(base)
|
|
52
|
+
|
|
53
|
+
cutoff = Time.now - MAX_AGE_SECONDS
|
|
54
|
+
|
|
55
|
+
Dir.children(base).each do |session|
|
|
56
|
+
session_dir = File.join(base, session)
|
|
57
|
+
next unless File.directory?(session_dir)
|
|
58
|
+
|
|
59
|
+
Dir.glob(File.join(session_dir, '*.rb')).each do |file|
|
|
60
|
+
File.delete(file) if File.mtime(file) < cutoff
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Dir.rmdir(session_dir) if Dir.children(session_dir).empty?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_session!(session)
|
|
68
|
+
return if session.is_a?(String) && session.match?(VALID_NAME) && !session.include?('..')
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "invalid session name: #{session.inspect}"
|
|
71
|
+
end
|
|
72
|
+
private_class_method :validate_session!
|
|
73
|
+
|
|
74
|
+
def write_unique(dir, stamp, dsl)
|
|
75
|
+
suffix = nil
|
|
76
|
+
loop do
|
|
77
|
+
name = suffix ? "#{stamp}_#{suffix}.rb" : "#{stamp}.rb"
|
|
78
|
+
path = File.join(dir, name)
|
|
79
|
+
begin
|
|
80
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL) { |f| f.write(dsl) }
|
|
81
|
+
return path
|
|
82
|
+
rescue Errno::EEXIST
|
|
83
|
+
suffix = (suffix || 0) + 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
private_class_method :write_unique
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Canvas
|
|
7
|
+
# Protocol for Unix socket IPC between Claude Code and Canvas Bridge.
|
|
8
|
+
# Uses newline-delimited JSON.
|
|
9
|
+
module Protocol
|
|
10
|
+
# Encode a message hash to newline-delimited JSON
|
|
11
|
+
# @param message [Hash] Message to encode
|
|
12
|
+
# @return [String] JSON string with trailing newline
|
|
13
|
+
def self.encode(message)
|
|
14
|
+
JSON.generate(message) + "\n"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Decode a JSON string to a message hash
|
|
18
|
+
# @param json [String] JSON string
|
|
19
|
+
# @return [Hash, nil] Decoded message with symbol keys, or nil if invalid
|
|
20
|
+
def self.decode(json)
|
|
21
|
+
JSON.parse(json, symbolize_names: true)
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Parse a buffer containing potentially multiple messages
|
|
27
|
+
# @param buffer [String] Buffer with newline-delimited JSON
|
|
28
|
+
# @return [Array<Array<Hash>, String>] [messages, remaining_buffer]
|
|
29
|
+
def self.parse_buffer(buffer)
|
|
30
|
+
lines = buffer.split("\n", -1)
|
|
31
|
+
remaining = lines.pop || ''
|
|
32
|
+
|
|
33
|
+
messages = lines.filter_map do |line|
|
|
34
|
+
decode(line) unless line.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
[messages, remaining]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Message factory methods
|
|
41
|
+
module Messages
|
|
42
|
+
# Claude → Bridge: Create a new session
|
|
43
|
+
def self.create(name, layout: :fluid, theme: :default)
|
|
44
|
+
{ type: 'create', name: name, layout: layout, theme: theme }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Claude → Bridge: Push DSL content to session
|
|
48
|
+
# @param source_dir [String, nil] Git root of the pushing side's cwd
|
|
49
|
+
# (DocStore.git_root(Dir.pwd) computed by the caller), or nil
|
|
50
|
+
# outside a repo. Not the bridge's own cwd -- see Session#source_dir.
|
|
51
|
+
def self.push(name, dsl, source_dir: nil)
|
|
52
|
+
{ type: 'push', name: name, dsl: dsl, source_dir: source_dir }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Claude → Bridge: Close a session
|
|
56
|
+
def self.close(name)
|
|
57
|
+
{ type: 'close', name: name }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Claude → Bridge: Fetch a session's current raw DSL + theme/layout
|
|
61
|
+
# (canvas-snapshot, stream_weaver-ps84) -- the live in-memory source
|
|
62
|
+
# of truth, not the ephemeral ~/.streamweaver/history/ trail, which
|
|
63
|
+
# expires after 7 days and isn't written by every code path that can
|
|
64
|
+
# populate a session's DSL.
|
|
65
|
+
def self.get_dsl(name)
|
|
66
|
+
{ type: 'get_dsl', name: name }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Claude → Bridge: Set iTerm pane ID for a session
|
|
70
|
+
def self.set_pane_id(name, pane_id)
|
|
71
|
+
{ type: 'set_pane_id', name: name, pane_id: pane_id }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Bridge → Claude: Session is ready
|
|
75
|
+
def self.ready(name, url)
|
|
76
|
+
{ type: 'ready', name: name, url: url }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Bridge → Claude: User interaction event
|
|
80
|
+
def self.event(name, event_type, data)
|
|
81
|
+
{ type: 'event', name: name, event: event_type, data: data }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|