{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://skill-map.ai/spec/v1/plugins-doctor.schema.json",
  "title": "PluginsDoctorReport",
  "description": "Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts across built-in and drop-in plugins plus the structured issue / warning lists the human renderer produces, and the runtime contribution rejections persisted by the last scan. The `elapsedMs` top-level field is the command's own wall-clock (see `cli-contract.md` §Elapsed time).",
  "type": "object",
  "required": ["ok", "kind", "counts", "issues", "warnings", "contributionErrors", "elapsedMs"],
  "additionalProperties": false,
  "properties": {
    "ok": {
      "type": "boolean",
      "const": true,
      "description": "Always `true` on the happy path. Error envelopes use the shared `{ ok: false, error: { code, message } }` shape from `cli-contract.md` §Error envelope and do NOT carry this schema's other fields. A doctor run that surfaces issues still returns `ok: true` (the verb succeeded; the issues live under `issues[]` and gate the exit code)."
    },
    "kind": {
      "type": "string",
      "const": "plugins.doctor",
      "description": "Discriminator pinning this envelope to the plugins-doctor verb."
    },
    "counts": {
      "type": "object",
      "required": ["enabled", "disabled", "loaded", "incompatible", "invalid", "loadError", "warnings"],
      "additionalProperties": false,
      "description": "Aggregate counts across both built-in and drop-in plugins. `loaded` is the count of plugins whose runtime imported cleanly (status=enabled); `incompatible` folds the `incompatible-spec` and `incompatible-catalog` buckets; `invalid` is `invalid-manifest`; `loadError` is `load-error` + `id-collision`. The split mirrors the human table without committing to its label catalog.",
      "properties": {
        "enabled": { "type": "integer", "minimum": 0 },
        "disabled": { "type": "integer", "minimum": 0 },
        "loaded": { "type": "integer", "minimum": 0 },
        "incompatible": { "type": "integer", "minimum": 0 },
        "invalid": { "type": "integer", "minimum": 0 },
        "loadError": { "type": "integer", "minimum": 0 },
        "warnings": { "type": "integer", "minimum": 0 }
      }
    },
    "issues": {
      "type": "array",
      "description": "One entry per plugin in a non-success state (`incompatible-spec`, `incompatible-catalog`, `invalid-manifest`, `load-error`, `id-collision`). Empty when every plugin is `enabled` or intentionally `disabled`. Iteration order matches the human renderer.",
      "items": {
        "type": "object",
        "required": ["id", "status", "reason"],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "Qualified extension id `<plugin>/<ext>`. The bare plugin id form is reserved for the bundle (aggregate) macro path in CLI / BFF requests and never appears in persisted state."
          },
          "status": {
            "type": "string",
            "enum": [
              "incompatible-spec",
              "incompatible-catalog",
              "invalid-manifest",
              "load-error",
              "id-collision"
            ],
            "description": "Failure mode. Matches the `IDiscoveredPlugin.status` enum minus `enabled` / `disabled` (those never land in `issues[]`)."
          },
          "reason": {
            "type": "string",
            "description": "Sanitised human-readable explanation. May be empty when the loader supplied none."
          }
        }
      }
    },
    "warnings": {
      "type": "array",
      "description": "Informational warnings that do NOT gate the exit code: `precondition-kind-unknown` (an extractor declares a `precondition.kind` no installed Provider emits), `unknown-slot` (a view contribution targets a slot id absent from the closed catalog), `unknown-input-type` (a declared setting picks an input-type id absent from the closed catalog; AJV rejects it at load for drop-ins, this is the catalog-drift defence-in-depth path mirroring `unknown-slot`), and `recommended-action-missing` (an action's `precondition.analyzerIds` names an analyzer no loaded plugin declares). Iteration order matches the human renderer.",
      "items": {
        "type": "object",
        "required": ["id", "kind", "message"],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "Qualified extension id (`<plugin>/<ext>`) for `precondition-kind-unknown` and `recommended-action-missing`, `<plugin>/<ext>/<contributionId>` for `unknown-slot`, or `<plugin>/<ext>/<settingId>` for `unknown-input-type`."
          },
          "kind": {
            "type": "string",
            "enum": ["precondition-kind-unknown", "unknown-slot", "unknown-input-type", "recommended-action-missing"],
            "description": "Warning discriminator."
          },
          "message": {
            "type": "string",
            "description": "Sanitised human-readable explanation."
          }
        }
      }
    },
    "contributionErrors": {
      "type": "array",
      "description": "View contributions the last persisted scan REJECTED at emit time (the \"off-shape visible\" follow-up): an `ctx.emitContribution(...)` call whose ref was not a declared contribution, or whose payload failed the target slot's payload schema. Read from `scan_contribution_errors`; empty when the last scan had no rejected emissions, or when no scan has run yet (fresh project / missing DB). Each entry gates the exit code (any contribution error → exit 1). Iteration order matches the human renderer (`pluginId`, `extensionId`, `nodePath`, `emittedAt` ASC).",
      "items": {
        "type": "object",
        "required": ["pluginId", "extensionId", "nodePath", "reason", "message"],
        "additionalProperties": false,
        "properties": {
          "pluginId": {
            "type": "string",
            "minLength": 1,
            "description": "Plugin id of the extension whose emission was rejected."
          },
          "extensionId": {
            "type": "string",
            "minLength": 1,
            "description": "Extension id (within the plugin) that emitted the rejected contribution."
          },
          "nodePath": {
            "type": "string",
            "minLength": 1,
            "description": "Target node path the contribution was emitted against."
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "description": "Rejection reason: the literal `undeclared-contribution-ref`, or the AJV error string when the payload failed the slot's payload schema."
          },
          "message": {
            "type": "string",
            "description": "Sanitised human-readable diagnostic (mirrors the ephemeral `extension.error` event of kind `contribution-rejected`)."
          },
          "contributionId": {
            "type": "string",
            "description": "Resolved contribution id. Absent for the `undeclared-contribution-ref` shape (no contribution was resolved)."
          },
          "slot": {
            "type": "string",
            "description": "Resolved target slot. Absent for the `undeclared-contribution-ref` shape."
          }
        }
      }
    },
    "elapsedMs": {
      "type": "integer",
      "minimum": 0,
      "description": "Command's own wall-clock duration in milliseconds (see `cli-contract.md` §Elapsed time)."
    }
  }
}
