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.
Files changed (381) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +77 -0
  3. data/.beads/README.md +81 -0
  4. data/.beads/config.yaml +54 -0
  5. data/.beads/metadata.json +7 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +91 -0
  8. data/CHANGELOG.md +294 -0
  9. data/CLAUDE.md +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +565 -0
  12. data/Rakefile +13 -0
  13. data/assets/streamweaver-hero.jpg +0 -0
  14. data/bench/baselines/ledger.rb +58 -0
  15. data/bench/baselines/warroom.rb +51 -0
  16. data/bench/fixtures/ledger.rb +76 -0
  17. data/bench/fixtures/warroom.rb +60 -0
  18. data/bench/results/6daf8b8.md +102 -0
  19. data/bench/results/c8d749b.md +102 -0
  20. data/bench/results/eb06ab9.md +102 -0
  21. data/bench/run.rb +151 -0
  22. data/bench/support.rb +84 -0
  23. data/copy_this/dto_run_sample_run_7631.html +220 -0
  24. data/docs/SERVICE_MODE.md +302 -0
  25. data/docs/architecture/how_streamweaver_works.md +772 -0
  26. data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
  27. data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
  28. data/docs/canvas-ipc-session-summary.md +127 -0
  29. data/docs/canvas-panel-workflow.md +172 -0
  30. data/docs/canvas-read.md +147 -0
  31. data/docs/canvas-roadmap.md +275 -0
  32. data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
  33. data/docs/claude-code-companion-skill-spec.md +567 -0
  34. data/docs/components_reference.md +1223 -0
  35. data/docs/crud-patterns.md +265 -0
  36. data/docs/endpoints.md +122 -0
  37. data/docs/for_llms.md +1 -0
  38. data/docs/form-patterns.md +165 -0
  39. data/docs/frontend-only.md +169 -0
  40. data/docs/html-artifact-audit.md +238 -0
  41. data/docs/ideas/2025-01-01-as-a-service.md +203 -0
  42. data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
  43. data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
  44. data/docs/ideas/dhh-review-future-refactors.md +142 -0
  45. data/docs/opal-jamstack.md +47 -0
  46. data/docs/opal-npm-direction.md +184 -0
  47. data/docs/opal-spike-findings.md +194 -0
  48. data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
  49. data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
  50. data/docs/plans/2026-01-02-charts-design.md +288 -0
  51. data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
  52. data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
  53. data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
  54. data/docs/plans/canvas-claude-project-design.md +238 -0
  55. data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
  56. data/docs/plans/org-doc-preview-surfaces.md +293 -0
  57. data/docs/plans/shadcn-polish-plan.md +417 -0
  58. data/docs/porting-artifacts.md +252 -0
  59. data/docs/reference/agent-skills-comparison.md +369 -0
  60. data/docs/reference/travel-state-prd.artifact.html +780 -0
  61. data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
  62. data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
  63. data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
  64. data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
  65. data/docs/research/frontend-only-matrix.md +278 -0
  66. data/docs/research/streamweaver-way-spike-findings.md +529 -0
  67. data/docs/resource-dsl.md +342 -0
  68. data/docs/routing.md +302 -0
  69. data/docs/ruby-ui-comparison.md +319 -0
  70. data/docs/shared-dsl-fragments.md +356 -0
  71. data/docs/streamweaver-for-ai-agents.md +401 -0
  72. data/docs/streamweaver-frontend-vision.md +123 -0
  73. data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
  74. data/docs/streamweaver_canvas/save_example.rb +9 -0
  75. data/docs/templates.md +213 -0
  76. data/docs/testing.md +155 -0
  77. data/docs/theming-hooks.md +281 -0
  78. data/docs/tutorials/the-streamweaver-way.md +882 -0
  79. data/docs/university/capability-inventory.md +137 -0
  80. data/docs/university/dependency-survey.md +114 -0
  81. data/docs/university/design-spec.md +217 -0
  82. data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
  83. data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
  84. data/docs/university/mockups/course-list-dark.png +0 -0
  85. data/docs/university/mockups/course-list-light.png +0 -0
  86. data/docs/university/mockups/course_canvas_mockup.rb +741 -0
  87. data/docs/university/mockups/step-screen-dark.png +0 -0
  88. data/docs/university/mockups/step-screen-light.png +0 -0
  89. data/docs/university/roadmap.md +72 -0
  90. data/docs/university/send-to-coworker.md +77 -0
  91. data/docs/university/worker-session-mining.md +109 -0
  92. data/docs/visual/browser-rendering-spike.md +173 -0
  93. data/docs/visual/builder-visual-plan-analysis.md +378 -0
  94. data/docs/visual/sw-plan-format-exploration.md +511 -0
  95. data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
  96. data/docs/visual-skills/PROGRESS.md +60 -0
  97. data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
  98. data/docs/visual-skills/analysis/components.md +317 -0
  99. data/docs/visual-skills/analysis/overlap.md +360 -0
  100. data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
  101. data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
  102. data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
  103. data/docs/visual-skills/analysis/visual-explainer.md +964 -0
  104. data/docs/visual-skills/blog/blog-series-outline.md +39 -0
  105. data/docs/visual-skills/blog/token-efficiency.md +73 -0
  106. data/docs/visual-skills/design/architecture.md +1725 -0
  107. data/docs/visual-skills/design/codex-review.md +340 -0
  108. data/docs/visual-skills/design/dhh-review.md +209 -0
  109. data/docs/visual-skills/design/evolution.md +100 -0
  110. data/docs/visual-skills/design/gemini-review.md +55 -0
  111. data/docs/visual-skills/design/review-synthesis.md +149 -0
  112. data/docs/visual-skills/implementation/STATE.md +22 -0
  113. data/docs/visual-skills/implementation/plan.md +347 -0
  114. data/docs/visual-skills/implementation/spike-findings.md +237 -0
  115. data/docs/visual-skills/implementation/tasks.md +702 -0
  116. data/docs/visual-skills/lessons-learned/process.md +145 -0
  117. data/examples/README.md +96 -0
  118. data/examples/advanced/all_components.rb +177 -0
  119. data/examples/advanced/examples_browser.rb +394 -0
  120. data/examples/advanced/teachables_browser.rb +261 -0
  121. data/examples/advanced/theme_tweaker.rb +366 -0
  122. data/examples/advanced/tutorial.rb +1528 -0
  123. data/examples/agentic/agentic_form.rb +30 -0
  124. data/examples/agentic/agentic_form_autoclose.rb +30 -0
  125. data/examples/agentic/cultivation_tracker.rb +69 -0
  126. data/examples/basic/hello_world.rb +22 -0
  127. data/examples/basic/opal_tabs_table.rb +44 -0
  128. data/examples/basic/todo_list.rb +39 -0
  129. data/examples/button_loading_test.rb +36 -0
  130. data/examples/canvas/mermaid_canvas_demo.sh +151 -0
  131. data/examples/charts/bar_chart_demo.rb +57 -0
  132. data/examples/charts/line_chart_demo.rb +73 -0
  133. data/examples/charts/pie_area_demo.rb +87 -0
  134. data/examples/charts/stacked_bar_chart_demo.rb +69 -0
  135. data/examples/claude_code/README.md +85 -0
  136. data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
  137. data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
  138. data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
  139. data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
  140. data/examples/claude_code/codebreaker/README.md +213 -0
  141. data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
  142. data/examples/claude_code/tutorial/README.md +102 -0
  143. data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
  144. data/examples/claude_code/verification_flow/README.md +126 -0
  145. data/examples/components/annotated_code_demo.rb +54 -0
  146. data/examples/components/callout_demo.rb +33 -0
  147. data/examples/components/checkbox_group_demo.rb +46 -0
  148. data/examples/components/design_review.css +468 -0
  149. data/examples/components/design_review_demo.rb +44 -0
  150. data/examples/components/design_review_dsl.rb +300 -0
  151. data/examples/components/diff_block_demo.rb +112 -0
  152. data/examples/components/events_demo.rb +184 -0
  153. data/examples/components/form_demo.rb +62 -0
  154. data/examples/components/lesson_demo.rb +53 -0
  155. data/examples/components/markdown_demo.rb +110 -0
  156. data/examples/components/mermaid_demo.rb +166 -0
  157. data/examples/components/pareto_set.rb +64 -0
  158. data/examples/components/prd_demo.rb +21 -0
  159. data/examples/components/prd_dsl.rb +385 -0
  160. data/examples/components/quiz_demo.rb +47 -0
  161. data/examples/components/run_viewer_demo.rb +100 -0
  162. data/examples/components/score_and_collapsible_demo.rb +82 -0
  163. data/examples/components/select_stale_value_smoke_test.rb +39 -0
  164. data/examples/components/table_demo.rb +247 -0
  165. data/examples/components/timer_with_state_demo.rb +58 -0
  166. data/examples/components/todo_due_dates.rb +63 -0
  167. data/examples/components/uat_gaps_demo.rb +112 -0
  168. data/examples/dashboard/feed_simulator.rb +60 -0
  169. data/examples/dashboard/live_dashboard.rb +80 -0
  170. data/examples/dashboard_components.rb +108 -0
  171. data/examples/deferred_fragments_demo.rb +62 -0
  172. data/examples/generate_more_spike/README.md +63 -0
  173. data/examples/generate_more_spike/app.rb +441 -0
  174. data/examples/git_health.sh +401 -0
  175. data/examples/layout/layout_components_demo.rb +174 -0
  176. data/examples/layout/modal_demo.rb +215 -0
  177. data/examples/layout/navigation_demo.rb +227 -0
  178. data/examples/layout/route_tabs_demo.rb +52 -0
  179. data/examples/layout/routing_demo.rb +52 -0
  180. data/examples/layout/scroll_box_demo.rb +154 -0
  181. data/examples/lazy_fragments_demo.rb +151 -0
  182. data/examples/my_todos/README.md +29 -0
  183. data/examples/my_todos/my_todos.rb +312 -0
  184. data/examples/my_todos/store.rb +86 -0
  185. data/examples/opal/reactive_demo.rb +62 -0
  186. data/examples/opal/scenarios/s1_counter.rb +15 -0
  187. data/examples/opal/scenarios/s2_search_filter.rb +17 -0
  188. data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
  189. data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
  190. data/examples/opal/scenarios/s5_watch.rb +39 -0
  191. data/examples/opal/scenarios/s6_on_start.rb +20 -0
  192. data/examples/opal/scenarios/s7_wizard.rb +33 -0
  193. data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
  194. data/examples/opal/scenarios/s9_dashboard.rb +27 -0
  195. data/examples/operations_dashboard_demo.rb +167 -0
  196. data/examples/panel_demo.sh +447 -0
  197. data/examples/parity/assets/tyrion_slice.css +86 -0
  198. data/examples/parity/rivet_people_slice.rb +182 -0
  199. data/examples/parity/tyrion_components.css +777 -0
  200. data/examples/parity/tyrion_warroom_components.rb +414 -0
  201. data/examples/parity/tyrion_warroom_slice.rb +549 -0
  202. data/examples/puma_dev/README.md +124 -0
  203. data/examples/puma_dev/config.ru +34 -0
  204. data/examples/puma_dev/standalone_app.rb +34 -0
  205. data/examples/scaffolding/blog.rb +49 -0
  206. data/examples/scaffolding/utf_lite.rb +105 -0
  207. data/examples/styling/feedback_demo.rb +229 -0
  208. data/examples/styling/style_showcase.rb +172 -0
  209. data/examples/styling/theme_demo.rb +508 -0
  210. data/examples/timer_health_checker.rb +111 -0
  211. data/examples/timer_showcase.rb +131 -0
  212. data/examples/tutorials/render_markdown.rb +112 -0
  213. data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
  214. data/examples/tutorials/tutorial_content.rb +1082 -0
  215. data/examples/visual_skills/design_deck_demo.rb +314 -0
  216. data/examples/visual_skills/explainer_demo.rb +499 -0
  217. data/exe/streamweaver +9 -0
  218. data/gsd/ROADMAP-1.0.md +167 -0
  219. data/gsd/STATE.md +13 -0
  220. data/gsd/research/market-positioning-research.md +139 -0
  221. data/gsd/research/production-patterns-research.md +270 -0
  222. data/gsd/research/repo-audit-1.0.md +274 -0
  223. data/lib/stream_weaver/action_token.rb +58 -0
  224. data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
  225. data/lib/stream_weaver/adapter/base.rb +591 -0
  226. data/lib/stream_weaver/adapter/opal.rb +334 -0
  227. data/lib/stream_weaver/adapter/static.rb +1118 -0
  228. data/lib/stream_weaver/admin.rb +176 -0
  229. data/lib/stream_weaver/app.rb +1821 -0
  230. data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
  231. data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
  232. data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
  233. data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
  234. data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
  235. data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
  236. data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
  237. data/lib/stream_weaver/canvas/bridge.rb +239 -0
  238. data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
  239. data/lib/stream_weaver/canvas/client.rb +298 -0
  240. data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
  241. data/lib/stream_weaver/canvas/doc_store.rb +252 -0
  242. data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
  243. data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
  244. data/lib/stream_weaver/canvas/gist_store.rb +135 -0
  245. data/lib/stream_weaver/canvas/helpers.rb +109 -0
  246. data/lib/stream_weaver/canvas/history.rb +90 -0
  247. data/lib/stream_weaver/canvas/protocol.rb +86 -0
  248. data/lib/stream_weaver/canvas/reader.rb +853 -0
  249. data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
  250. data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
  251. data/lib/stream_weaver/canvas/session.rb +132 -0
  252. data/lib/stream_weaver/cli.rb +3235 -0
  253. data/lib/stream_weaver/component_assets.rb +70 -0
  254. data/lib/stream_weaver/component_registry.rb +67 -0
  255. data/lib/stream_weaver/component_renderer.rb +48 -0
  256. data/lib/stream_weaver/components/annotated_code.rb +53 -0
  257. data/lib/stream_weaver/components/api_endpoint.rb +42 -0
  258. data/lib/stream_weaver/components/callout.rb +59 -0
  259. data/lib/stream_weaver/components/chart.rb +84 -0
  260. data/lib/stream_weaver/components/code_block.rb +77 -0
  261. data/lib/stream_weaver/components/comparison.rb +39 -0
  262. data/lib/stream_weaver/components/decision.rb +38 -0
  263. data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
  264. data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
  265. data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
  266. data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
  267. data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
  268. data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
  269. data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
  270. data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
  271. data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
  272. data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
  273. data/lib/stream_weaver/components/diff_block.rb +125 -0
  274. data/lib/stream_weaver/components/doc_header.rb +57 -0
  275. data/lib/stream_weaver/components/image_block.rb +70 -0
  276. data/lib/stream_weaver/components/implementation_map.rb +33 -0
  277. data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
  278. data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
  279. data/lib/stream_weaver/components/mermaid.rb +79 -0
  280. data/lib/stream_weaver/components/pipeline.rb +63 -0
  281. data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
  282. data/lib/stream_weaver/components/slide_container.rb +141 -0
  283. data/lib/stream_weaver/components/timeline_event.rb +58 -0
  284. data/lib/stream_weaver/components/wireframe.rb +29 -0
  285. data/lib/stream_weaver/components/wireframe_block.rb +34 -0
  286. data/lib/stream_weaver/components.rb +2711 -0
  287. data/lib/stream_weaver/css.rb +230 -0
  288. data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
  289. data/lib/stream_weaver/display_dsl.rb +1004 -0
  290. data/lib/stream_weaver/export/html_exporter.rb +478 -0
  291. data/lib/stream_weaver/feed.rb +34 -0
  292. data/lib/stream_weaver/feed_builder.rb +31 -0
  293. data/lib/stream_weaver/fonts.rb +33 -0
  294. data/lib/stream_weaver/interaction_runner.rb +487 -0
  295. data/lib/stream_weaver/iterm.rb +460 -0
  296. data/lib/stream_weaver/layout_registry.rb +92 -0
  297. data/lib/stream_weaver/opal/bridge.rb +52 -0
  298. data/lib/stream_weaver/opal/builder.rb +193 -0
  299. data/lib/stream_weaver/opal/env.rb +29 -0
  300. data/lib/stream_weaver/opal/reactive_state.rb +75 -0
  301. data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
  302. data/lib/stream_weaver/opal/renderer.rb +77 -0
  303. data/lib/stream_weaver/opal/runtime.rb +250 -0
  304. data/lib/stream_weaver/opal/shell.rb +145 -0
  305. data/lib/stream_weaver/opal/string_bridge.rb +50 -0
  306. data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
  307. data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
  308. data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
  309. data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
  310. data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
  311. data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
  312. data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
  313. data/lib/stream_weaver/opal_entry.rb +136 -0
  314. data/lib/stream_weaver/org/inline.rb +95 -0
  315. data/lib/stream_weaver/org/reader.rb +561 -0
  316. data/lib/stream_weaver/org/recording_context.rb +86 -0
  317. data/lib/stream_weaver/org/source_splitter.rb +63 -0
  318. data/lib/stream_weaver/org/writer.rb +300 -0
  319. data/lib/stream_weaver/page_shell.rb +530 -0
  320. data/lib/stream_weaver/portfile.rb +79 -0
  321. data/lib/stream_weaver/pushable.rb +42 -0
  322. data/lib/stream_weaver/resource/default_views.rb +111 -0
  323. data/lib/stream_weaver/resource/field_input.rb +25 -0
  324. data/lib/stream_weaver/resource/state_keys.rb +16 -0
  325. data/lib/stream_weaver/resource/store.rb +18 -0
  326. data/lib/stream_weaver/resource.rb +104 -0
  327. data/lib/stream_weaver/server.rb +1471 -0
  328. data/lib/stream_weaver/service.rb +1240 -0
  329. data/lib/stream_weaver/service_client.rb +104 -0
  330. data/lib/stream_weaver/session_store.rb +186 -0
  331. data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
  332. data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
  333. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
  334. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
  335. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
  336. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
  337. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
  338. data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
  339. data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
  340. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
  341. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
  342. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
  343. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
  344. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
  345. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
  346. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
  347. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
  348. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
  349. data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
  350. data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
  351. data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
  352. data/lib/stream_weaver/streamer.rb +63 -0
  353. data/lib/stream_weaver/templates/choices.rb +149 -0
  354. data/lib/stream_weaver/templates/code.rb +157 -0
  355. data/lib/stream_weaver/templates/confirm.rb +110 -0
  356. data/lib/stream_weaver/templates/diff.rb +195 -0
  357. data/lib/stream_weaver/templates/info.rb +142 -0
  358. data/lib/stream_weaver/templates/table.rb +169 -0
  359. data/lib/stream_weaver/templates/wizard.rb +271 -0
  360. data/lib/stream_weaver/theme/auto_mode.rb +116 -0
  361. data/lib/stream_weaver/theme/presets.rb +555 -0
  362. data/lib/stream_weaver/theme.rb +639 -0
  363. data/lib/stream_weaver/university/canvas.rb +924 -0
  364. data/lib/stream_weaver/university/course.rb +699 -0
  365. data/lib/stream_weaver/university/demos/counter.rb +33 -0
  366. data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
  367. data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
  368. data/lib/stream_weaver/university/demos.rb +50 -0
  369. data/lib/stream_weaver/university/listener.rb +467 -0
  370. data/lib/stream_weaver/university/progress.rb +201 -0
  371. data/lib/stream_weaver/university/runner.rb +138 -0
  372. data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
  373. data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
  374. data/lib/stream_weaver/utils.rb +31 -0
  375. data/lib/stream_weaver/version.rb +5 -0
  376. data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
  377. data/lib/stream_weaver/views.rb +3940 -0
  378. data/lib/stream_weaver.rb +122 -0
  379. data/llms.txt +1269 -0
  380. data/sig/stream_weaver.rbs +4 -0
  381. metadata +598 -0
