{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://pcbastudio.com/schemas/finding.schema.json",
  "title": "PCBA Studio review report",
  "description": "Output of `pcba review --format json`. Stable contract for CI and agents.",
  "type": "object",
  "required": ["schema_version", "tool", "summary", "coverage", "findings"],
  "properties": {
    "schema_version": { "type": "integer", "const": 1 },
    "tool": {
      "type": "object",
      "required": ["name", "version"],
      "properties": {
        "name": { "type": "string" },
        "version": { "type": "string" }
      }
    },
    "project": { "type": "object" },
    "baseline": {
      "type": "object",
      "required": ["path", "suppressed", "stale"],
      "properties": {
        "path": { "type": "string" },
        "suppressed": { "type": "integer", "minimum": 0 },
        "stale": { "type": "array", "items": { "type": "string" } }
      }
    },
    "changes": {
      "type": "object",
      "required": ["base", "files", "omitted"],
      "properties": {
        "base": { "type": "string" },
        "files": { "type": "array", "items": { "type": "string" } },
        "omitted": { "type": "integer", "minimum": 0 }
      }
    },
    "summary": {
      "type": "object",
      "required": ["total"],
      "properties": {
        "total": { "type": "integer", "minimum": 0 },
        "error": { "type": "integer", "minimum": 0 },
        "warning": { "type": "integer", "minimum": 0 },
        "info": { "type": "integer", "minimum": 0 }
      }
    },
    "coverage": {
      "type": "object",
      "description": "Which rules ran and which could not. An empty `findings` list is NOT proof of a clean design — a consumer must read this before reporting a pass.",
      "properties": {
        "ran": { "type": "array", "items": { "type": "string" } },
        "skipped": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Rule id plus the reason it could not run."
        },
        "required": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Present only when the review was run with required rules (`--require-rule`, MCP `required_rules`, action `required-rules`)."
        },
        "unmet": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Required rules that could not run, plus `cross_domain.pin.mismatch` when it compared zero signals. Non-empty means the CLI exits 2."
        },
        "not_applicable": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Rules written for another schematic format (KiCad rules on an Altium project, the Altium open-circuit heuristic on a KiCad project), with the format they apply to."
        },
        "rules": {
          "type": "array",
          "description": "Every rule with its outcome, computed before baseline and changed-file filtering. A rule that judged nothing is not_checked, never passed.",
          "items": {
            "type": "object",
            "required": ["rule_id", "status"],
            "properties": {
              "rule_id": { "type": "string" },
              "status": { "enum": ["passed", "failed", "info", "not_checked", "not_applicable"] },
              "reason": { "type": "string", "description": "Why the rule was not checked or not applicable." },
              "findings": { "type": "integer", "minimum": 1 },
              "unchecked": {
                "type": "array",
                "items": { "type": "string" },
                "description": "Items the rule could not judge (a part without component data, a rail of unknown voltage)."
              }
            }
          }
        },
        "tools": {
          "type": "object",
          "description": "External tools of a KiCad review (erc, netlist, drc: kicad-cli; spice: ngspice): whether each ran, its version, or why not.",
          "additionalProperties": {
            "type": "object",
            "required": ["ran"],
            "properties": {
              "ran": { "type": "boolean" },
              "tool": { "type": "string" },
              "version": { "type": "string" },
              "reason": { "type": "string" }
            }
          }
        },
        "simulations": {
          "type": "array",
          "description": "Each declared SPICE testbench with its outcome.",
          "items": {
            "type": "object",
            "required": ["name", "status"],
            "properties": {
              "name": { "type": "string" },
              "status": { "enum": ["pass", "warn", "fail", "not_run"] },
              "netlist": { "type": "string" },
              "measure": { "type": "string" },
              "value": { "type": "number" },
              "pass": { "type": "array", "items": { "type": "number" } },
              "warn": { "type": ["array", "null"], "items": { "type": "number" } },
              "reason": { "type": "string" }
            }
          }
        },
        "statistics": {
          "type": "object",
          "description": "What the cross-domain pin comparison actually covered. Counted from the same signal sets the rule compares.",
          "properties": {
            "schematic_signals": { "type": "integer", "minimum": 0 },
            "ioc_signals": { "type": "integer", "minimum": 0 },
            "compared_signals": { "type": "integer", "minimum": 0 },
            "ambiguous_signals": {
              "type": "array",
              "items": { "type": "string" },
              "description": "Signals on more than one pin of one side; never compared first-wins."
            },
            "uncompared_signals": {
              "type": "array",
              "description": "Each configured CubeMX placement that was not compared, with the reason. Coverage detail, not a finding.",
              "items": {
                "type": "object",
                "required": ["signal", "ioc_pin", "ioc_key", "reason"],
                "properties": {
                  "signal": { "type": "string" },
                  "ioc_pin": { "type": "string" },
                  "ioc_key": { "type": "string" },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "part_conflict",
                      "ambiguous_ioc",
                      "ambiguous_schematic",
                      "no_schematic",
                      "pin_not_in_schematic",
                      "different_signal_at_pin",
                      "unnamed_connection",
                      "unconnected_pin"
                    ]
                  },
                  "schematic_artifact": { "type": "string" },
                  "schematic_net": { "type": "string" }
                }
              }
            }
          }
        }
      }
    },
    "truncated": {
      "type": "object",
      "description": "Present only when the report was capped.",
      "properties": {
        "shown": { "type": "integer" },
        "total": { "type": "integer" },
        "note": { "type": "string" }
      }
    },
    "findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } }
  },
  "$defs": {
    "finding": {
      "type": "object",
      "required": [
        "id",
        "rule_id",
        "title",
        "severity",
        "domain",
        "location",
        "evidence",
        "explanation",
        "consequence",
        "recommendation",
        "detector",
        "confidence"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Stable across runs so a baseline can suppress this exact finding. Derived from rule + location, never from the message text — rewording a message must not resurrect a waived finding. cross_domain.firmware.hardcoded_pin_drift is the exception: its id is derived from rule + .ioc + pin + .ioc key, because its location is only the first of the firmware references."
        },
        "rule_id": { "type": "string", "pattern": "^[a-z0-9_]+(\\.[a-z0-9_]+)+$" },
        "title": { "type": "string", "minLength": 1 },
        "severity": { "enum": ["error", "warning", "info"] },
        "domain": { "enum": ["schematic", "ioc", "firmware", "cross_domain", "board"] },
        "location": { "$ref": "#/$defs/location" },
        "evidence": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/evidence" },
          "description": "At least one. A finding without evidence is an assertion, and engineers do not act on assertions."
        },
        "explanation": { "type": "string", "minLength": 1 },
        "consequence": { "type": "string", "minLength": 1 },
        "recommendation": { "type": "string", "minLength": 1 },
        "detector": {
          "enum": ["deterministic", "heuristic", "ai_inferred", "external", "manual"],
          "description": "How this was produced. `deterministic` means the files prove it. Never render a `heuristic` or `ai_inferred` finding as though it were a fact."
        },
        "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
        "rule_version": { "type": "string" }
      }
    },
    "location": {
      "type": "object",
      "required": ["artifact"],
      "description": "Where in the design. Something beyond `artifact` should always be set — a finding an engineer cannot navigate to is one they will not act on.",
      "properties": {
        "artifact": { "type": "string" },
        "sheet": { "type": "string" },
        "component": { "type": "string" },
        "pin": { "type": "string" },
        "line": { "type": "integer", "minimum": 1 },
        "key": { "type": "string" }
      }
    },
    "evidence": {
      "type": "object",
      "required": ["kind", "value"],
      "properties": {
        "kind": {
          "type": "string",
          "description": "e.g. schematic_net, ioc_assignment, firmware_reference, geometry, mcu_database"
        },
        "value": {
          "type": "string",
          "description": "What was actually read out of the file — quoted, not paraphrased, so it can be grepped for."
        },
        "source": { "type": "string" }
      }
    }
  }
}
