pixelweaver 0.1.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 (392) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +18 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +16 -0
  6. package/.python-version +1 -0
  7. package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
  8. package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
  9. package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
  10. package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
  11. package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
  12. package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
  13. package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
  14. package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
  15. package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
  16. package/.rlsbl/bases/CHANGELOG.md +5 -0
  17. package/.rlsbl/bases/LICENSE +21 -0
  18. package/.rlsbl/changes/.validated +1 -0
  19. package/.rlsbl/changes/0.1.0.jsonl +85 -0
  20. package/.rlsbl/changes/0.1.0.md +13 -0
  21. package/.rlsbl/changes/unreleased.jsonl +0 -0
  22. package/.rlsbl/config.json +6 -0
  23. package/.rlsbl/hashes.json +14 -0
  24. package/.rlsbl/hooks/post-release.sh +8 -0
  25. package/.rlsbl/hooks/pre-checks.sh +5 -0
  26. package/.rlsbl/hooks/pre-release.sh +8 -0
  27. package/.rlsbl/lint/go.toml +17 -0
  28. package/.rlsbl/lint/npm.toml +19 -0
  29. package/.rlsbl/lint/python.toml +25 -0
  30. package/.rlsbl/releases/unreleased.toml +0 -0
  31. package/.rlsbl/releases/v0.1.0.toml +3 -0
  32. package/.rlsbl/version +1 -0
  33. package/.selfdoc/hashes/hashes.json +146 -0
  34. package/.strictcli/schema.json +227 -0
  35. package/CHANGELOG.md +17 -0
  36. package/CLAUDE.md +100 -0
  37. package/LICENSE +21 -0
  38. package/README.md +116 -0
  39. package/assets/icon.png +0 -0
  40. package/docs/_README.md +117 -0
  41. package/docs/cli-config.md +35 -0
  42. package/docs/cli-dev.md +21 -0
  43. package/docs/cli-diagnose.md +12 -0
  44. package/docs/cli-index.md +30 -0
  45. package/docs/cli-list.md +18 -0
  46. package/docs/cli-mcp.md +18 -0
  47. package/docs/cli-new.md +26 -0
  48. package/docs/cli-open.md +21 -0
  49. package/docs/cli-serve.md +21 -0
  50. package/docs/cli-stop.md +12 -0
  51. package/docs/gen-index.md +36 -0
  52. package/docs/index.md +13 -0
  53. package/docs/server-src-pixelweaver-__main__.md +12 -0
  54. package/docs/server-src-pixelweaver-autosave.md +12 -0
  55. package/docs/server-src-pixelweaver-bridge.md +12 -0
  56. package/docs/server-src-pixelweaver-cli.md +12 -0
  57. package/docs/server-src-pixelweaver-config.md +12 -0
  58. package/docs/server-src-pixelweaver-connections.md +12 -0
  59. package/docs/server-src-pixelweaver-main.md +12 -0
  60. package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
  61. package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
  62. package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
  63. package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
  64. package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
  65. package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
  66. package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
  67. package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
  68. package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
  69. package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
  70. package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
  71. package/docs/server-src-pixelweaver-mcp_server.md +12 -0
  72. package/docs/server-src-pixelweaver-protocol.md +12 -0
  73. package/docs/server-src-pixelweaver-state.md +12 -0
  74. package/docs/server-src-pixelweaver-storage.md +12 -0
  75. package/docs/server-src-pixelweaver-websocket.md +12 -0
  76. package/docs/server-src-pixelweaver.md +12 -0
  77. package/e2e/app-launch.test.ts +35 -0
  78. package/e2e/menus.test.ts +26 -0
  79. package/e2e/tools.test.ts +27 -0
  80. package/e2e/undo-redo.test.ts +11 -0
  81. package/eslint.config.js +62 -0
  82. package/index.html +13 -0
  83. package/package.json +48 -0
  84. package/playwright.config.ts +19 -0
  85. package/plugins/builtin/.gitkeep +0 -0
  86. package/plugins/builtin/advanced-fill-tool.ts +146 -0
  87. package/plugins/builtin/circle-tool.ts +186 -0
  88. package/plugins/builtin/diamond-tool.ts +182 -0
  89. package/plugins/builtin/dither-tool.ts +186 -0
  90. package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
  91. package/plugins/builtin/drawing-utils.test.ts +495 -0
  92. package/plugins/builtin/drawing-utils.ts +431 -0
  93. package/plugins/builtin/effects/blur.ts +97 -0
  94. package/plugins/builtin/effects/color-effects.ts +278 -0
  95. package/plugins/builtin/effects/flip.ts +83 -0
  96. package/plugins/builtin/effects/glow.ts +118 -0
  97. package/plugins/builtin/effects/outline.ts +110 -0
  98. package/plugins/builtin/effects/rotate.ts +357 -0
  99. package/plugins/builtin/effects/scale.ts +258 -0
  100. package/plugins/builtin/effects/shadow.ts +111 -0
  101. package/plugins/builtin/effects/sharpen.ts +102 -0
  102. package/plugins/builtin/effects.test.ts +715 -0
  103. package/plugins/builtin/eraser-tool.ts +23 -0
  104. package/plugins/builtin/eyedropper-tool.ts +83 -0
  105. package/plugins/builtin/fill-tool.ts +93 -0
  106. package/plugins/builtin/gradient-tool.ts +204 -0
  107. package/plugins/builtin/gradient.test.ts +142 -0
  108. package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
  109. package/plugins/builtin/importers/aseprite-parser.ts +497 -0
  110. package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
  111. package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
  112. package/plugins/builtin/line-tool.ts +267 -0
  113. package/plugins/builtin/make-stroke-tool.ts +271 -0
  114. package/plugins/builtin/noise-dither.test.ts +151 -0
  115. package/plugins/builtin/noise-tool.ts +131 -0
  116. package/plugins/builtin/pattern-stamp-tool.ts +162 -0
  117. package/plugins/builtin/pencil-tool.ts +25 -0
  118. package/plugins/builtin/rect-tool.ts +179 -0
  119. package/plugins/builtin/selection-tool.ts +388 -0
  120. package/plugins/builtin/selection.test.ts +195 -0
  121. package/plugins/builtin/tool-plugins.test.ts +529 -0
  122. package/public/favicon.svg +7 -0
  123. package/public/sw.js +91 -0
  124. package/pyproject.toml +49 -0
  125. package/scripts/eslint-wave-a-list.sh +24 -0
  126. package/scripts/eslint-wave-a-status.sh +25 -0
  127. package/scripts/fix-index-signature-access.py +127 -0
  128. package/scripts/fix-unchecked-index.py +128 -0
  129. package/scripts/fix-unchecked-vars.py +127 -0
  130. package/scripts/fix-wave-a-bangs.py +36 -0
  131. package/scripts/fix-wave-a-templates.py +108 -0
  132. package/scripts/fix-wave-b-void-expr.py +167 -0
  133. package/scripts/generate-command-params.py +540 -0
  134. package/scripts/migrate-single-frame-to-multi.py +171 -0
  135. package/scripts/smoke-test.sh +77 -0
  136. package/selfdoc.json +10 -0
  137. package/server/README.md +0 -0
  138. package/server/src/pixelweaver/__init__.py +1 -0
  139. package/server/src/pixelweaver/__main__.py +4 -0
  140. package/server/src/pixelweaver/autosave.py +114 -0
  141. package/server/src/pixelweaver/bridge.py +127 -0
  142. package/server/src/pixelweaver/cli.py +199 -0
  143. package/server/src/pixelweaver/config.py +24 -0
  144. package/server/src/pixelweaver/connections.py +54 -0
  145. package/server/src/pixelweaver/main.py +271 -0
  146. package/server/src/pixelweaver/mcp_bridge.py +189 -0
  147. package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
  148. package/server/src/pixelweaver/mcp_export_tools.py +291 -0
  149. package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
  150. package/server/src/pixelweaver/mcp_history_tools.py +106 -0
  151. package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
  152. package/server/src/pixelweaver/mcp_lock.py +37 -0
  153. package/server/src/pixelweaver/mcp_project_tools.py +302 -0
  154. package/server/src/pixelweaver/mcp_read_tools.py +163 -0
  155. package/server/src/pixelweaver/mcp_registry.py +247 -0
  156. package/server/src/pixelweaver/mcp_resources.py +312 -0
  157. package/server/src/pixelweaver/mcp_server.py +234 -0
  158. package/server/src/pixelweaver/protocol.py +219 -0
  159. package/server/src/pixelweaver/state.py +267 -0
  160. package/server/src/pixelweaver/storage.py +293 -0
  161. package/server/src/pixelweaver/websocket.py +282 -0
  162. package/server/tests/__init__.py +0 -0
  163. package/server/tests/conftest.py +17 -0
  164. package/server/tests/test_api.py +96 -0
  165. package/server/tests/test_bridge.py +161 -0
  166. package/server/tests/test_health.py +9 -0
  167. package/server/tests/test_integration.py +86 -0
  168. package/server/tests/test_mcp_bridge.py +293 -0
  169. package/server/tests/test_mcp_lock.py +34 -0
  170. package/server/tests/test_mcp_registry.py +679 -0
  171. package/server/tests/test_mcp_resources.py +648 -0
  172. package/server/tests/test_protocol.py +125 -0
  173. package/server/tests/test_state.py +87 -0
  174. package/server/tests/test_storage.py +306 -0
  175. package/server/tests/test_websocket.py +275 -0
  176. package/src/App.svelte +107 -0
  177. package/src/app.css +215 -0
  178. package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
  179. package/src/lib/animation/animation-commands.test.ts +228 -0
  180. package/src/lib/animation/animation-commands.ts +540 -0
  181. package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
  182. package/src/lib/animation/animation-preview.svelte.ts +151 -0
  183. package/src/lib/animation/clipboard.ts +134 -0
  184. package/src/lib/animation/frame-model.svelte.ts +437 -0
  185. package/src/lib/animation/frame-model.test.ts +314 -0
  186. package/src/lib/animation/frame-selection.svelte.ts +77 -0
  187. package/src/lib/animation/frame-tags.svelte.ts +238 -0
  188. package/src/lib/animation/frame-tags.test.ts +136 -0
  189. package/src/lib/animation/import.test.ts +141 -0
  190. package/src/lib/animation/import.ts +112 -0
  191. package/src/lib/animation/spritesheet-export.test.ts +239 -0
  192. package/src/lib/animation/spritesheet-export.ts +314 -0
  193. package/src/lib/canvas/CanvasViewport.svelte +216 -0
  194. package/src/lib/canvas/canvas-init-plugin.ts +178 -0
  195. package/src/lib/canvas/canvas-renderer.ts +408 -0
  196. package/src/lib/canvas/canvas-state.svelte.ts +232 -0
  197. package/src/lib/canvas/canvas-state.test.ts +139 -0
  198. package/src/lib/canvas/input-handler.ts +221 -0
  199. package/src/lib/canvas/input-plugin.ts +150 -0
  200. package/src/lib/canvas/onion-skin.ts +94 -0
  201. package/src/lib/canvas/pixel-buffer.test.ts +249 -0
  202. package/src/lib/canvas/pixel-buffer.ts +151 -0
  203. package/src/lib/canvas/render-state.svelte.ts +18 -0
  204. package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
  205. package/src/lib/canvas/tile-mode.test.ts +53 -0
  206. package/src/lib/canvas/tile-mode.ts +92 -0
  207. package/src/lib/canvas/viewport-utils.ts +24 -0
  208. package/src/lib/canvas/zoom-utils.ts +31 -0
  209. package/src/lib/color/.gitkeep +0 -0
  210. package/src/lib/color/color-commands.ts +87 -0
  211. package/src/lib/color/color-state.svelte.ts +98 -0
  212. package/src/lib/color/color-state.test.ts +91 -0
  213. package/src/lib/color/color-utils.test.ts +220 -0
  214. package/src/lib/color/color-utils.ts +243 -0
  215. package/src/lib/color/palette-state.svelte.ts +127 -0
  216. package/src/lib/color/palette-state.test.ts +154 -0
  217. package/src/lib/color/palette.ts +79 -0
  218. package/src/lib/core/bootstrap.ts +66 -0
  219. package/src/lib/core/command-params.generated.ts +1549 -0
  220. package/src/lib/core/command-params.ts +20 -0
  221. package/src/lib/core/command-runner.ts +79 -0
  222. package/src/lib/core/commands.ts +134 -0
  223. package/src/lib/core/dispatcher.test.ts +548 -0
  224. package/src/lib/core/dispatcher.ts +361 -0
  225. package/src/lib/core/index.test.ts +7 -0
  226. package/src/lib/core/notification-state.svelte.ts +119 -0
  227. package/src/lib/core/plugin-api.ts +210 -0
  228. package/src/lib/core/plugin-discovery.test.ts +53 -0
  229. package/src/lib/core/plugin-discovery.ts +65 -0
  230. package/src/lib/core/plugin-loader.test.ts +159 -0
  231. package/src/lib/core/plugin-loader.ts +240 -0
  232. package/src/lib/core/plugin-types.ts +286 -0
  233. package/src/lib/core/registries.svelte.ts +74 -0
  234. package/src/lib/core/tool-options-state.svelte.ts +61 -0
  235. package/src/lib/dock/DockLayout.svelte +375 -0
  236. package/src/lib/dock/TabAddMenu.svelte +90 -0
  237. package/src/lib/dock/dock-persistence.ts +46 -0
  238. package/src/lib/dock/dock-plugin.ts +49 -0
  239. package/src/lib/dock/dock-presets.ts +156 -0
  240. package/src/lib/dock/dock-state.svelte.ts +77 -0
  241. package/src/lib/dock/dock-types.ts +2 -0
  242. package/src/lib/dock/dockview-theme.css +226 -0
  243. package/src/lib/dock/dockview-theme.ts +17 -0
  244. package/src/lib/dock/header-action-renderer.ts +77 -0
  245. package/src/lib/edit/clipboard-state.ts +34 -0
  246. package/src/lib/export/download.ts +201 -0
  247. package/src/lib/export/png-metadata.ts +181 -0
  248. package/src/lib/history/ActionLogPanel.svelte +418 -0
  249. package/src/lib/history/action-log-panel-plugin.ts +24 -0
  250. package/src/lib/history/action-log.svelte.ts +172 -0
  251. package/src/lib/history/action-log.test.ts +168 -0
  252. package/src/lib/history/history-commands.ts +403 -0
  253. package/src/lib/history/macros.svelte.ts +320 -0
  254. package/src/lib/history/macros.test.ts +224 -0
  255. package/src/lib/history/replay-engine.test.ts +241 -0
  256. package/src/lib/history/replay-engine.ts +149 -0
  257. package/src/lib/history/spec-format.test.ts +250 -0
  258. package/src/lib/history/spec-format.ts +210 -0
  259. package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
  260. package/src/lib/iso/iso-math.test.ts +77 -0
  261. package/src/lib/iso/iso-math.ts +77 -0
  262. package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
  263. package/src/lib/iso/seam-checker.test.ts +126 -0
  264. package/src/lib/iso/seam-checker.ts +93 -0
  265. package/src/lib/iso/tessellation.ts +67 -0
  266. package/src/lib/layers/compositor.test.ts +193 -0
  267. package/src/lib/layers/compositor.ts +175 -0
  268. package/src/lib/layers/layer-commands.test.ts +263 -0
  269. package/src/lib/layers/layer-commands.ts +429 -0
  270. package/src/lib/layers/layer-tree.svelte.ts +516 -0
  271. package/src/lib/layers/layer-tree.test.ts +383 -0
  272. package/src/lib/layers/layer-types.ts +56 -0
  273. package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
  274. package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
  275. package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
  276. package/src/lib/leveleditor/depth-sort.test.ts +70 -0
  277. package/src/lib/leveleditor/depth-sort.ts +39 -0
  278. package/src/lib/leveleditor/level-editor-commands.ts +353 -0
  279. package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
  280. package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
  281. package/src/lib/leveleditor/map-state.svelte.ts +372 -0
  282. package/src/lib/leveleditor/map-state.test.ts +243 -0
  283. package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
  284. package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
  285. package/src/lib/leveleditor/tiled-export.test.ts +144 -0
  286. package/src/lib/leveleditor/tiled-export.ts +374 -0
  287. package/src/lib/pywebview.d.ts +15 -0
  288. package/src/lib/recovery/.gitkeep +0 -0
  289. package/src/lib/recovery/idb-store.ts +118 -0
  290. package/src/lib/recovery/recovery-manager.test.ts +321 -0
  291. package/src/lib/recovery/recovery-manager.ts +115 -0
  292. package/src/lib/recovery/recovery-plugin.ts +55 -0
  293. package/src/lib/recovery/recovery-state.svelte.ts +21 -0
  294. package/src/lib/recovery/sw-plugin.ts +18 -0
  295. package/src/lib/recovery/sw-register.ts +17 -0
  296. package/src/lib/save/directory-format.ts +42 -0
  297. package/src/lib/save/project-snapshot.ts +139 -0
  298. package/src/lib/save/recent-projects.ts +56 -0
  299. package/src/lib/save/save-state.svelte.ts +35 -0
  300. package/src/lib/save/storage.ts +167 -0
  301. package/src/lib/save/zip-format.ts +45 -0
  302. package/src/lib/shortcuts/.gitkeep +0 -0
  303. package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
  304. package/src/lib/shortcuts/default-bindings.ts +61 -0
  305. package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
  306. package/src/lib/shortcuts/shortcut-init.ts +380 -0
  307. package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
  308. package/src/lib/shortcuts/shortcut-manager.ts +281 -0
  309. package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
  310. package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
  311. package/src/lib/sync/patch-applicator.ts +300 -0
  312. package/src/lib/sync/patch-types.ts +65 -0
  313. package/src/lib/sync/project-manager.ts +108 -0
  314. package/src/lib/sync/sync-init.ts +152 -0
  315. package/src/lib/sync/sync-plugin.ts +19 -0
  316. package/src/lib/sync/sync-state.svelte.ts +56 -0
  317. package/src/lib/sync/ws-client.test.ts +604 -0
  318. package/src/lib/sync/ws-client.ts +574 -0
  319. package/src/lib/tools/.gitkeep +0 -0
  320. package/src/lib/ui/.gitkeep +0 -0
  321. package/src/lib/ui/AboutDialog.svelte +113 -0
  322. package/src/lib/ui/ColorPicker.svelte +761 -0
  323. package/src/lib/ui/ContextMenu.svelte +216 -0
  324. package/src/lib/ui/ExportDialog.svelte +747 -0
  325. package/src/lib/ui/FrameStrip.svelte +854 -0
  326. package/src/lib/ui/LayerPanel.svelte +810 -0
  327. package/src/lib/ui/MenuBar.svelte +590 -0
  328. package/src/lib/ui/NewProjectDialog.svelte +803 -0
  329. package/src/lib/ui/PluginManagerPanel.svelte +475 -0
  330. package/src/lib/ui/PromptDialog.svelte +252 -0
  331. package/src/lib/ui/RecoveryDialog.svelte +295 -0
  332. package/src/lib/ui/ResizeDialog.svelte +416 -0
  333. package/src/lib/ui/StatusBar.svelte +145 -0
  334. package/src/lib/ui/ToolbarPanel.svelte +488 -0
  335. package/src/lib/ui/animation-menu-commands.ts +194 -0
  336. package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
  337. package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
  338. package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
  339. package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
  340. package/src/lib/ui/dialog-state.svelte.ts +70 -0
  341. package/src/lib/ui/edit-commands.ts +271 -0
  342. package/src/lib/ui/file-commands.ts +275 -0
  343. package/src/lib/ui/file-open.ts +99 -0
  344. package/src/lib/ui/help-commands.ts +93 -0
  345. package/src/lib/ui/image-commands.ts +181 -0
  346. package/src/lib/ui/layer-menu-commands.ts +420 -0
  347. package/src/lib/ui/menu-builder.ts +224 -0
  348. package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
  349. package/src/lib/ui/notifications/notification-plugin.ts +29 -0
  350. package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
  351. package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
  352. package/src/lib/ui/plugin-state.svelte.ts +62 -0
  353. package/src/lib/ui/select-commands.ts +75 -0
  354. package/src/lib/ui/theme-plugin.ts +18 -0
  355. package/src/lib/ui/theme.svelte.ts +45 -0
  356. package/src/lib/ui/theme.test.ts +51 -0
  357. package/src/lib/ui/toolbar-config.ts +90 -0
  358. package/src/lib/ui/toolbar-plugin.ts +39 -0
  359. package/src/lib/ui/view-commands.ts +629 -0
  360. package/src/lib/variants/BisectionExportDialog.svelte +500 -0
  361. package/src/lib/variants/VariantPanel.svelte +822 -0
  362. package/src/lib/variants/bisection-export.test.ts +113 -0
  363. package/src/lib/variants/bisection-export.ts +148 -0
  364. package/src/lib/variants/palette-extraction.test.ts +111 -0
  365. package/src/lib/variants/palette-extraction.ts +84 -0
  366. package/src/lib/variants/palette-interpolation.test.ts +113 -0
  367. package/src/lib/variants/palette-interpolation.ts +87 -0
  368. package/src/lib/variants/palette-swap.test.ts +101 -0
  369. package/src/lib/variants/palette-swap.ts +114 -0
  370. package/src/lib/variants/variant-commands.ts +594 -0
  371. package/src/lib/variants/variant-panel-plugin.ts +27 -0
  372. package/src/lib/variants/variant-randomizer.ts +101 -0
  373. package/src/lib/variants/variant-state.svelte.ts +166 -0
  374. package/src/lib/variants/variant-state.test.ts +138 -0
  375. package/src/main.ts +14 -0
  376. package/src/vite-env.d.ts +3 -0
  377. package/svelte.config.js +2 -0
  378. package/todo/.done/audit-design-decisions.md +812 -0
  379. package/todo/.done/audit-implementation-plan.md +1235 -0
  380. package/todo/.done/happy-path-polish.md +177 -0
  381. package/todo/.done/pixelweaver-full-build.md +937 -0
  382. package/todo/.done/server-multi-frame-design.md +405 -0
  383. package/todo/.done/typed-dispatcher-design.md +435 -0
  384. package/todo/.done/unified-toolbar-and-action-system.md +323 -0
  385. package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
  386. package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
  387. package/todo/comprehensive-audit.md +1085 -0
  388. package/tsconfig.app.json +26 -0
  389. package/tsconfig.json +7 -0
  390. package/tsconfig.node.json +20 -0
  391. package/uv.lock +1167 -0
  392. package/vite.config.ts +32 -0
