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,1085 @@
1
+ # PixelWeaver Comprehensive Codebase Audit
2
+
3
+ **Date:** 2026-04-10
4
+ **Scope:** Full audit of the PixelWeaver pixel art editor -- Svelte 5 frontend, Python server (wesktop/granian), and plugin system.
5
+ **Note:** Items specific to the former Tauri 2 stack (now replaced by wesktop/granian) have been moved to `todo/.obsolete/comprehensive-audit-obsolete-items.md`.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Severity Summary](#severity-summary)
12
+ - [Critical Findings](#critical-findings)
13
+ - [C1. Menu/toolbar/palette commands bypass the dispatcher](#c1-menutoolbarpalette-commands-bypass-the-dispatcher)
14
+ - [C2. Undo/redo commands dispatch themselves, creating phantom stack entries](#c2-undoredo-commands-dispatch-themselves-creating-phantom-stack-entries)
15
+ - [C3. Pencil/eraser undo snapshots are always empty](#c3-pencileraser-undo-snapshots-are-always-empty)
16
+ - [C5. Server: MCP save handler imports from wrong module](#c5-server-mcp-save-handler-imports-from-wrong-module)
17
+ - [High -- Bugs](#high----bugs)
18
+ - [H6. removeLayer uses wrong flat index](#h6-removelayer-uses-wrong-flat-index)
19
+ - [H7. Animation preview uses stale frame duration](#h7-animation-preview-uses-stale-frame-duration)
20
+ - [H8. Negative hue shift produces wrong colors](#h8-negative-hue-shift-produces-wrong-colors)
21
+ - [H9. 90/270-degree rotation destroys non-square buffers](#h9-90270-degree-rotation-destroys-non-square-buffers)
22
+ - [High -- Architecture and Design](#high----architecture-and-design)
23
+ - [H1. 38 of 40 commands have empty undo but still push to undo stack](#h1-38-of-40-commands-have-empty-undo-but-still-push-to-undo-stack)
24
+ - [H2. Core imports from UI layer](#h2-core-imports-from-ui-layer)
25
+ - [H3. UI imports directly from plugin](#h3-ui-imports-directly-from-plugin)
26
+ - [H4. Plugin API has no mutators](#h4-plugin-api-has-no-mutators)
27
+ - [H5. menu-commands-plugin.ts is a 1333-line god file](#h5-menu-commands-plugints-is-a-1333-line-god-file)
28
+ - [High -- Server](#high----server)
29
+ - [H10. Path traversal via project name](#h10-path-traversal-via-project-name)
30
+ - [H11. No canvas dimension validation](#h11-no-canvas-dimension-validation)
31
+ - [H13. Broadcast iterates mutable list](#h13-broadcast-iterates-mutable-list)
32
+ - [H14. MCPLock is created but never used](#h14-mcplock-is-created-but-never-used)
33
+ - [High -- Type Safety](#high----type-safety)
34
+ - [H15. plugins/ excluded from ESLint](#h15-plugins-excluded-from-eslint)
35
+ - [H16. noUncheckedIndexedAccess not enabled](#h16-nouncheckedindexedaccess-not-enabled)
36
+ - [H17. 200+ unsafe casts](#h17-200-unsafe-casts)
37
+ - [High -- Accessibility](#high----accessibility)
38
+ - [A1. No focus-visible indicators](#a1-no-focus-visible-indicators)
39
+ - [A2. Modals lack focus traps](#a2-modals-lack-focus-traps)
40
+ - [A3. Missing dialog ARIA roles](#a3-missing-dialog-aria-roles)
41
+ - [A4. Icon-only buttons lack aria-label](#a4-icon-only-buttons-lack-aria-label)
42
+ - [A5. Command palette missing combobox ARIA pattern](#a5-command-palette-missing-combobox-aria-pattern)
43
+ - [A6. Menus lack menu ARIA roles and keyboard navigation](#a6-menus-lack-menu-aria-roles-and-keyboard-navigation)
44
+ - [A7. Context menu has no viewport boundary clamping](#a7-context-menu-has-no-viewport-boundary-clamping)
45
+ - [A8. Canvas has tabindex but no role or label](#a8-canvas-has-tabindex-but-no-role-or-label)
46
+ - [Medium -- DRY Violations](#medium----dry-violations)
47
+ - [M1. getActiveBuffer cast pattern repeated 69 times](#m1-getactivebuffer-cast-pattern-repeated-69-times)
48
+ - [M2. Snapshot entire buffer boilerplate in 9 files](#m2-snapshot-entire-buffer-boilerplate-in-9-files)
49
+ - [M3. Pencil and eraser are structurally identical](#m3-pencil-and-eraser-are-structurally-identical)
50
+ - [M4. Shape tool boilerplate](#m4-shape-tool-boilerplate)
51
+ - [M5. Identical undo handler across 25 files](#m5-identical-undo-handler-across-25-files)
52
+ - [M6. Zoom steps duplicated verbatim](#m6-zoom-steps-duplicated-verbatim)
53
+ - [M7. Palette helpers copy-pasted between files](#m7-palette-helpers-copy-pasted-between-files)
54
+ - [M8. rgbaToHex reimplemented in 3 places](#m8-rgbatohex-reimplemented-in-3-places)
55
+ - [M9. Importer reset-state boilerplate](#m9-importer-reset-state-boilerplate)
56
+ - [M10. Dialog CSS duplicated](#m10-dialog-css-duplicated)
57
+ - [Medium -- State and Logic](#medium----state-and-logic)
58
+ - [M11. add_layer never sets previousActiveLayerId](#m11-add_layer-never-sets-previousactivelayerid)
59
+ - [M12. Dispatcher callback iteration during modification](#m12-dispatcher-callback-iteration-during-modification)
60
+ - [M13. input-handler exports plain let bindings](#m13-input-handler-exports-plain-let-bindings)
61
+ - [M14. tile-mode creates new OffscreenCanvas every frame](#m14-tile-mode-creates-new-offscreencanvas-every-frame)
62
+ - [M15. restoreLayer undo crashes if parent group was removed](#m15-restorelayer-undo-crashes-if-parent-group-was-removed)
63
+ - [M16. onCommand listener return value discarded](#m16-oncommand-listener-return-value-discarded)
64
+ - [M17. RecoveryManager start called but stop never called](#m17-recoverymanager-start-called-but-stop-never-called)
65
+ - [M18. WebSocket connect overwrites old socket without closing](#m18-websocket-connect-overwrites-old-socket-without-closing)
66
+ - [M19. syncPlugin swallows initializeSync rejection](#m19-syncplugin-swallows-initializesync-rejection)
67
+ - [M20. Clipboard trapped as module-level variable in god file](#m20-clipboard-trapped-as-module-level-variable-in-god-file)
68
+ - [Medium -- Server](#medium----server)
69
+ - [M21. save_project is sync I/O in async context](#m21-save_project-is-sync-io-in-async-context)
70
+ - [M22. Autosave clears dirty flag even on failure](#m22-autosave-clears-dirty-flag-even-on-failure)
71
+ - [M23. register_all_tools runs at import time](#m23-register_all_tools-runs-at-import-time)
72
+ - [M24. export_frame_png ignores the frame parameter](#m24-export_frame_png-ignores-the-frame-parameter)
73
+ - [M25. WebSocket crash path never closes socket](#m25-websocket-crash-path-never-closes-socket)
74
+ - [Medium -- CSS and UI](#medium----css-and-ui)
75
+ - [M26. Hardcoded colors instead of design tokens](#m26-hardcoded-colors-instead-of-design-tokens)
76
+ - [M27. --text-on-accent referenced but never defined](#m27---text-on-accent-referenced-but-never-defined)
77
+ - [M28. Light theme broken hover states](#m28-light-theme-broken-hover-states)
78
+ - [M29. Global transition applies to canvas and animation elements](#m29-global-transition-applies-to-canvas-and-animation-elements)
79
+ - [M30. prompt() used for user input](#m30-prompt-used-for-user-input)
80
+ - [Dead Code](#dead-code)
81
+ - [Entire Unused Modules](#entire-unused-modules)
82
+ - [Significant Unused Exports from Active Modules](#significant-unused-exports-from-active-modules)
83
+ - [Half-Implemented Features](#half-implemented-features)
84
+ - [Static Analysis Enforcement Gaps](#static-analysis-enforcement-gaps)
85
+ - [TypeScript](#typescript)
86
+ - [ESLint](#eslint)
87
+ - [Python (Ruff)](#python-ruff)
88
+ - [JSON.parse Without Validation](#jsonparse-without-validation)
89
+ - [Server Test Coverage Gaps](#server-test-coverage-gaps)
90
+ - [Top 10 Recommendations by Impact](#top-10-recommendations-by-impact)
91
+
92
+ ---
93
+
94
+ ## Severity Summary
95
+
96
+ | Severity | Count | Description |
97
+ |----------|------:|-------------|
98
+ | Critical | 4 | Broken core functionality or security vulnerabilities that block normal use |
99
+ | High -- Bugs | 4 | Incorrect behavior producing wrong results |
100
+ | High -- Architecture | 5 | Structural problems that compound maintenance cost and block feature work |
101
+ | High -- Server | 4 | Server-side bugs including security and stability issues |
102
+ | High -- Type Safety | 3 | Systemic gaps in compile-time safety |
103
+ | High -- Accessibility | 8 | Barriers that prevent assistive technology users from operating the app |
104
+ | Medium -- DRY | 10 | Duplicated code increasing maintenance burden |
105
+ | Medium -- State/Logic | 10 | Subtle logic errors and resource leaks |
106
+ | Medium -- Server | 5 | Server-side logic and reliability issues |
107
+ | Medium -- CSS/UI | 5 | Visual inconsistencies and platform compatibility gaps |
108
+ | Dead Code | -- | 6 unused modules, 40+ unused exports, 4+ half-implemented features |
109
+ | Static Analysis | -- | Gaps in TS, ESLint, and Ruff configurations |
110
+ | Test Coverage | -- | 10+ server modules with zero test coverage |
111
+ | **Total** | **58+** | |
112
+
113
+ ---
114
+
115
+ ## Critical Findings
116
+
117
+ ### C1. Menu/toolbar/palette commands bypass the dispatcher
118
+
119
+ **Severity:** Critical
120
+ **Impact:** All UI-triggered commands are non-undoable, unlogged, and unobservable.
121
+
122
+ All four UI entry points call `cmd.execute({}, {})` directly instead of routing through the dispatcher:
123
+
124
+ | Location | Line | Entry Point |
125
+ |----------|------|-------------|
126
+ | `src/core/menu-builder.ts` | 67 | Menu bar clicks |
127
+ | `src/ui/panels/ToolbarPanel.svelte` | 184 | Toolbar button clicks |
128
+ | `src/ui/panels/TabAddMenu.svelte` | 27 | Tab add menu |
129
+ | `src/core/command-palette-state.svelte.ts` | 54 | Command palette selection |
130
+
131
+ **Files to change:** `menu-builder.ts`, `ToolbarPanel.svelte`, `TabAddMenu.svelte`, `command-palette-state.svelte.ts`
132
+ **Effort:** Small -- replace `cmd.execute({}, {})` with `dispatch(cmd.id, params)` at each site.
133
+
134
+ ---
135
+
136
+ ### C2. Undo/redo commands dispatch themselves, creating phantom stack entries
137
+
138
+ **Severity:** Critical
139
+ **Impact:** Undo never undoes the user's last action. Redo is similarly broken.
140
+
141
+ `menu-commands-plugin.ts:206-215` registers `undo` as a dispatchable command. When dispatched:
142
+
143
+ 1. The dispatcher pushes an `undo` entry onto the undo stack.
144
+ 2. The `undo` command calls `undoLast()`.
145
+ 3. `undoLast()` pops the top of the stack -- which is the `undo` entry just pushed, not the user's actual last action.
146
+
147
+ The same pattern applies to `redo`.
148
+
149
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts`, `src/core/dispatcher.ts`
150
+ **Effort:** Small -- make undo/redo special dispatcher methods that are never pushed to the stack, or mark them as non-undoable commands that skip stack insertion.
151
+
152
+ ---
153
+
154
+ ### C3. Pencil/eraser undo snapshots are always empty
155
+
156
+ **Severity:** Critical
157
+ **Impact:** Undoing any pencil or eraser stroke restores nothing; pixel data is permanently lost.
158
+
159
+ | Location | Line | Problem |
160
+ |----------|------|---------|
161
+ | `plugins/builtin/pencil-tool.ts` | 113 | `_snapshot: []` hardcoded to empty |
162
+ | `plugins/builtin/eraser-tool.ts` | 107 | `_snapshot: []` hardcoded to empty |
163
+
164
+ The comment in both files says "snapshot is filled by the canvas layer before dispatch" but no code performs this fill step.
165
+
166
+ **Files to change:** `pencil-tool.ts`, `eraser-tool.ts`, and potentially the canvas layer or a new snapshot utility.
167
+ **Effort:** Medium -- need to implement the snapshot-before-dispatch mechanism and verify it works with the dispatcher.
168
+
169
+ ---
170
+
171
+ ### C5. Server: MCP save handler imports from wrong module
172
+
173
+ **Severity:** Critical
174
+ **Impact:** MCP-initiated saves always write to `./projects` regardless of the user's `--data-dir` flag.
175
+
176
+ `mcp_registry.py:596` imports `_data_dir` from `main.py`. The MCP process runs separately and never calls `main.set_data_dir()`, so `_data_dir` retains its default value.
177
+
178
+ **Files to change:** `server/src/pixelweaver/mcp_registry.py`
179
+ **Effort:** Small -- use the MCP server's own data dir reference or read it from a shared configuration source.
180
+
181
+ ---
182
+
183
+ ## High -- Bugs
184
+
185
+ ### H6. removeLayer uses wrong flat index
186
+
187
+ **Severity:** High
188
+ **Impact:** After deleting a layer, the wrong layer gets selected.
189
+
190
+ `layer-tree.svelte.ts:239-240` finds the index of the first *other* pixel layer in the flat list instead of using the removed layer's former position to determine the nearest neighbor.
191
+
192
+ **Files to change:** `src/state/layer-tree.svelte.ts`
193
+ **Effort:** Small
194
+
195
+ ---
196
+
197
+ ### H7. Animation preview uses stale frame duration
198
+
199
+ **Severity:** High
200
+ **Impact:** Frames with different durations play at incorrect speeds during preview.
201
+
202
+ `animation-preview.svelte.ts:100-108` computes `frameDuration` once before entering a `while` loop that may advance through multiple frames, each potentially having a different duration.
203
+
204
+ **Files to change:** `src/state/animation-preview.svelte.ts`
205
+ **Effort:** Small -- recompute `frameDuration` inside the loop body.
206
+
207
+ ---
208
+
209
+ ### H8. Negative hue shift produces wrong colors
210
+
211
+ **Severity:** High
212
+ **Impact:** Color effects produce incorrect hues when shift is negative.
213
+
214
+ `effects/color-effects.ts:134` uses `(hsv.h + degrees) % 360`. In JavaScript, the modulo operator returns negative values for negative inputs (e.g., `(-10) % 360 === -10`).
215
+
216
+ **Fix:** Use `((hsv.h + degrees) % 360 + 360) % 360` to normalize to `[0, 360)`.
217
+
218
+ **Files to change:** `plugins/builtin/effects/color-effects.ts`
219
+ **Effort:** Small
220
+
221
+ ---
222
+
223
+ ### H9. 90/270-degree rotation destroys non-square buffers
224
+
225
+ **Severity:** High
226
+ **Impact:** Rotating rectangular canvases by 90 or 270 degrees produces corrupted output with out-of-bounds reads.
227
+
228
+ `effects/rotate.ts:33-59` maps source coordinates assuming `w === h`. For non-square buffers, pixel reads go out of bounds.
229
+
230
+ **Files to change:** `plugins/builtin/effects/rotate.ts`
231
+ **Effort:** Small -- swap width/height in the output buffer and fix coordinate mapping.
232
+
233
+ ---
234
+
235
+ ## High -- Architecture and Design
236
+
237
+ ### H1. 38 of 40 commands have empty undo but still push to undo stack
238
+
239
+ **Severity:** High
240
+ **Impact:** Non-undoable actions (zoom, toggle theme, open dialog, etc.) consume undo slots, polluting the undo stack and evicting real undoable actions when the stack limit is reached.
241
+
242
+ All 38 commands in `menu-commands-plugin.ts` that register with the dispatcher get pushed to the undo stack despite having empty `undo()` implementations.
243
+
244
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts`, `src/core/dispatcher.ts`
245
+ **Effort:** Medium -- add an `undoable: boolean` flag to command registration, make the dispatcher skip stack insertion for non-undoable commands, and mark all 38 commands appropriately.
246
+
247
+ ---
248
+
249
+ ### H2. Core imports from UI layer
250
+
251
+ **Severity:** High
252
+ **Impact:** Architectural layering violation. Core cannot be tested, bundled, or reasoned about independently of the UI.
253
+
254
+ `plugin-api.ts:21` imports `notificationState` from `../ui/notifications/`. The dependency should flow UI -> Core, not the reverse.
255
+
256
+ **Files to change:** `src/core/plugin-api.ts`, potentially extract a notification interface in core.
257
+ **Effort:** Medium -- define a notification interface in core, have the UI layer provide the implementation.
258
+
259
+ ---
260
+
261
+ ### H3. UI imports directly from plugin
262
+
263
+ **Severity:** High
264
+ **Impact:** Breaks the plugin boundary. The plugin system's encapsulation is bypassed.
265
+
266
+ `menu-commands-plugin.ts:38` directly imports selection state from `plugins/builtin/selection-tool.js`. Selection state should be exposed through the plugin API or a shared state module.
267
+
268
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts`, potentially `src/core/plugin-api.ts`
269
+ **Effort:** Medium
270
+
271
+ ---
272
+
273
+ ### H4. Plugin API has no mutators
274
+
275
+ **Severity:** High
276
+ **Impact:** All 3 importers must bypass the plugin API entirely, directly importing and mutating 5+ internal modules (canvasState, layer-tree, frame-model, palette-state, dispatcher).
277
+
278
+ `PluginAPI` only provides read-only getters that return `unknown`. There is no way for a plugin to perform legitimate mutations (set canvas size, add layers, add frames, set pixels) through the official API.
279
+
280
+ **Files to change:** `src/core/plugin-api.ts`
281
+ **Effort:** Large -- design and implement mutator methods (`setCanvasSize()`, `addLayer()`, `addFrame()`, `setPixels()`, etc.) and migrate importers to use them.
282
+
283
+ ---
284
+
285
+ ### H5. menu-commands-plugin.ts is a 1333-line god file
286
+
287
+ **Severity:** High
288
+ **Impact:** Maintenance nightmare. 38 commands, 50+ menu items, duplicated zoom logic, clipboard state management, DOM queries via `document.querySelector('.canvas-viewport')`, and `prompt()` calls all in one file.
289
+
290
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts` (split into multiple files)
291
+ **Effort:** Large -- split into domain-specific files:
292
+
293
+ - `file-commands.ts` -- new, open, save, export, import
294
+ - `edit-commands.ts` -- undo, redo, clipboard, select all, deselect
295
+ - `view-commands.ts` -- zoom, grid, theme, fullscreen
296
+ - `layer-commands.ts` -- add/remove/merge/flatten
297
+ - `image-commands.ts` -- resize, crop, rotate, flip
298
+ - `animation-commands.ts` -- frame management, playback
299
+
300
+ ---
301
+
302
+ ## High -- Server
303
+
304
+ ### H10. Path traversal via project name
305
+
306
+ **Severity:** High (Security)
307
+ **Impact:** An attacker can write files anywhere on the filesystem by setting `CreateProjectRequest.name` to a traversal path like `../../../etc/evil`.
308
+
309
+ | Location | Line |
310
+ |----------|------|
311
+ | `server/src/pixelweaver/main.py` | 98 |
312
+ | `server/src/pixelweaver/storage.py` | 30 |
313
+
314
+ `CreateProjectRequest.name` is unconstrained; no sanitization or path-component validation is performed.
315
+
316
+ **Files to change:** `server/src/pixelweaver/main.py`, `server/src/pixelweaver/storage.py`
317
+ **Effort:** Small -- validate that the name contains no path separators or `..` components; reject or sanitize.
318
+
319
+ ---
320
+
321
+ ### H11. No canvas dimension validation
322
+
323
+ **Severity:** High (Security/Stability)
324
+ **Impact:** A request with `width=100000, height=100000` allocates ~40 GB of zeros, causing instant OOM.
325
+
326
+ `state.py:88` accepts arbitrary dimensions. The MCP interface limits to 1024x1024 but the REST API has no limit.
327
+
328
+ **Files to change:** `server/src/pixelweaver/state.py`, `server/src/pixelweaver/main.py`
329
+ **Effort:** Small -- add a max dimension constant and validate in both REST and state creation paths.
330
+
331
+ ---
332
+
333
+ ### H13. Broadcast iterates mutable list
334
+
335
+ **Severity:** High
336
+ **Impact:** If `send_json` triggers a disconnect callback during iteration, the connection list is mutated mid-loop, causing skipped messages or a `RuntimeError`.
337
+
338
+ `connections.py:43-48` iterates the connection list directly while sending.
339
+
340
+ **Files to change:** `server/src/pixelweaver/connections.py`
341
+ **Effort:** Small -- iterate over a snapshot (`list(connections)`) or collect disconnected clients and remove after iteration.
342
+
343
+ ---
344
+
345
+ ### H14. MCPLock is created but never used
346
+
347
+ **Severity:** High
348
+ **Impact:** The documented concurrency protection between MCP and WebSocket mutations is completely unimplemented. Concurrent mutations can corrupt state.
349
+
350
+ | Location | Line |
351
+ |----------|------|
352
+ | `server/src/pixelweaver/mcp_server.py` | 53 |
353
+ | `server/src/pixelweaver/mcp_lock.py` | entire file |
354
+
355
+ **Files to change:** `server/src/pixelweaver/mcp_server.py`, mutation handlers
356
+ **Effort:** Medium -- acquire the lock in all MCP and WebSocket mutation paths.
357
+
358
+ ---
359
+
360
+ ## High -- Type Safety
361
+
362
+ ### H15. plugins/ excluded from ESLint
363
+
364
+ **Severity:** High
365
+ **Impact:** 35+ plugin files containing 200+ type assertions are completely unlinted. Bugs, style violations, and unsafe patterns go undetected.
366
+
367
+ `eslint.config.js` excludes the `plugins/` directory entirely.
368
+
369
+ **Files to change:** `eslint.config.js`
370
+ **Effort:** Medium -- remove the exclusion, fix the resulting lint errors (expect ~50-100 initial errors).
371
+
372
+ ---
373
+
374
+ ### H16. noUncheckedIndexedAccess not enabled
375
+
376
+ **Severity:** High
377
+ **Impact:** All `array[i]` and `record[key]` access is silently assumed to succeed. No compile-time protection against out-of-bounds or missing-key access.
378
+
379
+ `tsconfig.app.json` does not set `noUncheckedIndexedAccess: true`.
380
+
381
+ **Files to change:** `tsconfig.app.json`
382
+ **Effort:** Medium -- enable the flag and fix resulting type errors (expect 100-200 sites needing `?.` or explicit checks).
383
+
384
+ ---
385
+
386
+ ### H17. 200+ unsafe casts
387
+
388
+ **Severity:** High
389
+ **Impact:** `Record<string, unknown>` for command params forces unsafe `as X` casts everywhere. Zero compile-time safety for command parameters.
390
+
391
+ This is systemic across all tools, effects, and command handlers. The root cause is the untyped `params` design in the command/dispatcher system.
392
+
393
+ **Files to change:** `src/core/dispatcher.ts`, `src/core/plugin-api.ts`, all command/tool files
394
+ **Effort:** Large -- design typed command param interfaces, update dispatcher generics, migrate all command registrations.
395
+
396
+ ---
397
+
398
+ ## High -- Accessibility
399
+
400
+ ### A1. No focus-visible indicators
401
+
402
+ **Severity:** High
403
+ **Impact:** Keyboard-only users cannot see which element has focus. The app is effectively unusable without a mouse.
404
+
405
+ No component defines `:focus-visible` outlines. The global CSS reset removes browser defaults without replacing them.
406
+
407
+ **Files to change:** `src/app.css`, potentially individual component styles
408
+ **Effort:** Small -- add a global `:focus-visible` rule with a visible outline, then refine per-component as needed.
409
+
410
+ ---
411
+
412
+ ### A2. Modals lack focus traps
413
+
414
+ **Severity:** High
415
+ **Impact:** Pressing Tab inside a dialog moves focus to elements behind the overlay. Users can interact with hidden content.
416
+
417
+ Affected dialogs:
418
+
419
+ - `NewProjectDialog.svelte`
420
+ - `ExportDialog.svelte`
421
+ - `AboutDialog.svelte`
422
+
423
+ **Files to change:** All three dialog components
424
+ **Effort:** Medium -- implement a focus trap utility (or use a library) and apply to all dialogs.
425
+
426
+ ---
427
+
428
+ ### A3. Missing dialog ARIA roles
429
+
430
+ **Severity:** High
431
+ **Impact:** Screen readers do not announce dialogs as modal overlays. Users may not realize a dialog has opened.
432
+
433
+ All three dialog components (`NewProjectDialog`, `ExportDialog`, `AboutDialog`) suppress Svelte a11y warnings with `<!-- svelte-ignore -->` instead of adding `role="dialog"` and `aria-modal="true"`.
434
+
435
+ **Files to change:** `NewProjectDialog.svelte`, `ExportDialog.svelte`, `AboutDialog.svelte`
436
+ **Effort:** Small
437
+
438
+ ---
439
+
440
+ ### A4. Icon-only buttons lack aria-label
441
+
442
+ **Severity:** High
443
+ **Impact:** Assistive technology announces these as unlabeled buttons. ~20 buttons are affected.
444
+
445
+ Buttons in `FrameStrip.svelte`, `LayerPanel.svelte`, and `ToolbarPanel.svelte` have only `title` attributes (not universally exposed to assistive tech) with no `aria-label`.
446
+
447
+ **Files to change:** `FrameStrip.svelte`, `LayerPanel.svelte`, `ToolbarPanel.svelte`
448
+ **Effort:** Small -- add `aria-label` matching the existing `title` text.
449
+
450
+ ---
451
+
452
+ ### A5. Command palette missing combobox ARIA pattern
453
+
454
+ **Severity:** High
455
+ **Impact:** Screen readers cannot navigate the filtered command list or understand the autocomplete behavior.
456
+
457
+ `CommandPalette.svelte` is missing:
458
+
459
+ - `role="combobox"` on the input
460
+ - `aria-autocomplete="list"`
461
+ - `aria-activedescendant` pointing to the highlighted item
462
+ - `role="listbox"` on the results container
463
+ - `role="option"` on each result item
464
+
465
+ **Files to change:** `src/ui/CommandPalette.svelte`
466
+ **Effort:** Medium
467
+
468
+ ---
469
+
470
+ ### A6. Menus lack menu ARIA roles and keyboard navigation
471
+
472
+ **Severity:** High
473
+ **Impact:** Menus are not navigable via keyboard. No ArrowUp/Down, Home/End support.
474
+
475
+ Affected components:
476
+
477
+ - `MenuBar.svelte`
478
+ - `ContextMenu.svelte`
479
+
480
+ Neither component implements `role="menu"`, `role="menuitem"`, or keyboard event handlers for standard menu navigation.
481
+
482
+ **Files to change:** `src/ui/MenuBar.svelte`, `src/ui/ContextMenu.svelte`
483
+ **Effort:** Medium
484
+
485
+ ---
486
+
487
+ ### A7. Context menu has no viewport boundary clamping
488
+
489
+ **Severity:** High (Accessibility/Usability)
490
+ **Impact:** Context menus opened near screen edges appear partially or fully off-screen.
491
+
492
+ `ContextMenu.svelte` positions at raw click coordinates without checking against viewport bounds.
493
+
494
+ **Files to change:** `src/ui/ContextMenu.svelte`
495
+ **Effort:** Small -- measure menu dimensions after render, clamp position to keep within viewport.
496
+
497
+ ---
498
+
499
+ ### A8. Canvas has tabindex but no role or label
500
+
501
+ **Severity:** High
502
+ **Impact:** The canvas receives focus (via `tabindex="0"`) but screen readers announce nothing meaningful about it.
503
+
504
+ `CanvasViewport.svelte:147` sets `tabindex="0"` without a corresponding `role` (e.g., `role="img"` or `role="application"`) or `aria-label`.
505
+
506
+ **Files to change:** `src/ui/panels/CanvasViewport.svelte`
507
+ **Effort:** Small
508
+
509
+ ---
510
+
511
+ ## Medium -- DRY Violations
512
+
513
+ ### M1. getActiveBuffer cast pattern repeated 69 times
514
+
515
+ **Severity:** Medium
516
+ **Impact:** ~70 lines of duplicated unsafe casting across 24 files.
517
+
518
+ The following pattern appears 69 times:
519
+
520
+ ```typescript
521
+ const buffer = (ctx as { getActiveBuffer?: () => PixelBuffer }).getActiveBuffer?.();
522
+ if (!buffer) return;
523
+ ```
524
+
525
+ **Fix:** Extract a typed `getBuffer(ctx)` helper in a shared utility module.
526
+
527
+ **Files to change:** 24 tool/effect files, plus a new utility function
528
+ **Effort:** Small -- mechanical replacement once the helper exists.
529
+
530
+ ---
531
+
532
+ ### M2. Snapshot entire buffer boilerplate in 9 files
533
+
534
+ **Severity:** Medium
535
+ **Impact:** ~45 lines of duplicated snapshot logic. `color-effects.ts` already has a local `snapshotAll()` proving the abstraction is natural.
536
+
537
+ Nine files build an all-points array and call `snapshotPixels` with identical boilerplate.
538
+
539
+ **Fix:** Extract `snapshotAll(buffer): SnapshotData` into `drawing-utils.ts`.
540
+
541
+ **Files to change:** 9 effect/tool files, `drawing-utils.ts`
542
+ **Effort:** Small
543
+
544
+ ---
545
+
546
+ ### M3. Pencil and eraser are structurally identical
547
+
548
+ **Severity:** Medium
549
+ **Impact:** ~60 saveable lines. The two tools are ~100 lines each and differ only in:
550
+
551
+ | Aspect | Pencil | Eraser |
552
+ |--------|--------|--------|
553
+ | Command name | `pencil` | `eraser` |
554
+ | Description verb | "Draw" | "Erase" |
555
+ | Pixel color | Foreground color | Transparent |
556
+ | Icon | `pencil` | `eraser` |
557
+
558
+ **Fix:** Create a `makeStrokeTool(config)` factory.
559
+
560
+ **Files to change:** `pencil-tool.ts`, `eraser-tool.ts`, new `stroke-tool-factory.ts`
561
+ **Effort:** Small
562
+
563
+ ---
564
+
565
+ ### M4. Shape tool boilerplate
566
+
567
+ **Severity:** Medium
568
+ **Impact:** ~50 saveable lines across 5 files with identical structure.
569
+
570
+ Duplications:
571
+
572
+ - Center/radii math duplicated between circle tool and diamond tool
573
+ - Bounding box normalization duplicated between rect tool and dither tool
574
+ - Overall structure identical across rect, circle, diamond, line, dither
575
+
576
+ **Fix:** Create a `makeShapeCommand(config)` factory.
577
+
578
+ **Files to change:** 5 shape tool files, new factory module
579
+ **Effort:** Medium
580
+
581
+ ---
582
+
583
+ ### M5. Identical undo handler across ~25 files
584
+
585
+ **Severity:** Medium
586
+ **Impact:** ~60 saveable lines. Every pixel-modifying command repeats the same 5-line undo handler pattern.
587
+
588
+ **Fix:** Extract `makeSnapshotUndo()` into `drawing-utils.ts`.
589
+
590
+ **Files to change:** ~25 tool/effect files, `drawing-utils.ts`
591
+ **Effort:** Small
592
+
593
+ ---
594
+
595
+ ### M6. Zoom steps duplicated verbatim
596
+
597
+ **Severity:** Medium
598
+ **Impact:** ~30 saveable lines. `ZOOM_STEPS`, `zoomStepUp()`, and `zoomStepDown()` are copy-pasted.
599
+
600
+ | Location | Lines |
601
+ |----------|-------|
602
+ | `src/canvas/input-handler.ts` | 16-31 |
603
+ | `plugins/builtin/menu-commands-plugin.ts` | 96-109 |
604
+
605
+ **Fix:** Single source of truth in a `zoom-utils.ts` module.
606
+
607
+ **Files to change:** `input-handler.ts`, `menu-commands-plugin.ts`, new `zoom-utils.ts`
608
+ **Effort:** Small
609
+
610
+ ---
611
+
612
+ ### M7. Palette helpers copy-pasted between files
613
+
614
+ **Severity:** Medium
615
+ **Impact:** ~25 saveable lines. `collectPixelLayerIds` and `findLayer` are duplicated.
616
+
617
+ | Location | Contains |
618
+ |----------|----------|
619
+ | `plugins/builtin/effects/palette-swap.ts` | `collectPixelLayerIds`, `findLayer` |
620
+ | `plugins/builtin/effects/palette-extraction.ts` | `collectPixelLayerIds`, `findLayer` |
621
+ | `src/state/layer-tree.svelte.ts` | `getLayer(id)`, `flattenLayers()` (overlapping semantics) |
622
+
623
+ **Fix:** Use `layer-tree.svelte.ts` exports; delete the copy-pasted versions.
624
+
625
+ **Files to change:** `palette-swap.ts`, `palette-extraction.ts`
626
+ **Effort:** Small
627
+
628
+ ---
629
+
630
+ ### M8. rgbaToHex reimplemented in 3 places
631
+
632
+ **Severity:** Medium
633
+ **Impact:** ~8 saveable lines. `color-utils.ts` already exports this function.
634
+
635
+ | Location | Line |
636
+ |----------|------|
637
+ | `plugins/builtin/aseprite-importer-plugin.ts` | 80 |
638
+ | `plugins/builtin/effects/seam-checker.ts` | 30 |
639
+ | `src/utils/color-utils.ts` | (canonical, already exported) |
640
+
641
+ **Fix:** Import from `color-utils.ts` in both files.
642
+
643
+ **Files to change:** `aseprite-importer-plugin.ts`, `seam-checker.ts`
644
+ **Effort:** Small
645
+
646
+ ---
647
+
648
+ ### M9. Importer reset-state boilerplate
649
+
650
+ **Severity:** Medium
651
+ **Impact:** ~24 saveable lines. All 3 importers (aseprite, piskel, sky-spec) perform identical state reset: set canvas size, deserialize empty layers/frames.
652
+
653
+ **Fix:** Extract `resetProjectState(width, height, fps?)` utility.
654
+
655
+ **Files to change:** 3 importer plugins, new shared utility
656
+ **Effort:** Small
657
+
658
+ ---
659
+
660
+ ### M10. Dialog CSS duplicated
661
+
662
+ **Severity:** Medium
663
+ **Impact:** ~100 lines duplicated between two dialogs. The following classes are near-identical in both:
664
+
665
+ - `.modal-overlay`
666
+ - `.modal`
667
+ - `.btn`
668
+ - `.form-row`
669
+ - `.form-label`
670
+ - `.form-input`
671
+
672
+ | File | Approximate CSS lines |
673
+ |------|-----------------------|
674
+ | `NewProjectDialog.svelte` | ~100 |
675
+ | `ExportDialog.svelte` | ~100 |
676
+
677
+ **Fix:** Extract shared dialog styles into a `dialog.css` module or a shared Svelte component.
678
+
679
+ **Files to change:** `NewProjectDialog.svelte`, `ExportDialog.svelte`, new shared CSS/component
680
+ **Effort:** Small
681
+
682
+ ---
683
+
684
+ ## Medium -- State and Logic
685
+
686
+ ### M11. add_layer never sets previousActiveLayerId
687
+
688
+ **Severity:** Medium
689
+ **Impact:** Undoing an "add layer" command always restores `undefined` as the active layer instead of the layer that was active before the addition.
690
+
691
+ `layer-commands.ts:55-81` does not capture `_previousActiveLayerId` before adding the layer.
692
+
693
+ **Files to change:** `plugins/builtin/layer-commands.ts`
694
+ **Effort:** Small
695
+
696
+ ---
697
+
698
+ ### M12. Dispatcher callback iteration during modification
699
+
700
+ **Severity:** Medium
701
+ **Impact:** If a listener unsubscribes during notification dispatch, the callback array is mutated mid-iteration, causing skipped callbacks.
702
+
703
+ `dispatcher.ts:146-148` iterates the callback array directly.
704
+
705
+ **Fix:** Iterate over a copy of the array, or use a copy-on-write pattern.
706
+
707
+ **Files to change:** `src/core/dispatcher.ts`
708
+ **Effort:** Small
709
+
710
+ ---
711
+
712
+ ### M13. input-handler exports plain let bindings
713
+
714
+ **Severity:** Medium
715
+ **Impact:** Svelte components that read `isDrawing`, `isPanning`, or `pressure` will not re-render when these values change, because they are plain `let` exports rather than `$state` runes.
716
+
717
+ `input-handler.ts:55-57`
718
+
719
+ **Files to change:** `src/canvas/input-handler.ts`
720
+ **Effort:** Small -- change to `$state` runes.
721
+
722
+ ---
723
+
724
+ ### M14. tile-mode creates new OffscreenCanvas every frame
725
+
726
+ **Severity:** Medium
727
+ **Impact:** GC pressure in the render loop. A new `OffscreenCanvas` is allocated on every frame.
728
+
729
+ `tile-mode.ts:64-66` creates a fresh canvas instead of caching and reusing one (as `canvas-renderer.ts` already does).
730
+
731
+ **Files to change:** `src/canvas/tile-mode.ts`
732
+ **Effort:** Small
733
+
734
+ ---
735
+
736
+ ### M15. restoreLayer undo crashes if parent group was removed
737
+
738
+ **Severity:** Medium
739
+ **Impact:** If a parent group and its child are both removed, undoing the child restore crashes because `tree.getLayer(position.parentId)` returns `undefined`.
740
+
741
+ `layer-commands.ts:39-45` does not check whether the parent group still exists before calling `siblings.splice(...)`.
742
+
743
+ **Files to change:** `plugins/builtin/layer-commands.ts`
744
+ **Effort:** Small -- add a null check and handle the missing-parent case.
745
+
746
+ ---
747
+
748
+ ### M16. onCommand listener return value discarded
749
+
750
+ **Severity:** Medium
751
+ **Impact:** Event listener leak on HMR. The unsubscribe function returned by `onCommand` is never stored or called.
752
+
753
+ `canvas-init-plugin.ts:76`
754
+
755
+ **Files to change:** `plugins/builtin/canvas-init-plugin.ts`
756
+ **Effort:** Small -- store the unsubscribe function and call it in a cleanup path.
757
+
758
+ ---
759
+
760
+ ### M17. RecoveryManager start called but stop never called
761
+
762
+ **Severity:** Medium
763
+ **Impact:** The interval timer and visibility event listener leak during HMR reloads.
764
+
765
+ `recovery-plugin.ts:19-53` calls `start()` but never calls `stop()`.
766
+
767
+ **Files to change:** `plugins/builtin/recovery-plugin.ts`
768
+ **Effort:** Small -- call `stop()` in a plugin cleanup/dispose handler.
769
+
770
+ ---
771
+
772
+ ### M18. WebSocket connect overwrites old socket without closing
773
+
774
+ **Severity:** Medium
775
+ **Impact:** The old WebSocket's `onclose` handler may fire after the new socket is assigned, triggering a double-reconnect loop.
776
+
777
+ `ws-client.ts:93-132` reassigns the socket reference without closing the previous one.
778
+
779
+ **Files to change:** `src/sync/ws-client.ts`
780
+ **Effort:** Small -- close the existing socket before creating a new one.
781
+
782
+ ---
783
+
784
+ ### M19. syncPlugin swallows initializeSync rejection
785
+
786
+ **Severity:** Medium
787
+ **Impact:** If `initializeSync()` fails, the promise rejection is silently swallowed, potentially hiding startup errors.
788
+
789
+ `sync-plugin.ts:17` calls the async function without `.catch()`.
790
+
791
+ **Files to change:** `plugins/builtin/sync-plugin.ts`
792
+ **Effort:** Small -- add `.catch()` with appropriate error handling/logging.
793
+
794
+ ---
795
+
796
+ ### M20. Clipboard trapped as module-level variable in god file
797
+
798
+ **Severity:** Medium
799
+ **Impact:** No other module can access the clipboard state. Copy/paste cannot be implemented or extended outside `menu-commands-plugin.ts`.
800
+
801
+ `menu-commands-plugin.ts:92` declares the clipboard as a module-scoped variable.
802
+
803
+ **Fix:** Move clipboard state to a dedicated `clipboard-state.ts` module exposed through the plugin API.
804
+
805
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts`, new `clipboard-state.ts`
806
+ **Effort:** Small
807
+
808
+ ---
809
+
810
+ ## Medium -- Server
811
+
812
+ ### M21. save_project is sync I/O in async context
813
+
814
+ **Severity:** Medium
815
+ **Impact:** Blocks the event loop during disk writes, stalling all other connections.
816
+
817
+ | Location | Line |
818
+ |----------|------|
819
+ | `server/src/pixelweaver/autosave.py` | 101 |
820
+ | `server/src/pixelweaver/main.py` | 107 |
821
+
822
+ **Fix:** Wrap in `asyncio.to_thread()` or use `aiofiles`.
823
+
824
+ **Files to change:** `autosave.py`, `main.py`
825
+ **Effort:** Small
826
+
827
+ ---
828
+
829
+ ### M22. Autosave clears dirty flag even on failure
830
+
831
+ **Severity:** Medium
832
+ **Impact:** If a project fails to save, the dirty flag is cleared anyway. The failed project won't be retried until the next mutation, risking data loss.
833
+
834
+ `autosave.py:99-106` clears `_dirty` unconditionally after the save attempt.
835
+
836
+ **Fix:** Only clear `_dirty` for projects that saved successfully.
837
+
838
+ **Files to change:** `server/src/pixelweaver/autosave.py`
839
+ **Effort:** Small
840
+
841
+ ---
842
+
843
+ ### M23. register_all_tools runs at import time
844
+
845
+ **Severity:** Medium
846
+ **Impact:** Importing `mcp_server.py` triggers filesystem reads (tool registration). Breaks test isolation and makes the module impossible to import without side effects.
847
+
848
+ `mcp_server.py:154` calls `register_all_tools()` at module scope.
849
+
850
+ **Fix:** Move to an explicit initialization function called at server startup.
851
+
852
+ **Files to change:** `server/src/pixelweaver/mcp_server.py`
853
+ **Effort:** Small
854
+
855
+ ---
856
+
857
+ ### M24. export_frame_png ignores the frame parameter
858
+
859
+ **Severity:** Medium
860
+ **Impact:** Exporting any frame always exports frame 0, regardless of which frame was requested.
861
+
862
+ `storage.py:156` accepts a `frame` argument but never uses it in the export logic.
863
+
864
+ **Files to change:** `server/src/pixelweaver/storage.py`
865
+ **Effort:** Small
866
+
867
+ ---
868
+
869
+ ### M25. WebSocket crash path never closes socket
870
+
871
+ **Severity:** Medium
872
+ **Impact:** On server-side WebSocket errors, the client socket is never explicitly closed. The client may hang indefinitely waiting for a response.
873
+
874
+ `main.py:160-172` catches exceptions in the WebSocket handler but does not close the socket in the error path.
875
+
876
+ **Files to change:** `server/src/pixelweaver/main.py`
877
+ **Effort:** Small
878
+
879
+ ---
880
+
881
+ ## Medium -- CSS and UI
882
+
883
+ ### M26. Hardcoded colors instead of design tokens
884
+
885
+ **Severity:** Medium
886
+ **Impact:** ~30 hardcoded color values across 10+ Svelte files bypass the theming system.
887
+
888
+ Examples: `#ffffff`, `rgba(255,255,255,...)`, `#000000`, etc. used directly instead of CSS custom properties (`--bg-*`, `--text-*`, `--border`, `--accent`).
889
+
890
+ **Files to change:** 10+ Svelte component files
891
+ **Effort:** Medium -- search and replace with appropriate design tokens.
892
+
893
+ ---
894
+
895
+ ### M27. --text-on-accent referenced but never defined
896
+
897
+ **Severity:** Medium
898
+ **Impact:** Components silently fall back to `#fff`, which may not be correct for all accent colors or themes.
899
+
900
+ | File | Fallback |
901
+ |------|----------|
902
+ | `AboutDialog.svelte` | `#fff` |
903
+ | `ToolbarPanel.svelte` | `#fff` |
904
+
905
+ The variable `--text-on-accent` is never defined in `app.css` for either the dark or light theme.
906
+
907
+ **Files to change:** `src/app.css`
908
+ **Effort:** Small -- define `--text-on-accent` in both themes.
909
+
910
+ ---
911
+
912
+ ### M28. Light theme broken hover states
913
+
914
+ **Severity:** Medium
915
+ **Impact:** Hover overlays using white-alpha values are invisible on white backgrounds in light theme.
916
+
917
+ | File | Line | Problem |
918
+ |------|------|---------|
919
+ | `LayerPanel.svelte` | 471 | `rgba(255,255,255,0.04)` on white |
920
+ | `FrameStrip.svelte` | 486 | `rgba(255,255,255,0.04)` on white |
921
+ | `dockview-theme.css` | 63, 67 | Same white-alpha overlays |
922
+
923
+ **Fix:** Use theme-aware tokens or invert the overlay color for light theme.
924
+
925
+ **Files to change:** `LayerPanel.svelte`, `FrameStrip.svelte`, `dockview-theme.css`
926
+ **Effort:** Small
927
+
928
+ ---
929
+
930
+ ### M29. Global transition applies to canvas and animation elements
931
+
932
+ **Severity:** Medium
933
+ **Impact:** The 150ms transition on all elements causes visible drag lag on the canvas, color picker, and animation playback.
934
+
935
+ `app.css:98-101` applies `transition: 150ms ease` to `background-color`, `color`, and `border-color` on the universal `*` selector.
936
+
937
+ **Fix:** Scope the transition to theme-sensitive containers only, excluding canvas and interactive elements.
938
+
939
+ **Files to change:** `src/app.css`
940
+ **Effort:** Small
941
+
942
+ ---
943
+
944
+ ### M30. prompt() used for user input
945
+
946
+ **Severity:** Medium
947
+ **Impact:** `window.prompt()` is a blocking browser API that does not exist in Tauri's desktop WebView. These calls silently return `null` in production.
948
+
949
+ | Location | Line |
950
+ |----------|------|
951
+ | `menu-commands-plugin.ts` | 382 |
952
+ | `menu-commands-plugin.ts` | 669 |
953
+
954
+ **Fix:** Replace with custom dialog components.
955
+
956
+ **Files to change:** `plugins/builtin/menu-commands-plugin.ts`, potentially new dialog components
957
+ **Effort:** Medium
958
+
959
+ ---
960
+
961
+ ## Dead Code
962
+
963
+ ### Entire Unused Modules
964
+
965
+ The following modules are fully unused -- no imports from any other module in the codebase:
966
+
967
+ | Module | Exports | Purpose |
968
+ |--------|---------|---------|
969
+ | `src/canvas/reference-image.svelte.ts` | `referenceImageState`, `renderReferenceImage` | Reference image overlay |
970
+ | `src/canvas/guides.svelte.ts` | `guidesState`, `renderGuides`, `snapPosition` | Guide lines and snapping |
971
+ | `src/core/plugin-api-docs.ts` | `getAPIDocumentation` | Auto-generated API docs |
972
+ | `src/ui/plugin-state.svelte.ts` | `pluginState` | Plugin UI state |
973
+ | `plugins/builtin/symmetry-tool.ts` | `getSymmetryConfig`, `setSymmetryConfig`, `getSymmetricPoints` | Symmetry drawing |
974
+ | `server/src/pixelweaver/thumbnails.py` | `generate_thumbnail`, `generate_region_thumbnail` | Thumbnail generation |
975
+
976
+ **Effort:** Small -- delete the files. If any are intended for future use, move to a `_planned/` directory or document the intent.
977
+
978
+ ### Significant Unused Exports from Active Modules
979
+
980
+ | Module | Unused Exports |
981
+ |--------|---------------|
982
+ | `src/core/dispatcher.ts` | `onLog`, `onSpecExport`, `canUndo`, `canRedo`, `setMaxUndoSize`, `getContext` |
983
+ | `src/canvas/onion-skin.svelte.ts` | Entire module (13 exports, zero consumers) |
984
+ | `src/state/animation-preview.svelte.ts` | `getMode`, `setMode`, `getSpeedMultiplier`, `setSpeed`, `getPreviewFrameIndex`, `PreviewMode` |
985
+ | `src/state/frame-tags.svelte.ts` | `getTagsForFrame`, `getFrameRange`, `updateTag` |
986
+ | `src/state/palette-state.svelte.ts` | `getProjectPalette`, `getGlobalPalettes`, `addGlobalPalette`, `removeGlobalPalette`, `setActivePalette`, `getAutoCollectedColors`, `snapToActivePalette` |
987
+ | `src/state/color-state.svelte.ts` | `getActiveColorSpace`, `setActiveColorSpace` |
988
+ | `src/core/shortcut-registry.svelte.ts` | `updateBindingKey` |
989
+ | `src/core/plugin-loader.ts` | `getLoadedPlugin`, `getAllLoadedPlugins` |
990
+ | `src/state/layer-tree.svelte.ts` | `getPath`, `getVisiblePixelLayers`, `flattenGroup` |
991
+
992
+ ### Half-Implemented Features
993
+
994
+ | Feature | Location | Status |
995
+ |---------|----------|--------|
996
+ | `invert_selection` | Plugin command | Logs `console.warn("not yet implemented")` |
997
+ | `select_by_color` | Plugin command | Logs `console.warn("not yet implemented")` |
998
+ | `crop_to_selection` | Plugin command | Warns pixel data remapping is unimplemented |
999
+ | `save_project` | Plugin command | Just logs to console |
1000
+ | Shortcut actions | `shortcut-init.ts:65-68` | Four actions return no-op functions |
1001
+
1002
+ ---
1003
+
1004
+ ## Static Analysis Enforcement Gaps
1005
+
1006
+ ### TypeScript
1007
+
1008
+ | Setting | Status | Impact |
1009
+ |---------|--------|--------|
1010
+ | `noUncheckedIndexedAccess` | Not enabled (`tsconfig.app.json`) | Array/object index access assumed safe |
1011
+ | `noUnusedLocals` | Only in node config, not app config | Dead variables accumulate silently |
1012
+ | `noUnusedParameters` | Only in node config, not app config | Unused params hide API surface |
1013
+ | `noFallthroughCasesInSwitch` | Only in node config | Switch fallthrough bugs possible |
1014
+ | `exactOptionalPropertyTypes` | Not enabled | `undefined` vs missing property conflated |
1015
+
1016
+ ### ESLint
1017
+
1018
+ | Issue | Impact |
1019
+ |-------|--------|
1020
+ | `plugins/` excluded from linting | 35+ files, 200+ type assertions unlinted |
1021
+ | Uses `recommended` not `strict-type-checked` | `no-unsafe-assignment`, `no-unsafe-member-access`, `no-unsafe-call`, `no-unsafe-return`, `no-unsafe-argument` all OFF |
1022
+ | `consistent-type-assertions` OFF | Dangerous `as` casts not flagged |
1023
+ | `no-non-null-assertion` OFF | `!` operator used without checks |
1024
+ | `strict-boolean-expressions` OFF | Truthy/falsy coercions not flagged |
1025
+ | Currently reports 36 errors | Existing errors across Svelte files (unused vars, missing each-keys) |
1026
+
1027
+ ### Python (Ruff)
1028
+
1029
+ | Missing Rule Set | Purpose |
1030
+ |-----------------|---------|
1031
+ | `S` (security/bandit) | Security vulnerability detection |
1032
+ | `B` (bugbear) | Common bug patterns |
1033
+ | `C4` (comprehensions) | Comprehension optimizations |
1034
+ | `SIM` (simplifications) | Code simplification suggestions |
1035
+ | `ASYNC` (async pitfalls) | Async/await anti-patterns |
1036
+
1037
+ Additionally: no dependency upper bounds in `pyproject.toml`, allowing untested major version upgrades.
1038
+
1039
+ ### JSON.parse Without Validation
1040
+
1041
+ All of the following sites parse unvalidated external data and cast with `as X` -- no runtime schema validation:
1042
+
1043
+ | File | Line | Data Source |
1044
+ |------|------|-------------|
1045
+ | `src/ui/dock-persistence.ts` | 24 | localStorage |
1046
+ | `src/ui/toolbar-config.ts` | 29 | localStorage |
1047
+ | `src/sync/ws-client.ts` | 263 | WebSocket message |
1048
+ | `plugins/builtin/sky-spec-plugin.ts` | 369 | File import |
1049
+ | `plugins/builtin/piskel-importer-plugin.ts` | 142 | File import |
1050
+ | `src/state/macros.svelte.ts` | 27 | localStorage |
1051
+
1052
+ ---
1053
+
1054
+ ## Server Test Coverage Gaps
1055
+
1056
+ The following server modules and code paths have zero test coverage:
1057
+
1058
+ | Module / Path | What is Untested |
1059
+ |---------------|-----------------|
1060
+ | `autosave.py` | `AutoSaver` class entirely |
1061
+ | `cli.py` | Subcommand parsing, default argument fallbacks |
1062
+ | `mcp_server.py` | `_format_result`, `_make_tool_handler`, `register_all_tools`, stdio transport |
1063
+ | `connections.py` | No dedicated test file; broadcast exclude logic only tested indirectly |
1064
+ | `state.py` | No dedicated test file |
1065
+ | `main.py` lifespan | Loading projects at startup, starting/stopping autosaver |
1066
+ | `main.py` WebSocket | Actual WebSocket endpoint never integration-tested |
1067
+ | `storage.py` edge cases | Corrupted JSON, missing directories, pixel data size mismatches |
1068
+ | `mcp_registry.py` handlers | `export_png`, `export_spritesheet`, `get_palette`, `get_canvas_thumbnail`, `get_region`, `open_project`, `get_project_info`, `save_project`, `resize_canvas`, `apply_effect` |
1069
+
1070
+ ---
1071
+
1072
+ ## Top 10 Recommendations by Impact
1073
+
1074
+ | Priority | Finding | Action | Effort | Rationale |
1075
+ |----------|---------|--------|--------|-----------|
1076
+ | 1 | C1 | Route all UI command triggers through `dispatch()` instead of `cmd.execute()` | Small | Unblocks undo/redo, logging, and observability for every command in the app |
1077
+ | 2 | C2 | Make undo/redo special dispatcher methods, not registered commands | Small | Without this, undo literally cannot work even after C1 is fixed |
1078
+ | 3 | C3 | Implement snapshot-before-dispatch for pencil and eraser tools | Medium | Drawing is the primary user action; undo must work for strokes |
1079
+ | 4 | H5 | Split `menu-commands-plugin.ts` into domain-specific files | Large | Prerequisite for safely fixing most other plugin-layer issues |
1080
+ | 5 | H4 | Add mutator methods to `PluginAPI` (`setCanvasSize`, `addLayer`, `addFrame`, `setPixels`) | Large | Eliminates the need for plugins to bypass the API, restoring encapsulation |
1081
+ | 6 | M1-M5 | Extract shared tool factories: `getBuffer()`, `snapshotAll()`, `makeStrokeTool()`, `makeShapeCommand()`, shared undo handler | Medium | Eliminates ~285 lines of duplication, reduces bug surface, makes adding new tools trivial |
1082
+ | 7 | H15-H17 | Enable `noUncheckedIndexedAccess`, lint `plugins/`, design typed command params | Large | Systemic type safety improvement; catches bugs at compile time instead of runtime |
1083
+ | 8 | H10, H11 | Sanitize project names, cap canvas dimensions on the server | Small | Closes two security holes (path traversal and OOM via unbounded dimensions) |
1084
+ | 9 | A1-A3 | Add `:focus-visible` outlines, focus traps, and `role="dialog"` / `aria-modal="true"` to all dialogs | Medium | Minimum viable accessibility for keyboard and screen reader users |
1085
+ | 10 | Dead code | Remove 6 unused modules, 40+ unused exports, and half-implemented stubs | Small | Reduces codebase noise, prevents confusion, lowers onboarding cost |