{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://river-review.the3396.com/schemas/review-artifact.schema.json",
  "title": "River Review Review Artifact",
  "description": "Structured output for a complete River Review run, covering plan, context, findings, and debug info.",
  "type": "object",
  "required": ["version", "timestamp", "phase", "status"],
  "additionalProperties": false,
  "properties": {
    "version": {
      "type": "string",
      "const": "1",
      "description": "Schema version. Always \"1\" for this revision. New schemas should be created as separate files (e.g. review-artifact.v2.schema.json) per the versioning policy in pages/reference/stable-interfaces.md."
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp of when the review run completed."
    },
    "phase": {
      "type": "string",
      "enum": ["upstream", "midstream", "downstream"],
      "description": "SDLC phase the review was executed against."
    },
    "status": {
      "type": "string",
      "enum": ["ok", "no-changes", "skipped-by-label", "error"],
      "description": "Terminal status of the review run."
    },
    "plan": {
      "type": "object",
      "description": "Execution plan computed before the review.",
      "additionalProperties": false,
      "properties": {
        "selectedSkills": {
          "type": "array",
          "description": "Skills that were selected for execution.",
          "items": {
            "type": "object",
            "required": ["id", "name"],
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string",
                "description": "Unique skill identifier."
              },
              "name": {
                "type": "string",
                "description": "Human-readable skill name."
              },
              "phase": {
                "type": "string",
                "enum": ["upstream", "midstream", "downstream"],
                "description": "Phase the skill targets."
              },
              "modelHint": {
                "type": "string",
                "enum": ["cheap", "balanced", "high-accuracy"],
                "description": "Preferred model tier hint. Mirrors the ModelHintEnum used in src/lib/skillYamlSchema.mjs."
              }
            }
          }
        },
        "skippedSkills": {
          "type": "array",
          "description": "Skills that were excluded from the run and the reasons why.",
          "items": {
            "type": "object",
            "required": ["id", "reasons"],
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string",
                "description": "Unique skill identifier."
              },
              "reasons": {
                "type": "array",
                "items": { "type": "string" },
                "description": "List of reasons the skill was skipped."
              }
            }
          }
        },
        "plannerMode": {
          "type": "string",
          "enum": ["off", "order", "prune"],
          "description": "Mode the AI planner was invoked with."
        },
        "plannerReasons": {
          "type": "array",
          "description": "Per-skill reasoning produced by the AI planner.",
          "items": {
            "type": "object",
            "required": ["id", "reason"],
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string",
                "description": "Skill identifier."
              },
              "reason": {
                "type": "string",
                "description": "Explanation from the planner."
              }
            }
          }
        },
        "impactTags": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Tags describing the impact area of the changes (e.g. security, performance)."
        },
        "executionOrder": {
          "type": "array",
          "items": { "type": "string", "enum": ["deterministic", "heuristic", "llm"] },
          "description": "Declared multi-layer execution order derived from the selected skills' evaluationType (Epic #1347 S2, merged from #1339). Declaration only — reordering enforcement (strict_block routing) is S4. Additive/optional."
        },
        "contextLift": {
          "type": "object",
          "additionalProperties": false,
          "required": ["totalSkillTokens", "loadedSkillTokens", "liftRatio"],
          "description": "Progressive-disclosure savings metric (Epic #1347 S3): candidate skill-body tokens vs actually selected. Declaration/metric only. Additive/optional.",
          "properties": {
            "totalSkillTokens": { "type": "number", "minimum": 0 },
            "loadedSkillTokens": { "type": "number", "minimum": 0 },
            "liftRatio": { "type": "number", "minimum": 0, "maximum": 1 }
          }
        },
        "estimatedCost": {
          "type": "object",
          "additionalProperties": false,
          "required": ["tokens", "source"],
          "description": "Rough pre-execution cost estimate. Advisory only: deliberately NOT a gate-decision input (a cost-based downgrade would need S3 measured metrics first). Additive/optional.",
          "properties": {
            "tokens": { "type": "number", "minimum": 0 },
            "source": { "type": "string", "enum": ["token-estimator"] }
          }
        }
      }
    },
    "decision": {
      "type": "string",
      "enum": ["auto-approve", "human-review-recommended", "human-review-required"],
      "description": "Top-level review verdict derived from findings (src/lib/scoring/engine.mjs deriveVerdict). Maps to the proposal's pass / warn / fail: auto-approve≈pass, human-review-recommended≈warn, human-review-required≈fail. Recommendation only; does NOT bypass HITL policy. Optional; absent on older artifacts."
    },
    "suggestedLoopSignal": {
      "type": "string",
      "enum": ["NO_SIGNAL", "REVISE_REQUIRED", "CONVERGED", "ESCALATE_HUMAN"],
      "description": "Layer-1 loop signal for agentic fix loops (Epic #1171 item3). Derived deterministically from decision + blocking finding counts. NO_SIGNAL: cannot determine loop action; REVISE_REQUIRED: blocking findings (critical/major) present — agent should revise and re-run; CONVERGED: no blocking findings and decision is auto-approve — agent may stop; ESCALATE_HUMAN: decision is human-review-required — agent must hand off. Additive/optional; absent on older artifacts. Does NOT affect decision/verdict and is NOT a GO/NO-GO gate. Layer 3 values (STOP_MAX_ITERATIONS, STOP_POLICY_REQUIRED) are caller-synthesized and are never emitted by River Review."
    },
    "gate": {
      "type": "object",
      "additionalProperties": false,
      "required": ["decision", "reasonCode", "tier", "inputs", "inputsHash", "schemaVersion"],
      "description": "Machine-readable gate signal for loop-running hosts (Epic #1347 S2 / #1349). River Review DERIVES the decision; EXECUTION (continuing, stopping, notifying, expiring an observation window) is the host's responsibility — this block is advisory until a host enforces it. Trust boundary: the block is trustworthy only when derived outside the reviewed agent's write authority (host/CI checkout); hosts verify integrity by replaying `inputs` through src/lib/gate-decision.mjs deriveGateDecision and comparing (inputsHash is a lightweight regression summary, NOT a tamper-proof control). Additive/optional; absent on older artifacts.",
      "properties": {
        "decision": {
          "type": "string",
          "enum": ["GO", "GO_WITH_OBSERVATION", "NO_GO", "ESCALATE"],
          "description": "GO: autonomous continuation permitted (field tier). GO_WITH_OBSERVATION: proceed with a time-boxed async-review window (hill tier). NO_GO: revise required or verdict undetermined (fail-safe: unknown inputs always land here, never on GO). ESCALATE: mandatory human approval before proceeding (cliff tier)."
        },
        "reasonCode": {
          "type": "string",
          "enum": [
            "GATE_CONFIG_CHANGED",
            "HUMAN_APPROVAL_REQUIRED",
            "DECISION_ESCALATED",
            "OSCILLATION_DETECTED",
            "RISK_MAP_HUMAN_REVIEW",
            "UNKNOWN_RISK_ACTION",
            "SKIPPED_BY_POLICY",
            "NOT_EXECUTED",
            "BLOCKING_FINDINGS",
            "MINOR_FINDINGS_OBSERVE",
            "UNDETERMINED",
            "RISK_MAP_OBSERVE",
            "CONVERGED_CLEAN",
            "UNKNOWN_SIGNAL"
          ],
          "description": "First-match rule that produced the decision (rule order documented in src/lib/gate-decision.mjs). GATE_CONFIG_CHANGED is the bootstrap cliff: any diff touching .river/** escalates so the gate config cannot be used to unguard itself (including deleting the risk map)."
        },
        "tier": {
          "type": "string",
          "enum": ["cliff", "hill", "field"],
          "description": "Risk tier (崖/丘/原っぱ). NO_GO reports field: it is a revise instruction, not a supervision tier."
        },
        "inputs": {
          "type": "object",
          "description": "Echo of every decision input, sufficient for a host-side replay check. Includes riskMapPresent/riskMapDigest so hosts can compare the loaded risk map against a trusted baseline (the risk map lives inside the reviewed repo and is writable by the agent under review)."
        },
        "inputsHash": {
          "type": "string",
          "pattern": "^[0-9a-f]{16}$",
          "description": "sha256 (first 16 hex) of the canonicalized inputs (fixed field list, sorted keys, undefined→null). For S3 same-inputs regression comparison only; not a security control."
        },
        "configSnapshot": {
          "type": "object",
          "description": "Gate-related config values in effect (observation expiry, circuit-breaker advisory limits). When the host has its own limits, the stricter value wins — repo config is writable by the reviewed agent, so hosts should treat their own settings as the source of truth."
        },
        "observation": {
          "type": "object",
          "additionalProperties": false,
          "required": ["expiresInHours", "onExpiry"],
          "description": "Present only for GO_WITH_OBSERVATION. On expiry the host stops the loop AND treats changes originating from `files` as unreviewed (re-review required).",
          "properties": {
            "expiresInHours": { "type": "number", "exclusiveMinimum": 0 },
            "onExpiry": {
              "type": "string",
              "enum": ["stop"],
              "description": "S2 permits only 'stop'. 'promote' is deliberately NOT in the enum until the S4 enforcement implementation lands, so hosts cannot be invited to auto-promote past the default-stop contract."
            },
            "files": {
              "type": "array",
              "items": { "type": "string" },
              "description": "Changed files under observation (capped at 100 entries)."
            }
          }
        },
        "schemaVersion": { "type": "string", "enum": ["1"] }
      }
    },
    "usage": {
      "type": "object",
      "additionalProperties": false,
      "description": "Provider/model usage for the run. Populated when an LLM was used; token / cost fields are present only when the surface reports them. Optional; absent for heuristic / dry-run / plan-only artifacts.",
      "properties": {
        "provider": {
          "type": "string",
          "description": "LLM provider (e.g. openai, anthropic, google)."
        },
        "model": {
          "type": "string",
          "description": "Model identifier used for the run."
        },
        "input_tokens": {
          "type": "number",
          "minimum": 0,
          "description": "Input (prompt) tokens consumed, when reported by the provider."
        },
        "output_tokens": {
          "type": "number",
          "minimum": 0,
          "description": "Output (completion) tokens produced, when reported by the provider."
        },
        "estimated_cost_usd": {
          "type": "number",
          "minimum": 0,
          "description": "Estimated cost in USD, when computable."
        }
      }
    },
    "trace": {
      "type": "object",
      "additionalProperties": false,
      "description": "Run trace metadata for debugging, cost analysis, and agent handoff. Optional; absent on older artifacts.",
      "properties": {
        "run_id": {
          "type": "string",
          "description": "Unique identifier for this review run, generated when the artifact is finalized. Independent of the result-store id assigned by --save (.river/runs/); do not assume the two are equal."
        }
      }
    },
    "findings": {
      "type": "array",
      "description": "Review findings produced during the run. Each item conforms to the issue schema in output.schema.json.",
      "items": { "$ref": "#/$defs/finding" }
    },
    "context": {
      "type": "object",
      "description": "Repository and diff context captured at review time.",
      "additionalProperties": false,
      "properties": {
        "repoRoot": {
          "type": "string",
          "description": "Absolute path to the repository root."
        },
        "defaultBranch": {
          "type": "string",
          "description": "Default branch name (e.g. main)."
        },
        "mergeBase": {
          "type": "string",
          "description": "Git merge-base commit SHA used for diffing."
        },
        "changedFiles": {
          "type": "array",
          "items": { "type": "string" },
          "description": "List of file paths included in the review diff."
        },
        "tokenEstimate": {
          "type": "number",
          "minimum": 0,
          "description": "Estimated token count of the optimized diff text."
        },
        "rawTokenEstimate": {
          "type": "number",
          "minimum": 0,
          "description": "Estimated token count of the raw (unoptimized) diff text."
        },
        "reduction": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Percentage of tokens saved by diff optimization (0-100)."
        }
      }
    },
    "debug": {
      "type": "object",
      "additionalProperties": true,
      "description": "Free-form debug information. Structure is not guaranteed across versions.",
      "properties": {
        "execution": {
          "type": "object",
          "additionalProperties": true,
          "description": "Execution-stage diagnostics. Additive over time.",
          "properties": {
            "snapshot": {
              "type": "object",
              "additionalProperties": true,
              "description": "Carry-over context written by buildExecutionPlan and consumed by --plan replay execution (#878 A2-3). Used to avoid context-snapshot drift between plan and replay time. Optional; absent on older artifacts.",
              "properties": {
                "fileTypes": {
                  "type": "array",
                  "items": { "type": "string" },
                  "description": "File-type tags derived at plan creation time."
                },
                "relatedADRs": {
                  "type": "array",
                  "items": { "type": "string" },
                  "description": "Architecture Decision Records related to the change at plan creation time."
                },
                "reviewMode": {
                  "type": "string",
                  "description": "Review mode resolved at plan creation time."
                },
                "riskAssessment": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Risk-map evaluation result captured at plan creation time."
                }
              }
            }
          }
        }
      }
    }
  },
  "$defs": {
    "finding": {
      "type": "object",
      "description": "A single review finding. Mirrors the issue schema in output.schema.json so that findings can be cross-validated by both schemas.",
      "required": ["id", "ruleId", "title", "message", "severity", "phase", "file"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "description": "Unique identifier for the issue within this run.",
          "minLength": 1
        },
        "ruleId": {
          "type": "string",
          "description": "Identifier of the rule or skill that produced the issue.",
          "minLength": 1
        },
        "title": {
          "type": "string",
          "description": "Short human-readable title for the issue.",
          "minLength": 1
        },
        "message": {
          "type": "string",
          "description": "Detailed explanation of the issue and context.",
          "minLength": 1
        },
        "severity": {
          "type": "string",
          "description": "Severity level assigned to the issue.",
          "enum": ["info", "minor", "major", "critical"]
        },
        "phase": {
          "type": "string",
          "description": "SDLC phase where this issue was found.",
          "enum": ["upstream", "midstream", "downstream"]
        },
        "file": {
          "type": "string",
          "description": "File path or logical name relevant to the issue.",
          "minLength": 1
        },
        "line": {
          "type": "integer",
          "description": "Line number related to the issue when applicable.",
          "minimum": 1
        },
        "lineEnd": {
          "type": "integer",
          "description": "End line number for multi-line issues.",
          "minimum": 1
        },
        "confidence": {
          "type": "string",
          "description": "Confidence level of the finding.",
          "enum": ["high", "medium", "low"]
        },
        "status": {
          "type": "string",
          "description": "Current lifecycle status of the finding.",
          "enum": ["open", "suppressed", "verified"]
        },
        "evidence": {
          "type": "array",
          "description": "Evidence snippets supporting this finding.",
          "items": { "type": "string" }
        },
        "reviewer": {
          "type": "string",
          "description": "Identifier of the skill or agent that produced this finding."
        },
        "suggestion": {
          "type": "string",
          "description": "Optional fix or follow-up hint."
        },
        "sourceKind": {
          "type": "string",
          "description": "Provenance kind for synthesis layers (e.g. Independent Review Synthesis). Optional; absent for findings produced by core skills.",
          "enum": ["ai-review", "human-review", "self-review"]
        },
        "agreement": {
          "type": "array",
          "description": "Auxiliary metadata listing reviewer identifiers that independently raised this finding (after dedup). Synthesis layers MUST NOT use this for majority-vote decisions; severity is decided on evidence quality.",
          "items": { "type": "string", "minLength": 1 },
          "uniqueItems": true
        },
        "consensusLevel": {
          "type": "string",
          "enum": ["single", "multi", "consensus"],
          "description": "Display-only metadata derived from agreement.length. 'consensus'=3+ reviewers, 'multi'=2, 'single'=0-1. MUST NOT be used to override severity."
        },
        "validatedStatus": {
          "type": "string",
          "description": "Synthesis verdict for the finding. Distinct from `status` (which tracks lifecycle: open/suppressed/verified).",
          "enum": [
            "confirmed",
            "dismissed-hallucination",
            "dismissed-duplicate",
            "needs-human-judgment"
          ]
        }
      }
    }
  }
}