@@ -0,0 +1,265 @@
1
+ # StreamWeaver CRUD Patterns
2
+
3
+ StreamWeaver handles CRUD differently from Rails. Because button callbacks are plain Ruby, data mutation happens directly — no controller layer, no POST-redirect-GET cycle. The URL routing layer handles deep-linking to edit views.
4
+
5
+ ## The Core Insight
6
+
7
+ A StreamWeaver button callback IS a POST handler:
8
+
9
+ ```ruby
10
+ button "Save" do |s|
11
+ initiative = load_initiative(s[:editing_id])
12
+ initiative[:title] = s[:edit_title]
13
+ initiative[:status] = s[:edit_status]
14
+ save_initiative(initiative)
15
+ s[:editing_initiative] = false # exit edit mode
16
+ s[:flash] = "Saved!"
17
+ end
18
+ ```
19
+
20
+ No controller. No form action. No redirect. Just Ruby.
21
+
22
+ ---
23
+
24
+ ## Read (Index + Show)
25
+
26
+ Standard StreamWeaver patterns — load from YAML/store, render in app block:
27
+
28
+ ```ruby
29
+ # Index: list all initiatives
30
+ initiatives = load_initiatives
31
+ table initiatives do
32
+ column :title
33
+ column :status
34
+ column(:id) { |i| button("View") { |s| s[:initiative_id] = i[:id] } }
35
+ end
36
+
37
+ # Show: render detail when initiative_id is set
38
+ if state[:initiative_id]
39
+ init = load_initiative(state[:initiative_id])
40
+ card do
41
+ header3 init[:title]
42
+ text "Status: #{init[:status]}"
43
+ button "Edit" do |s|
44
+ s[:editing_initiative] = true
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Create
53
+
54
+ Use a modal or inline form. State holds the draft fields; the save button commits to the store.
55
+
56
+ ```ruby
57
+ button "New Initiative", style: :primary do |s|
58
+ s[:creating_initiative] = true
59
+ s[:new_init_title] = ""
60
+ s[:new_init_status] = "active"
61
+ end
62
+
63
+ modal :new_initiative, title: "New Initiative" do
64
+ text_field :new_init_title, placeholder: "Title", submit: false
65
+ select :new_init_status, %w[active paused completed], submit: false
66
+
67
+ modal_footer do
68
+ button "Create", style: :primary do |s|
69
+ id = create_initiative(title: s[:new_init_title], status: s[:new_init_status])
70
+ s[:creating_initiative] = false
71
+ s[:initiative_id] = id # navigate to new record
72
+ s[:flash] = "Initiative created"
73
+ end
74
+ button "Cancel" do |s|
75
+ s[:creating_initiative] = false
76
+ end
77
+ end
78
+ end
79
+ ```
80
+
81
+ The `modal :new_initiative` opens when `state[:new_initiative_open]` is true (StreamWeaver convention: `modal :key` → controlled by `state[:key_open]`).
82
+
83
+ ---
84
+
85
+ ## Update (Edit Form)
86
+
87
+ ### Option A: Modal Edit (no URL change)
88
+
89
+ Good for quick edits on a list view. State holds the record being edited.
90
+
91
+ ```ruby
92
+ initiatives.each do |init|
93
+ hstack do
94
+ text init[:title]
95
+ button "Edit" do |s|
96
+ s[:editing_id] = init[:id]
97
+ # load current values into edit fields
98
+ s[:edit_title] = init[:title]
99
+ s[:edit_status] = init[:status]
100
+ end
101
+ end
102
+ end
103
+
104
+ if state[:editing_id]
105
+ modal :edit_initiative, title: "Edit Initiative" do
106
+ text_field :edit_title, placeholder: "Title", submit: false
107
+ select :edit_status, %w[active paused completed], submit: false
108
+
109
+ modal_footer do
110
+ button "Save", style: :primary do |s|
111
+ update_initiative(s[:editing_id], title: s[:edit_title], status: s[:edit_status])
112
+ s[:editing_id] = nil
113
+ s[:flash] = "Saved"
114
+ end
115
+ button "Cancel" do |s|
116
+ s[:editing_id] = nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Option B: URL-Addressable Edit Page
124
+
125
+ Pairs with `route_with` routing (see `routing.md`). Visiting `/initiative/init-001/edit` seeds `state[:editing_initiative] = true`.
126
+
127
+ ```ruby
128
+ # route_with parser handles: /initiative/:id/edit → { initiative_id: id, editing_initiative: true }
129
+ # route_with builder handles: editing_initiative? → /initiative/:id/edit
130
+
131
+ if state[:editing_initiative] && state[:initiative_id]
132
+ init = load_initiative(state[:initiative_id])
133
+
134
+ # Seed edit fields on first load
135
+ if state[:edit_seeded_for] != state[:initiative_id]
136
+ state[:edit_title] = init[:title]
137
+ state[:edit_status] = init[:status]
138
+ state[:edit_seeded_for] = state[:initiative_id]
139
+ end
140
+
141
+ card do
142
+ header3 "Edit: #{init[:title]}"
143
+
144
+ text_field :edit_title, label: "Title", submit: false
145
+ select :edit_status, %w[active paused completed], submit: false
146
+
147
+ hstack do
148
+ button "Save", style: :primary do |s|
149
+ update_initiative(s[:initiative_id], title: s[:edit_title], status: s[:edit_status])
150
+ s[:editing_initiative] = false # URL drops back to /initiative/:id
151
+ s[:flash] = "Saved"
152
+ end
153
+ button "Cancel", style: :secondary do |s|
154
+ s[:editing_initiative] = false
155
+ end
156
+ end
157
+ end
158
+ end
159
+ ```
160
+
161
+ **Field seeding pattern**: The `edit_seeded_for` guard ensures edit fields are seeded from the record once per initiative, not overwritten on every re-render. Without it, every button click would reset the form to the stored value.
162
+
163
+ ---
164
+
165
+ ## Delete
166
+
167
+ ```ruby
168
+ button "Delete", style: :danger do |s|
169
+ s[:confirm_delete_id] = init[:id]
170
+ end
171
+
172
+ if state[:confirm_delete_id]
173
+ alert(variant: :warning) do
174
+ text "Delete this initiative? This cannot be undone."
175
+ hstack do
176
+ button "Confirm Delete", style: :danger do |s|
177
+ delete_initiative(s[:confirm_delete_id])
178
+ s[:confirm_delete_id] = nil
179
+ s[:initiative_id] = nil # navigate away
180
+ s[:flash] = "Deleted"
181
+ end
182
+ button "Cancel" do |s|
183
+ s[:confirm_delete_id] = nil
184
+ end
185
+ end
186
+ end
187
+ end
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Flash Messages
193
+
194
+ StreamWeaver has no built-in flash. Use `state[:flash]` with a conditional render:
195
+
196
+ ```ruby
197
+ if state[:flash]
198
+ alert(variant: :success) { text state[:flash] }
199
+ # Auto-clear: set flash to nil after it's rendered once
200
+ # Use a counter trick or a dedicated clear button
201
+ end
202
+ ```
203
+
204
+ For auto-dismiss, use a toast:
205
+
206
+ ```ruby
207
+ # In button callback:
208
+ s[:toasts] ||= []
209
+ s[:toasts] << { id: SecureRandom.hex(4), message: "Saved!", type: :success, duration: 3000 }
210
+
211
+ # In render:
212
+ toast_container # renders toasts from state[:toasts]
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Store Adapter Pattern
218
+
219
+ Keep data access decoupled from the UI. Centralizing it makes the CRUD callbacks readable:
220
+
221
+ ```ruby
222
+ module InitiativeStore
223
+ YAML_PATH = File.expand_path("~/cultiv-os/cabinet/initiatives/master-initiatives.yaml")
224
+
225
+ def self.all
226
+ YAML.safe_load_file(YAML_PATH, symbolize_names: true)[:initiatives] || []
227
+ end
228
+
229
+ def self.find(id)
230
+ all.find { |i| i[:id] == id }
231
+ end
232
+
233
+ def self.update(id, attrs)
234
+ data = YAML.safe_load_file(YAML_PATH)
235
+ idx = data["initiatives"].index { |i| i["id"] == id }
236
+ return false unless idx
237
+ data["initiatives"][idx].merge!(attrs.transform_keys(&:to_s))
238
+ File.write(YAML_PATH, data.to_yaml)
239
+ true
240
+ end
241
+ end
242
+
243
+ # In app:
244
+ button "Save" do |s|
245
+ InitiativeStore.update(s[:initiative_id], title: s[:edit_title], status: s[:edit_status])
246
+ s[:editing_initiative] = false
247
+ end
248
+ ```
249
+
250
+ ---
251
+
252
+ See [docs/resource-dsl.md](resource-dsl.md) for the implemented `resource` DSL.
253
+
254
+ ---
255
+
256
+ ## Decision Guide
257
+
258
+ | Interaction | Pattern |
259
+ |---|---|
260
+ | Read-only list | `table` + state filter |
261
+ | Quick edit (no URL change) | Modal edit + `state[:editing_id]` |
262
+ | Deep-linkable edit form | `route_with` + URL-addressable edit page |
263
+ | Inline edit (click cell) | Not yet supported — use modal |
264
+ | Bulk update | `checkbox_group` + bulk action button |
265
+ | Delete with confirmation | Confirm state flag + conditional alert |
data/docs/endpoints.md ADDED
@@ -0,0 +1,122 @@
1
+ # Custom HTTP Endpoints — `endpoint`
2
+
3
+ The "never rewrite in Sinatra" escape hatch: register a real HTTP route (webhook
4
+ receiver, JSON API, file download, health check) directly from the app DSL,
5
+ without leaving StreamWeaver or hand-rolling a separate Sinatra/Rails service.
6
+
7
+ **This is not URL routing.** [`docs/routing.md`](routing.md) (`route_by` /
8
+ `route_with` / `page`) maps URL paths to *state* so the same StreamWeaver view
9
+ renders differently per path — it's still an HTML page render. `endpoint` is
10
+ the opposite: a genuine Rack route that bypasses StreamWeaver's state
11
+ machinery, component tree, and view rendering entirely. Use `route_by`/`page`
12
+ for "different tab/view depending on the URL." Use `endpoint` for "something
13
+ that isn't a StreamWeaver page at all" — a webhook, a JSON API, a file
14
+ download.
15
+
16
+ ## Basic usage
17
+
18
+ ```ruby
19
+ app "My App" do
20
+ endpoint :get, "/api/status" do |req|
21
+ { ok: true, uptime: 42 } # Hash -> 200 application/json
22
+ end
23
+
24
+ endpoint :post, "/webhook/github" do |req|
25
+ payload = req.body.read # req is a Rack::Request
26
+ handle_github_event(payload)
27
+ [202, {}, "queued"] # Rack triplet passes through verbatim
28
+ end
29
+
30
+ # ... normal StreamWeaver UI DSL below, in the same app
31
+ header1 "My App"
32
+ end.run!
33
+ ```
34
+
35
+ Supported verbs: `:get`, `:post`, `:put`, `:patch`, `:delete`.
36
+
37
+ ## Return value conventions
38
+
39
+ The block's return value is converted into the HTTP response:
40
+
41
+ | Return type | Response |
42
+ |---|---|
43
+ | `Hash` | `200`, `Content-Type: application/json`, body is `JSON.generate`d |
44
+ | `String` | `200`, `Content-Type: text/html`, body is the string as-is |
45
+ | `[status, headers, body]` (Array) | Passed through to Rack verbatim — full control (custom status, headers, streaming, file downloads, etc.) |
46
+ | anything else | `200`, `Content-Type: text/plain`, body is `#to_s` |
47
+
48
+ Use the Rack-triplet form for anything that doesn't fit the Hash/String
49
+ shortcuts — custom status codes, redirects, non-JSON/HTML content types, or
50
+ file downloads:
51
+
52
+ ```ruby
53
+ endpoint :get, "/export/report.csv" do |req|
54
+ csv = generate_report_csv
55
+ [200, { 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="report.csv"' }, csv]
56
+ end
57
+ ```
58
+
59
+ ## The block argument
60
+
61
+ The block receives the raw [`Rack::Request`](https://www.rubydoc.info/gems/rack/Rack/Request)
62
+ for the current request — not StreamWeaver state, not a component. Use it like
63
+ you would in any Rack app:
64
+
65
+ ```ruby
66
+ endpoint :get, "/api/echo" do |req|
67
+ { query: req.params } # query string / route params
68
+ end
69
+
70
+ endpoint :post, "/webhook/github" do |req|
71
+ req.body.read # raw request body
72
+ end
73
+ ```
74
+
75
+ **Gotcha:** if the incoming request's `Content-Type` is
76
+ `application/x-www-form-urlencoded` or `multipart/form-data`, Sinatra will
77
+ already have consumed the body to build `params` *before* your block runs —
78
+ `req.body.read` comes back empty in that case. Use `req.params` for form
79
+ submissions instead, or make sure the sender uses a different `Content-Type`
80
+ (e.g. `application/json`, `text/plain`) for endpoints that need the raw body.
81
+
82
+ ## No CSRF, no session, no state
83
+
84
+ `endpoint` is a raw Rack route. It does **not** go through StreamWeaver's
85
+ session/state machinery, CSRF protection, or component rendering — the block
86
+ you write is the entire request handler. If you need auth, verify a webhook
87
+ signature, parse JSON, etc., do it yourself inside the block just as you would
88
+ in a plain Sinatra route.
89
+
90
+ ## Precedence: internal routes always win
91
+
92
+ StreamWeaver's own framework routes (`/update`, `/action/*`, `/submit`,
93
+ `/event/*`, `/form/*`, `/theme/*`, `/sw/*`, plus a handful of others like `/`,
94
+ `/stream`, `/deck/*`) are defined before any `endpoint` you register. Sinatra
95
+ dispatches to the *first* route that matches a request, so if an `endpoint`
96
+ path collides with one of these, the internal route always wins — your block
97
+ is simply never reached.
98
+
99
+ `endpoint` warns at registration time when it detects a collision with a
100
+ known-reserved path/prefix, so the mistake is loud instead of silently eaten:
101
+
102
+ ```
103
+ StreamWeaver: endpoint POST /update collides with a StreamWeaver-internal route
104
+ and will never be reached — the internal route always wins.
105
+ ```
106
+
107
+ Pick paths outside StreamWeaver's reserved prefixes (`/action/`, `/event/`,
108
+ `/form/`, `/theme/`, `/sw/`) and outside `/update` and `/submit` to avoid this
109
+ entirely.
110
+
111
+ ## Service mode (`streamweaver run`)
112
+
113
+ In standalone mode (`App#run!`), endpoints are served at the path exactly as
114
+ registered (e.g. `GET /api/status`).
115
+
116
+ In multi-app service mode, each app is mounted under `/apps/:app_id`, so its
117
+ endpoints are scoped under that prefix too: an app registered as `svc123`
118
+ serves `endpoint :get, "/api/status"` at `GET /apps/svc123/api/status` — and
119
+ equally via the app's human-readable slug, e.g. `GET /apps/my-dashboard/api/status`. The
120
+ same precedence rule applies — StreamWeaver's fixed `/apps/:app_id/...` routes
121
+ (update, action, event, form, theme) are defined first and always win on
122
+ collision.
data/docs/for_llms.md ADDED
@@ -0,0 +1 @@
1
+ ../llms.txt
@@ -0,0 +1,165 @@
1
+ # StreamWeaver Form Patterns
2
+
3
+ Canonical patterns for form/input/selection UIs. Use this to pick the right component for your use case.
4
+
5
+ ## Pattern Reference
6
+
7
+ | Pattern | Use Case | Component | Example |
8
+ |---|---|---|---|
9
+ | Instant feedback | Filters, toggles | Default inputs (auto-submit) | `select :filter, options` |
10
+ | Multi-select then act | Batch operations | `checkbox_group` or `submit: false` + button | Check items then "Delete Selected" |
11
+ | Traditional form | Edit/create with Save/Cancel | `form` block | `form :edit_person do...end` |
12
+ | Single selection for later action | Pick category then add item | `select` with `submit: false` | `select :cat, names, submit: false` |
13
+ | Scrollable list | Long item lists | `scroll_box` | `scroll_box(max_height: "400px") do...end` |
14
+
15
+ ## Examples
16
+
17
+ ### Instant Feedback (default)
18
+ ```ruby
19
+ # Every change triggers a server round-trip and re-render
20
+ select :filter, ["All", "Active", "Done"]
21
+ text_field :search, placeholder: "Search..."
22
+ ```
23
+
24
+ ### Single Selection for Later Action
25
+ ```ruby
26
+ # User picks a category, then clicks Add — no round-trip on select change
27
+ cat_names = categories.map { |c| c["name"] }
28
+ select :category, cat_names, default: cat_names.first, submit: false
29
+
30
+ button "Add Item", style: :primary do |s|
31
+ chosen = s[:category] # string name, not an index
32
+ # ... use chosen
33
+ end
34
+ ```
35
+
36
+ ### Multi-Select Then Act
37
+ ```ruby
38
+ checkbox_group :selected_items do
39
+ items.each { |item| item item.id do text item.name end }
40
+ end
41
+
42
+ button "Delete Selected" do |s|
43
+ ids = s[:selected_items] # array of checked values
44
+ # ... delete them
45
+ end
46
+ ```
47
+
48
+ ### Traditional Form
49
+ ```ruby
50
+ form :edit_person do
51
+ text_field :name, placeholder: "Name"
52
+ text_field :email, placeholder: "Email"
53
+ submit "Save" do |s|
54
+ data = s[:edit_person] # { name: "...", email: "..." }
55
+ # ... save
56
+ end
57
+ cancel "Cancel"
58
+ end
59
+ ```
60
+
61
+ ### Scrollable List
62
+ ```ruby
63
+ scroll_box(max_height: "400px") do
64
+ items.each do |item|
65
+ hstack spacing: :xs, align: :center do
66
+ text item.name
67
+ button "Edit", id: "edit_#{item.id}" do |s|
68
+ # ...
69
+ end
70
+ end
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## Anti-Patterns
76
+
77
+ ### 1. Button-as-Radio
78
+ **Wrong** — Using buttons + state to simulate selection:
79
+ ```ruby
80
+ # DON'T DO THIS
81
+ hstack do
82
+ categories.each_with_index do |cat, i|
83
+ button cat, style: state[:cat] == i ? :primary : :secondary do |s|
84
+ s[:cat] = i
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ **Right** — Use `select` or `radio_group`:
91
+ ```ruby
92
+ select :cat, category_names, submit: false
93
+ ```
94
+
95
+ ### 2. Integer State Keys
96
+ **Wrong** — Storing array indices in session state:
97
+ ```ruby
98
+ s[:cat] = i # breaks if list order changes
99
+ ```
100
+
101
+ **Right** — Store the string name, resolve to index at action time:
102
+ ```ruby
103
+ s[:cat_name] = cat["name"]
104
+ # later:
105
+ idx = categories.index { |c| c["name"] == s[:cat_name] } || 0
106
+ ```
107
+
108
+ ### 3. Forgetting `submit: false`
109
+ **Wrong** — Inputs inside button-action workflows auto-submit by default:
110
+ ```ruby
111
+ text_field :label # triggers round-trip on every keystroke!
112
+ button "Add" do |s| ... end
113
+ ```
114
+
115
+ **Right**:
116
+ ```ruby
117
+ text_field :label, submit: false
118
+ button "Add" do |s| ... end
119
+ ```
120
+
121
+ ### 4. Session Cookie Overflow
122
+ Large text values in state overflow the 4KB cookie limit.
123
+ **Fix**: Use `transient: true` to exclude from the session cookie:
124
+ ```ruby
125
+ text_area :paste_area, transient: true
126
+ ```
127
+
128
+ ### 5. Stale Session After Restart
129
+ Static dev secret preserves cookies across restarts. If your state schema changed, old cookies cause errors.
130
+ **Fix**: Store state as recoverable strings (not fragile indices). Use `--reset` flag or clear cookies after schema changes.
131
+
132
+ ### 6. Transient Fields Never Resync From the Server
133
+ `transient: true` is documented above (#4) as "exclude from the session cookie" — but the
134
+ Alpine adapter's morph-merge goes further than that: it also skips writing the server's value
135
+ back into the browser's reactive store on *every* subsequent update, not just full-cookie
136
+ snapshots (`adapter/alpinejs.rb`, the `htmx:beforeSwap` handler — `if (transientKeys.has(k))
137
+ return;`). In practice this means once a transient field's DOM node exists, only the user's own
138
+ typing ever changes what it shows — a server-side `s[:x] = ""` after a button handler, or a
139
+ fresh prefill for a *different* record reusing the same state key, is silently dropped.
140
+
141
+ **Wrong** — clearing a transient field after its own submit handler:
142
+ ```ruby
143
+ text_field :custom_item, submit: false, transient: true
144
+ button "Add" do |s|
145
+ add_item(s[:custom_item])
146
+ s[:custom_item] = "" # never reaches the browser — field still shows the old value
147
+ end
148
+ ```
149
+
150
+ **Wrong** — reusing one transient key across multiple records' edit forms (e.g. a schema-driven
151
+ edit UI where every row's Time/Date field binds to the same `:edit_time`/`:edit_date` key
152
+ instead of a per-row key): opening row B's edit form after row A's still shows row A's value,
153
+ because the fresh prefill for row B never overwrites what's already sitting in the browser.
154
+
155
+ **Fix**: if a field's value must ever be authoritatively reset by the server (a post-submit
156
+ clear, a fresh prefill on reopen), don't mark it `transient: true` unless it's genuinely a
157
+ one-shot, never-cleared, never-reused input (the large-paste-area case #4 was written for).
158
+ Short single-value fields rarely have a real cookie-size reason to be transient — dropping the
159
+ flag is usually simpler than working around the resync gap. If cookie size is the actual
160
+ concern *and* the field needs server-driven resets, the two needs are currently in tension —
161
+ there's no flag that gives "excluded from cookie" without also losing resync.
162
+
163
+ Found via `~/work/cultiv-ai/apps/health_dashboard/app.rb`, 2026-08-20 — a custom-item field
164
+ that never visually cleared after Add, and an entry-edit form's Time/Date fields that leaked
165
+ one edit's value into the next.