{
  "$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.2.0",
  "title": "Multi-Agent Pipeline  -  Phase 4 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 4 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 4 Step 2.1): the stable id a finding keeps across review rounds.",
  "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."
          }
        }
      }
    }
  },
  "$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 7 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."
        }
      }
    }
  }
}
