# 04 — ADR-0001: native DSH Excalidraw plugin with an original scene core - **Status:** Accepted (2026-10-28) - **Supersedes:** none - **Related:** [Options comparison](03-options-comparison.md), [capability contract](07-capability-contract.md), [verification](08-verification.md) - **Deciders:** migration lead, with upstream and platform evidence reviews ## 1. Context DeepSeek Harness has no drawing capability. The reference behavior lives in `zsviczian/obsidian-excalidraw-plugin`, which is a 106,925-line Obsidian plugin with an embedded, eval'd Excalidraw fork and an unresolved AGPL-3.0/MIT license contradiction. The task asks for the *core* behaviors — interactive editing, durable files, agent control, plugin lifecycle — at the smallest reliable scope, with evidence that the result works. The platform evidence established that a DSH client plugin is a plain classic-script bundle registered through `window.__ModuleLoader__.load`, that React comes from the page (18.3.1 in the live shell), that plugin UI is a React component in a slot, that the Host can register JSON-Schema tools and a `connection`-fenced HTTP route, and that every registration is a disposable effect. See [`02-dsh-platform.md`](02-dsh-platform.md). The render spike established that the genuine `@excalidraw/excalidraw@0.18.1` component mounts and is interactive inside that exact bundle format with React external, with local fonts and no CDN. See [`05-render-spike.md`](05-render-spike.md). ## 2. Decision Build **one MIT-licensed npm package** with two halves: 1. **Host half** — original TypeScript scene core and Cordis plugin: - a `.excalidraw` JSON v2 parser/normalizer/validator that preserves unknown well-formed fields; - a durable `SceneStore` with atomic writes, fsync, read-back hash verification, per-file locking, sidecar revisions, idempotent request ids and backup recovery; - five agent tools with object-rooted JSON Schemas; - an HTTP API under `/dsh-excalidraw/api`, admitted by `ctx.get('connection').requestRejection`; - a static asset route for fonts/locales/CSS, because Harness package assets are images-only. 2. **Browser half** — the genuine Excalidraw React component registered into `main` and `sidebar.panellist`, with: - one scene list, explicit Save and `Cmd/Ctrl+S`, dirty-state tracking; - conflict detection and a Reload / Overwrite recovery path; - editor lifecycle evidence posted to the Host (`mounted`, `canvas-ready`, `saved`). The renderer is reused from the MIT upstream package. **No source from the AGPL Obsidian plugin is copied.** The Obsidian CLI is not embedded, wrapped, or required. The architecture is option **C′** of the comparison: shared native core + native DSH adapter; an Obsidian backend is a documented future seam, not current code. ### 2.1 Scope decisions inside the decision | Decision | Why | |---|---| | Plain `.excalidraw` JSON only, not `.excalidraw.md` | The Markdown envelope is an Obsidian-specific container (frontmatter, text sections, LZString chunks). The plain format is the portable interchange format and the one Excalidraw itself loads/saves. | | Original scene model, no `@excalidraw/element` at runtime | The 0.18.0 fork-version sub-packages do not resolve outside a bundler (`@excalidraw/math/*` exports are types-only) and the package is a browser app. The Host needs a tiny, auditable model; the renderer re-validates on open. | | Revision sidecar, not in-file revision | Keeps `.excalidraw` files byte-compatible with Excalidraw and portable; the sidecar can be deleted and rebuilt from the file hash. | | Optimistic concurrency + idempotency | Matches the upstream CLI protocol's `expectedRevision` design (evidence: [`01-upstream-evidence.md` §7](01-upstream-evidence.md)) and makes agent retries safe. | | Single-file 8 MiB client bundle for v0.1 | Proven to work in the real page. Chunking is a performance optimization with new loader risk; it is deferred to v0.2 with a measured target. | | No `engines.dsh` range yet | The plugin was verified on 0.2.1-rc.2 only; claiming a wider range would be unevidenced. | ## 3. Consequences ### Positive - The person edits the same real canvas and scene model that Excalidraw uses; no static preview, no imitation renderer. - The agent and the person share one validation and persistence path (`ExcalidrawService`), so behavior cannot drift between surfaces. - File safety is testable: save evidence is hash-based, conflicts are reproducible, and a corrupted file recovers from the newest backup. - The original code is small enough to audit (≈2,300 lines of TypeScript/JSX/JS including tests, excluding vendored assets and generated bundles) and MIT-licensed. - Installation is a standard bundle row; disabling it removes routes, tools, styles and the service through `ctx.effect`. ### Negative / accepted costs - **Boot cost:** the 8 MiB client bundle is fetched and parsed at boot for every page load. Mitigation is the v0.2 chunking work; the cost is measured and documented, not hidden. - **Renderer pin:** Excalidraw 0.18.1 is pinned. Upgrading requires re-running the E2E and re-checking the CSS/asset route. - **No CRDT:** concurrent edits are detected and surfaced, not merged. The task asks for reliable persistence and conflict recovery, not collaborative merging. - **Approximate text metrics on the Host:** agent-created text width uses a deterministic approximation; Excalidraw re-measures on open. Documented as a partial-fidelity area. - **Narrow app state:** only background color, grid size and grid mode persist. - **Obsidian files:** `.excalidraw.md` drawings are not readable; users must export plain `.excalidraw` first. ### Neutral - The plugin adds one large asset directory (fonts ≈13 MiB, locales ≈1.7 MiB) to the repository. This is the cost of offline rendering with correct fonts; a v0.2 download-on-demand option is possible but would weaken offline guarantees. ## 4. Alternatives considered and why not chosen | Alternative | Reason not chosen | |---|---| | A — bridge the Obsidian CLI | No standalone CLI exists; the protocol is in-process inside Obsidian; no Obsidian runtime was available; the interactive canvas would remain in Obsidian. See [comparison §2.1](03-options-comparison.md#21-option-a-is-not-runnable-as-described). | | B — port the whole plugin | 106,925 lines, Obsidian APIs, embedded eval build, AGPL/MIT ambiguity, and features outside the four required capabilities. See [comparison §2.2](03-options-comparison.md#22-option-b-is-technically-possible-but-disproportionate-and-license-risky). | | Custom minimal canvas (no Excalidraw) | Would preserve the file format but not the interaction behavior (roughness, bindings, text editing, fonts). The spike proved the genuine component is viable at acceptable risk; a custom canvas would be a larger behavioral gap for less reuse. | | iframe of a standalone Excalidraw app | Violates the Harness UI rule (React in a slot, host theme), adds a second document with its own auth/theme, and cannot share the Host service cleanly. | | Use `@excalidraw/element` on the Host | The published sub-packages do not resolve outside their monorepo/bundler (documented probe failure in the render spike), and they are part of the browser app; a small original model is more reliable. | ## 5. Verification obligations A change to this decision is only credible if these still pass: 1. `node scripts/verify.mjs` — typecheck, build, 56 Host/core tests, packaging assertions. 2. The 13-step real-browser E2E in an isolated Harness (`test/e2e/README.md`), including the concurrent-writer conflict and overwrite path. 3. The real-Host agent probe: five tools visible and callable, stale fence, idempotent retry, disk hash match. 4. The golden corpus: every plain fixture parses, validates, round-trips and keeps its bindings. 5. License inventory remains accurate; no AGPL source is copied. ## 6. Revisit triggers - Harness changes its client-loader format, tool contract, or `connection` service in a way that breaks the E2E. - Excalidraw releases a version whose React peer range or CSS/asset layout changes materially. - A user requirement appears for `.excalidraw.md` import/export, real-time collaboration, or Obsidian round-tripping. - Boot-time measurements show the single-file bundle is a user-visible problem; then implement chunking and re-run the E2E. - Upstream clarifies the AGPL/MIT contradiction in a way that changes reuse options.