**Refactoring:** Extract `createEditor()` to `packages/core/src/editor/` folder with domain modules sharing an `EditorContext` interface. App store becomes a thin Vue wrapper.
### 2. `src/composables/use-canvas-input.ts` — 1,501 lines, the Input Monolith
Handles 8 drag types, touch/gesture, wheel zoom, double-click, hit testing, snap guides, auto-layout indicators, reparenting, and hover cursors — all in one function.
**Refactoring:** Extract by concern:
-`use-drag-move.ts` — move with snap, reparent, auto-layout break
-`use-drag-resize.ts` — resize with aspect ratio
-`use-drag-rotate.ts` — rotation with snap
-`use-touch-input.ts` — touch/gesture/pinch
-`use-hit-test.ts` — hit testing (already pure functions)
### 3. State is fully mutable to all consumers
`editor.state` is a raw `shallowReactive` — every composable mutates it freely (`store.state.panX = ...`, `store.state.zoom = ...`). Zero uses of `readonly`/`shallowReadonly` anywhere.
**Refactoring:** Expose `shallowReadonly(state)` as the public API, add explicit setter methods for mutations that need to happen from outside the store.
Multiple `void asyncFn()` calls where rejections are silently swallowed:
-`void switchPage()` in `addPage`/`deletePage`
-`void reloadFromDisk()` in file watcher
-`void loadFontsForNodes()` in paste handler
-`saveFigFile`, `writeFile`, `reloadFromDisk` — no try/catch, silent data loss risk
**Refactoring:** Wrap fire-and-forget calls in a `safeFire(promise, context)` helper that catches and shows a toast. Add try/catch to all async store methods.
### 5. Module-level state in composables
`use-chat.ts` declares all state (`providerID`, `apiKey`, `modelID`, etc.) at module scope outside the composable function. `use-menu.ts` calls `useFileDialog()` and `useEditorStore()` at module scope — technically incorrect for VueUse composables that expect `setup()` context.
**Refactoring:** Move state inside the composable function or convert `use-chat.ts` to a proper store. Move `useFileDialog()` inside the exported function.
### 6. Bounding-box computation duplicated ~10 times
The `let minX = Infinity; ... Math.min/max` pattern appears in `editor.ts` (7×), `tools/read.ts`, `tools/vector.ts`, `clipboard.ts`, `renderer/overlays.ts`, `figma-api.ts`.
**Refactoring:** Add a generic `computeBounds(items, getRect): Rect` to `geometry.ts`, replace all copies.
### 7. "Node not found" guard repeated 44 times in tools
Every tool function starts with `const node = figma.getNodeById(id); if (!node) return { error: ... }`.
**Refactoring:** Add `requireNode(figma, id)` to `tools/schema.ts` that either returns the node or throws a typed error caught by the tool executor.
### 8. Barrel import overuse — 58 consumers use `@open-pencil/core` vs 2 using subpaths
Subpath exports (`/scene-graph`, `/tools`, `/renderer`, etc.) exist but are almost unused. The barrel `export * from './constants'` dumps 91 exports.
**Refactoring:** Migrate app imports to use subpath exports. Improves tree-shaking and makes dependency intent explicit.
### 9. `kiwi-serialize.ts` misplaced
495-line file at the core root that imports exclusively from `./kiwi/*`. Belongs inside `kiwi/`.
**Refactoring:** Move to `packages/core/src/kiwi/kiwi-serialize.ts`, update imports.
### 10. `tools/stock-photo.ts` throws instead of returning `{ error }`
All other tools return `{ error: string }` for AI consumers, but `stock-photo.ts` throws.
**Refactoring:** Change to return `{ error }` like all other tools.
8 `.token.*` rules leak globally. Scope or move to `app.css`.
### 14. Dead exports (7 functions)
-`copyFill`, `copyStroke`, `copyEffect`, `copyStyleRun` in `copy.ts` (only array variants used)
-`createPropertyChange` in `undo.ts`
-`designTokens` in `tools/codegen.ts`
-`queryFonts` in `fonts.ts`
### 15. Renderer circular imports
`renderer.ts` ↔ `ai-overlays.ts` and `renderer.ts` ↔ `rulers.ts` — runtime cycles through type + value imports. Extract a `SkiaRendererContext` interface to break the cycle.
### 16. Duplicate font weight maps
Weight-to-name mapping exists in both `figma-api-proxy.ts` and `fonts.ts`. Extract to a shared constant.
### 17. Rotated corners reimplemented
`renderer/overlays.ts` and `snap.ts` reimplement `rotatedCorners()` instead of importing from `geometry.ts`.
### 18. Duplicate .fig scaffold/zip assembly
Same DOCUMENT NodeChange in `clipboard.ts` and `fig-export.ts`. Same zip structure in `fig-export-worker.ts` and `fig-export.ts`.
Lines ~1215, ~1295, ~1390, ~1460 in editor.ts each follow the identical 50-line pattern: compute bbox → calculate parent offset → find z-index → create container → reparent children → push undo. Only the node type and a few props differ. ~200 lines that should be `wrapSelectionInContainer(type, extraProps)`.
### 20. `console.warn`/`console.error` (30 total)
All legitimate error handlers but would benefit from a structured logger. The bare `console.error(e)` in `EditorView.vue:67` should add context.