{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/reviewer-output.schema.json",
  "version": "1.5.0",
  "title": "Multi-Agent Pipeline  -  Phase 3 reviewer output",
  "description": "Contract for a single code-reviewer subagent's JSON output in Phase 4 Step 2. Every host dispatches 3 parallel reviewers; the middle slot is CLI-aware: Claude Code (Fable, Opus, Sonnet); Copilot CLI (Opus, GPT-5.4, Sonnet); Codex CLI dispatches 3 (gpt-5.6 at xhigh, gpt-5.4, gpt-5.6 at medium). Every reviewer must return an object matching this shape before Opus triage merges them. v1.1.0 adds the rule-ID conformance checklist: when the orchestrator supplies a ${CRITERIA} block (Phase 3 Step 1.78), the reviewer must return one conformance row per selected rule ID. Findings alone cannot answer 'was this applied completely' - a reviewer that opened nothing returns the same empty findings array as one that checked everything. v1.2.0 adds the optional per-finding fingerprint (Phase 3 Step 2.1): the stable id a finding keeps across review rounds. v1.4.0 adds the optional per-finding `security` envelope (OWASP + CWE + CVSS + evidence + remediation): the security-auditor emits reviewer-output objects whose findings carry it, so a blocking security finding merges at Step 3.0 and blocks Phase 4 like any reviewer blocker. General reviewers omit it. Its shape mirrors security-finding.schema.json, kept in step by smoke-security-schema-parity.sh. v1.5.0 adds the optional per-finding `quote`, `preExisting` and `baselineEvidence` (see triage-output.schema.json).",
  "type": "object",
  "additionalProperties": false,
  "required": ["findings", "approved"],
  "properties": {
    "findings": {
      "type": "array",
      "description": "Raw findings. May be empty. Duplicates and out-of-scope items are acceptable  -  Opus triage will deduplicate and filter.",
      "items": {
        "$ref": "#/$defs/finding"
      }
    },
    "approved": {
      "type": "boolean",
      "description": "Reviewer's own verdict. MUST be false if any finding has severity=blocking, true otherwise. Triage may override."
    },
    "reviewer": {
      "type": "string",
      "description": "Model label for this output (e.g. 'fable', 'opus', 'sonnet', 'gpt'). Present once the parallel reviewer outputs are merged into the Phase 4 array so triage/consensus can attribute each finding to its source. Optional on a single reviewer's raw pre-merge output."
    },
    "conformance": {
      "type": "array",
      "description": "One row per rule ID in the supplied ${CRITERIA} block, and none outside it. Required whenever a ${CRITERIA} block was supplied; omitted entirely when it was not. This is the denominator that makes completeness checkable: the set is fixed on disk before the reviewer runs, so it cannot be narrowed after seeing the diff.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["ruleId", "verdict"],
        "properties": {
          "ruleId": {
            "type": "string",
            "minLength": 1,
            "description": "Must appear in the supplied criteria list."
          },
          "verdict": {
            "type": "string",
            "enum": ["conformant", "violated", "not-applicable"],
            "description": "conformant = checked and honoured, requires file+line evidence. violated = checked and breached, requires a matching findings[] entry with the same ruleId. not-applicable = cannot bind here, requires a reason. There is deliberately no 'unchecked' value: an ID neither checked nor waived fails the stage rather than passing quietly."
          },
          "file": {
            "type": "string",
            "description": "Where the rule was checked. Required for conformant: a verdict with no evidence certifies completeness nobody established."
          },
          "line": {
            "type": "integer",
            "minimum": 0
          },
          "reason": {
            "type": "string",
            "description": "Why the rule does not apply. Required for not-applicable."
          }
        }
      }
    },
    "fileCoverage": {
      "type": "array",
      "description": "One row per file in the supplied review denominator, and none outside it. Required whenever a denominator was supplied. The set is computed by review-file-filter.mjs and written to disk BEFORE the reviewer runs, for the same reason selectedRules[] is: after seeing the diff, 'I did not look at that one' and 'there was nothing there' become the same sentence. An empty findings[] from a reviewer that skipped nine of ten files is not a clean review.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["path", "verdict"],
        "properties": {
          "path": {
            "type": "string",
            "minLength": 1,
            "description": "Must appear in the supplied denominator, byte for byte."
          },
          "verdict": {
            "type": "string",
            "enum": ["reviewed", "skipped"],
            "description": "reviewed = read in full. skipped = not read, and the reason says why. There is deliberately no 'partial': a file read in part is read, and what was not understood belongs in a finding."
          },
          "reason": {
            "type": "string",
            "minLength": 4,
            "description": "Required when verdict is 'skipped'. Names why the file was not read (too large for the budget, truncated by the diff cap, binary content). 'Not relevant' is a review decision, not a skip reason."
          }
        }
      }
    }
  },
  "$defs": {
    "finding": {
      "type": "object",
      "additionalProperties": false,
      "required": ["severity", "file", "line", "issue", "fix"],
      "properties": {
        "severity": {
          "type": "string",
          "enum": ["blocking", "important", "suggestion"],
          "description": "blocking=must fix (bugs/security/data-loss/arch violations), important=should fix (quality/perf), suggestion=nice-to-have."
        },
        "file": {
          "type": "string",
          "minLength": 1,
          "description": "Path relative to repo root. Reviewers must not invent non-existent files."
        },
        "line": {
          "type": "integer",
          "minimum": 0,
          "description": "Line number. 0 = whole-file finding."
        },
        "issue": {
          "type": "string",
          "minLength": 4,
          "description": "What is wrong. One short sentence."
        },
        "fix": {
          "type": "string",
          "minLength": 4,
          "description": "Concrete remediation. Not 'refactor this'  -  actionable guidance."
        },
        "ruleId": {
          "type": "string",
          "minLength": 1,
          "description": "Stable ID of the cited rule, e.g. SEC-01. Present when the finding comes from a rule in the ${CRITERIA} block, omitted otherwise. A cited ID carries evidence a reviewer opinion does not: the author can look it up and disagree with the rule rather than with the reviewer. Must be an ID from the supplied block - inventing one is a validator failure."
        },
        "criteriaSource": {
          "type": "string",
          "minLength": 1,
          "description": "Which criteria source the rule came from: a registry name, a module-guide path, or 'exception-marker-audit'. Lets Phase 5 attribute findings to the standard that produced them."
        },
        "fingerprint": {
          "type": "string",
          "pattern": "^F:[0-9a-f]{8}$",
          "description": "Stable cross-round identity of the finding, computed by finding-fingerprint.mjs from (file, ruleId or normalized issue text). Never includes the line. On iteration >= 2 a reviewer that recognises an entry from <previous-round-findings> echoes its fingerprint; otherwise leave it unset and the script fills it in."
        },
        "security": {
          "type": "object",
          "additionalProperties": false,
          "required": ["owaspCategory", "cwe", "cvss", "evidence", "confidence", "remediation"],
          "description": "OPTIONAL security envelope. Present when the finding comes from the security-auditor (Phase 3 Step 3.0 merge) or /multi-agent:security-review; absent on a general code-reviewer finding. Its shape is the security block of security-finding.schema.json, kept in step by smoke-security-schema-parity.sh (self-contained, no cross-file $ref). validate-reviewer.mjs ignores it; triage carries it through unchanged so Phase 5 can report CVSS + CWE + remediation.",
          "properties": {
            "owaspCategory": {
              "type": "string",
              "pattern": "^(A(0[1-9]|10):2021|M[1-9]:2024|M10:2024)( .+)?$",
              "description": "OWASP Top 10 2021 id (web/API) or OWASP Mobile Top 10 2024 id, optionally followed by a human title."
            },
            "cwe": {
              "type": "string",
              "pattern": "^CWE-[0-9]{1,5}$",
              "description": "The specific CWE weakness id. One per finding."
            },
            "cve": {
              "type": "string",
              "pattern": "^CVE-[0-9]{4}-[0-9]{4,}$",
              "description": "A published CVE, for a known-vulnerable dependency finding."
            },
            "cvss": {
              "type": "object",
              "additionalProperties": false,
              "required": ["vector", "baseScore", "band"],
              "description": "CVSS 3.1 base metrics; baseScore and band are computed from the vector by security_cvss_score.",
              "properties": {
                "vector": {
                  "type": "string",
                  "pattern": "^CVSS:3[.]1/AV:[NALP]/AC:[LH]/PR:[NLH]/UI:[NR]/S:[UC]/C:[NLH]/I:[NLH]/A:[NLH]$",
                  "description": "Full CVSS 3.1 base vector."
                },
                "baseScore": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 10,
                  "description": "0.0 .. 10.0 base score."
                },
                "band": {
                  "type": "string",
                  "enum": ["none", "low", "medium", "high", "critical"],
                  "description": "Qualitative band; maps to the finding severity (critical/high -> blocking, medium -> important, low/none -> suggestion)."
                }
              }
            },
            "evidence": {
              "type": "string",
              "minLength": 8,
              "description": "What in the code proves the finding, cited by file:line."
            },
            "counterevidence": {
              "type": "string",
              "description": "What would disprove it, or the condition under which it is a false positive."
            },
            "confidence": {
              "type": "string",
              "enum": ["high", "medium", "low"],
              "description": "How sure the auditor is the finding is real."
            },
            "confidenceRationale": {
              "type": "string",
              "description": "Why the confidence is what it is."
            },
            "severityChangeConditions": {
              "type": "string",
              "description": "The fact that, if learned, would move the severity."
            },
            "remediation": {
              "type": "string",
              "minLength": 8,
              "description": "Remediation steps in prose."
            },
            "remediationDiff": {
              "type": "object",
              "additionalProperties": false,
              "required": ["before", "after"],
              "description": "The fix as a before/after pair.",
              "properties": {
                "before": {
                  "type": "string"
                },
                "after": {
                  "type": "string"
                }
              }
            },
            "endpoint": {
              "type": "string",
              "description": "The HTTP route or RPC method for a service-layer finding."
            },
            "method": {
              "type": "string",
              "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
              "description": "HTTP method for an endpoint finding."
            },
            "fixVerification": {
              "type": "string",
              "description": "How to confirm the fix worked - the test to add or check to run."
            }
          }
        },
        "quote": {
          "type": "string",
          "minLength": 1,
          "description": "Verbatim text of the cited line (or of the lines starting there). verify-citations.mjs requires it to match after whitespace is collapsed; a line number alone only says the line exists."
        },
        "preExisting": {
          "type": "boolean",
          "description": "The finding is claimed to exist on the base commit already. In an unattended run the claim needs baselineEvidence."
        },
        "baselineEvidence": {
          "type": "object",
          "additionalProperties": false,
          "required": ["sha"],
          "description": "What backs a pre-existing claim, checked in unattended runs by verify-citations.mjs (_pre-existing.mjs). sha must be the run's base commit or an ancestor of it; for a code finding the cited line must be present in that file there; for a failing test, testId must be in state.baseline.tests.failing or logPath must be a non-empty log of the check run at the base.",
          "properties": {
            "sha": {
              "type": "string",
              "minLength": 7,
              "description": "The base commit the claim is about."
            },
            "testId": {
              "type": "string",
              "minLength": 1,
              "description": "The failing test the claim names, as it appears in state.baseline.tests.failing."
            },
            "logPath": {
              "type": "string",
              "minLength": 1,
              "description": "A log of the check run at sha."
            }
          }
        }
      }
    }
  }
}
