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,239 @@
|
|
|
1
|
+
# learnhotwire.com Full Syllabus vs. StreamWeaver: Coverage Matrix
|
|
2
|
+
|
|
3
|
+
Extends `docs/research/2026-08-17-hotwire-concept-map.md` (13 concepts, still the deeper prose
|
|
4
|
+
treatment for its topics — read it first) from a hand-picked concept list to the **full
|
|
5
|
+
learnhotwire.com course syllabus** (8 sections, ~70 lessons). Reuses that map's findings
|
|
6
|
+
verbatim where a syllabus item maps onto one of its 13 concepts; every row below cites the
|
|
7
|
+
concept-map section number it draws from, or says "new" when the item wasn't covered there.
|
|
8
|
+
|
|
9
|
+
**Since the concept map was written (2026-08-17), the route-tabs epic shipped** (2026-08-21,
|
|
10
|
+
8/8 stories — `cd27596`, `9695b94`, `d3d79e2`, etc., `CHANGELOG.md` Unreleased). This upgrades
|
|
11
|
+
concept-map §11 from "HAVE, narrow gap" to a fuller HAVE: `tabs :key, url: true` gives
|
|
12
|
+
client-side pushState/popstate tab routing with server-authoritative GET parsing, and the old
|
|
13
|
+
`lazy: true` POST-morph tab mode is now deprecated in favor of a future lazy-route-tabs
|
|
14
|
+
replacement (not yet built — see Recommended story additions).
|
|
15
|
+
|
|
16
|
+
**Also found while grounding this matrix, not in the concept map**: `form_for`
|
|
17
|
+
(`lib/stream_weaver/app.rb:598`, shipped 2026-07-10, `af351f3`, "FAC-P3.2a") is a real
|
|
18
|
+
block-yielding, resource-bound form builder — seeds from a record, infers create-vs-update,
|
|
19
|
+
renders fields via `Resource::FieldInput`, validates, and wires submit to
|
|
20
|
+
coerce→validate→store→PRG transition. The concept map's §10 says Rails-style `form_with(model:)`
|
|
21
|
+
binding "is not yet implemented... on the roadmap" — that claim is **stale**; `form_for` is
|
|
22
|
+
that primitive, already shipped and predates the concept map by five weeks. It is real code but
|
|
23
|
+
under-documented: not mentioned in `docs/resource-dsl.md` or `docs/for_llms.md`, only in
|
|
24
|
+
`docs/streamweaver-frontend-vision.md` and an old design doc. Rows below use `form_for`, not
|
|
25
|
+
"missing," wherever the syllabus calls for record-bound forms.
|
|
26
|
+
|
|
27
|
+
**learnhotwire/rails repo**: fetched via `gh api` (WebFetch 404'd — GitHub blocks the plain
|
|
28
|
+
HTML fetch, `gh` reads the API fine). It's Rails' actual "My Todos" app: models
|
|
29
|
+
`Project`/`Todo`/`User`/`Membership`/`Session`; `Todo` uses `acts_as_list scope: :project`
|
|
30
|
+
(drag-reorder), `broadcasts_refreshes_to :project`, and `after_create_commit`/
|
|
31
|
+
`after_update_commit`/`after_destroy_commit` callbacks calling `broadcast_action_later_to`/
|
|
32
|
+
`broadcast_replace_later_to`/`broadcast_remove_to` against a single `_todo.html.erb` partial.
|
|
33
|
+
Stimulus controllers present: `autogrow`, `autosubmit`, `hello` (word-count-from-scratch demo),
|
|
34
|
+
`reset_form`, `sortable` (drag-and-drop), `tabs`/`tabs2`, `todo`, `todolist`, plus a `bridge/`
|
|
35
|
+
dir for native app bridging (out of syllabus scope, not covered below). Parity-demo column
|
|
36
|
+
below is grounded in this actual structure, not invented from the syllabus text alone.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. Turbo Drive
|
|
41
|
+
|
|
42
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| Importmaps with Rails | Rails' zero-bundler JS dependency loading | N/A — StreamWeaver ships zero app-authored JS; all framework JS is inline `<script>`/CDN, no importmap concept exists to replace | N-A-BY-DESIGN | n/a |
|
|
45
|
+
| Enabling Turbo Drive; History pushState | Boosted `<a>`/form nav updates URL+history without full reload | `hx-boost="true"` at `<body>`, reference impl `lib/stream_weaver/views/canvas/reader_layout.erb` (concept map §1); separately, `route_by`/`route_with`/`tabs url:` push `HX-Push-Url`/client `pushState` for state-driven nav (§11) | PARTIAL | Todo app's project switcher (sidebar nav between projects) built with `hx-boost` body wrapper like the canvas-read reader layout, proving it's a documented, copy-pasteable pattern for a fresh app, not just a canvas-only trick |
|
|
46
|
+
| Turbo Page Cache | Instant back/forward via cached prior-page snapshots | None — every nav re-renders from server | MISSING | n/a — StreamWeaver's whole-rerun model has no snapshot cache; back/forward correctness comes from URL-authoritative state re-seeding (§11 HAVE), not a client cache, which is a different (server-truth) strategy, not a missing feature per se |
|
|
47
|
+
| The `data-turbo` Attribute (opt individual links/forms out of Turbo) | Per-element escape hatch from Drive interception | No boosted-nav-by-default exists to opt individual elements out of (see row above); `hx-boost` is opt-in at the body level in the one reference layout, so the equivalent question doesn't arise yet | N/A (contingent on Drive row) | n/a |
|
|
48
|
+
| Turbo Link Prefetching | Hover-triggered background fetch of the link target | None | MISSING | n/a — low priority; StreamWeaver's server-side render cost per nav is the same regardless of prefetch, no htmx `hx-boost`+preload equivalent wired |
|
|
49
|
+
| `data-turbo-confirm`; Turbo Custom Confirm Modal | Native `confirm()` before a destructive action, replaceable with a custom Turbo Stream-inserted dialog | `resource` destroy flow renders an inline warning `alert` + Confirm/Cancel buttons server-side (`docs/resource-dsl.md` "Destroy confirm", `state[:_sw_action] :destroy_confirm`) — same *outcome* (interstitial confirm before destroy), different mechanism (full state transition, not a client-side confirm() intercept) | PARTIAL | Todo delete button routes through `resource :todo`'s built-in destroy-confirm state, rendering an inline "Delete this todo?" alert before calling `store.destroy` — matches the course's custom-confirm-modal lesson's *goal* (no jarring native `confirm()`) via a different, already-shipped path |
|
|
50
|
+
| Disabling Form Buttons; `data-turbo-submits-with` | Auto-disable submit button + swap its label during a request | `hx-disabled-elt="this"` on every button (`adapter/alpinejs.rb:704`) — auto-disables on click, no label-swap equivalent | PARTIAL | Todo "Add" button auto-disables mid-request (already true for every StreamWeaver button); label doesn't change to "Adding…" — would need a small `submits_with:` option |
|
|
51
|
+
| `data-turbo-method` | Turn a plain `<a>` into a non-GET request | Not needed — StreamWeaver actions are POST-only via `button`/form submit, no `<a method=delete>` convention exists to replace | N-A-BY-DESIGN | n/a |
|
|
52
|
+
| `data-turbo-temporary` | Exclude an element from page-cache restoration | N/A — no page cache (see above) | N-A-BY-DESIGN | n/a |
|
|
53
|
+
| `data-turbo-track` | Force full reload when a tracked asset (e.g. compiled CSS) changes | N/A — no client asset bundle to track; theme CSS is served by the running process directly | N-A-BY-DESIGN | n/a |
|
|
54
|
+
| View Transitions | Native `document.startViewTransition()` on Turbo Drive nav for animated page swaps | None — Alpine `x-transition` exists for component-level enter/leave (e.g. `sw-confirmation-bar`, `alpinejs.rb:5419-5424`) but nothing wraps whole-page/whole-fragment swaps in a View Transition | MISSING | n/a — todo add/remove currently swaps instantly; a View Transitions API wrap around fragment morph would be the parity demo, not yet built |
|
|
55
|
+
| Form Redirects And Errors | Rails PRG (post-redirect-get) pattern + re-rendering a form with validation errors | `form_for`'s validate→errors_key→re-render-with-`alert(variant: :error)` path (`app.rb:598-644`) is a direct PRG-equivalent: on success it calls `on_success`/default show transition; on failure it re-renders the same form scope with an error alert, never losing entered values (scope-preserved state) | HAVE | Todo create form: submitting with a blank name re-renders the form with "Name can't be blank" via `form_for`'s built-in `validate:`/errors flow — same course beat (Rails re-renders `new.html.erb` with `@todo.errors`), StreamWeaver's mechanism already shipped |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Turbo Frames
|
|
60
|
+
|
|
61
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| Web Components From Scratch | Turbo Frame is just `customElements.define` — build a minimal one by hand to demystify it | Already dissected in concept map §2 (`alex_turbo_frames_transcript.txt`); StreamWeaver's `fragment` is a server DSL concept, not a client custom element, so there's no equivalent "build it from scratch" teaching moment needed — the mechanism is inherently server-driven | N/A — different mechanism, not a missing capability | n/a |
|
|
64
|
+
| Turbo Frame Inline Editing | Click "Edit", frame swaps to a form, submit swaps back to display, all scoped to one frame | Rails uses two matching-`dom_id` `turbo_frame_tag`s (display + edit) that swap by id; key elements: shared `dom_id`, the edit link living inside the frame it replaces, a narrow nested-resource `url:` for strong-params safety. StreamWeaver does the equivalent via one `fragment("todo-#{id}")` per row branching on `state[:editing_id]`, with `form_for(fields: TITLE_ONLY, record: todo)` inside it — every element rendered inside a fragment auto-scopes to it, so Edit and Save both swap just that row, and `form_for`'s declared `fields:` make the unsafe write unexpressible rather than just discouraged | HAVE | `examples/my_todos/` `/` — click Edit on any of 6 rows, only that row morphs to a form (verified server-side this story: edit fetch 1,327 bytes vs. 108,792-byte full page; save 806 bytes, completed glyph preserved across save) (browser-verified 2026-08-22, main-thread playwright pass complete) |
|
|
65
|
+
| Search With Turbo Frames | Type in a search box, frame updates with filtered results, URL unaffected | Rails names a sibling frame from outside it (`data: {turbo_frame: :todos}`) plus a Stimulus `autosubmit` controller calling `requestSubmit()`; key elements: `data-turbo-frame`, `method: :get` for a cacheable URL, a controller most authors get wrong (`submit()` vs `requestSubmit()`). StreamWeaver's `text_field` auto-submits on keystroke with zero controller — the `autosubmit` chapter collapses to nothing — but has no `data-turbo-frame` equivalent: an input can only target its own enclosing fragment or the whole container, never a sibling. Both arrangements filter correctly (the fragment-scoped param-merge bug the spike found is fixed, `deferred-fragments-src`/InteractionRunner) | HAVE (with a documented trade-off) | `examples/my_todos/` `/search` — arrangement A (field outside, Rails' layout): whole-container swap, 3,184 bytes. Arrangement B (field inside, scoped): 800-834 byte scoped morph. Verified server-side this story: outside "milk" then inside "coffee" in sequence produces zero state bleed (834-byte scoped response shows only "coffee" results) (browser-verified 2026-08-22, main-thread playwright pass complete) |
|
|
66
|
+
| Hovercards With Turbo Frames | Hover over a name, a lazy frame (`loading=lazy`) fetches and shows a card, keyed correctly per-position (dom_id lesson) | Rails uses `turbo_frame_tag todo.user, :hovercard, src:, loading: :lazy` inside a `div.hovercard`; key elements: CSS does the reveal (`display: none` → `:hover` → `block`), `loading="lazy"`'s IntersectionObserver does the fetch (only because the reveal made it visible), fetch-once, and the frame keyed by position (the todo) not content (the user) — the chapter demos keying by user as a live duplicate-id bug. StreamWeaver does the equivalent via `fragment(:"hovercard_#{todo_id}", lazy: true)` inside the same CSS `:hover` wrapper, keyed by `todo_id` per the same rule (llms.txt "Interactive IDs and keying") | HAVE | `examples/my_todos/` `/hover-cards` — verified server-side this story: 0 eager card fetches on initial GET (6 `sw-fragment-lazy` wrappers, `hx-trigger="intersect once"`, zero card content in the response body); booted with `SW_HOVERCARD_DELAY=1.5`, shell GET took 0.085s (was 9.151s eager); individual card POST fetch took 1.517s/1.530s, paid once, per card (browser-verified 2026-08-22, main-thread playwright pass complete) |
|
|
67
|
+
| Infinite Scroll Turbo Frames | Nested lazy frames — each response embeds the next page's already-lazy placeholder frame | Rails nests `turbo_frame_tag :todos_page, @pagy.page` around each page's rows plus a `loading: :lazy` frame for `@pagy.next`, keyed to the next page's URL; key elements: nesting (not replacing) so nothing is ever removed from the DOM, the `@pagy.next` guard so the last page dead-ends cleanly instead of blank-id "Content missing." StreamWeaver does the equivalent via a recursive `scroll_todos_page(n)` helper: page `n`'s rows render inline (in the shell for page 1 — true Turbo shape, corrected 2026-08-22 in dc06bcf after a clean-room skill test caught the earlier all-lazy shape shipping an empty shell), then page `n+1` is declared as a nested `fragment(:"todos_page_#{n+1}", lazy: true)` with an emptiness guard as the `@pagy.next` equivalent; nested ids `parent--child` per the framework's convention | HAVE | `examples/my_todos/` `/infinite-scroll` — verified server-side this story: walked all 6 pages via curl, payload sizes 809/877/934/993/1060/371 bytes (growth is only the lengthening nested-id string, not accumulated rows — O(1) per page vs. the spike's O(n) click-driven design), page 6 terminates with no further nested placeholder (browser-verified 2026-08-22, main-thread playwright pass complete) |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Turbo Streams
|
|
72
|
+
|
|
73
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
74
|
+
|---|---|---|---|---|
|
|
75
|
+
| Turbo Streams From Scratch; Turbo Stream HTTP Responses | Server response carries `<turbo-stream action=... target=...>` ops, from a normal controller action | Fragment `updates:` OOB swaps resolve within the same request/response cycle (§7 HAVE) | HAVE | Adding a todo returns the new row (`append`) plus an updated count badge (`replace`) in one action response — this is exactly fragment `updates: %i[todo_list count_badge]` today |
|
|
76
|
+
| Turbo Stream From Websockets; `broadcasts_to` and `turbo_stream_from` | Model-level broadcast wired to a subscribed channel, independent of any one request | `Streamer` (`lib/stream_weaver/streamer.rb`, ACTIONS: replace/append/prepend/remove/add_class/remove_class) over SSE, but only reachable from `every(seconds)` timer blocks, not from arbitrary model/store code (§7, §8 PARTIAL) | PARTIAL | A second browser tab open on the same todo list does NOT see a todo added from tab one until an `every` timer polls and re-diffs — Rails' model-callback broadcast has no StreamWeaver equivalent triggerable from `PostStore.create` itself |
|
|
77
|
+
| Broadcastable Module Source | Rails source-dive into how `broadcasts_to` is implemented | N/A — source-code-literacy lesson about Rails internals; StreamWeaver's `Streamer` source (`streamer.rb`) is the equivalent artifact to point an agent at, but there's no course-shaped "read the source" lesson need here | N/A — meta lesson, not a capability | n/a |
|
|
78
|
+
| `broadcasts_refreshes`; My Todos `broadcasts_refreshes` | Simplest broadcast mode: just tell subscribers "something changed here, go refetch the frame" (no diffing) | No equivalent — the closest primitive (`every` + `streamer.replace`) always pushes computed HTML, never a bare "refresh yourself" signal | MISSING | n/a — a `refresh_fragment(:todo_list)` SSE action that just triggers the client to re-GET that fragment (rather than computing+pushing HTML server-side) doesn't exist; would be a cheap, small primitive to add |
|
|
79
|
+
| Morph And Scroll Preservation | `refreshes_morph` mode uses Idiomorph so a full-list refresh doesn't reset scroll/focus | StreamWeaver already runs every request/action response through `alpine-morph`/Idiomorph (`HTMX_SWAP = "morph:innerHTML"`, `adapter/alpinejs.rb:29`) — this is the *default* swap strategy, not a special mode | HAVE (arguably ahead — it's the default, not opt-in) | Scrolling mid-list, then adding a todo via the form, doesn't jump scroll position or blur the active input — true today by default via morph swap, no special configuration needed |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Stimulus
|
|
84
|
+
|
|
85
|
+
Concept map §9 already gives the architectural verdict (Alpine `x-data` is the chosen substitute,
|
|
86
|
+
PARTIAL by design, no enforced lifecycle-cleanup guardrail). This section maps each specific
|
|
87
|
+
lesson topic onto that verdict rather than re-deriving it.
|
|
88
|
+
|
|
89
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
90
|
+
|---|---|---|---|---|
|
|
91
|
+
| Autosubmit (`autosubmit_controller.js`) | A form auto-submits on any field change | This is StreamWeaver's *default* behavior for every input, not an opt-in controller (`docs/for_llms.md` "ALL input components auto-submit on change") | HAVE — StreamWeaver is ahead here, it's the default not a bolt-on | n/a, already true everywhere |
|
|
92
|
+
| Hotwire Spark and Autogrow Textarea (`autogrow_controller.js`) | Textarea grows with content via a small Stimulus controller | No built-in `autogrow:` option found on `text_area`; would need a one-line inline Alpine `x-data` per the framework's own convention, or a registered component | MISSING (cheap) | Todo notes `text_area` grows with typed content — buildable today as one inline Alpine snippet per `docs/streamweaver-frontend-vision.md`'s stated Stimulus→Alpine substitution, just not shipped as a component option |
|
|
93
|
+
| Mutation And Intersection Observers | Low-level browser APIs Stimulus controllers wrap | Used internally in framework JS (`sw-mermaid-zoom.js`, `sw-sidebar-toc.js` — both `IntersectionObserver`/`MutationObserver` hits found in `lib/stream_weaver/assets/js/`) for scroll-spy/zoom features, but no DSL-level exposure for app authors to hang their own observer logic off | PARTIAL | n/a — the primitives exist in the codebase for framework features, not as an author-facing hook |
|
|
94
|
+
| Turbo Events With Stimulus | Listening for `turbo:*` lifecycle events from a controller | htmx equivalent events (`htmx:afterSwap`, etc.) are used internally (`for_llms.md`'s reader-layout pattern step 5: "Update active-link highlights via `htmx:afterSwap` listener") and are a documented pattern for app authors to hook into directly in inline Alpine/JS | HAVE | The reader layout's active-nav-highlight pattern (already documented) is itself a "listen for the framework's swap event" demo |
|
|
95
|
+
| Wrapping Third-Party Libraries; Morphing With Third-Party Libraries | Stimulus controller wraps e.g. a date picker, survives morph via `data-turbo-permanent`-style opt-out | `StreamWeaver.register_component` (`component_registry.rb`, concept map §9) lets an author wrap a Phlex-backed component as a real DSL verb; morph-survival for third-party JS state is the alpine-morph "save and restore manually" pattern already used internally (`adapter/alpinejs.rb:1044-1058` comments) but not documented as an author-facing recipe | PARTIAL | n/a — the registration mechanism exists; a worked "wrap a date picker, preserve its state across morph" doc example doesn't |
|
|
96
|
+
| Targets; Values; Value Changed Callbacks; Actions; Keyboard Events; Action Params; CSS Classes; Default CSS Classes; Bound Functions And Event Listeners; Controller Inheritance; Target Callbacks; Outlets | The full Stimulus API surface: typed properties, declarative event wiring, controller-to-controller communication | All of this maps to inline Alpine's own primitives (`x-data`, `@click`, `x-model`, `$dispatch`/`$watch` for outlet-like communication) rather than a StreamWeaver-specific API — StreamWeaver doesn't wrap or replace Alpine's API, it just inlines Alpine declarations into server-rendered markup | N-A-BY-DESIGN (StreamWeaver defers entirely to Alpine's own vocabulary here, doesn't reimplement it) | n/a — no StreamWeaver-specific demo needed; any Alpine tutorial covers this 1:1 |
|
|
97
|
+
| Turbo Morph Event | `turbo:morph-element` fires per morphed element, lets a controller re-init state that morph doesn't preserve | htmx's `htmx:afterSwap`/Alpine's own re-init are used the same way internally (`adapter/alpinejs.rb:1058` "afterSettle: alpine-morph initializes newly-morphed-in elements") but not documented as an author hook | PARTIAL | n/a |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 5. More Hotwire
|
|
102
|
+
|
|
103
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
104
|
+
|---|---|---|---|---|
|
|
105
|
+
| Cleaning Up The Edit View | Refactoring pass consolidating duplicated frame/form logic | N/A — course pacing/refactoring lesson, no distinct capability | N/A | n/a |
|
|
106
|
+
| Turbo Morph Internals with Idiomorph | How Idiomorph's diffing algorithm actually works | StreamWeaver already depends on Idiomorph directly (bd `stream_weaver-2ds`, per `for_llms.md` "Future direction" note) via `htmx-ext-alpine-morph` — same library, same mechanism, not a gap | N/A — shared dependency, no porting needed | n/a |
|
|
107
|
+
| Custom Model `broadcasts_to` | Hand-writing the broadcast callback Rails' `broadcasts_to` macro generates | Same gap as §3 above (no model-callback-triggered push) | MISSING (same root cause as §3) | n/a |
|
|
108
|
+
| Debugging Turbo Frames | Chrome devtools techniques for diagnosing frame mismatches | `SW_DEBUG=1` env var logs cookie/state size + SSE connections (`for_llms.md` "Debug logging") — general debug aid, not frame-specific since fragments don't have Turbo's ID-mismatch failure mode (concept map §4: self-heal via `HX-Retarget` instead) | N/A — different failure mode by design (§4) | n/a |
|
|
109
|
+
| Custom Turbo Stream Actions | Registering a new verb beyond replace/append/prepend/remove (e.g. a `notify` action) | `Streamer::ACTIONS` is a fixed `%i[replace append prepend remove add_class remove_class]` constant (`streamer.rb:17`) — no registration mechanism for a new action | MISSING (cheap-ish — small, additive change) | n/a — a hypothetical `streamer.toast(message)` custom action would need `ACTIONS` to become extensible |
|
|
110
|
+
| Custom Layouts With Turbo Frame Support | A layout that conditionally omits chrome when rendering inside a frame request | `layout:` app option (`:default`/`:wide`/`:full`/`:fluid`) is static per-app, not conditional on request type; fragments already omit outer chrome by construction (only the fragment's HTML is returned), so the underlying need (partial responses don't carry full layout) is already met structurally | N/A — met by fragment architecture, not by a layout-conditional feature | n/a |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 6. Testing Hotwire
|
|
115
|
+
|
|
116
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
117
|
+
|---|---|---|---|---|
|
|
118
|
+
| Integration Tests with Turbo Stream Responses / Turbo Frames / Turbo Broadcasts | Rack::Test-style assertions on stream/frame HTTP responses | `docs/testing.md`'s Rack::Test pattern (`post '/action/btn_...'`, inspect `last_response.body`) covers the response-shape-assertion case directly; no broadcast-specific ("assert a Streamer push happened") helper documented | PARTIAL | A spec posting to the todo "Add" action and asserting the response body contains the new row's fragment HTML — already a documented pattern (`docs/testing.md`); asserting an SSE `Streamer` push occurred has no documented helper |
|
|
119
|
+
| System Tests with Turbo Streams / Turbo Broadcasts / Infinite Scroll | Capybara/browser-driven tests of live-update behavior | `bin/smoke` is StreamWeaver's real-HTTP UAT battery (boots real servers, drives via `Net::HTTP`) but doesn't drive a real browser/JS; the `route-tabs.feature` "browser-verification" scenario shows the actual pattern used for JS-dependent behavior: playwright-cli, run from the main thread only, with screenshots as evidence (not an automated spec suite) | PARTIAL | `bin/smoke` extended with a todo-app fixture proves the HTTP-level contract; true "watch it live-update in a real browser" verification is playwright-cli-driven and manual/one-off per feature, same as route-tabs shipped it |
|
|
120
|
+
| Stimulus Drag and Drop System Test | Testing a `sortable_controller.js`-style reorder interaction end-to-end | No drag-and-drop exists in StreamWeaver at all (see §4 Wrapping Third-Party Libraries, and Kanban board explicitly documented as "No drag-and-drop" — `components.rb:808`) | MISSING (blocked on drag-and-drop capability itself) | n/a |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 7. Modal Dialogs
|
|
125
|
+
|
|
126
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
127
|
+
|---|---|---|---|---|
|
|
128
|
+
| Inserting the Dialog Element with Turbo Streams | A Turbo Stream response appends a `<dialog>` element into the DOM | `modal :key do ... end` (`app.rb:1083`) renders unconditionally in the initial page (a `div`-based overlay, `render_modal`, `adapter/alpinejs.rb:2633`), toggled by `state[:key_open]` — not inserted via a stream action, but present-and-toggled achieves the same visible outcome | PARTIAL — different insertion mechanism, same visible result | Todo "New Todo" modal: `modal :new_todo_modal do form_for(:todo) ... end`, opened by a button setting `state[:new_todo_modal_open] = true` — this exists today as a working pattern (`docs/for_llms.md` "Modal" section) |
|
|
129
|
+
| Opening the Dialog modally with Stimulus | JS calls `.showModal()` for real modal (focus-trap, `::backdrop`, Escape) semantics | StreamWeaver's modal is a styled `div` overlay with Alpine `@click.outside`/`@keydown.escape.window` handlers (`alpinejs.rb:2484-2485, 2653`), not a native `<dialog>` + `showModal()` — no built-in focus trap or `::backdrop` pseudo-element. Notably, the *mermaid fullscreen viewer* (`alpinejs.rb:3368-3398`) **does** use a real `<dialog>` with `showModal()`, proving the pattern is known and used elsewhere in the codebase, just not for the general-purpose `modal` component | PARTIAL — real `<dialog>` pattern exists in the codebase but isn't used for `modal` itself | n/a — porting `modal` to native `<dialog>`/`showModal()` would pick up free focus-trap/Escape/`::backdrop` behavior; today's div-based modal hand-rolls Escape but not focus-trap |
|
|
130
|
+
| Embedding Forms in Dialogs with Turbo Frames | A form lives inside a frame inside a dialog, so submit updates the dialog in place without closing it | `form_for` (or `form`) inside a `modal` block is exactly this composition, already working (see resource-dsl.md's default `new_view :modal`) | HAVE | `resource :todo` with default `new_view :modal` renders the create form inside a modal, submits, and either shows validation errors in-place (form_for re-render) or closes+transitions on success — this is the shipped default, not a custom build |
|
|
131
|
+
| Close Dialog Buttons | Explicit close button separate from Escape/backdrop-click | `render_modal_close_button` (`alpinejs.rb:2694`) — shipped | HAVE | Todo modal's × close button — already exists on every `modal` |
|
|
132
|
+
| Dialog Close Event cleanup | Native `<dialog>`'s `close` event fires cleanup (e.g. clearing form state) | No native `<dialog>` `close` event to hook (see above — div-based, not `<dialog>`-based); state clearing on close is whatever the author wires into the close button's callback manually | PARTIAL | n/a |
|
|
133
|
+
| Redirecting Out Of Turbo Frame Server-side | Server tells a frame "actually, navigate the whole page, not just this frame" (`turbo_frame: "_top"`) | `HX-Retarget: #app-container` full-container fallback (concept map §4) is architecturally the same move — server overrides the client's intended narrow target — but triggered by stale-action-ID mismatch, not by deliberate author intent to "break out" of a modal/frame on success | PARTIAL | A `resource` form's on-success transition (e.g. create todo → show todo) already re-renders the whole `#app-container`, which is StreamWeaver's version of "breaking out" since there's no narrower target to begin with in the whole-rerun model |
|
|
134
|
+
| Closing The Dialog And Updating The Page | Successful dialog submit both closes the dialog and updates the underlying list/page in one round trip | `form_for`'s `on_success` + `resource`'s modal-close-and-transition default (`docs/resource-dsl.md`) — submitting the "New Todo" modal form both closes the modal and updates the todo list fragment via `updates:`, in the same response | HAVE | Already the `resource :todo, ... new_view: :modal` default behavior — submit closes modal + list updates, one request |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 8. Source Code Walkthroughs
|
|
139
|
+
|
|
140
|
+
| Course lesson(s) | What it teaches | StreamWeaver equivalent today | Status | Parity demo |
|
|
141
|
+
|---|---|---|---|---|
|
|
142
|
+
| Turbo.js Overview; Turbo Frame Internals; Turbo Stream Internals; Turbo Stream Source Internals | Reading Turbo's own source to understand its mechanisms | N/A — meta/pedagogical lessons about a specific library's source, not a capability comparison. StreamWeaver's equivalent artifacts for an agent/author to read are `lib/stream_weaver/streamer.rb`, `lib/stream_weaver/interaction_runner.rb`, `lib/stream_weaver/adapter/alpinejs.rb` — already the canonical "read the source" targets, no porting needed | N/A | n/a |
|
|
143
|
+
| Trix Morphing With Web Components | How a specific rich-text-editor web component survives Turbo morphing | N/A — no rich-text-editor component exists in StreamWeaver to have this problem; would only become relevant if/when one is added | N/A | n/a |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Not covered by the original 13 concepts
|
|
148
|
+
|
|
149
|
+
Every syllabus item the concept map missed or compressed away, with a one-line verdict on
|
|
150
|
+
whether it matters for StreamWeaver:
|
|
151
|
+
|
|
152
|
+
1. **Importmaps / zero-bundler JS** — doesn't matter; StreamWeaver has no client build step at all, more radical than Rails' answer, not a gap.
|
|
153
|
+
2. **Turbo Page Cache** — matters a little; back/forward is still correct (URL-authoritative re-seed), just never instant-from-cache. Low priority — server-render cost, not correctness, is the trade.
|
|
154
|
+
3. **`data-turbo-track` / `data-turbo-temporary`** — doesn't matter; no asset bundle, no page cache, nothing to track or exempt.
|
|
155
|
+
4. **Turbo Link Prefetching** — matters a little for perceived latency on nav-heavy apps; not currently discussed anywhere in the repo.
|
|
156
|
+
5. **View Transitions API** — matters cosmetically; the concept map's morph coverage (§2, §7) covers *what* updates but not animated transitions between states.
|
|
157
|
+
6. **`data-turbo-submits-with` (button label swap during request)** — small, matters for polish; disabling exists (`hx-disabled-elt`), label-swap doesn't.
|
|
158
|
+
7. **Hovercards / infinite scroll (visibility-lazy nested frames)** — matters significantly; concept map §6 named the underlying primitive gap (no `loading=lazy` equivalent) but didn't name these two concrete, common use cases that depend on it.
|
|
159
|
+
8. **`broadcasts_refreshes` (bare "go refetch yourself" signal, no diffing)** — matters; StreamWeaver's `Streamer` always computes+pushes HTML, has no cheap "just refresh" mode. This is a real, previously-unnamed gap, distinct from §7/§8's "timer-triggered only" framing.
|
|
160
|
+
9. **Model-callback-triggered broadcasts (`broadcasts_to`, `broadcast_action_later_to` from `after_create_commit` etc.)** — matters significantly; concept map §7/§8 named "timer-triggered only, not broadcast-from-anywhere" but the syllabus's actual `Todo` model example (fetched above) makes concrete exactly how far that gap reaches: a StreamWeaver `PostStore.create` has no way to push a live update to *other* open sessions at all, only the process's own `every` timers can.
|
|
161
|
+
10. **`form_for` already covers most of what the concept map's §10 called "not yet implemented"** — this is a *correction*, not a gap: the concept map is stale on this specific point.
|
|
162
|
+
11. **Native `<dialog>`/`showModal()` for the general-purpose `modal` component** — matters for accessibility (focus trap) — not named in concept map at all; ironically the codebase already uses real `<dialog>` for the mermaid fullscreen viewer, so precedent exists internally.
|
|
163
|
+
12. **Custom Turbo Stream actions (extensible verb registration)** — small but real gap, not named in the concept map; `Streamer::ACTIONS` is a closed list.
|
|
164
|
+
13. **Drag-and-drop (Stimulus `sortable_controller.js` / `acts_as_list`)** — matters for the todo-app parity story specifically since reordering is core to "My Todos"; concept map only mentioned this in passing re: Kanban ("No drag-and-drop... 03 gap"), not as its own syllabus-driven line item.
|
|
165
|
+
14. **Dialog close-event cleanup** — small, follows from the native-`<dialog>` gap above.
|
|
166
|
+
15. **Autogrow textarea, autosubmit-as-Stimulus-controller** — autosubmit is a non-issue (StreamWeaver does this by default, arguably ahead); autogrow textarea is a small missing component option, not previously named.
|
|
167
|
+
16. **Testing Turbo Broadcasts / drag-and-drop system tests** — both blocked on the capability gaps above (broadcast-from-anywhere, drag-and-drop), so not independently actionable until those ship.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Recommended story additions
|
|
172
|
+
|
|
173
|
+
Beyond the seven already-known items (four-features spike, strict-ids keying, deferred
|
|
174
|
+
fragments, visibility-lazy fragments, dev-loud-failure overlay, four-features rebuild,
|
|
175
|
+
streamweaver-way skill), full-syllabus parity surfaces these:
|
|
176
|
+
|
|
177
|
+
**Cheap (docs/demo only — capability already exists)**
|
|
178
|
+
|
|
179
|
+
- Document `form_for` in `docs/resource-dsl.md` and `docs/for_llms.md` — it already delivers the concept map §10 "missing" capability; this is a documentation debt, not a build.
|
|
180
|
+
- Document the native-`<dialog>` pattern from the mermaid fullscreen viewer as the recommended recipe for authors who want real focus-trap/`::backdrop` modals, alongside the existing div-based `modal` DSL.
|
|
181
|
+
- Ship a companion "My Todos" example app (`examples/todo_app/` or similar) exercising `resource`, `form_for`, `tabs url:`, fragment `updates:`, and `every`-driven live refresh together — this is the single highest-leverage cheap item since it becomes the concrete parity-demo target for every row above, replacing "n/a — would need" with a real running app.
|
|
182
|
+
- Small `submits_with:`-style button option (label swap during request) — additive to existing `hx-disabled-elt` wiring.
|
|
183
|
+
|
|
184
|
+
**Real builds**
|
|
185
|
+
|
|
186
|
+
- **Broadcast-from-anywhere** — let `Streamer` be pushed to from arbitrary code (a `resource` store callback, not just `every` timer blocks), the direct fix for gaps #8/#9 above (`broadcasts_to`/`broadcast_action_later_to` equivalents). This also requires fixing the tracked session-scoping leak bug (concept map §7) first, since broadcast-from-anywhere makes that bug's blast radius worse, not smaller.
|
|
187
|
+
- **`refresh_fragment` bare-refresh Streamer action** — small addition to `Streamer::ACTIONS`/client, the `broadcasts_refreshes` equivalent (gap #8).
|
|
188
|
+
- **Extensible `Streamer::ACTIONS`** — a registration mechanism analogous to `ComponentRegistry` but for custom stream verbs (gap #12).
|
|
189
|
+
- **Drag-and-drop primitive** — a `sortable:` option on lists/tables/Kanban lanes wrapping a small Alpine/SortableJS-equivalent, wired to a reorder action — the single largest missing piece for a faithful "My Todos" port, since `acts_as_list`-driven reordering is core to the course's actual app.
|
|
190
|
+
- **Lazy route tabs** — already named as the deprecation target for `tabs lazy: true` (route-tabs epic's `deprecate-lazy-post-morph` story) but not yet designed/built; should fold in visibility-lazy semantics (not just click-lazy) to also cover the hovercard/infinite-scroll gap (#7) via the same underlying primitive as deferred/visibility-lazy fragments.
|
|
191
|
+
- **Autogrow textarea option** — small, standalone, doesn't block anything else.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Coverage counts
|
|
196
|
+
|
|
197
|
+
| Status | Count |
|
|
198
|
+
|---|---|
|
|
199
|
+
| HAVE | 15 |
|
|
200
|
+
| PARTIAL | 20 |
|
|
201
|
+
| MISSING | 9 |
|
|
202
|
+
| N-A-BY-DESIGN / N/A | 21 |
|
|
203
|
+
| **Total rows** | **65** |
|
|
204
|
+
|
|
205
|
+
Counted across all eight section tables above (one row per syllabus lesson-cluster). Updated
|
|
206
|
+
2026-08-22 (`my-todos-zero-js`): Turbo Frames' Hovercards and Infinite Scroll rows moved
|
|
207
|
+
MISSING → HAVE (`visibility-lazy-fragments` + `deferred-fragments-src` closed both), and Inline
|
|
208
|
+
Editing consolidated from a split HAVE/PARTIAL into a clean HAVE (the raw-`fragment` path now
|
|
209
|
+
delivers the row-scoped swap `resource` alone didn't). Roughly a third of the full syllabus is
|
|
210
|
+
architecturally N/A (no bundler, no page cache, Alpine's own API surface standing in 1:1 for
|
|
211
|
+
Stimulus's), and the PARTIAL bucket — still the largest — is dominated by two repeated root
|
|
212
|
+
causes: (1) push/broadcast is timer-triggered-only rather than triggerable from arbitrary server
|
|
213
|
+
code, and (2) the div-based `modal` hasn't picked up the native-`<dialog>` pattern already proven
|
|
214
|
+
elsewhere in the same codebase.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Epic ownership roadmap (added 2026-08-22)
|
|
219
|
+
|
|
220
|
+
Every non-N/A row above is owned by one Tyrion epic. The arc is chained: each pending
|
|
221
|
+
epic's first story (`shape-epic`) is blocked with its unlock condition, and its job is
|
|
222
|
+
to re-verify the owned rows and fill placeholder criteria before any build. An
|
|
223
|
+
orchestrating session reads `tyrion status` / `tyrion epic show <slug>` to see where
|
|
224
|
+
the arc stands.
|
|
225
|
+
|
|
226
|
+
| Epic (Tyrion slug) | Owns syllabus sections/rows | Status |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| streamweaver-way | Turbo Frames (all rows); cross-cutting: strict-ids keying, deferred/eager fragments, visibility-lazy, dev-loud failure, form_for docs; Way skill | ACTIVE (6/8 done, `my-todos-zero-js` in progress) |
|
|
229
|
+
| turbo-streams-parity | Turbo Streams (all non-N/A rows): broadcast-from-anywhere, refresh-fragment (broadcasts_refreshes), extensible stream actions, morph/scroll preservation | pending — blocked on streamweaver-way + now-view session-scoped-broadcast fix |
|
|
230
|
+
| modal-dialogs-parity | Modal Dialogs (all rows): native `<dialog>` modal, modal form flows, dialog lifecycle | pending — blocked on turbo-streams-parity |
|
|
231
|
+
| stimulus-role-parity | Stimulus rows not N-A-by-design: sortable drag-and-drop, submits-with label swap, autogrow textarea, Alpine lifecycle guardrails | pending — blocked on modal-dialogs-parity |
|
|
232
|
+
| (unowned, deliberate) | Turbo Drive attribute family rows marked PARTIAL (confirm/prefetch/temporary/track/view transitions) | folded into whichever epic's shape-epic re-verification finds them load-bearing; otherwise remain documented PARTIALs |
|
|
233
|
+
| now-view-support (drafted, un-imported) | session-scoped-broadcast (SSE leak) — gate for turbo-streams-parity | separate draft epic, imports on its own track |
|
|
234
|
+
|
|
235
|
+
Verification convention: as each epic completes, its owned rows gain a verified parity
|
|
236
|
+
entry in this file, in the format "Rails uses <mechanism> for <feature>, key elements
|
|
237
|
+
<X>; StreamWeaver does equivalent via <Y>" — the `my-todos-zero-js` story does this
|
|
238
|
+
for the Turbo Frames rows, and each later epic's closing story does the same for its
|
|
239
|
+
own rows. This file is the living answer to "are we matching everything."
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Frontend-Only Compatibility Matrix
|
|
2
|
+
|
|
3
|
+
**Spike:** disc-093 — which StreamWeaver components survive a backend-less context, and how do they fail.
|
|
4
|
+
**Status:** code-verified + server-verified. Cells needing a live browser are listed at the bottom.
|
|
5
|
+
**Date:** 2026-08-22
|
|
6
|
+
|
|
7
|
+
**Update 2026-08-23:** this is the spike-time snapshot, kept intact for history. Five defects it documents are now fixed (chart-export-allowlist, reader-dead-controls, checkbox-group-array, canvas-action-parity, deck-honest-ui — canvas-safe epic). Rows whose verdicts changed are marked `SUPERSEDED 2026-08-23` in place below, with the original text left untouched. For post-fix reality, the corrected compatibility table, and the before/after story for each fix, see [`docs/frontend-only.md`](../frontend-only.md).
|
|
8
|
+
|
|
9
|
+
## The three contexts
|
|
10
|
+
|
|
11
|
+
| Ctx | Name | What's behind it | Adapter mode | htmx? | Alpine? |
|
|
12
|
+
|---|---|---|---|---|---|
|
|
13
|
+
| A | Live canvas page | `Canvas::BridgeServer` + WebSocket, agent optionally listening on `canvas-wait` | `:websocket` | yes | yes |
|
|
14
|
+
| B | canvas-read doc | `Canvas::Reader` render-only server, no app session | `:websocket` | yes | yes |
|
|
15
|
+
| C | Exported static HTML | nothing | `:http` | **no** | only if `x-data` present |
|
|
16
|
+
|
|
17
|
+
Verdicts used below:
|
|
18
|
+
|
|
19
|
+
- **WORKS** — full intended function.
|
|
20
|
+
- **DEGRADES** — reduced but honest; the user can see that less is on offer.
|
|
21
|
+
- **SILENTLY-DEAD** — renders, looks interactive, does nothing. The dangerous class.
|
|
22
|
+
- **BREAKS** — errors, blanks, or visibly wrong output.
|
|
23
|
+
|
|
24
|
+
## Mechanisms
|
|
25
|
+
|
|
26
|
+
Nine rules generate every cell in this document. Read these and you can predict rows that aren't listed.
|
|
27
|
+
|
|
28
|
+
**M1 — `hx-post` is emitted in every context; almost nothing serves it.**
|
|
29
|
+
`Adapter::AlpineJS` calls `htmx_attrs` from 21 sites. Only **four** places in the entire adapter consult `websocket_mode?` for behavior (`render_radio_group`, `button_attrs`, `container_attributes`, `cdn_scripts` — `lib/stream_weaver/adapter/alpinejs.rb:570,691,753,824`), plus tabs at `:2386`. Every other `hx-post` renders identically in all three contexts. In A and B htmx **is** loaded, so the request fires and 404s. In C htmx is deliberately never loaded (`html_exporter.rb:307-313`), so the request never fires at all — no error, no network entry, nothing in the console. **C is the quietest failure mode of the three.**
|
|
30
|
+
|
|
31
|
+
> `SUPERSEDED 2026-08-23`: as of canvas-action-parity, the count of `websocket_mode?`-aware call sites is no longer 4 — five more sites (`clickable`, `menu_item`, form submit, `tag_buttons`, `chip_group`) were ported to `sendEvent`, and a coverage spec now inventories all call sites and their declared disposition. Seven sites remain unfixed (disc-106, see `docs/frontend-only.md`). The core rule (htmx renders everywhere, C is the quietest failure mode) still holds for those seven.
|
|
32
|
+
|
|
33
|
+
Verified route surface (all 404, live servers):
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
POST /canvas/<session>/update 404 (bridge has only /canvas/:name, /poll, /save-doc, /event)
|
|
37
|
+
POST /canvas/<session>/action/<id> 404
|
|
38
|
+
POST /canvas/<session>/form/<name> 404
|
|
39
|
+
POST /canvas/reader/update 404 (reader has only /, /browse, /open, /export, /save-doc, /delete-doc, /health)
|
|
40
|
+
POST /deck/select 404
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**M2 — `sendEvent` is defined only by `cdn_scripts` in websocket mode.**
|
|
44
|
+
`window.sendEvent` / `window.getFormState` live inside `websocket_init_script` (`alpinejs.rb:833-1019`), reached only via `adapter.cdn_scripts`. The bridge includes it (`bridge_server.rb:262`). **The reader deliberately does not** — see the comment at `reader.rb:683-686`: it keeps `mode: :websocket` for markup parity but omits `cdn_scripts` to avoid a doomed WebSocket connect. Result, measured on a live reader page: `sendEvent(` is **called 3 times and defined 0 times**. Every canvas-mode control in context B throws `ReferenceError` on click. The exporter renders in `:http` mode, so it never emits `sendEvent` at all.
|
|
45
|
+
|
|
46
|
+
> `SUPERSEDED 2026-08-23`: reader-dead-controls added an `inert:` adapter flag. The reader now renders `sendEvent`-calling controls as honestly disabled (with a title) instead of emitting the `@click`/`@change` handler at all — the `ReferenceError` this mechanism describes no longer happens for `button`, `radio_group`, or the five sites canvas-action-parity ported. See `docs/frontend-only.md`.
|
|
47
|
+
|
|
48
|
+
**M3 — Alpine-owned state works everywhere; app-state binding works nowhere.**
|
|
49
|
+
Components that declare their **own** `x-data` (collapsible, tabs, dropdown, copy_button, sortable table, theme_toggle, expandable_card, slide_container, timeline_event) are self-contained and behave identically in A, B and C. Components that emit a bare `x-model="key"` expect an enclosing scope holding that key — and none of the three contexts provides one:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
A <div id="app-container" x-data='{"wsConnected":false,"wsReconnecting":false}' hx-ext='alpine-morph'>
|
|
53
|
+
B <div id="app-container" data-sw-body-class="..."> # no x-data at all
|
|
54
|
+
C <div id="app-container"> # no x-data at all
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A's container has an `x-data` but it holds only the two WebSocket flags, never the pushed state keys (canvas pushes render with `state = {}`).
|
|
58
|
+
|
|
59
|
+
**M4 — Adapter-inlined JS travels; CDN-referenced JS does not.**
|
|
60
|
+
Anything the adapter writes with `view.script { raw ... }` is embedded in the HTML and survives into an export and past a CSP that blocks external hosts: `sw-mermaid-zoom.js`, `sw-route-tabs.js`, `sw-copy.js`, `sw-slide-nav.js`, `sw-keyboard.js`, `sw-sidebar-toc.js`, the deck selection JS, and the `chart` component's own Chart.js lazy loader. Anything referenced by URL dies under a CSP-locked viewer: Alpine, Prism (js + css), mermaid's ESM module, Google Fonts. `--offline` inlines **mermaid only** — not Alpine, not Prism, not Chart.js. An export whose interactivity rests on Alpine (tabs, collapsible, dropdown, theme toggle) is therefore **BREAKS**, not DEGRADES, in a viewer that blocks `cdn.jsdelivr.net`: `x-cloak` panels never un-hide.
|
|
61
|
+
|
|
62
|
+
**M5 — the exporter's Chart.js allowlist has drifted.**
|
|
63
|
+
`components_include?(Components::Chart)` (`html_exporter.rb:341`) gates the Chart.js CDN tag. But every shorthand chart DSL builds a `ChartBase` subclass, **not** `Components::Chart`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
bar_chart, hbar_chart -> Components::BarChart is_a?(Chart) = false
|
|
67
|
+
line_chart, area_chart,
|
|
68
|
+
sparkline -> Components::LineChart is_a?(Chart) = false
|
|
69
|
+
pie_chart, doughnut_chart -> Components::PieChart is_a?(Chart) = false
|
|
70
|
+
stacked_bar_chart -> Components::StackedBarChart is_a?(Chart) = false
|
|
71
|
+
chart -> Components::Chart is_a?(Chart) = TRUE
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
So an export containing `bar_chart` ships **no Chart.js**. The emitted markup is `x-init="if (typeof Chart !== 'undefined') { new Chart(...) }"` — the guard swallows the failure, leaving a chart-shaped empty bordered box with no console error. This is the exact drift the method's own comment claims was fixed for Alpine ("an allowlist here would silently drift every time one of them changes"); the Chart branch still has it.
|
|
75
|
+
|
|
76
|
+
> `SUPERSEDED 2026-08-23`: fixed by chart-export-allowlist. The gate now checks `components_include?(Components::Chart) || components_include?(Components::ChartBase)` (`html_exporter.rb:341-350`) — the whole family, proven against an anonymous `ChartBase` subclass the gate has never seen. Every shorthand chart method ships Chart.js in exports now. See `docs/frontend-only.md`.
|
|
77
|
+
|
|
78
|
+
**M6 — `url: true` tabs invert between canvas and export.**
|
|
79
|
+
`route_tabs? = component.url && !websocket_mode?` (`alpinejs.rb:2421`). On canvas (A and B) route tabs **degrade** to plain client tabs and log one warning per render pass to the agent's stderr — the human never sees it. In an **export** they are real route tabs: `sw-route-tabs.js` is inlined and `swRouteTabs.push()` runs. Route tabs are the only feature in this matrix that is *more* functional in a static export than on the live canvas.
|
|
80
|
+
|
|
81
|
+
**M7 — inlined JS that calls server-only routes.**
|
|
82
|
+
The deck selection engine hardcodes `fetch('/deck/select')`, `/deck/note`, `/deck/submit`, `/deck/generate`, `/deck/refresh` (`alpinejs.rb:5585+`); `local_asset` mints `/sw-asset/<key>/...` served only by `server.rb:222`; a toast's dismiss calls `htmx.ajax('POST','/toast/dismiss/<id>')` with no URL prefix. All 404 in A and B and fail silently in C.
|
|
83
|
+
|
|
84
|
+
> `SUPERSEDED 2026-08-23` (deck only): deck-honest-ui made the whole deck UI honestly read-only wherever `/deck/*` doesn't exist, gated by a new `deck_server:` flag at 11 construction sites (including service mode, a second uncovered instance of this bug the story found and fixed). `local_asset` and the toast dismiss route are unaffected by this epic. See `docs/frontend-only.md`.
|
|
85
|
+
|
|
86
|
+
**M8 — AppView-only page scripts are absent from all three contexts.**
|
|
87
|
+
The SSE client and the routing/popstate scripts are emitted by `AppView` (`views.rb:98-105`). A and B render through `AppContentView`; C renders through `ComponentRenderer.render_html`. Neither emits them. So `every`, `stream`, and `route_by` have **no client at all** — the DSL accepts them, the initial paint shows the placeholder, and nothing ever updates.
|
|
88
|
+
|
|
89
|
+
**M9 — `getFormState()` reads the DOM, not Alpine.**
|
|
90
|
+
`document.querySelectorAll('[x-model]')` (`alpinejs.rb:941`) harvests raw element values on button click. This is why plain inputs still deliver data on the live canvas despite M3. It also mishandles groups: for checkboxes it assigns `state[key] = el.checked`, so a `checkbox_group` whose N items share one `x-model` sends the **last item's boolean**, not the selected array.
|
|
91
|
+
|
|
92
|
+
> `SUPERSEDED 2026-08-23`: fixed by checkbox-group-array. The harvest now branches on `el.closest('.checkbox-group')` (later extended to `.sw-chip-group` for `chip_group`, disc-105): group items accumulate into an array in DOM order; a lone checkbox outside a group still returns a boolean. See `docs/frontend-only.md`.
|
|
93
|
+
|
|
94
|
+
## How to reproduce any row
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cd <repo>
|
|
98
|
+
|
|
99
|
+
# Context C — export
|
|
100
|
+
bundle exec ruby -Ilib exe/streamweaver export FIXTURE.rb -o /tmp/out.html
|
|
101
|
+
|
|
102
|
+
# Context B — canvas-read (kill it afterwards; verify the port is free)
|
|
103
|
+
SW_NO_OPEN=1 bundle exec ruby -Ilib exe/streamweaver canvas-read FIXTURE.rb &
|
|
104
|
+
curl -s "http://127.0.0.1:4800/?file=0" -o /tmp/reader.html
|
|
105
|
+
kill %1; lsof -i :4800 -sTCP:LISTEN # must be empty
|
|
106
|
+
|
|
107
|
+
# Context A — live canvas (create the session in-process so no browser tab opens)
|
|
108
|
+
ruby -Ilib -e 'require "stream_weaver"; require "stream_weaver/canvas/client"
|
|
109
|
+
StreamWeaver::Canvas::Client.ensure_bridge_running
|
|
110
|
+
p StreamWeaver::Canvas::Client.send_message(
|
|
111
|
+
StreamWeaver::Canvas::Protocol::Messages.create("probe", layout: :fluid))'
|
|
112
|
+
SW_NO_OPEN=1 bundle exec ruby -Ilib exe/streamweaver canvas-push probe < FIXTURE.rb
|
|
113
|
+
curl -s "http://127.0.0.1:4700/canvas/probe" -o /tmp/canvas.html
|
|
114
|
+
bundle exec ruby -Ilib exe/streamweaver canvas-close probe
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The per-row "Repro" column below is the discriminating grep against the file those commands produce.
|
|
118
|
+
|
|
119
|
+
## Inputs
|
|
120
|
+
|
|
121
|
+
| Component | A live canvas | B canvas-read | C export | Repro |
|
|
122
|
+
|---|---|---|---|---|
|
|
123
|
+
| `text_field` | DEGRADES — auto-submit `hx-post /update` 404s (no reactive re-render), but the typed value is harvested on button click (M9) | SILENTLY-DEAD — accepts typing, 404 on change, nothing ever reads it (M1+M2) | SILENTLY-DEAD — `hx-post` present, htmx absent; zero network activity (M1) | `grep -o 'hx-post="[^"]*"' out.html` |
|
|
124
|
+
| `text_field submit: false` | WORKS as value carrier | DEGRADES — value carrier with no consumer | DEGRADES — inert field, honest | `grep -c 'hx-post' out.html` |
|
|
125
|
+
| `text_area` | DEGRADES (as `text_field`) | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
126
|
+
| `date_field` | DEGRADES (as `text_field`) | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
127
|
+
| `checkbox` | DEGRADES — value harvested on button click | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
128
|
+
| `select` | DEGRADES — value harvested | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
129
|
+
| `radio_group` | **WORKS** — the one input ported to `sendEvent('change')` (M2); reaches the agent | SILENTLY-DEAD — `sendEvent` undefined, `ReferenceError` on change (M2) | SILENTLY-DEAD — falls back to `hx-post /update` in `:http` mode | `grep -c "window.sendEvent" reader.html` → 0 |
|
|
130
|
+
| `checkbox_group` | **BREAKS (silently wrong data)** — select-all/none work client-side, but M9 collapses the whole group to the last item's boolean instead of the selected array | SILENTLY-DEAD | SILENTLY-DEAD | `alpinejs.rb:941` vs `:620` |
|
|
131
|
+
| `chip_group` | DEGRADES — value harvested | SILENTLY-DEAD | SILENTLY-DEAD | `grep 'hx-post' ...` |
|
|
132
|
+
| `tag_buttons` | SILENTLY-DEAD — `hx-post /update` with `hx-vals`, not websocket-aware; clicking a tag does nothing | SILENTLY-DEAD | SILENTLY-DEAD | `alpinejs.rb:1986` |
|
|
133
|
+
| `code_editor` | DEGRADES — editing works, value harvested | SILENTLY-DEAD | SILENTLY-DEAD | `grep 'x-model' out.html` |
|
|
134
|
+
| `form` block | **SILENTLY-DEAD** — local Alpine `_form` state and Cancel-reset work, Save posts to `/form/<name>` → 404. Unlike `button`, `form` was **never** made websocket-aware, so the agent is not even notified | SILENTLY-DEAD | SILENTLY-DEAD | `grep -o 'hx-post="[^"]*form[^"]*"'` |
|
|
135
|
+
| `form_for` / `resource` forms | BREAKS — depends on routed store actions the bridge does not serve | BREAKS | BREAKS | `resource` raises without a real store |
|
|
136
|
+
|
|
137
|
+
> `SUPERSEDED 2026-08-23`: `checkbox_group` (A column, M9 harvest), `chip_group` (A column, ported + M9), `tag_buttons` (A column, ported), and `form` block (A column, ported) all changed. See the compatibility table in `docs/frontend-only.md` for corrected per-context verdicts; `radio_group`'s B column (honestly inert, no more `ReferenceError`) also changed.
|
|
138
|
+
|
|
139
|
+
## Buttons and actions
|
|
140
|
+
|
|
141
|
+
| Component | A live canvas | B canvas-read | C export | Repro |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| `button` (default) | **WORKS when an agent is listening.** With no `canvas-wait` holder the click still fires `showFeedback()`, which **replaces `#app-container`** with "✓ Submitted" or the `canvas_continue` spinner — the page is destroyed and the event goes nowhere | **SILENTLY-DEAD, self-disabling.** `@click` is `$el.disabled=true; sendEvent(...)`; the first statement runs, the second throws. The button greys itself out and nothing happens | SILENTLY-DEAD — `hx-post /action/<id>`, htmx absent, button stays enabled, no console output | `grep -o "sendEvent('action'" canvas.html` |
|
|
144
|
+
| `button submit: false` | WORKS (decorative by contract) | WORKS | WORKS | `grep -c 'hx-post' ` |
|
|
145
|
+
| `external_link_button` | DEGRADES — `window.open` fires; the paired `/submit` post 404s | DEGRADES | DEGRADES | `alpinejs.rb:2072` |
|
|
146
|
+
| `copy_button` | WORKS — Alpine + clipboard, no server | WORKS | WORKS (see browser list re `file://`) | `grep -c 'x-show' out.html` |
|
|
147
|
+
| `code_block copy: true` | WORKS | WORKS | WORKS | same |
|
|
148
|
+
| `clickable(action:)` | **SILENTLY-DEAD** — `hx-post /action/...`, never websocket-aware. Direct asymmetry with `button`, which was ported | SILENTLY-DEAD | SILENTLY-DEAD | `alpinejs.rb:2565` |
|
|
149
|
+
| `menu_item` with an action block | **SILENTLY-DEAD** — same asymmetry; the menu closes, the action never runs | SILENTLY-DEAD | SILENTLY-DEAD | `alpinejs.rb:7734` |
|
|
150
|
+
| `clickable(href:)` / `link_to` (external URL) | WORKS | WORKS | WORKS | — |
|
|
151
|
+
| `link_to` / `nav_item` (app-relative href) | DEGRADES — navigates off the canvas to a 404 | DEGRADES | DEGRADES — dead link | `grep -o 'href="/[^"]*"'` |
|
|
152
|
+
|
|
153
|
+
> `SUPERSEDED 2026-08-23`: `button` (B column) and `clickable(action:)` / `menu_item` (A and B columns) all changed — see `docs/frontend-only.md`. `external_link_button`'s unquoted-URL edge case is now tracked as disc-107 (not fixed, out of scope: quoting it would change HTTP-mode markup).
|
|
154
|
+
|
|
155
|
+
## Display (pure markup — the safe zone)
|
|
156
|
+
|
|
157
|
+
All of the following are static HTML with no server dependency and no Alpine requirement: **WORKS in A, B and C.**
|
|
158
|
+
|
|
159
|
+
`text`, `md` / `markdown`, `header1`–`header6`, `card`, `callout`, `alert`, `prose`, `pullquote`, `hero`, `doc_header`, `doc_section_header`, `badge`, `status_badge`, `status_dot`, `type_tag`, `stat_display`, `pulse_indicator`, `priority_item`, `activity_item`, `progress_bar`, `spinner`, `dir_tree`, `legend`, `flow_arrow`, `wireframe`, `wireframe_block`, `api_endpoint`, `kpi_dashboard`, `pipeline`, `implementation_map`, `comparison`, `decision`, `annotated_code`, `diff`, `score_table`, `lesson_text`, `app_header` (chrome only), `topbar`, `board` / `lane` / `board_card`, and every layout primitive (`columns`, `column`, `vstack`, `hstack`, `grid`, `grid_area`, `sticky`, `fullbleed`, `overlay`, `scroll_box`, `app_shell`, `sidebar`, `main`, `div`, `section`).
|
|
160
|
+
|
|
161
|
+
Two exceptions inside that list:
|
|
162
|
+
|
|
163
|
+
| Component | A | B | C | Repro |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| `code_block` (syntax highlighting) | WORKS — bridge loads highlight.js | WORKS — reader loads highlight.js | WORKS — export loads Prism from CDN; **BREAKS to unstyled code under CSP** (M4) | `grep -o 'prismjs@[0-9.]*' out.html` |
|
|
166
|
+
| `image_block` with a relative `src` | BREAKS — no route serves the file | BREAKS | BREAKS unless `--inline-images` or the file is colocated | `bundle exec ... export f.rb --inline-images` |
|
|
167
|
+
| `local_asset` / `stylesheets:` local path | BREAKS — mints `/sw-asset/...`, 404 (M7) | BREAKS | BREAKS | `grep -o '/sw-asset/[^"]*'` |
|
|
168
|
+
| `use_stylesheet` | WORKS — raw CSS is inlined, not referenced | WORKS | WORKS | `grep -c '<style>' out.html` |
|
|
169
|
+
|
|
170
|
+
## Tables
|
|
171
|
+
|
|
172
|
+
| Component | A | B | C | Repro |
|
|
173
|
+
|---|---|---|---|---|
|
|
174
|
+
| `table` (static) | WORKS | WORKS | WORKS | — |
|
|
175
|
+
| `table sortable: true` | WORKS — pure Alpine client sort | WORKS | WORKS (needs Alpine; BREAKS under CSP) | `grep -o 'x-data' out.html` |
|
|
176
|
+
| `table sticky_header:` / `striped:` | WORKS (CSS only) | WORKS | WORKS | — |
|
|
177
|
+
| `table` with action buttons in cells | inherits `button` | inherits `button` | inherits `button` | — |
|
|
178
|
+
|
|
179
|
+
## Navigation and tabs
|
|
180
|
+
|
|
181
|
+
| Component | A | B | C | Repro |
|
|
182
|
+
|---|---|---|---|---|
|
|
183
|
+
| `tabs` (eager, default) | WORKS — `@click activeTab = N`, pure client | WORKS | WORKS — the `hx-post` session sync is `hx-swap="none"` and was never user-visible, so dropping it costs nothing | `grep -c swRouteTabs out.html` → 0 |
|
|
184
|
+
| `tabs url: true` | DEGRADES — plain client tabs; the URL is untouched and the warning goes to the **agent's** stderr, not the user (M6) | DEGRADES — identical | **WORKS** — real route tabs, `sw-route-tabs.js` inlined, `pushState` live | `grep -c swRouteTabs` → 2 in export, 0 on canvas |
|
|
185
|
+
| `tabs lazy: true` (deprecated) | SILENTLY-DEAD → blank. Inactive panels contain only `<!-- lazy: tab N not rendered -->`; the `hx-post /update` that would fill them 404s, so switching reveals an **empty panel** (`stream_weaver-pkh`) | SILENTLY-DEAD → blank | SILENTLY-DEAD → blank (htmx absent) | `grep -c 'lazy: tab' out.html` |
|
|
186
|
+
| `collapsible` / `accordion` | WORKS | WORKS | WORKS (Alpine) | `grep -c x-show` |
|
|
187
|
+
| `expandable_card` | WORKS | WORKS | WORKS (Alpine) | same |
|
|
188
|
+
| `dropdown` (open/close) | WORKS | WORKS | WORKS (Alpine) | same |
|
|
189
|
+
| `navbar` / `nav_item` / `breadcrumbs` | DEGRADES — renders correctly, hrefs go nowhere | DEGRADES | DEGRADES | `grep -o 'href="/[^"]*"'` |
|
|
190
|
+
| `sidebar_toc` | WORKS — `sw-sidebar-toc.js` is inlined | WORKS | WORKS | `grep -c 'sw-sidebar-toc'` |
|
|
191
|
+
| `keyboard_shortcuts` | WORKS as a legend; bound actions inherit `button` | WORKS as a legend | WORKS as a legend | `grep -c 'sw-keyboard'` |
|
|
192
|
+
| `modal` | DEGRADES — Alpine close works; **opening** is server-state driven, so the agent must re-push | SILENTLY-DEAD — cannot be opened | SILENTLY-DEAD — cannot be opened | `grep -o 'hx-post="[^"]*action[^"]*"'` |
|
|
193
|
+
| `route_by` / `route` / `page` | SILENTLY-DEAD — no routing script is emitted at all (M8) | SILENTLY-DEAD | SILENTLY-DEAD | `views.rb:103` is AppView-only |
|
|
194
|
+
|
|
195
|
+
## Diagrams and charts
|
|
196
|
+
|
|
197
|
+
| Component | A | B | C | Repro |
|
|
198
|
+
|---|---|---|---|---|
|
|
199
|
+
| `mermaid` (incl. `zoom:`) | WORKS — engine inlined, library from CDN | WORKS | WORKS; `--offline` inlines the library so it survives CSP (**except `elk: true`**, which has no global build) | `grep -c 'sw-mermaid-zoom' out.html` |
|
|
200
|
+
| `chart type: …` (`Components::Chart`) | WORKS | WORKS | WORKS — self-loading CDN, immune to M5 | `grep -c SW_CHART_CDN out.html` → 2 |
|
|
201
|
+
| `bar_chart` / `hbar_chart` | WORKS — bridge hardcodes `chart.umd` | WORKS — reader ERB hardcodes it | **SILENTLY-DEAD** — no Chart.js emitted (M5); `typeof Chart !== 'undefined'` guard swallows it; empty bordered box | `grep -c chart.umd out.html` → **0** |
|
|
202
|
+
| `line_chart` / `area_chart` / `sparkline` | WORKS | WORKS | **SILENTLY-DEAD** (M5) | same |
|
|
203
|
+
| `pie_chart` / `doughnut_chart` | WORKS | WORKS | **SILENTLY-DEAD** (M5) | same |
|
|
204
|
+
| `stacked_bar_chart` | WORKS | WORKS | **SILENTLY-DEAD** (M5) | same |
|
|
205
|
+
|
|
206
|
+
> `SUPERSEDED 2026-08-23`: every C-column SILENTLY-DEAD verdict in this section is fixed — all shorthand chart methods now ship Chart.js in exports (chart-export-allowlist). See `docs/frontend-only.md`.
|
|
207
|
+
|
|
208
|
+
## Deck and slides
|
|
209
|
+
|
|
210
|
+
| Component | A | B | C | Repro |
|
|
211
|
+
|---|---|---|---|---|
|
|
212
|
+
| `slide_container` / `slide` | WORKS — `sw-slide-nav.js` inlined | WORKS | WORKS | `grep -c 'sw-slide-nav'` |
|
|
213
|
+
| `design_deck` / `deck_slide` (navigation) | WORKS | WORKS | WORKS | same |
|
|
214
|
+
| `design_deck` **option selection** | **SILENTLY-DEAD, and it lies.** `swDeckSelect` applies the selected class and `aria-checked` **first**, then `fetch('/deck/select')` → 404 with no `.catch`. The user sees a confirmed selection that was never recorded and never reached the agent | SILENTLY-DEAD, identical | SILENTLY-DEAD, identical (failed fetch instead of 404) | `alpinejs.rb:5608-5636`; `curl -X POST .../deck/select` → 404 |
|
|
215
|
+
| deck notes textarea (`@blur`) | SILENTLY-DEAD — `fetch('/deck/note')` 404 | SILENTLY-DEAD | SILENTLY-DEAD | `/deck/note` → 404 |
|
|
216
|
+
| `deck_summary` / `model_selector` / `confirmation_bar` / generate-more | SILENTLY-DEAD — all route through `/deck/*` (M7) | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
217
|
+
|
|
218
|
+
> `SUPERSEDED 2026-08-23`: all three SILENTLY-DEAD rows above are fixed by deck-honest-ui. The deck now renders read-only wherever `/deck/*` can't exist (A, B, C all become honestly DEGRADES instead of lying SILENTLY-DEAD), gated by a `deck_server:` flag at 11 construction sites — including service mode, a second previously-undocumented instance of this bug the fix also caught. See `docs/frontend-only.md`.
|
|
219
|
+
|
|
220
|
+
## Theme components
|
|
221
|
+
|
|
222
|
+
| Component | A | B | C | Repro |
|
|
223
|
+
|---|---|---|---|---|
|
|
224
|
+
| `theme_toggle` | WORKS — Alpine + localStorage | WORKS | WORKS (BREAKS under CSP, M4) | `grep -c 'x-data' out.html` |
|
|
225
|
+
| `theme_switcher` | WORKS | WORKS | WORKS (same caveat) | same |
|
|
226
|
+
| `theme_preset` | WORKS — CSS only | WORKS | WORKS | — |
|
|
227
|
+
| `layout_toggle` | WORKS — client `@click` | WORKS | WORKS (same caveat) | `grep -c '@click'` |
|
|
228
|
+
|
|
229
|
+
## Feedback, streaming, services
|
|
230
|
+
|
|
231
|
+
| Component | A | B | C | Repro |
|
|
232
|
+
|---|---|---|---|---|
|
|
233
|
+
| `toast_container` | **SILENTLY-DEAD** — renders an empty div; toasts come from `state[:_toasts]`, always empty here. (The canvas's own `streamweaver canvas-toast` overlay is a **separate** bridge mechanism and does work in A) | SILENTLY-DEAD | SILENTLY-DEAD; dismiss would also throw — `htmx` is undefined | `alpinejs.rb:2772` |
|
|
234
|
+
| `canvas_continue` | WORKS — hidden marker read by `sendEvent`'s `showFeedback` | inert, no visible surface | inert, no visible surface | `grep -c 'sw-canvas-continue'` |
|
|
235
|
+
| `every(n)` timers | **SILENTLY-DEAD** — no SSE client emitted (M8); the initial placeholder paints and never changes | SILENTLY-DEAD | SILENTLY-DEAD | rendered length is the placeholder only |
|
|
236
|
+
| `stream do …` | SILENTLY-DEAD (M8) | SILENTLY-DEAD | SILENTLY-DEAD | same |
|
|
237
|
+
| `endpoint(verb, path)` | SILENTLY-DEAD — declared, never routed | SILENTLY-DEAD | SILENTLY-DEAD | `curl` the path → 404 |
|
|
238
|
+
| `resource` DSL | BREAKS — routed CRUD with no router | BREAKS | BREAKS | store validation raises at build |
|
|
239
|
+
| `service_client` / `feed` | out of scope — these are the real-backend case | — | — | — |
|
|
240
|
+
|
|
241
|
+
## Headline counts
|
|
242
|
+
|
|
243
|
+
Roughly 95 distinct DSL surfaces were classified.
|
|
244
|
+
|
|
245
|
+
| | WORKS | DEGRADES | SILENTLY-DEAD | BREAKS |
|
|
246
|
+
|---|---|---|---|---|
|
|
247
|
+
| A — live canvas | 64 | 11 | 16 | 4 |
|
|
248
|
+
| B — canvas-read | 62 | 4 | 25 | 4 |
|
|
249
|
+
| C — export | 61 | 4 | 26 | 4 |
|
|
250
|
+
|
|
251
|
+
> `SUPERSEDED 2026-08-23`: these counts are the pre-fix snapshot and no longer add up post-fix (multiple rows moved SILENTLY-DEAD/BREAKS → WORKS/DEGRADES across all three columns — see the sections above). This doc does not restate the counts; `docs/frontend-only.md` has the corrected per-row table.
|
|
252
|
+
|
|
253
|
+
The static-display core (about two thirds of the DSL) is safe everywhere. Every failure is concentrated in the same three places: anything that round-trips, anything that talks to `/deck/*`, and anything that needs a running timer.
|
|
254
|
+
|
|
255
|
+
## Needs browser confirmation
|
|
256
|
+
|
|
257
|
+
These follow from the code but cannot be settled without loading a page. Browser work belongs to the main session.
|
|
258
|
+
|
|
259
|
+
1. **Alpine's reaction to orphan `x-model`** (M3). Does an `x-model="role"` with no enclosing `x-data` throw a visible console error, silently no-op, or (for `x-model="name"`) fall through to `window.name`? This decides whether the input rows in B and C are SILENTLY-DEAD or BREAKS.
|
|
260
|
+
2. **Export route tabs under `file://`** (M6). `swRouteTabs.push()` calls `history.pushState` with a query string; several browsers throw `SecurityError` on `file://` origins. If it throws inside `@click`, tab switching in an exported doc is BREAKS, not WORKS.
|
|
261
|
+
3. **Export tabs under a CSP that blocks `cdn.jsdelivr.net`.** `x-data="{ activeTab: swRouteTabs.read(...) }"` should throw when Alpine is blocked, leaving every `x-cloak` panel hidden — predicted BREAKS (blank document), needs confirmation.
|
|
262
|
+
4. **`copy_button` from `file://`.** `navigator.clipboard` requires a secure context; whether `file://` qualifies varies by browser.
|
|
263
|
+
5. **The self-disabling button in context B.** Confirm the click leaves the button permanently greyed with a `ReferenceError: sendEvent is not defined` in the console.
|
|
264
|
+
6. **`bar_chart` in an export.** Confirm the observable is an empty bordered box with **no** console error (the `typeof Chart` guard), not a visible failure.
|
|
265
|
+
7. **Deck option selection in an export.** Confirm the selected state visibly sticks after the failed fetch — the "it lies" claim rests on the optimistic class being applied before the request.
|
|
266
|
+
8. **`checkbox_group` payload on a live canvas.** Confirm the JSON delivered to `canvas-wait` carries a boolean rather than the selected array (M9).
|
|
267
|
+
|
|
268
|
+
> `SUPERSEDED 2026-08-23`: items 1 and 2 remain genuinely open (file:// pushState + orphan x-model are still unconfirmed — see `docs/frontend-only.md`'s "file:// open questions"). Items 5, 6, 7, and 8 were confirmed as part of the fixes above and are no longer open questions — each fix's story recorded the browser evidence. Item 3 (CSP blocking Alpine) was confirmed during this pass: `x-cloak`/`x-show` content does vanish under a CSP-locked viewer while flat content survives, exactly as predicted.
|
|
269
|
+
|
|
270
|
+
## Source references
|
|
271
|
+
|
|
272
|
+
- `lib/stream_weaver/adapter/alpinejs.rb` — `websocket_mode?` at `:41`; the four behavioral branches at `:570`, `:691`, `:753`, `:824`; tabs at `:2321-2445`; deck JS at `:5585`; `getFormState` at `:941`
|
|
273
|
+
- `lib/stream_weaver/canvas/bridge_server.rb` — route surface at `:87-213`; page shell at `:216-285`
|
|
274
|
+
- `lib/stream_weaver/canvas/reader.rb` — route surface at `:454-671`; `render_doc` and the deliberate `cdn_scripts` omission at `:683-728`
|
|
275
|
+
- `lib/stream_weaver/export/html_exporter.rb` — htmx exclusion at `:307-313`; Alpine conditional at `:333`; the drifted Chart branch at `:341`; `--offline` at `:208`
|
|
276
|
+
- `lib/stream_weaver/views.rb` — AppView-only SSE and routing scripts at `:98-105`; `AppContentView` at `:3704`
|
|
277
|
+
|
|
278
|
+
**Note (2026-08-23):** the source line numbers above are the spike-time snapshot and will have shifted after the five fixes landed (each added new methods/branches to `alpinejs.rb` and `html_exporter.rb`). Treat them as approximate; the fix-story notes in tyrion (chart-export-allowlist, reader-dead-controls, checkbox-group-array, canvas-action-parity, deck-honest-ui) carry the current line numbers.
|