{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://skill-map.ai/spec/v1/plugins-registry.schema.json",
  "title": "PluginsRegistry",
  "description": "Two shapes in one file: (1) the per-plugin manifest that authors ship as `plugin.json` (see `$defs/PluginManifest`); (2) the aggregate registry the implementation produces on disk (`<cwd>/.skill-map/plugins.json`), which lists all discovered plugins with their compat status. Both shapes are normative. camelCase keys throughout.",
  "type": "object",
  "oneOf": [
    { "$ref": "#/$defs/PluginsRegistry" },
    { "$ref": "#/$defs/PluginManifest" }
  ],
  "$defs": {
    "PluginManifest": {
      "type": "object",
      "required": ["version", "specCompat", "catalogCompat", "description"],
      "additionalProperties": false,
      "description": "Plugin manifest written as `<plugin>/plugin.json`. The plugin id comes from the directory name (structure-as-truth, per `architecture.md` §Plugin discovery), it is NOT a manifest field. Manifests carrying an `id` key are rejected as `invalid-manifest`.",
      "properties": {
        "version": {
          "type": "string",
          "description": "Plugin semver."
        },
        "specCompat": {
          "type": "string",
          "description": "Semver range this plugin is compatible with (e.g. `^1.0.0`, `>=0.3.0 <0.4.0`). Checked via `semver.satisfies(specVersion, this)` at load time."
        },
        "catalogCompat": {
          "type": "string",
          "description": "Required semver range against the kernel's view-slots + input-types catalog version (e.g. `^1.0.0`). Independent from `specCompat` because the catalog evolves on its own cadence (new slots ship as minor bumps; rename/remove ships as catalog-major bumps that trigger `sm plugins upgrade`). Mismatch surfaces as `incompatible-catalog` plugin status."
        },
        "description": {
          "type": "string",
          "minLength": 1,
          "description": "Required short description shown in `sm plugins list` and the UI. English-only per AGENTS.md."
        },
        "order": {
          "type": "number",
          "description": "Optional visual ordering hint, inspector-only. The inspector renders one collapsible section per plugin (grouping the plugin's `inspector.body.panel.*` contributions); sections are sorted ASC by this value (default 100), tie-break by plugin id. Does NOT affect extension execution order, which is governed by `phase` (analyzers) and registration order."
        },
        "storage": {
          "type": "object",
          "description": "Persistence declaration for this plugin. Absent = plugin does not persist state.",
          "required": ["mode"],
          "additionalProperties": false,
          "properties": {
            "mode": { "const": "kv", "description": "Shared `state_plugin_kvs` table, scoped by plugin id. The only storage mode." },
            "schema": {
              "type": "string",
              "description": "Optional. JSON Schema (path relative to plugin root) that validates the value on every `ctx.store.set(key, value)`. Absent = permissive (no validation, status quo). The kernel AJV-compiles the schema at load time; a missing or unparseable schema file surfaces as `load-error`."
            }
          }
        },
        "author": { "type": "string" },
        "license": { "type": "string", "description": "SPDX identifier." },
        "homepage": { "type": "string", "format": "uri" },
        "repository": { "type": "string", "format": "uri" }
      }
    },
    "PluginsRegistry": {
      "type": "object",
      "required": ["schemaVersion", "plugins"],
      "additionalProperties": false,
      "properties": {
        "schemaVersion": { "type": "integer", "const": 1 },
        "plugins": {
          "type": "array",
          "items": { "$ref": "#/$defs/DiscoveredPlugin" }
        }
      }
    },
    "DiscoveredPlugin": {
      "type": "object",
      "required": ["id", "path", "manifest", "status"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "description": "Plugin id, derived from the plugin directory name (structure-as-truth). The kernel computes this at discovery; it is NOT a manifest field." },
        "path": { "type": "string", "description": "Absolute path to the plugin directory." },
        "manifest": { "$ref": "#/$defs/PluginManifest" },
        "status": {
          "type": "string",
          "enum": ["enabled", "disabled", "incompatible-spec", "incompatible-catalog", "invalid-manifest", "load-error", "id-collision"],
          "description": "Resolved state after discovery. `disabled` = user-disabled via config; `id-collision` = two plugin directories with the same name reachable from different roots (project + --plugin-dir combinations), both blocked, no precedence; `incompatible-catalog` = manifest's `catalogCompat` does not satisfy the kernel's catalog version (resolved via `sm plugins upgrade`); others = automatic."
        },
        "statusReason": {
          "type": ["string", "null"],
          "description": "Human-readable detail when status is anything other than `enabled`."
        }
      }
    }
  }
}