@@ -0,0 +1,405 @@
1
+ # Server-side multi-frame storage
2
+
3
+ ## Context
4
+
5
+ The Python companion server (`server/src/pixelweaver/`) acts as the
6
+ single source of truth for collaborative sessions and MCP tool calls. Its
7
+ authoritative data model has **no concept of animation frames**: each
8
+ canvas holds exactly one `pixel_data` dict keyed by layer id. Every path
9
+ that touches "a frame" currently resolves to the canvas's only pixel
10
+ buffer.
11
+
12
+ The frontend (`src/lib/animation/frame-model.svelte.ts:15-37`) has full
13
+ multi-frame support: an ordered list of `Frame` objects, per-frame
14
+ `durationMs`, a current-frame pointer, global FPS, and an origin. When a
15
+ project is saved locally (Tauri file dialog) the snapshot includes
16
+ `frames` and `frameTags` (`src/lib/save/project-snapshot.ts:73-95`). When
17
+ the same project flows through the collab/MCP server, those extra frames
18
+ are silently dropped — only the layer slice of the currently-active
19
+ frame survives.
20
+
21
+ This matters for:
22
+
23
+ - **MCP export**: `export_spritesheet` currently pretends each *canvas*
24
+ is an animation frame (`mcp_registry.py:946-958`); it cannot reach
25
+ real per-frame pixel data.
26
+ - **MCP frame tools**: `add_frame`, `remove_frame`, `duplicate_frame`,
27
+ `reorder_frame`, `set_frame_duration`, `set_global_fps` are already
28
+ registered (`mcp_registry.py:442-493`) but they dispatch opaque
29
+ commands that the frontend executes — the server never models the
30
+ result, so `get_canvas_info` hard-codes `"frame_count": 1`
31
+ (`mcp_registry.py:1211`).
32
+ - **Autosave / project save / load**: on-disk layout hardcodes
33
+ `frames/0/` (`storage.py:72-82, 145`) and the JSON carries no frame
34
+ list.
35
+ - **State round-trip through the MCP bridge**: `serialize_state` /
36
+ `deserialize_into_state` only flatten layer pixel data
37
+ (`mcp_bridge.py:86-155`).
38
+
39
+ The recent guardrail in `export_frame_png` at `storage.py:188-192`
40
+ raises `ValueError` for any `frame != 0` rather than silently returning
41
+ frame 0. That explicit failure is the scaffold this design builds on:
42
+ once multi-frame support lands the guard becomes a normal bounds check.
43
+
44
+ ## Current data model
45
+
46
+ ### Python server
47
+
48
+ `CanvasState` (`state.py:17-41`):
49
+
50
+ - `name`, `width`, `height`
51
+ - `layers: list[dict]` — layer tree (id, name, visible, opacity, blendMode, locked, …)
52
+ - `pixel_data: dict[str, bytes]` — `layer_id -> raw RGBA bytes`
53
+
54
+ `ProjectState` (`state.py:44-64`):
55
+
56
+ - `name`, `width`, `height`
57
+ - `canvases: dict[str, CanvasState]`
58
+ - `command_history`, `redo_stack`
59
+
60
+ No fields exist for frame list, current frame, global FPS, per-frame
61
+ duration, onion skin, or frame tags.
62
+
63
+ ### On-disk layout (`storage.py:36-99`)
64
+
65
+ ```
66
+ <base_dir>/<project>/
67
+ project.json
68
+ canvases/<canvas>/
69
+ canvas.json -- name, width, height, layers
70
+ frames/0/
71
+ layer-<id>.png -- raw RGBA per layer, written by PIL
72
+ history.json
73
+ ```
74
+
75
+ `frames/0` is literally hard-coded in `_save_canvas` and `_load_canvas`.
76
+
77
+ ### Wire format (WS + REST)
78
+
79
+ - `CanvasState.to_dict()` excludes pixel data (`state.py:28-35`).
80
+ - `build_full_state_patch` base64-encodes the **one** pixel buffer per
81
+ layer into a `full_state` patch (`state.py:166-201`).
82
+ - `PixelPatch` already carries `frame_index` (`protocol.py:142-150`) and
83
+ `FramePatch` models `add`/`remove`/`reorder`/`set_duration`
84
+ (`protocol.py:170-179`). The schemas are multi-frame-aware; the
85
+ *producers* on the server are not — nothing ever sets `frame_index` to
86
+ anything other than 0.
87
+ - `get_sync_state` returns canvases without any frame list
88
+ (`state.py:136-163`).
89
+
90
+ ### Frontend wire expectations
91
+
92
+ - `applyServerNativeState` in `patch-applicator.ts:118-156` drops any
93
+ incoming canvas state onto `getCurrentFrame()` — a silent collapse to
94
+ a single frame.
95
+ - `project-manager.ts:80-102` reads the server `GET /api/projects/{name}`
96
+ shape (`{ canvases: {...} }`) and only extracts `{name, width, height}`.
97
+ - `ws-client.ts:16-26` types the sync payload as `{canvases, layers,
98
+ history}` with no frames.
99
+
100
+ ## Target data model
101
+
102
+ ### `FrameState` dataclass
103
+
104
+ Add to `state.py`:
105
+
106
+ ```python
107
+ @dataclass
108
+ class FrameState:
109
+ id: str # stable uuid, matches frontend Frame.id
110
+ duration_ms: int | None # None -> derive from global_fps
111
+ # layer_id -> raw RGBA bytes for THIS frame
112
+ pixel_data: dict[str, bytes] = field(default_factory=dict)
113
+ ```
114
+
115
+ ### `CanvasState` changes
116
+
117
+ - Remove `pixel_data: dict[str, bytes]`.
118
+ - Add `frames: list[FrameState]`.
119
+ - Add `current_frame_index: int = 0`.
120
+ - Add `global_fps: float = 12.0`.
121
+ - Add `origin_x: int = 0`, `origin_y: int = 0`.
122
+ - Optionally `frame_tags: list[dict]` — named ranges from
123
+ `src/lib/animation/frame-tags.svelte.ts`. (Frontend already round-trips
124
+ these in its local save; parking them as an opaque
125
+ `list[dict[str, Any]]` on the server is cheapest.)
126
+
127
+ `layers` stays shared across all frames (layer metadata like name,
128
+ visibility, opacity is per-layer, not per-frame — matches
129
+ `frame-model.svelte.ts:15-22` where a layer's *pixel data* is per frame
130
+ but the layer tree lives in `layer-tree.svelte.ts`).
131
+
132
+ ### Convenience accessors
133
+
134
+ - `CanvasState.current_frame() -> FrameState` — replaces `canvas.pixel_data[id]` usage.
135
+ - `CanvasState.frame_at(index: int) -> FrameState` — bounds-checked.
136
+ - `CanvasState.frame_count() -> int`.
137
+
138
+ ### Invariants
139
+
140
+ - A canvas always has at least one frame (mirrors the frontend's
141
+ "cannot remove last frame" rule at `frame-model.svelte.ts:141`).
142
+ - `0 <= current_frame_index < len(frames)`.
143
+ - All frames share the same layer id set; a layer missing pixel data in
144
+ a frame is equivalent to a transparent buffer (matches the current
145
+ tolerance of missing `pixel_data[layer_id]`).
146
+
147
+ ## Affected files and changes
148
+
149
+ | File | What changes |
150
+ |---|---|
151
+ | `state.py` | New `FrameState` dataclass; refactor `CanvasState` to hold `frames`, `current_frame_index`, `global_fps`, `origin_*`, `frame_tags`; update `create_project` to seed one `FrameState`; update `build_full_state_patch` to emit per-frame pixel data; update `get_sync_state` to include frames. |
152
+ | `storage.py` | Update `_save_canvas` / `_load_canvas` to walk a real frame list; rewrite `frames/N/` layout (see format choice below); update `export_frame_png` to index into `frames[frame]` and drop the `frame != 0` guard. |
153
+ | `main.py` | Update `GET /api/projects/{name}` response (via `to_dict`) so the frontend `project-manager.ts` sees frame metadata; export endpoint `/export/png/{canvas}/{frame}` already takes `frame` — wire it through. |
154
+ | `mcp_bridge.py` | `serialize_state` / `deserialize_into_state` walk frames not `pixel_data`. |
155
+ | `mcp_registry.py` | Make all drawing tools accept `frame_index` (default = current); implement real handlers for `add_frame`, `remove_frame`, `duplicate_frame`, `reorder_frame`, `set_frame_duration`, `set_global_fps` instead of dispatching opaque frontend commands; fix `get_canvas_info` frame count; fix `export_spritesheet` to iterate real frames (not canvases); add `set_current_frame`, `get_frame_count`, `get_current_frame` read tools. |
156
+ | `protocol.py` | No shape changes needed for `PixelPatch` / `FramePatch` (already carry `frame_index` and frame actions). Add a `current_frame_index` field to the `state_sync` payload docs. |
157
+ | `websocket.py` | `handle_message` is frame-agnostic today; no direct changes. `build_full_state_patch` change in `state.py` is enough. |
158
+ | `autosave.py` | No structural change — still just calls `save_project`. Only a doc update if we want "dirty frame subset" granularity later. |
159
+ | `tests/test_storage.py` | Update `TestExportFramePng` to cover multi-frame; update save/load round-trip to assert `frames/N/` for N > 0. |
160
+ | `tests/test_api.py` | Update export endpoint test for N > 0. |
161
+ | `tests/test_mcp_registry.py` | Add coverage for real frame tool handlers + `frame_index` on drawing tools. |
162
+ | `tests/test_mcp_bridge.py` | Update `serialize_state` / `deserialize_into_state` round-trip to include frames. |
163
+ | `src/lib/sync/ws-client.ts` | Extend `StateSnapshot` type with optional `frames` per canvas. |
164
+ | `src/lib/sync/project-manager.ts` | Extend `ProjectInfo` / `CanvasInfo` with `frameCount`, `currentFrameIndex`. |
165
+ | `src/lib/sync/patch-applicator.ts` | `applyServerNativeState` should rebuild the full frame list instead of collapsing to `getCurrentFrame()`. |
166
+
167
+ ## Wire protocol changes
168
+
169
+ Good news: `PixelPatch.frame_index` (`protocol.py:147`) and `FramePatch`
170
+ (`protocol.py:170-179`) **already exist**. The wire schema was designed
171
+ multi-frame-ready; only the server-side producers need to catch up.
172
+
173
+ Changes required:
174
+
175
+ - `build_full_state_patch` must emit one `PixelPatch`-equivalent entry
176
+ per `(frame, layer)` pair, or (preferred) extend the `full_state`
177
+ snapshot shape so each canvas carries a `frames: [{id, durationMs,
178
+ layers: [{id, pixelData}]}]` array.
179
+ - `get_sync_state` (`state.py:136-163`) needs to grow a `frames` field
180
+ per canvas and a `globalFps` / `currentFrameIndex` scalar pair.
181
+ - No version bump needed **if** the new fields are additive and the
182
+ frontend treats missing fields as "legacy single frame". Because
183
+ server and frontend ship in lockstep and there is only one installed
184
+ version at a time (see Backward compatibility below), a hard swap
185
+ with no fallback is acceptable.
186
+
187
+ Frontend currently does **not** send frame data on the WS. Commands flow
188
+ one at a time; a full-state snapshot only moves server → client. So
189
+ server-to-client is the only direction that changes.
190
+
191
+ ## MCP tool surface changes
192
+
193
+ | Tool | Current state | Change |
194
+ |---|---|---|
195
+ | `add_frame`, `remove_frame`, `duplicate_frame`, `reorder_frame`, `set_frame_duration`, `set_global_fps` | Registered, but dispatched as opaque commands for the frontend to execute (`mcp_registry.py:442-493`). Server state never sees the mutation. | Implement as real state mutations; broadcast a `FramePatch` + full-state fallback. |
196
+ | `draw_pixels`, `erase_pixels`, `flood_fill`, `draw_line`, `draw_rect`, `draw_ellipse`, `draw_diamond`, `draw_gradient` | Dispatched as commands on the active canvas, implicitly "current frame". | Accept an optional `frame_index` parameter; default = `canvas.current_frame_index`. |
197
+ | `get_pixel`, `get_region`, `get_palette`, `get_canvas_thumbnail` | Read from the single `pixel_data` dict. | Read from `canvas.current_frame()` (or accept `frame_index`). |
198
+ | `get_canvas_info` | Hardcodes `"frame_count": 1` (`mcp_registry.py:1211`). | Return `len(canvas.frames)` and `current_frame_index`. |
199
+ | `get_layer_tree` | Reports `has_data` from `canvas.pixel_data` (`mcp_registry.py:1231`). | Check `canvas.current_frame().pixel_data`, or accept `frame_index`. |
200
+ | `export_png` | Calls `export_frame_png(project, canvas.name)` (frame defaults to 0). | Accept `frame_index` parameter. |
201
+ | `export_spritesheet` | Treats each *canvas* as a frame (`mcp_registry.py:946-958`). | Iterate real frames of one canvas, arrange into a strip. |
202
+
203
+ New read-only tools to add:
204
+
205
+ - `get_frame_count` — `{canvas_name?}` → `{count, current_index}`
206
+ - `get_current_frame` — returns `{index, duration_ms}`
207
+ - `set_current_frame` — `{frame_index}` (mutates `current_frame_index`)
208
+ - `get_global_fps` / `set_global_fps` (the latter already registered; implement it)
209
+
210
+ ## On-disk format choice
211
+
212
+ Three options for where per-frame pixel data lives on disk.
213
+
214
+ | Aspect | A. `frames/N/layer-<id>.png` | B. Flat binary blob | C. Frame manifest + blobs |
215
+ |---|---|---|---|
216
+ | Layout | `frames/0/layer-a.png`, `frames/1/layer-a.png`, … | One `pixels.bin` per canvas with a header | `frames.json` + `frames/<id>/layer-<id>.png` |
217
+ | Human-inspectable | Yes (PNGs per frame) | No | Yes |
218
+ | Random-access per frame | Good (one dir per frame) | Excellent (seek by offset) | Good |
219
+ | Per-frame metadata (duration, tag) | Must live in `canvas.json` (index-keyed) | Header must grow | Naturally in `frames.json` |
220
+ | Migration cost from current layout | Near zero — already have `frames/0/` | High — new format, new reader/writer | Medium — add a manifest file |
221
+ | Save granularity (dirty N frames only) | Easy | Harder (must rewrite the blob) | Easy |
222
+ | Diff-friendliness (git, rsync) | Best (per-frame PNG) | Worst | Best |
223
+ | Frame reorder cost | Rename dirs or update index in `canvas.json` | Rewrite the blob | Update `frames.json` |
224
+
225
+ ### Recommendation: **Option C (frame manifest + per-frame PNG blobs)**
226
+
227
+ ```
228
+ canvases/<canvas>/
229
+ canvas.json -- name, width, height, layers (unchanged)
230
+ frames.json -- [{id, duration_ms, tag?}, ...], current_frame_index, global_fps, origin
231
+ frames/
232
+ <frame_id>/
233
+ layer-<layer_id>.png
234
+ ```
235
+
236
+ - Frame identity is by uuid (`frame.id`) not by numeric index — reorders
237
+ touch only `frames.json`, no file renames.
238
+ - `frames.json` is small and trivially diffable.
239
+ - Per-frame subdirectory mirrors current `frames/0/` layout, so
240
+ `_save_layer_png` / `_load_layer_png` stay as-is.
241
+ - Autosave can write only changed frame subdirs once a dirty-frame set
242
+ is tracked (future work; the format makes it possible).
243
+
244
+ Option A is the path of least *change* but forces frame metadata into
245
+ `canvas.json` with a brittle index-keyed shape and makes reorder awkward.
246
+ Option B loses transparency and diffability.
247
+
248
+ ## Backward compatibility
249
+
250
+ `package.json` reports `0.0.1` and `pyproject.toml` reports `0.1.0`.
251
+ There is no release channel, no distributed build, no external consumer:
252
+ this is pre-release.
253
+
254
+ Per the CLAUDE.md rule ("Never add backward compatibility shims,
255
+ wrapper methods, or legacy API surfaces to unreleased projects"):
256
+
257
+ - **Delete the old single-frame path entirely.** No dual-read shim.
258
+ - `frames/0/` style directories created by the current code will simply
259
+ fail to load after the change — that is acceptable for developer data.
260
+ - The `_save_canvas` / `_load_canvas` functions are rewritten, not
261
+ extended with branches.
262
+ - The `pixel_data` attribute on `CanvasState` is removed (not
263
+ deprecated), and every reader is updated in the same commit.
264
+
265
+ If a dev has local test projects they want to keep, the recommended
266
+ workflow is a one-shot `scripts/migrate-single-frame-to-multi.py` that
267
+ walks `<data_dir>/*/canvases/*/frames/0/` and writes a new `frames.json`
268
+ + renames `0` to a fresh uuid. This script lives in `scripts/` per the
269
+ project convention, not in the server runtime.
270
+
271
+ ## Migration plan
272
+
273
+ Each step is one commit; tests stay green at every step.
274
+
275
+ 1. **Add `FrameState` dataclass and `CanvasState.frames` alongside the
276
+ old `pixel_data`**, with `frames[0].pixel_data is canvas.pixel_data`
277
+ (shared reference). All existing code still works. Add unit tests for
278
+ the new accessors. *No behavior change.*
279
+ 2. **Rewrite all server-internal readers** (`mcp_registry.py`,
280
+ `state.py:build_full_state_patch`, `mcp_bridge.py`, `storage.py`) to
281
+ go through `canvas.current_frame().pixel_data` instead of
282
+ `canvas.pixel_data`. Existing tests still pass because there is still
283
+ only one frame. *No behavior change.*
284
+ 3. **Drop the `pixel_data` attribute from `CanvasState`.** Only the
285
+ `frames` list remains. Update `_init_` / `create_project` to seed
286
+ `frames=[FrameState(...)]`. Run full test suite.
287
+ 4. **Implement the new on-disk layout** (Option C). Rewrite
288
+ `_save_canvas` / `_load_canvas`. Update `test_storage.py`. This is
289
+ the breaking-change commit for on-disk projects.
290
+ 5. **Extend `get_sync_state` and `build_full_state_patch`** to carry
291
+ all frames, not just the current one. Update the frontend
292
+ `patch-applicator.ts:applyServerNativeState` to rebuild the full
293
+ frame list via `deserializeFrames`. Update `ws-client.ts` types.
294
+ 6. **Remove the `frame != 0` guardrail from `export_frame_png`**
295
+ (`storage.py:188-192`); replace with a proper `0 <= frame <
296
+ len(canvas.frames)` bounds check. Update the REST export endpoint
297
+ test to cover N > 0.
298
+ 7. **Implement real frame MCP tool handlers** (`add_frame`, `remove_frame`,
299
+ `duplicate_frame`, `reorder_frame`, `set_frame_duration`,
300
+ `set_global_fps`) — they mutate `canvas.frames` directly instead of
301
+ dispatching opaque commands, then broadcast a full-state patch. Fix
302
+ `get_canvas_info` to report real counts. Add
303
+ `get_frame_count` / `set_current_frame` / `get_current_frame` read
304
+ tools.
305
+ 8. **Add `frame_index` parameter to drawing tools.** Defaults to the
306
+ current frame; validated against `canvas.frames`. Update
307
+ `test_mcp_registry.py`.
308
+ 9. **Rewrite `export_spritesheet`** to iterate `canvas.frames` on a
309
+ single canvas instead of iterating canvases. Update its MCP schema
310
+ to accept `canvas_name` and optional `frame_range`.
311
+ 10. **One-shot migration script** at
312
+ `scripts/migrate-single-frame-to-multi.py` that walks legacy data
313
+ dirs and rewrites them. Not wired into the runtime.
314
+
315
+ Rollback points: step 3 is the point of no return for in-memory code
316
+ (the old attribute is gone). Step 4 is the point of no return for
317
+ on-disk data. Everything before those can be reverted cleanly.
318
+
319
+ ## Test strategy
320
+
321
+ New tests:
322
+
323
+ - `test_storage.py::TestMultiFrameSaveLoad` — round-trip a canvas with
324
+ three frames, differing pixel data, differing durations, reorder,
325
+ reload, assert equality.
326
+ - `test_storage.py::TestExportFramePng::test_frame_n` — export frame 0
327
+ and frame 2, assert they differ.
328
+ - `test_mcp_registry.py::TestFrameTools` — add / remove / duplicate /
329
+ reorder / set_duration, each followed by a full-state patch
330
+ broadcast assertion.
331
+ - `test_mcp_registry.py::TestDrawingWithFrameIndex` — draw on frame 1
332
+ and assert frame 0 is untouched.
333
+ - `test_mcp_bridge.py::test_serialize_deserialize_multi_frame` —
334
+ round-trip serialization preserves frames, durations, and current index.
335
+
336
+ Existing tests to update:
337
+
338
+ - `test_storage.py` — `frame_dir` assertions currently use
339
+ `frames/0/` (line 46). Switch to resolving the frame id via
340
+ `frames.json`.
341
+ - `test_api.py` — any export endpoint tests that implicitly assume
342
+ `frame=0`.
343
+ - `test_mcp_registry.py::test_get_canvas_info` — expect
344
+ `frame_count == len(canvas.frames)`.
345
+ - Any test that constructs `CanvasState(...)` directly with
346
+ `pixel_data=...` must switch to `frames=[FrameState(...)]`.
347
+
348
+ Integration / e2e:
349
+
350
+ - A Playwright scenario that creates a project, adds frames, closes
351
+ and reopens the app, and verifies all frames survive through the
352
+ autosave → load cycle.
353
+
354
+ ## Effort estimate
355
+
356
+ **Large.** Rationale:
357
+
358
+ - 11 files changed across four subsystems (state, storage, MCP, WS, frontend sync).
359
+ - Six MCP tool handlers move from stub dispatch to real implementation.
360
+ - On-disk format rewrite (even small) carries test-suite fallout.
361
+ - Frontend `patch-applicator.ts` needs careful work to rebuild frames
362
+ without duplicating `deserializeFrames`.
363
+ - Migration script is extra surface area even if one-shot.
364
+
365
+ A disciplined 10-commit sequence per the migration plan lands this in
366
+ roughly 1.5–2 focused sessions; skipping careful staging risks a broken
367
+ test suite for a day or more.
368
+
369
+ ## Files that will change
370
+
371
+ Data model and in-memory state:
372
+
373
+ - `server/src/pixelweaver/state.py`
374
+
375
+ Storage and on-disk format:
376
+
377
+ - `server/src/pixelweaver/storage.py`
378
+ - `scripts/migrate-single-frame-to-multi.py` (new, one-shot)
379
+
380
+ REST API:
381
+
382
+ - `server/src/pixelweaver/main.py`
383
+
384
+ MCP surface:
385
+
386
+ - `server/src/pixelweaver/mcp_registry.py`
387
+ - `server/src/pixelweaver/mcp_bridge.py`
388
+
389
+ WebSocket protocol (payload producers only; types already support frames):
390
+
391
+ - `server/src/pixelweaver/websocket.py` (doc updates)
392
+ - `server/src/pixelweaver/protocol.py` (doc updates)
393
+
394
+ Frontend sync layer:
395
+
396
+ - `src/lib/sync/ws-client.ts`
397
+ - `src/lib/sync/project-manager.ts`
398
+ - `src/lib/sync/patch-applicator.ts`
399
+
400
+ Tests:
401
+
402
+ - `server/tests/test_storage.py`
403
+ - `server/tests/test_api.py`
404
+ - `server/tests/test_mcp_registry.py`
405
+ - `server/tests/test_mcp_bridge.py`