{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Design Plugin — per-repo config",
  "description": "Per-repo configuration consumed by the design plugin's dev-server, slash commands, and skills. Place at <repo>/.design/config.json.",
  "type": "object",
  "additionalProperties": false,
  "required": ["name", "designRoot"],
  "properties": {
    "name": {
      "type": "string",
      "description": "Short repo / product name. Shown in server header and as a token in skill prompts."
    },
    "projectLabel": {
      "type": "string",
      "description": "Long label for the design browser (window title and big H1). Defaults to <name> + ' Design'."
    },
    "designRoot": {
      "type": "string",
      "description": "Repo-relative root where canvas + design system files live. Default convention: '.design' (top-level, alongside .git, .claude, etc.). Older repos may use '.ai/design'.",
      "default": ".design"
    },
    "canvasGroups": {
      "type": "array",
      "description": "Top-level groupings shown in the file tree. Each entry maps a folder under designRoot to a sidebar section.",
      "items": {
        "type": "object",
        "required": ["label", "path"],
        "properties": {
          "label": { "type": "string" },
          "path": {
            "type": "string",
            "description": "Folder under designRoot to scan for .html canvases."
          }
        },
        "additionalProperties": false
      },
      "default": [
        { "label": "Design system", "path": "system" },
        { "label": "Canvases", "path": "ui" }
      ]
    },
    "rootClass": {
      "type": "string",
      "description": "Body / canvas root CSS class that the project's tokens are scoped under. Generated canvases use <body class=\"<rootClass>\">.",
      "default": "app"
    },
    "themeDefault": {
      "type": "string",
      "enum": ["dark", "light"],
      "description": "Default theme attribute (data-theme) for new canvases. Can only ever be a single theme name — it is NOT the signal for whether a DS ships more than one theme. For that, check 'designSystems[].themes'.",
      "default": "dark"
    },
    "tokensCssRel": {
      "type": "string",
      "description": "Project-wide tokens CSS path, relative to designRoot. Used as the fallback when 'designSystems' is unset OR when an entry omits its own 'tokensCssRel'. New canvases <link> to this file by default. Per DDR-048, per-DS entries auto-resolve to '<designSystems[i].path>/colors_and_type.css' when their own field is missing.",
      "default": "system/colors_and_type.css"
    },
    "teamAccentDefault": {
      "type": ["string", "null"],
      "description": "Optional default value for data-team attribute on new canvases. Set to null if the project doesn't use multi-team accent overrides."
    },
    "handoffTargets": {
      "type": "array",
      "description": "Where /design:handoff emits target artifacts. Empty array disables handoff. Special `path` values: `registry:item` emits a shadcn registry-item.json sidecar next to the canvas (Phase 3.6 default — consume via `bunx shadcn add file://./<Slug>.registry.json`). Any other path is treated as a repo-relative target dir for legacy direct-migration handoff. Recommended single-entry default: `[{ label: 'shadcn registry', path: 'registry:item', platform: 'web' }]`.",
      "items": {
        "type": "object",
        "required": ["label", "path"],
        "properties": {
          "label": { "type": "string" },
          "path": {
            "type": "string",
            "description": "`registry:item` for shadcn sidecar emit, or repo-relative target dir for direct migration."
          },
          "platform": { "type": "string", "enum": ["web", "mobile", "desktop", "other"] }
        },
        "additionalProperties": false
      },
      "default": []
    },
    "newCanvasDir": {
      "type": "string",
      "description": "Default folder (under designRoot) where /design:new scaffolds new canvas .tsx files (Phase 3.6+; older repos may still hold .html canvases pending one-shot migration).",
      "default": "ui"
    },
    "newComponentDir": {
      "type": "string",
      "description": "Default folder (under designRoot) where /design:new puts shared component .jsx files.",
      "default": "ui/components"
    },
    "extensions": {
      "type": "array",
      "description": "User-added directories the docs renderer / completeness-critic should track but not validate against the standard schema. Each entry maps a folder (under the DS) to a label.",
      "items": {
        "type": "object",
        "required": ["label", "path"],
        "properties": {
          "label": { "type": "string" },
          "path": { "type": "string" }
        },
        "additionalProperties": false
      },
      "default": []
    },
    "completenessProfile": {
      "type": "string",
      "enum": ["minimal", "standard", "strict"],
      "description": "Severity profile for the design-system-completeness-critic. 'minimal' = Core blockers only; 'standard' = Core + most Conventional checks; 'strict' = all checks incl. voice/tone/hard-stops in README.",
      "default": "standard"
    },
    "activeFamilies": {
      "type": "array",
      "description": "Token families this project has in scope. Computed during /design:setup-ds. The completeness-critic uses this to gate Conventional checks (e.g. type-mono specimen only required when 'mono' is here).",
      "items": {
        "type": "string",
        "enum": ["accent", "status", "presence", "mono"]
      },
      "default": ["accent"]
    },
    "accentStrategy": {
      "type": "string",
      "description": "How many --accent* families exist. The completeness-critic C7 gates the actual family count against this declaration. Default: 'single' (backwards-compatible). 'chromatic-N' is parameterized — write the literal N (e.g. 'chromatic-5'). Per DDR-043.",
      "pattern": "^(single|paired|chromatic-[1-9][0-9]?)$",
      "default": "single"
    },
    "colorSpace": {
      "type": "string",
      "enum": ["oklch", "hsl", "hex", "lab"],
      "description": "Color space the project uses in tokens CSS. The completeness-critic V2 gates the actual token format against this declaration. Default: 'oklch' (backwards-compatible). Per DDR-043.",
      "default": "oklch"
    },
    "aestheticAmbition": {
      "type": "string",
      "enum": ["restrained", "confident", "expressive", "maximalist"],
      "description": "How chromatically/decoratively expressive the DS is. Inferred by ux-research-agent from brand character during /design:setup-ds (NOT a picker), then anchors the structural knobs (accentStrategy, shadow/decor) and sets the default opt_out_scope for canvases under this DS (restrained|confident → palette, expressive → aesthetic, maximalist → full). Default 'restrained' is the legacy/no-bootstrap fallback only — a bootstrapped DS always writes the inferred value. Per DDR-073.",
      "default": "restrained"
    },
    "dsFidelity": {
      "type": "string",
      "enum": ["advisory", "strict"],
      "description": "How hard DS reuse is enforced during canvas generation/edit. 'advisory' (default) = reuse findings (invented brand mark/icons, reinvented components, parallel product shell) surface as warnings. 'strict' = when the DS ships a canonical specimen (logo, iconography, component, platform showcase), reinventing it is a blocker the auto-fix loop must clear. Orthogonal to opt_out_scope on the SAME axis of 'how much the DS binds': a canvas's explicit opt_out_scope=full always wins over strict (explicit free-use beats project policy). A11y is independent of this knob. Per DDR-141.",
      "default": "advisory"
    },
    "moodboard": {
      "type": "object",
      "description": "Knobs for the /design:setup-ds Stage-3 direction gate (the design-language moodboard). Per DDR-147.",
      "properties": {
        "variants": {
          "type": "integer",
          "minimum": 1,
          "maximum": 3,
          "default": 3,
          "description": "How many direction tiles the interactive direction gate composes by default. The SINGLE authoritative home of this default — docs reference this knob, never a literal. Effective count still degrades to the number of pairwise-distinct research seeds (floor 1), and --quick / autonomous / re-bootstrap force 1. Default tiles are main-agent seed-only compositions (no web, no fan-out); the blind-sub-agent + self-harvest path is an explicit escalation. Per DDR-147."
        }
      },
      "additionalProperties": false
    },
    "designSystems": {
      "type": "array",
      "description": "Design systems available in this project. Single-DS projects have one entry; multi-DS projects (marketing vs. admin vs. mobile) list each here. Each canvas's .meta.json declares which DS it uses via the 'designSystem' field. The System view's DS picker iterates this array.",
      "items": {
        "type": "object",
        "required": ["name", "path"],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]*$",
            "description": "Kebab-case slug. Used to match canvas .meta.json.designSystem references and as the ?ds=<name> query param on /_system-data."
          },
          "path": {
            "type": "string",
            "description": "Folder under designRoot (e.g. 'system/marketing'). The DS-scoped System view reads tokens, previews, and ui_kits from inside this folder."
          },
          "description": { "type": "string" },
          "tokensCssRel": {
            "type": "string",
            "description": "Per-DS tokens CSS path, relative to designRoot. When omitted, auto-resolves to '<path>/colors_and_type.css' (DDR-048). Set explicitly when the DS's tokens live under a non-standard filename."
          },
          "rootClass": {
            "type": "string",
            "description": "Optional root class applied to canvases in this DS (overrides the project-wide rootClass for this DS's previews + new canvases)."
          },
          "themeDefault": {
            "type": "string",
            "enum": ["dark", "light"],
            "description": "Per-DS theme default. Overrides the project-wide themeDefault when rendering this DS's specimens."
          },
          "themes": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Authoritative list of theme names this DS's tokens CSS actually ships (e.g. ['dark','light']). This is the ONLY field that should be checked to decide whether a DS is dual-theme — 'themeDefault' can never hold 'both' (enum is dark|light only), so a single-theme entry MUST still list its one theme (e.g. ['dark']), not omit the field. Bootstrap/re-bootstrap always populates this from the theme-default discovery answer. Drives the System view's theme toggle, design-system-completeness-critic V18/V18c, and /design:new step 9's per-theme reality-check capture."
          },
          "newCanvasDir": {
            "type": "string",
            "description": "Override of the project-wide newCanvasDir for canvases scaffolded inside this DS."
          },
          "newComponentDir": {
            "type": "string",
            "description": "Override of the project-wide newComponentDir for components scaffolded inside this DS."
          }
        },
        "additionalProperties": false
      },
      "default": []
    },
    "defaultDesignSystem": {
      "type": ["string", "null"],
      "description": "Name of the DS used when a canvas's .meta.json has no 'designSystem' field. For single-DS projects, this is the only DS. For multi-DS, pick the most common.",
      "default": null
    },
    "linkedHub": {
      "type": "object",
      "description": "Pairs this repo with a Maude hub for cross-machine file sync (Phase 9 Task 3+4). Written by 'maude design link <url> --token <hex>'. Token NEVER lives in this committed config — it's stored per-machine in ~/.config/maude/hubs.json. When present, 'maude design serve' starts the bidirectional Yjs↔fs sync agent (Task 4).",
      "required": ["url", "linkedAt"],
      "properties": {
        "url": {
          "type": "string",
          "description": "Hub base URL (normalized — trailing slash trimmed, scheme + host lowercased). The sync agent constructs ws(s)://<host> from this for the HocuspocusProvider connection."
        },
        "linkedAt": {
          "type": "number",
          "description": "Unix-ms timestamp from when 'maude design link' was first run on this repo."
        },
        "adopt": {
          "type": "boolean",
          "description": "When true, the first sync after boot pushes local disk state up to the hub unconditionally (use case: bootstrapping the hub from a populated repo, or hub-was-wiped recovery). Cleared after first successful adopt."
        },
        "syncTsx": {
          "type": "boolean",
          "default": true,
          "description": "DDR-079 (supersedes DDR-072) — project-level TSX sync, ON BY DEFAULT. Omit the field to sync ALL .tsx canvases to this hub (the common case: a linked teammate sees the project's TSX without a hidden opt-in). Set false to opt the whole project OUT. A per-canvas .meta.json \"syncable\": false still excludes one canvas (the sidecar always wins). Inert unless the cross-origin sandbox is active (MAUDE_CANVAS_ORIGIN_SPLIT != 0) — the DDR-060 Lock-2 coupling is preserved. TSX bodies execute, so syncing broadens a WebRTC/self-nav exfil residual to every synced canvas — link only hubs you operate or trust; the dev-server prints a loud boot banner against non-loopback hubs."
        },
        "workspaceId": {
          "type": "string",
          "description": "DDR-192 §5 — stable workspace identity for the hub document namespace (ws/<workspace-id>/<branch>/<slug>). Authoritative when present; the cloud control plane sets it at provisioning time. Absent, the sync runtime derives it from the git origin remote."
        },
        "syncFiles": {
          "type": "boolean",
          "default": true,
          "description": "Sync v2 — the journal-driven file plane (Plane B): the downward project-file pull AND the widened upward sweep. Default ON; set false to fall back to the pre-Sync-v2 reach (binary media under assets/ only). Settable from the Sync panel's Settings section."
        },
        "propagateDeletes": {
          "type": "boolean",
          "default": true,
          "description": "Sync v2 Increment 6 — a deletion here becomes a deletion everywhere (losers quarantined into _trash/, never unlinked; cumulative breakers hold bulk removals for confirmation). Default ON; set false to hold every absence instead. Settable from the Sync panel's Settings section."
        },
        "resolveFirstAnchor": {
          "type": "string",
          "enum": ["keep-local", "keep-cloud"],
          "description": "How to settle a FIRST-ANCHOR hold for the whole set at once: keep-local pushes this machine's copies up; keep-cloud takes the project's and parks yours beside them. ABSENT means keep asking — the hold is not an error. Settable from the Sync panel's Settings section."
        },
        "fileEvents": {
          "type": "boolean",
          "default": true,
          "description": "Sync v2 Increment 2 (DDR-226 §4) — the file-event control channel (poke). Default ON where the hub advertises ledger support; set false to fall back to the 20 s poll cadence. The documented rollback for the poke — a config key because the target user has no terminal (DDR-177)."
        }
      },
      "additionalProperties": false
    },
    "generation": {
      "type": "object",
      "description": "BYOK AI-media generation preferences (feature-ai-media-generation, DDR-16x). NON-SECRET ONLY — provider API keys are NEVER in this committed config; they live in the OS keychain (native) or ~/.config/maude/keys.json mode 0600 (browser). This block only carries routing/UI preferences. Hot-reloadable — Settings saves take effect without a server restart.",
      "properties": {
        "defaultImageProvider": {
          "type": "string",
          "description": "Provider id used when an image-generation request leaves the provider unspecified (e.g. 'gemini'). Must be a registered provider that supports the image modality.",
          "default": "gemini"
        },
        "defaultAudioProvider": {
          "type": "string",
          "description": "Provider id used when an audio-generation request (music / sfx / voiceover) leaves the provider unspecified. Must be a registered provider that supports the audio modality (v1: 'elevenlabs').",
          "default": "elevenlabs"
        },
        "defaultModels": {
          "type": "object",
          "description": "Per-provider default model id (e.g. { \"gemini\": \"gemini-2.5-flash-image\" }). Used when a request leaves the model unspecified.",
          "additionalProperties": { "type": "string" },
          "default": {}
        },
        "preferLocalWhenAvailable": {
          "type": "boolean",
          "description": "When a local runtime (Ollama / ComfyUI / Draw Things, post-v1) is detected on loopback, prefer it over a cloud provider for the same modality (free, offline, private). Inert until local engines ship (Phase 5).",
          "default": false
        },
        "transcription": {
          "type": "object",
          "description": "Speech-to-text / subtitle engine preference (Task 2.6, DDR-164). The engine is never switched behind your back: a chosen-but-unavailable engine errors clearly, and `auto` — a mode you SELECT — resolves to the best engine this machine is set up for while stating which one it picked. `maude design transcribe --provider` overrides this per-call.",
          "properties": {
            "provider": {
              "type": "string",
              "enum": ["auto", "whisper", "elevenlabs", "groq"],
              "description": "Which engine transcribes when no --provider flag is given. `auto` = use the cloud engine whose key is set (ElevenLabs Scribe first), else local whisper — the UI and the CLI both state what it resolved to; `whisper` = local whisper.cpp (free, offline, no key); `elevenlabs` = ElevenLabs Scribe (cloud, key, audio leaves the machine); `groq` = Groq Whisper (cloud, key). Absent ⇒ whisper.",
              "default": "whisper"
            },
            "model": {
              "type": "string",
              "description": "Optional engine-specific model id override (e.g. a Groq 'whisper-large-v3-turbo', an ElevenLabs 'scribe_v1', or a local ggml model name). Absent ⇒ the engine's own default."
            },
            "whisperModel": {
              "type": "string",
              "enum": ["tiny", "base", "base.en", "small", "large-v3-turbo"],
              "description": "Preferred managed local whisper.cpp model id (Task 2.7). Downloaded via Settings → Subtitles → 'Download model' into a per-machine cache; `maude design transcribe --provider whisper` auto-resolves it (no --model). Absent ⇒ any downloaded multilingual model. 'base' is the recommended default; '.en' variants are English-only."
            }
          },
          "additionalProperties": false
        },
        "providers": {
          "type": "object",
          "description": "Per-provider non-secret toggles keyed by provider id. `enabled: false` hides a provider from the generate UI even if a key is present. `localEndpoint` overrides the default host:port for a local runtime.",
          "additionalProperties": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether this provider is offered in the generate UI. Default true.",
                "default": true
              },
              "localEndpoint": {
                "type": "string",
                "description": "Override host:port for a local runtime provider (e.g. '127.0.0.1:11434' for Ollama). Ignored for cloud providers."
              }
            },
            "additionalProperties": false
          },
          "default": {}
        }
      },
      "additionalProperties": false
    }
  }
}
