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.
- package/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- 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 |
|