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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ module Deck
6
+ # Fixed top bar for cancel confirmation.
7
+ # Slides down from top with confirm/cancel buttons and auto-hide timer.
8
+ #
9
+ # sw- CSS classes:
10
+ # sw-confirmation-bar - fixed top bar wrapper
11
+ # sw-confirmation-bar--visible - visible state (slide-down)
12
+ # sw-confirmation-bar__message - message text
13
+ # sw-confirmation-bar__actions - button container
14
+ # sw-confirmation-bar__btn - action button base
15
+ # sw-confirmation-bar__btn--confirm - confirm (destructive) button
16
+ # sw-confirmation-bar__btn--cancel - cancel (dismiss) button
17
+ # sw-confirmation-bar__timer - auto-hide countdown display
18
+ #
19
+ # @example
20
+ # confirmation_bar(
21
+ # message: "Are you sure you want to cancel?",
22
+ # confirm_label: "Yes, Cancel",
23
+ # cancel_label: "Keep Going",
24
+ # auto_hide: 5
25
+ # )
26
+ class ConfirmationBar < Base
27
+ attr_reader :message, :confirm_label, :cancel_label, :auto_hide
28
+
29
+ # @param message [String] Confirmation message
30
+ # @param confirm_label [String] Label for confirm action (default: "Cancel")
31
+ # @param cancel_label [String] Label for dismiss action (default: "Keep Going")
32
+ # @param auto_hide [Integer, nil] Auto-hide after N seconds (default: 5, nil to disable)
33
+ # @param options [Hash] Additional options
34
+ def initialize(message:, confirm_label: "Cancel", cancel_label: "Keep Going",
35
+ auto_hide: 5, **options)
36
+ @message = message
37
+ @confirm_label = confirm_label
38
+ @cancel_label = cancel_label
39
+ @auto_hide = auto_hide
40
+ super(**options)
41
+ end
42
+
43
+ def render(view, state)
44
+ view.adapter.render_confirmation_bar(view, self, state)
45
+ end
46
+
47
+ # Whether auto-hide is enabled.
48
+ # @return [Boolean]
49
+ def auto_hide?
50
+ !@auto_hide.nil? && @auto_hide > 0
51
+ end
52
+
53
+ # CSS class list
54
+ def css_classes
55
+ "sw-confirmation-bar"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ module Deck
6
+ # A selectable option card within a DeckSlide.
7
+ # Displays a label, optional preview content (mermaid, code_block, etc.),
8
+ # optional aside text, recommended badge, and a notes textarea.
9
+ #
10
+ # ARIA: role="radio", aria-checked (selection state is T8 concern,
11
+ # but the structural ARIA attributes are set here).
12
+ #
13
+ # sw- CSS classes:
14
+ # sw-deck-option - option card wrapper
15
+ # sw-deck-option--recommended - when recommended: true
16
+ # sw-deck-option__radio - radio indicator circle
17
+ # sw-deck-option__header - label header area
18
+ # sw-deck-option__label - label text
19
+ # sw-deck-option__badge - "Recommended" badge
20
+ # sw-deck-option__preview - preview content area
21
+ # sw-deck-option__aside - aside text below preview
22
+ # sw-deck-option__notes - notes textarea container
23
+ # sw-deck-option__notes-input - the textarea itself
24
+ #
25
+ # @example
26
+ # option "Monolith", aside: "Simple deployment", recommended: true do
27
+ # mermaid "graph TD; A-->B", compact: true
28
+ # end
29
+ class DeckOption < Base
30
+ attr_reader :label, :aside, :recommended, :description
31
+ attr_accessor :children, :slide_id, :option_index
32
+
33
+ # @param label [String] Option label text
34
+ # @param aside [String, nil] Aside text displayed below the preview
35
+ # @param recommended [Boolean] Whether to show "Recommended" badge
36
+ # @param description [String, nil] Description text (for tooltip/aria)
37
+ # @param options [Hash] Additional options
38
+ def initialize(label, aside: nil, recommended: false, description: nil, **options)
39
+ @label = label
40
+ @aside = aside
41
+ @recommended = recommended
42
+ @description = description
43
+ @children = []
44
+ super(**options)
45
+ end
46
+
47
+ def render(view, state)
48
+ view.adapter.render_deck_option(view, self, state)
49
+ end
50
+
51
+ # CSS class list (selected state added dynamically by adapter)
52
+ def css_classes(selected: false)
53
+ classes = ["sw-deck-option"]
54
+ classes << "sw-deck-option--recommended" if @recommended
55
+ classes << "sw-deck-option--selected" if selected
56
+ classes.join(" ")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ module Deck
6
+ # A single decision slide within a DesignDeck.
7
+ # Contains DeckOption children rendered as a grid of option cards.
8
+ #
9
+ # Auto-column detection based on option count:
10
+ # 1 option -> 1 column
11
+ # 2 options -> 2 columns
12
+ # 3 options -> 3 columns
13
+ # 4+ options -> 2 columns
14
+ #
15
+ # sw- CSS classes:
16
+ # sw-deck-slide - slide wrapper
17
+ # sw-deck-slide__title - slide title
18
+ # sw-deck-slide__context - context text above options
19
+ # sw-deck-slide__grid - options grid container
20
+ #
21
+ # @example
22
+ # slide "arch", "System Architecture", context: "Choose the backend" do
23
+ # option "Monolith" do
24
+ # code_block "...", lang: "ts"
25
+ # end
26
+ # end
27
+ class DeckSlide < Base
28
+ attr_reader :id, :title, :context_text, :columns
29
+ attr_accessor :children
30
+
31
+ # @param id [String] Unique slide identifier
32
+ # @param title [String, nil] Slide title
33
+ # @param context [String, nil] Context text displayed above the options
34
+ # @param columns [Integer, nil] Override auto-detected column count
35
+ # @param options [Hash] Additional options
36
+ def initialize(id, title = nil, context: nil, columns: nil, **options)
37
+ @id = id.to_s
38
+ @title = title
39
+ @context_text = context
40
+ @columns = columns
41
+ @children = []
42
+ super(**options)
43
+ end
44
+
45
+ def render(view, state)
46
+ view.adapter.render_deck_slide(view, self, state)
47
+ end
48
+
49
+ # Auto-detect the number of grid columns from option count.
50
+ # Can be overridden with the columns: parameter.
51
+ #
52
+ # @return [Integer] Number of grid columns
53
+ def auto_columns
54
+ return @columns if @columns
55
+
56
+ count = option_count
57
+ case count
58
+ when 0, 1 then 1
59
+ when 2 then 2
60
+ when 3 then 3
61
+ else 2
62
+ end
63
+ end
64
+
65
+ # Count only DeckOption children (not other components)
66
+ def option_count
67
+ @children.count { |c| c.is_a?(DeckOption) }
68
+ end
69
+
70
+ # CSS class list
71
+ def css_classes
72
+ "sw-deck-slide"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,469 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'securerandom'
6
+
7
+ module StreamWeaver
8
+ module Components
9
+ module Deck
10
+ # File-backed state store for design deck selections, notes, and
11
+ # generate-more request queue.
12
+ # Keyed by session ID. Each session gets a JSON file in tmp/deck_state/.
13
+ #
14
+ # Thread-safe via file locking (flock). Multiple Puma threads and
15
+ # background agent threads can safely read/write the same file.
16
+ #
17
+ # State shape:
18
+ # {
19
+ # "selections" => { "slide_id" => "option_label" },
20
+ # "notes" => { "slide_id" => { "option_label" => "text" } },
21
+ # "generate" => { "status" => "idle", ... },
22
+ # "generate_queue" => [ { request data } ],
23
+ # "generated_options" => { "slide_id" => [ { option data } ] }
24
+ # }
25
+ #
26
+ # @example
27
+ # state = DeckState.new("abc123")
28
+ # state.select("arch", "Monolith")
29
+ # state.set_note("arch", "Monolith", "Simple deployment")
30
+ # state.selected?("arch", "Monolith") # => true
31
+ # state.selection("arch") # => "Monolith"
32
+ # state.note("arch", "Monolith") # => "Simple deployment"
33
+ class DeckState
34
+ attr_reader :session_id
35
+
36
+ # Base directory for state files (relative to app working dir)
37
+ DEFAULT_STATE_DIR = "tmp/deck_state"
38
+
39
+ # @param session_id [String] Unique session identifier
40
+ # @param state_dir [String] Override state directory (for testing)
41
+ def initialize(session_id, state_dir: nil)
42
+ @session_id = session_id
43
+ @state_dir = state_dir || DEFAULT_STATE_DIR
44
+ ensure_state_dir!
45
+ end
46
+
47
+ # Generate a new unique session ID
48
+ # @return [String] UUID-style session ID
49
+ def self.generate_session_id
50
+ SecureRandom.uuid
51
+ end
52
+
53
+ # Get or create a DeckState for the given session.
54
+ # Generates a new session ID if none provided.
55
+ #
56
+ # @param session_id [String, nil] Existing session ID or nil
57
+ # @param state_dir [String, nil] Override state directory
58
+ # @return [DeckState] The state instance
59
+ def self.for_session(session_id = nil, state_dir: nil)
60
+ session_id ||= generate_session_id
61
+ new(session_id, state_dir: state_dir)
62
+ end
63
+
64
+ # Select an option for a slide (radio semantics: one per slide)
65
+ #
66
+ # @param slide_id [String] Slide identifier
67
+ # @param option_label [String] Option label to select
68
+ def select(slide_id, option_label)
69
+ with_lock(:write) do |data|
70
+ data["selections"][slide_id.to_s] = option_label.to_s
71
+ data
72
+ end
73
+ end
74
+
75
+ # Get the selected option label for a slide
76
+ #
77
+ # @param slide_id [String] Slide identifier
78
+ # @return [String, nil] Selected option label or nil
79
+ def selection(slide_id)
80
+ read_data["selections"][slide_id.to_s]
81
+ end
82
+
83
+ # Check if a specific option is selected for a slide
84
+ #
85
+ # @param slide_id [String] Slide identifier
86
+ # @param option_label [String] Option label to check
87
+ # @return [Boolean]
88
+ def selected?(slide_id, option_label)
89
+ selection(slide_id) == option_label.to_s
90
+ end
91
+
92
+ # Get all selections
93
+ # @return [Hash] { slide_id => option_label }
94
+ def selections
95
+ read_data["selections"]
96
+ end
97
+
98
+ # Set a note for an option on a slide
99
+ #
100
+ # @param slide_id [String] Slide identifier
101
+ # @param option_label [String] Option label
102
+ # @param text [String] Note text
103
+ def set_note(slide_id, option_label, text)
104
+ with_lock(:write) do |data|
105
+ data["notes"][slide_id.to_s] ||= {}
106
+ data["notes"][slide_id.to_s][option_label.to_s] = text.to_s
107
+ data
108
+ end
109
+ end
110
+
111
+ # Get the note for an option on a slide
112
+ #
113
+ # @param slide_id [String] Slide identifier
114
+ # @param option_label [String] Option label
115
+ # @return [String, nil] Note text or nil
116
+ def note(slide_id, option_label)
117
+ notes = read_data["notes"][slide_id.to_s]
118
+ notes&.dig(option_label.to_s)
119
+ end
120
+
121
+ # Get all notes
122
+ # @return [Hash] { slide_id => { option_label => text } }
123
+ def notes
124
+ read_data["notes"]
125
+ end
126
+
127
+ # Mark this deck as submitted
128
+ def submit!
129
+ with_lock(:write) do |data|
130
+ data["submitted"] = true
131
+ data
132
+ end
133
+ end
134
+
135
+ # Check if this deck has been submitted
136
+ # @return [Boolean]
137
+ def submitted?
138
+ read_data["submitted"] == true
139
+ end
140
+
141
+ # Set the final notes (overall comments on the summary slide)
142
+ #
143
+ # @param text [String] Final notes text
144
+ def set_final_notes(text)
145
+ with_lock(:write) do |data|
146
+ data["final_notes"] = text.to_s
147
+ data
148
+ end
149
+ end
150
+
151
+ # Get the final notes
152
+ # @return [String, nil]
153
+ def final_notes
154
+ read_data["final_notes"]
155
+ end
156
+
157
+ # =========================================
158
+ # Model Selector State (T14)
159
+ # =========================================
160
+
161
+ # Set the selected AI model for generate-more.
162
+ #
163
+ # @param model_id [String] Model identifier
164
+ def set_model(model_id)
165
+ with_lock(:write) do |data|
166
+ data["selected_model"] = model_id.to_s
167
+ data
168
+ end
169
+ end
170
+
171
+ # Get the currently selected model ID.
172
+ # @return [String, nil]
173
+ def selected_model
174
+ read_data["selected_model"]
175
+ end
176
+
177
+ # =========================================
178
+ # Generate-More State (T10)
179
+ # =========================================
180
+
181
+ # Start a generate request. Transitions to :generating.
182
+ # Returns the request_id for versioning.
183
+ #
184
+ # @param slide_id [String] Slide to generate options for
185
+ # @param count [Integer] Number of options to generate
186
+ # @param prompt [String, nil] User's custom prompt
187
+ # @return [String] The request_id
188
+ def start_generate(slide_id, count, prompt: nil)
189
+ request_id = SecureRandom.hex(8)
190
+ with_lock(:write) do |data|
191
+ data["generate"] = {
192
+ "status" => "generating",
193
+ "slide_id" => slide_id.to_s,
194
+ "requested_count" => count,
195
+ "received_count" => 0,
196
+ "prompt" => prompt,
197
+ "request_id" => request_id,
198
+ "started_at" => Time.now.to_f
199
+ }
200
+ # Queue the request for agent polling
201
+ data["generate_queue"] ||= []
202
+ data["generate_queue"] << {
203
+ "session_id" => @session_id,
204
+ "slide_id" => slide_id.to_s,
205
+ "count" => count,
206
+ "prompt" => prompt,
207
+ "request_id" => request_id,
208
+ "timestamp" => Time.now.to_f
209
+ }
210
+ data
211
+ end
212
+ request_id
213
+ end
214
+
215
+ # Cancel a pending generation. Transitions to :idle.
216
+ # Sets a cancellation flag so agents can detect it.
217
+ def cancel_generate
218
+ with_lock(:write) do |data|
219
+ gen = data["generate"] || {}
220
+ if gen["status"] == "generating"
221
+ gen["status"] = "cancelled"
222
+ data["generate"] = gen
223
+ # Remove from queue
224
+ data["generate_queue"] = []
225
+ end
226
+ data
227
+ end
228
+ end
229
+
230
+ # Transition a timed-out or cancelled generate to idle.
231
+ def reset_generate
232
+ with_lock(:write) do |data|
233
+ gen = data["generate"] || {}
234
+ gen["status"] = "idle"
235
+ data["generate"] = gen
236
+ data
237
+ end
238
+ end
239
+
240
+ # Mark generation as timed out.
241
+ def timeout_generate
242
+ with_lock(:write) do |data|
243
+ gen = data["generate"] || {}
244
+ if gen["status"] == "generating"
245
+ gen["status"] = "timed_out"
246
+ data["generate"] = gen
247
+ end
248
+ data
249
+ end
250
+ end
251
+
252
+ # Get the current generate state hash.
253
+ # @return [Hash] Generate state or empty idle state
254
+ def generate_state
255
+ data = read_data
256
+ data["generate"] || { "status" => "idle" }
257
+ end
258
+
259
+ # Check if currently generating.
260
+ # @return [Boolean]
261
+ def generating?
262
+ generate_state["status"] == "generating"
263
+ end
264
+
265
+ # Get the generate status.
266
+ # @return [Symbol] :idle, :generating, :timed_out, :cancelled
267
+ def generate_status
268
+ (generate_state["status"] || "idle").to_sym
269
+ end
270
+
271
+ # Take pending requests from the queue (for agent polling).
272
+ # Returns pending requests and clears the queue atomically.
273
+ #
274
+ # @return [Array<Hash>] Pending requests
275
+ def take_pending_requests!
276
+ requests = []
277
+ with_lock(:write) do |data|
278
+ requests = data["generate_queue"] || []
279
+ data["generate_queue"] = []
280
+ data
281
+ end
282
+ requests
283
+ end
284
+
285
+ # Add a generated option to the state (called by agent).
286
+ # Increments received_count and transitions to :idle when all received.
287
+ #
288
+ # @param slide_id [String] Slide the option belongs to
289
+ # @param option_data [Hash] Option data (label, description, etc.)
290
+ # @param request_id [String] Request ID for versioning
291
+ # @return [Hash] Updated generate state
292
+ def add_generated_option(slide_id, option_data, request_id:)
293
+ result_gen = nil
294
+ with_lock(:write) do |data|
295
+ gen = data["generate"] || {}
296
+
297
+ # Request versioning: ignore stale pushes
298
+ if gen["request_id"] != request_id
299
+ result_gen = gen
300
+ next data
301
+ end
302
+
303
+ # Ignore if cancelled or timed out
304
+ unless gen["status"] == "generating"
305
+ result_gen = gen
306
+ next data
307
+ end
308
+
309
+ # Add option to generated_options store
310
+ data["generated_options"] ||= {}
311
+ data["generated_options"][slide_id.to_s] ||= []
312
+ data["generated_options"][slide_id.to_s] << option_data.merge("generated" => true)
313
+
314
+ # Update received count
315
+ gen["received_count"] = (gen["received_count"] || 0) + 1
316
+
317
+ # If all received, transition to idle
318
+ if gen["received_count"] >= gen["requested_count"]
319
+ gen["status"] = "idle"
320
+ end
321
+
322
+ data["generate"] = gen
323
+ result_gen = gen.dup
324
+ data
325
+ end
326
+ result_gen || { "status" => "idle" }
327
+ end
328
+
329
+ # Get generated options for a slide.
330
+ # @param slide_id [String] Slide identifier
331
+ # @return [Array<Hash>] Generated options
332
+ def generated_options(slide_id)
333
+ data = read_data
334
+ (data["generated_options"] || {})[slide_id.to_s] || []
335
+ end
336
+
337
+ # Get all generated options across all slides.
338
+ # @return [Hash] { slide_id => [options] }
339
+ def all_generated_options
340
+ read_data["generated_options"] || {}
341
+ end
342
+
343
+ # Check if a generate request has been cancelled.
344
+ # @return [Boolean]
345
+ def generate_cancelled?
346
+ generate_state["status"] == "cancelled"
347
+ end
348
+
349
+ # Read all state data
350
+ # @return [Hash] Full state hash
351
+ def to_h
352
+ read_data
353
+ end
354
+
355
+ # Clear all state for this session
356
+ def clear!
357
+ with_lock(:write) do |_data|
358
+ empty_state
359
+ end
360
+ end
361
+
362
+ # Delete the state file entirely
363
+ def delete!
364
+ File.delete(state_file_path) if File.exist?(state_file_path)
365
+ end
366
+
367
+ # Path to the JSON state file
368
+ # @return [String]
369
+ def state_file_path
370
+ File.join(@state_dir, "#{@session_id}.json")
371
+ end
372
+
373
+ # Clean up stale state files older than the given age
374
+ #
375
+ # @param max_age_seconds [Integer] Maximum age in seconds (default: 24 hours)
376
+ # @param state_dir [String] Override state directory
377
+ # @return [Integer] Number of files cleaned up
378
+ def self.cleanup_stale(max_age_seconds: 86400, state_dir: DEFAULT_STATE_DIR)
379
+ return 0 unless Dir.exist?(state_dir)
380
+
381
+ count = 0
382
+ cutoff = Time.now - max_age_seconds
383
+ Dir.glob(File.join(state_dir, "*.json")).each do |file|
384
+ if File.mtime(file) < cutoff
385
+ File.delete(file) rescue nil
386
+ count += 1
387
+ end
388
+ end
389
+ count
390
+ end
391
+
392
+ private
393
+
394
+ def empty_state
395
+ {
396
+ "selections" => {},
397
+ "notes" => {},
398
+ "submitted" => false,
399
+ "final_notes" => "",
400
+ "generate" => { "status" => "idle" },
401
+ "generate_queue" => [],
402
+ "generated_options" => {}
403
+ }
404
+ end
405
+
406
+ def ensure_state_dir!
407
+ FileUtils.mkdir_p(@state_dir) unless Dir.exist?(@state_dir)
408
+ end
409
+
410
+ # Read state from file. Returns empty state if file doesn't exist.
411
+ def read_data
412
+ with_lock(:read) do |data|
413
+ data
414
+ end
415
+ end
416
+
417
+ # Execute a block with file locking.
418
+ # For :write mode, the block receives current data and must return updated data.
419
+ # For :read mode, the block receives current data and returns it.
420
+ def with_lock(mode)
421
+ path = state_file_path
422
+ lock_mode = (mode == :write) ? File::LOCK_EX : File::LOCK_SH
423
+
424
+ # Create file if it doesn't exist
425
+ unless File.exist?(path)
426
+ if mode == :write
427
+ File.open(path, File::CREAT | File::WRONLY | File::EXCL) do |f|
428
+ f.flock(File::LOCK_EX)
429
+ f.write(JSON.generate(empty_state))
430
+ end rescue Errno::EEXIST
431
+ # If EEXIST, another thread created it -- that's fine
432
+ else
433
+ return yield(empty_state)
434
+ end
435
+ end
436
+
437
+ File.open(path, "r+") do |f|
438
+ f.flock(lock_mode)
439
+ content = f.read
440
+ data = content.empty? ? empty_state : JSON.parse(content)
441
+
442
+ result = yield(data)
443
+
444
+ if mode == :write
445
+ f.rewind
446
+ f.truncate(0)
447
+ f.write(JSON.generate(result))
448
+ f.flush
449
+ end
450
+
451
+ result
452
+ end
453
+ rescue JSON::ParserError
454
+ # Corrupted file -- reset to empty
455
+ if mode == :write
456
+ File.open(path, "w") do |f|
457
+ f.flock(File::LOCK_EX)
458
+ result = yield(empty_state)
459
+ f.write(JSON.generate(result))
460
+ result
461
+ end
462
+ else
463
+ yield(empty_state)
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end