%%-- Image Horse — whole-app system architecture (AS SHIPPED, v8.81, 2026-09-21)
%%--
%%-- The downloadable source behind the hand-drawn map on /architecture. Both
%%-- are maintained BY HAND and have drifted apart before; when they disagree,
%%-- check the code, not either picture.
%%--
%%-- The dashed "Network" plane is optional: sever it and the solid client
%%-- subgraph is a complete, working editor. Demo mode is sacred.
%%--
%%-- WHAT THIS DIAGRAM IS CAREFUL ABOUT, because each one has been drawn wrong
%%-- here before:
%%--
%%--   - The engine runs in a WORKER and has been the default since v8.32
%%--     (ADR-024). Main-thread blocking per heavy operation went 129-137 ms
%%--     to 0. The OffscreenCanvas belongs to the ENGINE worker, not the codec.
%%--   - The service worker is BUILT BUT OFF. The default build statically
%%--     removes it (__IH_SW_MODE__ === "off", ADR-019). It has never shipped
%%--     on, which is why this file does NOT say "works offline" — a cold load
%%--     with no network fails.
%%--   - WebGPU is one blur kernel plus a self-test, with ZERO consumers on the
%%--     pixel path. It is not a backend the engine renders through.
%%--   - The rayon worker pool is GONE, not planned: measured 8-31x SLOWER than
%%--     single-threaded and rejected. No SharedArrayBuffer, no COOP/COEP, no
%%--     wasm threads anywhere.
%%--   - PatchMatch SHIPPED in v7.46 (Magic Eraser). Built --features
%%--     tiles,patchmatch.
%%--   - React 19, not 18. Seven Zustand stores, not five.
%%--   - The cloud copy is the EDIT ARCHIVE, uploaded for ANY signed-in user
%%--     (useEditPersistence.ts). It is not the op log and it is not Pro-only;
%%--     this file used to draw it as both.
%%--   - Fonts are NOT in the wasm beyond one fallback face. Liberation Sans,
%%--     Serif and Mono are static files, fetched at runtime and handed to the
%%--     engine through register_font (ADR-058).
%%--
%%-- ⚠️ EVERY COMMENT LINE HERE CARRIES CHARACTERS AFTER THE %% ON PURPOSE.
%%-- Mermaid strips comments with [^\n]+, which needs at least one character
%%-- after the marker, so a BARE %% silently breaks the whole file. Three lines
%%-- in this header used to be "%%" plus a single trailing space — parsing only
%%-- because of that space, one whitespace-trimming save away from breaking, with
%%-- nothing in the repo enforcing it. Use %%-- and the hazard is gone.
%%--
flowchart TB
    subgraph client["BROWSER — no account, and no server in the edit path"]
        direction TB

        subgraph ui["UI plane · main thread · React 19 + TypeScript + Vite"]
            SHELL["AppShell<br/>composition + layout only<br/>(being dismantled, stage 4)"]
            REG["Tool registry<br/>ToolModule per tool"]
            SESS["Session hooks<br/>image · selection · canvas · mask"]
            STORES["Zustand · 7 stores<br/>UI · Tool · Gallery · Annotation<br/>Guides · Perspective · TextBox"]
        end

        subgraph ew["Engine worker · THE DEFAULT since v8.32 (ADR-024)"]
            ENGINE["WASM engine · stamp_tool<br/>own wasm instance + ImageHorseTool<br/>kernels · TileBuffer · OpLog · PatchMatch<br/>built --features tiles,patchmatch"]
            OSC[("Transferred OffscreenCanvas<br/>composite drawn from THIS thread")]
        end

        subgraph cw["Codec worker · no wasm"]
            CODEC["encode · thumbnail · resize<br/>OffscreenCanvas.convertToBlob / drawImage"]
        end

        subgraph persist["Persistence plane · IndexedDB / Dexie · no backup, no server copy"]
            ORIG["originals<br/>SHA-256 content-addressed · immutable"]
            OPS["opLogs + keyframes"]
            GAL["gallery manifest"]
            CACHE["renderCache<br/>disposable"]
        end

        LOCAL["localStorage<br/>lightweight prefs · kill switches"]
        FONTS["Static font files<br/>Liberation Sans · Serif · Mono<br/>fetched at runtime, same origin"]
        SW["Service worker · BUILT, NOT SHIPPED<br/>statically removed from the default<br/>build (ADR-019) — never registered"]
        WGPU["WebGPU · one blur kernel + self-test<br/>zero consumers on the pixel path"]
    end

    subgraph edge["NETWORK — optional, additive only"]
        direction TB
        CLERK["Clerk<br/>identity"]
        CONVEX["Convex<br/>prefs sync · entitlements · gallery"]
        AIPROXY["AI proxy → Replicate<br/>the server holds the provider key"]
        SHARE["Share delivery<br/>ShareViewer"]
    end

    %% core editing path — every pixel operation crosses into the worker
    SHELL --> STORES
    STORES --> REG
    REG -->|ToolContext| ENGINE
    SESS -->|batched pointer events| ENGINE
    ENGINE -->|zero-copy blit| OSC
    OSC -->|presented in place| SHELL
    FONTS -->|register_font| ENGINE

    %% encode + persist
    SESS -->|pixels transferable| CODEC
    CODEC -->|Blob| CACHE
    SESS --> ORIG
    SESS --> OPS
    SESS --> GAL
    STORES --> LOCAL
    CACHE -.->|invalid → keyframe + replay| ENGINE

    %% kill switch: ih_engine_worker=0 drops the engine back onto the main thread
    LOCAL -.->|ih_engine_worker=0 opts out| ENGINE

    %% optional network plane
    ui -. auth .-> CLERK
    STORES -. signed in .-> CONVEX
    SESS -. signed in: edit archive .-> CONVEX
    SESS -. paid: server op .-> AIPROXY
    AIPROXY -. result .-> SESS
    GAL -. free+: create link .-> SHARE

    classDef optional stroke-dasharray:5 4;
    class edge,CLERK,CONVEX,AIPROXY,SHARE optional;

    %% Built but not on the shipped path — present in the repo, absent at runtime.
    classDef dark stroke-dasharray:3 3;
    class SW,WGPU dark;

    %% The only two stores that are the user's irreplaceable truth.
    classDef truth fill:transparent,stroke-width:2px;
    class ORIG,OPS truth;
