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,549 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# stream_weaver-8mj pixel-parity rebuild: Forrest's bar is IDENTICAL --
|
|
5
|
+
# "if we cannot make it look identical, then we have not succeeded." The
|
|
6
|
+
# prior slice (gsd/analysis/08-design-parity-fights.md, superseded by this
|
|
7
|
+
# rewrite) invented its own class names (tyrion-card, tyrion-board, ...)
|
|
8
|
+
# and fought StreamWeaver's own component CSS to skin them. This version
|
|
9
|
+
# instead emits tyrion's OWN class names (wr-board, wr-card, topbar,
|
|
10
|
+
# sidebar, story-row, as-parchment, ...) using only primitive
|
|
11
|
+
# div/phrase/header/clickable calls -- no Board/Lane/BoardCard/Sidebar/
|
|
12
|
+
# Navbar -- so the REAL public/shared.css (consumed at runtime, never
|
|
13
|
+
# copied into this repo) governs every element directly, with nothing of
|
|
14
|
+
# StreamWeaver's own competing for the same classes.
|
|
15
|
+
#
|
|
16
|
+
# Reference read (READ-ONLY, never copied): ~/work/tyrion web/app.rb,
|
|
17
|
+
# web/views/layout.rb, web/views/war_room.rb, web/views/active_story.rb,
|
|
18
|
+
# web/public/shared.css. Every class name/structure below was re-derived
|
|
19
|
+
# by reading those files, not copy-pasted.
|
|
20
|
+
#
|
|
21
|
+
# Fixture data only -- invented story titles, no real Tyrion project data.
|
|
22
|
+
#
|
|
23
|
+
# Env vars (both optional; the slice degrades gracefully without either):
|
|
24
|
+
# SW_PARITY_CSS=/path/to/tyrion/web/public/shared.css
|
|
25
|
+
# Serves the REAL CSS at runtime via `stylesheets:` local-path
|
|
26
|
+
# auto-detection. Unset: only this slice's own minimal seam CSS loads,
|
|
27
|
+
# and every wr-*/topbar/sidebar/as-* class renders unstyled (bare
|
|
28
|
+
# browser defaults) -- a stark but honest way to see exactly how much
|
|
29
|
+
# of the look comes from the real stylesheet.
|
|
30
|
+
# SW_PARITY_ASSETS=/path/to/tyrion/web/public/assets
|
|
31
|
+
# Real crest/dragon/lantern/map art via a local-file endpoint. Unset:
|
|
32
|
+
# unicode glyph fallbacks (🦁/🐉) and the plain CSS gradient tyrion's
|
|
33
|
+
# own shared.css already falls back to when its background-image url
|
|
34
|
+
# 404s (see tyrion_slice.css).
|
|
35
|
+
#
|
|
36
|
+
# Run: SW_PARITY_CSS=... SW_PARITY_ASSETS=... ruby examples/parity/tyrion_warroom_slice.rb
|
|
37
|
+
|
|
38
|
+
require_relative "../../lib/stream_weaver"
|
|
39
|
+
require "base64"
|
|
40
|
+
require "time"
|
|
41
|
+
|
|
42
|
+
PARITY_ASSETS_DIR = ENV["SW_PARITY_ASSETS"]
|
|
43
|
+
PARITY_CSS_PATH = ENV["SW_PARITY_CSS"]&.then { |p| File.expand_path(p) }
|
|
44
|
+
SLICE_CSS_PATH = File.expand_path("assets/tyrion_slice.css", __dir__)
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Real art, resolved to /sw-asset/ URLs *before* App.new -- ComponentAssets
|
|
48
|
+
# is a module-level registry (no App instance required), which sidesteps a
|
|
49
|
+
# real ordering gap: `stylesheets:` is resolved eagerly when App.new runs,
|
|
50
|
+
# but App#local_asset needs `self` to be the App instance, which only
|
|
51
|
+
# exists *inside* the DSL block that App.new's `stylesheets:` keyword
|
|
52
|
+
# argument is evaluated before. There is no way to build a `local_asset`
|
|
53
|
+
# URL early enough to hand to `stylesheets:` through the public API alone
|
|
54
|
+
# -- calling the registry directly is the only route. See catalog finding
|
|
55
|
+
# below for the fuller writeup (this is a second surfacing of the same
|
|
56
|
+
# gap, not a new one).
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
def parity_art_url(basename)
|
|
59
|
+
return nil unless PARITY_ASSETS_DIR
|
|
60
|
+
path = File.join(PARITY_ASSETS_DIR, basename)
|
|
61
|
+
return nil unless File.exist?(path)
|
|
62
|
+
|
|
63
|
+
"/sw-asset/#{StreamWeaver::ComponentAssets.register_file(File.expand_path(path))}/#{basename}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
CREST_URL = parity_art_url("LionCrest.png")
|
|
67
|
+
DRAGON_URL = parity_art_url("dragon.png")
|
|
68
|
+
LANTERN_URL = parity_art_url("lantern.png")
|
|
69
|
+
MAP_URL = parity_art_url("strategy_map.png")
|
|
70
|
+
|
|
71
|
+
stylesheet_list = [PARITY_CSS_PATH, SLICE_CSS_PATH].compact
|
|
72
|
+
extra_asset_dirs = [PARITY_ASSETS_DIR].compact
|
|
73
|
+
extra_asset_dirs << File.dirname(PARITY_CSS_PATH) if PARITY_CSS_PATH
|
|
74
|
+
|
|
75
|
+
# Real shared.css hardcodes `.topbar`/`.wr-board` background-image as a
|
|
76
|
+
# *relative* url("assets/strategy_map.png") -- there's no way to point
|
|
77
|
+
# that at our resolved MAP_URL without either rewriting the real CSS
|
|
78
|
+
# (never copied/edited, per the ticket's own constraint) or shipping an
|
|
79
|
+
# override rule of our own. `stylesheets:` entries that aren't a real
|
|
80
|
+
# local file pass through unchanged (see App#resolve_stylesheet_href) --
|
|
81
|
+
# including a `data:` URI, which isn't caught by the URL-scheme guard
|
|
82
|
+
# there (no `//` after the colon) -- so a small inline override, appended
|
|
83
|
+
# last, gets the real photo in without a temp file or an extra
|
|
84
|
+
# assets_dirs: entry.
|
|
85
|
+
if MAP_URL
|
|
86
|
+
bg_css = <<~CSS
|
|
87
|
+
body .topbar { background-image: linear-gradient(to bottom, rgba(0,0,0,.48), rgba(13,10,7,.88)), url('#{MAP_URL}'); }
|
|
88
|
+
body .wr-board { background: linear-gradient(180deg, rgba(8,10,11,.18), rgba(8,10,11,.28)), url('#{MAP_URL}') center/cover no-repeat; }
|
|
89
|
+
CSS
|
|
90
|
+
stylesheet_list << "data:text/css;base64,#{Base64.strict_encode64(bg_css)}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Store: in-memory, DB-shaped -- records live here, never in the state hash.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
module StoryStore
|
|
97
|
+
# Mirrors the real tyrion server's *current* lane shape (stream_weaver-8mj
|
|
98
|
+
# A/B fidelity pass): Queue 6 / Active 0 / Blocked 0 / Done 8, curled from
|
|
99
|
+
# http://127.0.0.1:4579/warroom -- fixture slugs below are invented
|
|
100
|
+
# (slug-style, matching this file's existing medieval theme), never the
|
|
101
|
+
# real project's story names. With Active and Blocked both empty, the
|
|
102
|
+
# "Here Be Dragons" watermark (unconditionally rendered in both this
|
|
103
|
+
# slice and the real war_room.rb -- see the render_wr_col call below) has
|
|
104
|
+
# nothing to be covered by in those two lanes and reads exactly as it
|
|
105
|
+
# does live: centered, undimmed, not fighting card backgrounds for the
|
|
106
|
+
# gaps it shows through.
|
|
107
|
+
DEFS = [
|
|
108
|
+
{ slug: "forge-the-beacon-relay", epic_slug: "field-ops", status: :done,
|
|
109
|
+
context: "Beacon-relay handshake wired between both outposts.",
|
|
110
|
+
intent: "Two outposts need a shared signal before the muster begins.",
|
|
111
|
+
next_action: nil,
|
|
112
|
+
criteria: [["Relay reaches both towers", :met], ["Signal survives storm interference", :met], ["Watch rotation briefed", :met]],
|
|
113
|
+
notes: [[:progress, "First handshake test succeeded at half range."], [:progress, "Full 4-league range held through the night watch."]],
|
|
114
|
+
stale: false },
|
|
115
|
+
{ slug: "mend-the-gate-hinge", epic_slug: "field-ops", status: :done,
|
|
116
|
+
context: "Gate hinge replaced and load-tested.",
|
|
117
|
+
intent: "Keep the east gate from seizing before winter.",
|
|
118
|
+
next_action: nil,
|
|
119
|
+
criteria: [["Hinge replaced", :met], ["Load test passed", :met]],
|
|
120
|
+
notes: [[:progress, "New hinge forged and fitted."], [:progress, "Load-tested to double the rated weight."]],
|
|
121
|
+
stale: false },
|
|
122
|
+
{ slug: "seal-the-old-cistern", epic_slug: "river-trade", status: :done,
|
|
123
|
+
context: "Cistern sealed; pressure test passed.",
|
|
124
|
+
intent: "Stop the slow leak before the dry season.",
|
|
125
|
+
next_action: nil,
|
|
126
|
+
criteria: [["Cistern sealed", :met], ["Pressure test passed", :met]],
|
|
127
|
+
notes: [[:progress, "Seal held at full pressure for a full day."]], stale: false },
|
|
128
|
+
{ slug: "clear-the-ashwood-pass", epic_slug: "field-ops", status: :done,
|
|
129
|
+
context: "Riders confirmed the pass safe for the muster.",
|
|
130
|
+
intent: "Confirm the ashwood pass is clear before the muster marches through.",
|
|
131
|
+
next_action: nil,
|
|
132
|
+
criteria: [["Riders dispatched", :met], ["Pass condition confirmed", :met]],
|
|
133
|
+
notes: [[:progress, "Riders returned at dusk -- pass reported clear."]], stale: false },
|
|
134
|
+
{ slug: "repair-the-north-bridge", epic_slug: "field-ops", status: :done,
|
|
135
|
+
context: "Crossbeams placed; load-bearing test passed.",
|
|
136
|
+
intent: "The north bridge must hold before the autumn floods.",
|
|
137
|
+
next_action: nil,
|
|
138
|
+
criteria: [["Timber delivered", :met], ["Crossbeams placed", :met], ["Load-bearing test", :met]],
|
|
139
|
+
notes: [[:progress, "Timber arrived a day ahead of schedule."], [:progress, "Load test held at double the rated weight."]],
|
|
140
|
+
stale: false },
|
|
141
|
+
{ slug: "translate-the-sunken-tablet", epic_slug: "river-trade", status: :done,
|
|
142
|
+
context: "Scriptorium translator resolved the old script.",
|
|
143
|
+
intent: "The tablet may name the vault's true owner.",
|
|
144
|
+
next_action: nil,
|
|
145
|
+
criteria: [["Tablet recovered", :met], ["Translator found", :met]],
|
|
146
|
+
notes: [[:progress, "Scriptorium translator confirmed the vault's owner."]], stale: false },
|
|
147
|
+
{ slug: "negotiate-toll-rights", epic_slug: "river-trade", status: :done,
|
|
148
|
+
context: "Guild council approved the toll rights.",
|
|
149
|
+
intent: "Toll rights unlock the southern supply route.",
|
|
150
|
+
next_action: nil,
|
|
151
|
+
criteria: [["Guild proposal submitted", :met], ["Council vote scheduled", :met]],
|
|
152
|
+
notes: [[:progress, "Council vote passed on the second reading."]], stale: false },
|
|
153
|
+
{ slug: "draft-the-harvest-ledger", epic_slug: "field-ops", status: :done,
|
|
154
|
+
context: "Ledger reconciled against the quartermaster's count.",
|
|
155
|
+
intent: "The harvest ledger must balance before winter stores are sealed.",
|
|
156
|
+
next_action: nil,
|
|
157
|
+
criteria: [["Ledger drafted", :met], ["Counts reconciled", :met]],
|
|
158
|
+
notes: [[:progress, "Ledger balanced on the first reconciliation pass."]], stale: false },
|
|
159
|
+
{ slug: "chart-the-southern-vault", epic_slug: "field-ops", status: :pending,
|
|
160
|
+
context: "Survey pass before the vault dig begins.",
|
|
161
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
162
|
+
{ slug: "restock-the-armory", epic_slug: "field-ops", status: :pending,
|
|
163
|
+
context: "Quartermaster count due before the next campaign.",
|
|
164
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
165
|
+
{ slug: "draft-the-spring-muster", epic_slug: "field-ops", status: :pending,
|
|
166
|
+
context: "First pass on troop assignments.",
|
|
167
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
168
|
+
{ slug: "ready-the-siege-engines", epic_slug: "field-ops", status: :pending,
|
|
169
|
+
context: "Inventory pass before the engines are wheeled out.",
|
|
170
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
171
|
+
{ slug: "survey-the-flooded-cellar", epic_slug: "river-trade", status: :pending,
|
|
172
|
+
context: "Waiting on a dry spell before the survey crew goes down.",
|
|
173
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
174
|
+
{ slug: "catalog-the-relic-hoard", epic_slug: "river-trade", status: :pending,
|
|
175
|
+
context: "First pass on the recovered hoard, pending an appraiser.",
|
|
176
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false }
|
|
177
|
+
].freeze
|
|
178
|
+
|
|
179
|
+
# Demo-only lane emptying (stream_weaver-8mj empty-state proof): set
|
|
180
|
+
# SW_PARITY_EMPTY_LANE=pending (or done/active/blocked) to force that
|
|
181
|
+
# lane empty regardless of the fixture above. Unset by default -- the
|
|
182
|
+
# mirrored shape above (Active/Blocked already empty) is the normal demo.
|
|
183
|
+
EMPTY_LANE = ENV["SW_PARITY_EMPTY_LANE"]&.to_sym
|
|
184
|
+
|
|
185
|
+
@stories = DEFS.each_with_index.map do |d, i|
|
|
186
|
+
d.merge(
|
|
187
|
+
id: i + 1,
|
|
188
|
+
last_note_at: d[:status] == :in_progress ? (d[:stale] ? Time.now - (9 * 3600) : Time.now - (i + 1) * 240) : nil,
|
|
189
|
+
completed_at: d[:status] == :done ? Time.now - ((i + 1) * 3600 * 9) : nil
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class << self
|
|
194
|
+
def all = @stories
|
|
195
|
+
def find(id) = @stories.find { |s| s[:id] == id.to_i }
|
|
196
|
+
def by_status(status) = status == EMPTY_LANE ? [] : @stories.select { |s| s[:status] == status }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def relative_time(t)
|
|
201
|
+
return "" unless t
|
|
202
|
+
mins = ((Time.now - t) / 60).round
|
|
203
|
+
return "#{mins}m ago" if mins < 60
|
|
204
|
+
hours = mins / 60
|
|
205
|
+
return "#{hours}h ago" if hours < 24
|
|
206
|
+
"#{hours / 24}d ago"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
STATUS_LABEL = { pending: "pending", in_progress: "in progress", done: "done", blocked: "blocked" }.freeze
|
|
210
|
+
STATUS_CSS = { pending: "pending", in_progress: "in-progress", done: "done", blocked: "blocked" }.freeze
|
|
211
|
+
|
|
212
|
+
App = StreamWeaver::App.new(
|
|
213
|
+
"Tyrion War Room (parity slice)",
|
|
214
|
+
chrome: false,
|
|
215
|
+
theme: :dashboard,
|
|
216
|
+
theme_overrides: { color_bg: "#0D0A07", color_text: "#F5E6C8" },
|
|
217
|
+
fonts: ["Cinzel:wght@400;600;700", "IBM+Plex+Mono:wght@300;400", "Lora:ital,wght@0,400;1,400;1,600"],
|
|
218
|
+
stylesheets: stylesheet_list,
|
|
219
|
+
assets_dirs: extra_asset_dirs
|
|
220
|
+
) do
|
|
221
|
+
# =========================================================================
|
|
222
|
+
# Shared chrome: topbar + sidebar, rendered on every page. Real DOM:
|
|
223
|
+
# web/views/layout.rb -- .topbar > .topbar-main + .topbar-nav, .sidebar.
|
|
224
|
+
# =========================================================================
|
|
225
|
+
|
|
226
|
+
def art_glyph(url, glyph, style:)
|
|
227
|
+
if url
|
|
228
|
+
div(style: "#{style}background-image:url('#{url}');background-size:contain;background-repeat:no-repeat;background-position:center;display:inline-block;")
|
|
229
|
+
else
|
|
230
|
+
phrase(glyph, style: "#{style}display:inline-block;text-align:center;")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
current_action = state[StreamWeaver::Resource::StateKeys::ACTION]
|
|
235
|
+
current_story = current_action.is_a?(Symbol) && current_action.to_s.start_with?("story_") ? StoryStore.find(current_action.to_s.delete_prefix("story_")) : nil
|
|
236
|
+
active_tab = if current_story
|
|
237
|
+
current_story[:status] == :in_progress ? :active : :warroom
|
|
238
|
+
else
|
|
239
|
+
:warroom
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
div(class: "shell") do
|
|
243
|
+
div(class: "topbar") do
|
|
244
|
+
div(class: "topbar-main") do
|
|
245
|
+
div(class: "topbar-brand") do
|
|
246
|
+
art_glyph(CREST_URL, "🦁", style: "height:44px;width:44px;margin-right:10px;vertical-align:middle;")
|
|
247
|
+
phrase("TYRION", style: "font-family:'Cinzel',serif;font-size:24px;font-weight:700;color:var(--gold-bright);letter-spacing:0.12em;")
|
|
248
|
+
end
|
|
249
|
+
phrase("·", class: "topbar-sep")
|
|
250
|
+
phrase("field-ops", class: "topbar-crumb")
|
|
251
|
+
phrase("·", class: "topbar-sep")
|
|
252
|
+
phrase(current_story ? current_story[:slug] : "warroom-parity", class: "topbar-crumb active")
|
|
253
|
+
div(class: "topbar-git") do
|
|
254
|
+
phrase("⎇ parity/warroom-slice", class: "pill pill-neutral")
|
|
255
|
+
phrase("✗ 2", class: "pill pill-amber")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
div(class: "topbar-nav") do
|
|
259
|
+
[
|
|
260
|
+
[:warroom, "⚔ War Room", "/"],
|
|
261
|
+
[:roadmap, "🗺 Roadmap", "#"],
|
|
262
|
+
[:active, "📖 Active Story", "#"],
|
|
263
|
+
[:global, "🌍 Global View", "#"],
|
|
264
|
+
[:discoveries, "💡 Discoveries", "#"],
|
|
265
|
+
[:about, "❓ About Tyrion", "#"]
|
|
266
|
+
].each do |id, label, href|
|
|
267
|
+
clickable(href: href, class: active_tab == id ? "demo-tab active" : "demo-tab") { phrase(label) }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
div(class: "sidebar") do
|
|
273
|
+
div(style: "padding:10px 14px 0;font-size:12px;color:var(--text-dim);font-family:var(--font-mono);") { phrase("field-ops › warroom-parity") }
|
|
274
|
+
div(class: "sidebar-section") { phrase("Stories · warroom-parity") }
|
|
275
|
+
|
|
276
|
+
StoryStore.all.each do |story|
|
|
277
|
+
row_class = case story[:status]
|
|
278
|
+
when :done then "story-row done"
|
|
279
|
+
when :in_progress then "story-row active"
|
|
280
|
+
else "story-row"
|
|
281
|
+
end
|
|
282
|
+
icon_glyph = story[:status] == :pending ? "○" : "●"
|
|
283
|
+
icon_color = case story[:status]
|
|
284
|
+
when :done then "var(--emerald)"
|
|
285
|
+
when :in_progress then "var(--amber)"
|
|
286
|
+
else "var(--ink-faint)"
|
|
287
|
+
end
|
|
288
|
+
clickable(href: "/stories/#{story[:id]}", class: row_class) do
|
|
289
|
+
phrase(icon_glyph, class: story[:status] == :in_progress ? "s-icon s-icon-pulse" : "s-icon", style: "color:#{icon_color}")
|
|
290
|
+
phrase(story[:slug], class: "s-name")
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Mirrors the reference's current discoveries shape: an active spike,
|
|
295
|
+
# zero promotable ("ready") discoveries -- so no ready row, matching
|
|
296
|
+
# Layout's `if disc[:ready_count] > 0` gate in the real app -- and 5
|
|
297
|
+
# unformalized marks.
|
|
298
|
+
div(class: "disc-strip") do
|
|
299
|
+
div(class: "sidebar-section") { phrase("Discoveries") }
|
|
300
|
+
clickable(href: "#", class: "disc-row") do
|
|
301
|
+
phrase("spike", class: "d-pill spike")
|
|
302
|
+
phrase("the flooded cellar timing", class: "d-label")
|
|
303
|
+
end
|
|
304
|
+
clickable(href: "#", class: "disc-row") do
|
|
305
|
+
phrase("5 marks", class: "d-pill mark")
|
|
306
|
+
phrase("unformalized", class: "d-label")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# =======================================================================
|
|
312
|
+
# War Room board -- real DOM: web/views/war_room.rb.
|
|
313
|
+
# =======================================================================
|
|
314
|
+
page(:warroom, "/") do
|
|
315
|
+
div(class: "main-content visible") do
|
|
316
|
+
div(class: "wr-board") do
|
|
317
|
+
# Always rendered, centered, behind the columns (z-index:1 vs
|
|
318
|
+
# .wr-columns' z-index:2) -- not an empty-lane conditional. The
|
|
319
|
+
# original slice's report mischaracterized this as an empty-state
|
|
320
|
+
# overlay; the real war_room.rb renders it unconditionally as a
|
|
321
|
+
# background watermark, only visible through the gaps between
|
|
322
|
+
# cards.
|
|
323
|
+
div(class: "wr-dragon-zone") do
|
|
324
|
+
div do
|
|
325
|
+
art_glyph(DRAGON_URL, "🐉", style: "width:70px;height:70px;opacity:.82;filter:drop-shadow(0 6px 12px rgba(0,0,0,.5));margin-bottom:10px;")
|
|
326
|
+
div(class: "wr-dragon-zone-copy") do
|
|
327
|
+
header3("Here Be Dragons")
|
|
328
|
+
# `.wr-dragon-zone-copy p` (real CSS) targets a bare <p> tag,
|
|
329
|
+
# not a class -- text() with no tone: renders exactly that
|
|
330
|
+
# (Components::Text's untoned branch emits `view.p { content
|
|
331
|
+
# }` with zero attrs), so three stacked <p>s (margin:0 in the
|
|
332
|
+
# real CSS) read the same as the original's one <p> + <br>s.
|
|
333
|
+
text("Uncertain requirements")
|
|
334
|
+
text("External dependency")
|
|
335
|
+
text("Needs human decision")
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
div(class: "wr-columns") do
|
|
341
|
+
render_wr_col("Queue", "Pending", StoryStore.by_status(:pending), "wr-col")
|
|
342
|
+
render_wr_col("Active Campaign", "In Progress", StoryStore.by_status(:in_progress), "wr-col", header_class: "active-hdr")
|
|
343
|
+
render_wr_col("Blocked Frontier", "Blocked", StoryStore.by_status(:blocked), "wr-col", header_class: "dragons-hdr", show_dragon: true, title_prefix: "🐉 ")
|
|
344
|
+
render_wr_col("Shipped Keep", "Done", StoryStore.by_status(:done), "wr-col", header_class: "done-hdr")
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
active_stories = StoryStore.by_status(:in_progress)
|
|
348
|
+
if active_stories.size == 1
|
|
349
|
+
render_single_resume_strip(active_stories.first)
|
|
350
|
+
elsif active_stories.size > 1
|
|
351
|
+
render_multi_lane_strip(active_stories)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# =======================================================================
|
|
358
|
+
# Story detail pages -- one real deep-linkable URL per fixture story
|
|
359
|
+
# (page() matches on a literal path, not a param placeholder -- with
|
|
360
|
+
# only 10 fixture stories, registering one page per id is the correct
|
|
361
|
+
# DSL-native way to get real /stories/:id URLs; `resource()` exists for
|
|
362
|
+
# this but pulls in its own DefaultViews scaffolding (edit/new/destroy
|
|
363
|
+
# markup this slice doesn't want fighting the real as-* CSS for
|
|
364
|
+
# control it doesn't need). Real DOM: web/views/active_story.rb.
|
|
365
|
+
# =======================================================================
|
|
366
|
+
StoryStore.all.each do |story|
|
|
367
|
+
page(:"story_#{story[:id]}", "/stories/#{story[:id]}") do
|
|
368
|
+
div(class: "main-content visible") do
|
|
369
|
+
div(class: "as-outer") do
|
|
370
|
+
div(class: "as-scroll") do
|
|
371
|
+
div(class: "as-parchment") do
|
|
372
|
+
clickable(href: "/", style: "display:inline-block;font-size:12px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);text-decoration:none;margin-bottom:12px;opacity:.7;") { phrase("← War Room") }
|
|
373
|
+
|
|
374
|
+
div(class: "as-hero-status") do
|
|
375
|
+
div(class: "status-badge #{STATUS_CSS.fetch(story[:status])}") do
|
|
376
|
+
div(class: "dot")
|
|
377
|
+
phrase(STATUS_LABEL.fetch(story[:status]))
|
|
378
|
+
end
|
|
379
|
+
if story[:last_note_at]
|
|
380
|
+
phrase("last note #{relative_time(story[:last_note_at])}", style: "font-size:13px;color:var(--ink-muted)")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
div(class: "as-hero-title") { phrase(story[:slug]) }
|
|
384
|
+
|
|
385
|
+
if story[:intent]
|
|
386
|
+
div(style: "background:rgba(180,140,80,.08);border-left:3px solid rgba(180,140,80,.35);border-radius:0 6px 6px 0;padding:10px 14px;margin-bottom:14px;") do
|
|
387
|
+
div(style: "font-size:10px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);letter-spacing:.1em;margin-bottom:6px;") { phrase("MISSION BRIEF") }
|
|
388
|
+
div(style: "font-family:'Lora',serif;font-size:14px;color:var(--ink-dim);font-style:italic;line-height:1.6;") { phrase(story[:intent]) }
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
div(class: "as-block") do
|
|
393
|
+
div(class: "as-block-label") { phrase("Current Context") }
|
|
394
|
+
div(style: "font-size:11px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);margin-top:-3px;margin-bottom:5px;") { phrase("set by implementing agent") }
|
|
395
|
+
div(class: "as-block-text") { phrase(story[:context]) }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
div(class: "as-block") do
|
|
399
|
+
div(class: "as-block-label") { phrase("Next Action") }
|
|
400
|
+
div(style: "font-size:11px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);margin-top:-3px;margin-bottom:5px;") { phrase("agent sets this before handing off") }
|
|
401
|
+
div(class: "as-block-text") { phrase(story[:next_action] || "(not set)") }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
div(class: "as-split") do
|
|
405
|
+
div do
|
|
406
|
+
div(class: "as-block-label", style: "margin-bottom:8px") { phrase("Criteria") }
|
|
407
|
+
if story[:criteria].any?
|
|
408
|
+
story[:criteria].each do |text, status|
|
|
409
|
+
div(class: status == :met ? "as-crit-row done" : "as-crit-row") do
|
|
410
|
+
div(class: status == :met ? "crit-check checked" : "crit-check")
|
|
411
|
+
div(class: "crit-text") { phrase(text) }
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
else
|
|
415
|
+
div(style: "font-size:13px;color:var(--ink-faint);font-style:italic;") { phrase("No criteria yet") }
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
div(style: "display:flex;flex-direction:column;") do
|
|
420
|
+
div(class: "as-block-label", style: "margin-bottom:8px") { phrase("Notes") }
|
|
421
|
+
note_anchor = story[:last_note_at] || story[:completed_at] || Time.now
|
|
422
|
+
story[:notes].each_with_index do |(kind, body), idx|
|
|
423
|
+
div(class: "as-note-entry #{kind}") do
|
|
424
|
+
div(class: "note-meta") { phrase("#{kind} · #{relative_time(note_anchor - (idx * 3600))}") }
|
|
425
|
+
div(class: "note-body") { phrase(body) }
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
# Static/decorative -- functional note-adding is out of
|
|
429
|
+
# scope for this pixel-parity pass (see catalog "not
|
|
430
|
+
# attempted").
|
|
431
|
+
div(class: "as-quick-note", style: "margin-top:auto;") do
|
|
432
|
+
div(style: "flex:1;font-family:var(--font-mono);font-size:14px;color:var(--ink-faint);font-style:italic;") { phrase("progress note + Enter…") }
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if story[:status] == :in_progress || story[:next_action] || story[:last_note_at]
|
|
439
|
+
div(class: "as-resume-strip") do
|
|
440
|
+
div(class: "as-rs-panel") do
|
|
441
|
+
div(class: "as-rs-label") { phrase("LAST NOTE") }
|
|
442
|
+
div(class: "as-rs-text") { phrase(story[:notes].first ? "#{relative_time(story[:last_note_at])} — \"#{story[:notes].first[1].to_s[0, 60]}\"" : "no notes yet") }
|
|
443
|
+
end
|
|
444
|
+
div(class: "as-rs-panel as-rs-beacon") do
|
|
445
|
+
art_glyph(LANTERN_URL, "🏮", style: "height:44px;width:44px;filter:drop-shadow(0 0 10px rgba(245,158,11,0.75));")
|
|
446
|
+
div do
|
|
447
|
+
div(class: "as-rs-label") { phrase("RESUME POINT") }
|
|
448
|
+
div(class: "as-rs-text") do
|
|
449
|
+
met = story[:criteria].count { |_, s| s == :met }
|
|
450
|
+
phrase(story[:criteria].any? ? "#{story[:slug]} · #{met}/#{story[:criteria].size} criteria met" : story[:slug])
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
div(class: "as-rs-panel") do
|
|
455
|
+
div(class: "as-rs-label") { phrase("NEXT ACTION") }
|
|
456
|
+
div(class: "as-rs-text") { phrase(story[:next_action] || "(not set)") }
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def render_wr_col(title, sub, stories, col_class, header_class: nil, show_dragon: false, title_prefix: "")
|
|
469
|
+
div(class: col_class) do
|
|
470
|
+
div(class: ["wr-col-header", header_class].compact.join(" ")) do
|
|
471
|
+
art_glyph(DRAGON_URL, "🐉", style: "height:22px;width:22px;opacity:.85;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4));") if show_dragon
|
|
472
|
+
div do
|
|
473
|
+
div(class: "wr-col-title") { phrase("#{title_prefix}#{title}") }
|
|
474
|
+
div(class: "wr-col-sub") { phrase(sub) }
|
|
475
|
+
end
|
|
476
|
+
phrase(stories.size.to_s, class: stories.size.positive? && show_dragon ? "wr-col-count danger" : "wr-col-count")
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
stories.each do |story|
|
|
480
|
+
if story[:status] == :done
|
|
481
|
+
render_wr_done_card(story)
|
|
482
|
+
else
|
|
483
|
+
render_wr_card(story)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def render_wr_card(story)
|
|
490
|
+
card_style = story[:status] == :blocked ? "border-left:3px solid #b91c1c;background:linear-gradient(180deg,rgba(220,180,160,.97),rgba(190,150,120,.94));" : ""
|
|
491
|
+
clickable(href: "/stories/#{story[:id]}") do
|
|
492
|
+
div(class: story[:stale] ? "wr-card stale" : "wr-card", style: card_style) do
|
|
493
|
+
div(class: "wr-stale-badge") { phrase("⚠ STALE #{relative_time(story[:last_note_at])}") } if story[:stale]
|
|
494
|
+
phrase(story[:epic_slug], class: "wr-card-id")
|
|
495
|
+
header4(story[:slug], class: "wr-card-title")
|
|
496
|
+
div(class: "wr-card-ctx") { phrase(story[:context].to_s[0, 60]) } if story[:context]
|
|
497
|
+
div(class: "wr-card-tags") { phrase(STATUS_LABEL.fetch(story[:status]), class: "wr-tag") }
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def render_wr_done_card(story)
|
|
503
|
+
clickable(href: "/stories/#{story[:id]}") do
|
|
504
|
+
div(class: "wr-card done-card") do
|
|
505
|
+
phrase(story[:epic_slug], class: "wr-card-id")
|
|
506
|
+
header4(story[:slug], class: "wr-card-title")
|
|
507
|
+
div(class: "wr-card-tags") { phrase("✓ done · #{relative_time(story[:completed_at])}", style: "font-size:11px;color:#166534;") }
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def render_single_resume_strip(story)
|
|
513
|
+
div(class: "wr-resume-strip") do
|
|
514
|
+
div(class: "wr-thread-card") do
|
|
515
|
+
div(class: "wr-tc-label") { phrase("⚡ Active") }
|
|
516
|
+
div(class: "wr-tc-text") { phrase(story[:slug]) }
|
|
517
|
+
end
|
|
518
|
+
div(class: "wr-thread-card beacon-card") do
|
|
519
|
+
div(class: "wr-tc-label") do
|
|
520
|
+
art_glyph(LANTERN_URL, "🏮", style: "height:20px;width:20px;filter:drop-shadow(0 0 8px rgba(245,158,11,.8));")
|
|
521
|
+
phrase(" Resume Point")
|
|
522
|
+
end
|
|
523
|
+
div(class: "wr-tc-title") { phrase(story[:slug]) }
|
|
524
|
+
div(class: "wr-tc-meta") { phrase("🕐 #{relative_time(story[:last_note_at])}") }
|
|
525
|
+
end
|
|
526
|
+
div(class: "wr-thread-card") do
|
|
527
|
+
div(class: "wr-tc-label") { phrase("→ Next Action") }
|
|
528
|
+
div(class: "wr-tc-text") { phrase(story[:next_action] || "(not set)") }
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def render_multi_lane_strip(active_stories)
|
|
534
|
+
div(class: "wr-resume-strip wr-resume-strip--multi") do
|
|
535
|
+
div(class: "wr-thread-card") do
|
|
536
|
+
div(class: "wr-tc-label") { phrase("⚡ #{active_stories.size} active lanes") }
|
|
537
|
+
div(class: "wr-tc-text") { phrase("No single resume point — each lane owns its own story.") }
|
|
538
|
+
end
|
|
539
|
+
active_stories.each do |s|
|
|
540
|
+
div(class: "wr-thread-card") do
|
|
541
|
+
div(class: "wr-tc-label") { phrase("🛤 field-ops") }
|
|
542
|
+
div(class: "wr-tc-title") { phrase(s[:slug]) }
|
|
543
|
+
div(class: "wr-tc-meta") { phrase("🕐 #{relative_time(s[:last_note_at])}") }
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
App.generate.run! if __FILE__ == $0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Running StreamWeaver with Puma-dev
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to run StreamWeaver apps with [Puma-dev](https://github.com/puma/puma-dev), which provides memorable URLs like `http://myapp.test` for your local development.
|
|
4
|
+
|
|
5
|
+
## Quick Comparison
|
|
6
|
+
|
|
7
|
+
| Feature | Standalone (`ruby app.rb`) | Puma-dev (`config.ru`) |
|
|
8
|
+
|---------|----------------------------|------------------------|
|
|
9
|
+
| **Start command** | `ruby app.rb` | `puma-dev link` (once) |
|
|
10
|
+
| **URL** | `http://localhost:4567` (auto-port) | `http://myapp.test` |
|
|
11
|
+
| **Browser** | Opens automatically | Manual (on-demand) |
|
|
12
|
+
| **Multiple apps** | Need separate terminals | All available via `.test` URLs |
|
|
13
|
+
| **Best for** | Quick scripts, one-offs | Development, multiple apps |
|
|
14
|
+
|
|
15
|
+
## Why Use Puma-dev?
|
|
16
|
+
|
|
17
|
+
- **Memorable URLs**: Access your app at `http://[directory-name].test` instead of remembering port numbers
|
|
18
|
+
- **Always Available**: No need to start the app manually - Puma-dev starts it on first request
|
|
19
|
+
- **Multiple Apps**: Run multiple StreamWeaver apps side-by-side with their own URLs
|
|
20
|
+
- **No Browser Auto-open**: Apps run on-demand without opening the browser automatically
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
### 1. Install Puma-dev
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# macOS
|
|
28
|
+
brew install puma/puma/puma-dev
|
|
29
|
+
sudo puma-dev -setup
|
|
30
|
+
puma-dev -install
|
|
31
|
+
|
|
32
|
+
# Linux
|
|
33
|
+
gem install puma-dev
|
|
34
|
+
puma-dev -setup
|
|
35
|
+
puma-dev -install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Create Your App
|
|
39
|
+
|
|
40
|
+
Create a directory for your app with a `config.ru` file:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# config.ru
|
|
44
|
+
require 'bundler/setup'
|
|
45
|
+
require 'stream_weaver'
|
|
46
|
+
|
|
47
|
+
App = app "My StreamWeaver App" do
|
|
48
|
+
header1 "Hello from Puma-dev!"
|
|
49
|
+
text_field :message, placeholder: "Enter a message"
|
|
50
|
+
|
|
51
|
+
if state[:message] && !state[:message].empty?
|
|
52
|
+
text "You said: #{state[:message]}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
run App
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Link to Puma-dev
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd /path/to/your/app
|
|
63
|
+
puma-dev link
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This creates a symlink in `~/.puma-dev/` pointing to your app directory.
|
|
67
|
+
|
|
68
|
+
### 4. Access Your App
|
|
69
|
+
|
|
70
|
+
Open your browser and navigate to:
|
|
71
|
+
```
|
|
72
|
+
http://[directory-name].test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For example, if your directory is named `myapp`, visit `http://myapp.test`.
|
|
76
|
+
|
|
77
|
+
## How It Works
|
|
78
|
+
|
|
79
|
+
When running under Puma-dev:
|
|
80
|
+
|
|
81
|
+
1. **Port Detection**: StreamWeaver detects the `PORT` environment variable set by Puma-dev
|
|
82
|
+
2. **No Auto-browser**: The browser won't auto-open (since `PORT` is set)
|
|
83
|
+
3. **Same Behavior**: All other StreamWeaver features work exactly the same
|
|
84
|
+
|
|
85
|
+
When running normally (`ruby app.rb`):
|
|
86
|
+
|
|
87
|
+
1. **Auto Port**: StreamWeaver finds an available port starting from 4567
|
|
88
|
+
2. **Auto Browser**: Opens your browser automatically
|
|
89
|
+
3. **Self-contained**: No external dependencies
|
|
90
|
+
|
|
91
|
+
## Environment Variable Priority
|
|
92
|
+
|
|
93
|
+
StreamWeaver checks for port in this order:
|
|
94
|
+
1. `options[:port]` - Explicitly passed to `run!`
|
|
95
|
+
2. `STREAMWEAVER_PORT` - Custom StreamWeaver port
|
|
96
|
+
3. `PORT` - Standard port (used by Puma-dev, Heroku, etc.)
|
|
97
|
+
4. Auto-detect - Find available port starting from 4567
|
|
98
|
+
|
|
99
|
+
## Converting Existing Apps
|
|
100
|
+
|
|
101
|
+
To run an existing StreamWeaver app with Puma-dev:
|
|
102
|
+
|
|
103
|
+
1. Create a `config.ru` in your app's directory
|
|
104
|
+
2. Replace `App.run!` with `run App` in `config.ru`
|
|
105
|
+
3. Link to Puma-dev: `puma-dev link`
|
|
106
|
+
4. Access at `http://[directory-name].test`
|
|
107
|
+
|
|
108
|
+
## Troubleshooting
|
|
109
|
+
|
|
110
|
+
**App not accessible?**
|
|
111
|
+
- Check Puma-dev is running: `puma-dev -status`
|
|
112
|
+
- Verify symlink exists: `ls -la ~/.puma-dev/`
|
|
113
|
+
- Check logs: `tail -f ~/Library/Logs/puma-dev.log` (macOS)
|
|
114
|
+
|
|
115
|
+
**Want both modes?**
|
|
116
|
+
Keep your original `app.rb` with `App.run!` for standalone mode, and create a separate `config.ru` with `run App` for Puma-dev mode.
|
|
117
|
+
|
|
118
|
+
## Files in This Example
|
|
119
|
+
|
|
120
|
+
- **`config.ru`** - Puma-dev/Rack configuration (use with `puma-dev link`)
|
|
121
|
+
- **`standalone_app.rb`** - Traditional standalone mode (use with `ruby standalone_app.rb`)
|
|
122
|
+
- **`README.md`** - This file
|
|
123
|
+
|
|
124
|
+
Try both to see the difference!
|