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