{
  "$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",
            "STRICT_BLOCK",
            "DETERMINISTIC_UNRUNNABLE",
            "SKIPPED_BY_POLICY",
            "NOT_EXECUTED",
            "COVERAGE_INCOMPLETE",
            "LLM_NOT_EXECUTED",
            "BLOCKING_FINDINGS",
            "CRITIC_VALIDATION_INCOMPLETE",
            "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": "File paths changed in the reviewed range. For a `--base` review this is git's own answer (`git diff --name-only`), so it also covers changes the diff text carries no hunk for — pure renames and binary changes included. It is therefore the changed-file set of the range, not the set of paths appearing in the diff text. When the diff came from an artifact instead, there is no git answer and the set is the one parsed out of the diff text."
        },
        "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)."
        }
      }
    },
    "executionManifest": {
      "type": "object",
      "additionalProperties": true,
      "description": "Execution Manifest for this run (#2015, Epic #2011 Phase 4): what River Review used to judge — river-review version, plugin host, flow, agents, skills, input artifacts, policy, runtime and config, each pinned by version and hash and stamped with a resolution status. Additive and optional; artifacts that predate it, and runs whose host does not build one, stay valid with the exact key set they had before. Placed at the TOP LEVEL rather than under `debug` on purpose: `debug` declares that its structure is not guaranteed across versions, which would make a consumer unable to rely on the block that is supposed to tell it whether a run is replayable. The AUTHORITATIVE structure is schemas/execution-manifest.schema.json — it is kept as a separate document (like schemas/paired-replay.schema.json) and validated separately, so this property stays permissive here; no schema in this repository resolves a cross-file $ref at runtime. Read it with src/lib/execution-manifest.mjs: verifyExecutionManifest for integrity and assessReplayability before treating any run as replayable.",
      "properties": {
        "kind": { "type": "string", "const": "execution-manifest" },
        "manifestId": { "type": "string", "pattern": "^RR-EXM-[0-9a-f]{12}$" },
        "manifestKey": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "manifestHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
      },
      "required": ["kind", "manifestId", "manifestKey", "manifestHash"]
    },
    "flow": {
      "type": "object",
      "additionalProperties": false,
      "description": "Review Flow pin the artifact was produced under (#2054 PR-3, Beta). Additive and optional: present only when `river review plan --entry <name>` was given, so artifacts without it keep the exact key set they had before. `entry` is the user-facing entry name from flows/entry-map.json, `id` / `version` identify the Flow document, and `sha256` is the digest of its canonical JSON as derived by deriveFlowPin in src/lib/execution-manifest.mjs — the same pin resolveTrigger (src/lib/trigger-resolver.mjs) mints. Carries no judgment: nothing in the artifact's gate / decision / findings is derived from it.",
      "required": ["entry", "id", "version", "sha256"],
      "properties": {
        "entry": { "type": "string", "minLength": 1 },
        "id": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 },
        "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
      }
    },
    "evidenceRequirements": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "description": "Input names the pinned Flow declares as `required: true` (#2054 PR-3, Beta). Additive and optional; emitted together with `flow`. A declaration of what the Flow needs, not a record of what was supplied."
    },
    "steps": {
      "type": "array",
      "description": "Per-step records of running the pinned Flow through src/lib/flow-runner.mjs (Epic #2011 AC7 P2, Beta): the runner's StepOutcome shape, verbatim. Additive and optional: present only on `river review exec --entry <name>`, in the Flow document's step order (`index` is the position in the document's `steps`). One record per Flow step even when a required input was not provable from the artifact (observe mode then records every step as `stopped`). Record only: no capability is wired in this slice, so outcomes are `not-implemented` / `skipped` / `stopped`, and nothing in `gate` / `decision` / `findings` is derived from this array (RA-1). `outcome` and `kind` are the runner's closed sets (STEP_OUTCOMES in flow-runner.mjs).",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["index", "id", "kind", "parallel", "parallelRun", "outcome"],
        "properties": {
          "index": { "type": "integer", "minimum": 0 },
          "id": { "type": "string", "minLength": 1 },
          "kind": { "type": "string", "enum": ["primitive", "reviewer"] },
          "parallel": { "type": "boolean" },
          "parallelRun": { "type": ["integer", "null"], "minimum": 0 },
          "outcome": {
            "type": "string",
            "enum": ["executed", "skipped", "degraded", "stopped", "not-implemented"]
          },
          "reason": { "type": "string", "minLength": 1 },
          "result": {
            "description": "Whatever the capability returned for an `executed` step. Never present in P2 (no capability is wired)."
          }
        }
      }
    },
    "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."
        },
        "scope": {
          "type": "string",
          "enum": ["in-diff", "pre-existing"],
          "description": "Whether the finding is introduced by this diff's added lines ('in-diff') or lives in a changed file but outside the added lines ('pre-existing'). Additive/optional metadata (#1644 Phase 1): derived deterministically by the verifier from the parsed diff, falling back to the reviewer's self-report. Absent or unknown MUST be read as 'in-diff' (fail-safe: never demote). Display-only — MUST NOT override severity or gate decisions."
        },
        "criterionRefs": {
          "type": "array",
          "description": "Optional acceptance-criterion / test-case identifiers this finding is tied to (e.g. AC-4, TC-7). Additive/optional traceability metadata (#1666, #1545 Phase 2) closing the Test/AC -> Finding back-reference. River Review does not own this ID namespace: the values are copied verbatim from the upstream artifact and are neither minted nor validated here. Absent when the artifact is missing. Display-only — MUST NOT override severity or gate decisions.",
          "items": { "type": "string", "minLength": 1 }
        },
        "artifactRefs": {
          "type": "array",
          "description": "Optional artifact anchors this finding is tied to (e.g. plan.md#AC-4, todo.md#TASK-3). Additive/optional traceability metadata (#1666, #1545 Phase 2). River Review does not own this ID namespace: the values are copied verbatim from the upstream artifact and are neither minted nor validated here. Absent when the artifact is missing. Display-only — MUST NOT override severity or gate decisions.",
          "items": { "type": "string", "minLength": 1 }
        },
        "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."
        },
        "sourceExecutionIds": {
          "type": "array",
          "description": "Opaque logical reviewer execution ids that produced members of this merged finding cluster (#2481). Additive/optional provenance only. Count is not a vote, and different ids do not prove different actors, models, providers, correctness, or independent verification.",
          "items": { "type": "string", "minLength": 1 },
          "uniqueItems": true
        },
        "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"
          ]
        },
        "validation": {
          "type": "object",
          "description": "Optional Finding Critic verdict (#1978 / #2334). Present only when the Evidence-Grounded Adversarial Review stage ran (opt-in: `review.findingCritic.mode: active` or env RIVER_FINDING_CRITIC=1); absent on every default run. Additive/optional — a consumer that ignores it reads the finding exactly as before. Distinct from `validatedStatus`: the two vocabularies only partially overlap and nothing here is written to `validatedStatus` (src/lib/finding-critic.mjs:80-99). Display and triage metadata. MUST NOT override finding severity or disposition. The host may opt in to a separate active-Critic incompleteness escalation via RIVER_GATE_CRITIC_VALIDATION=1 (#2343); default Gate behavior remains unchanged.",
          "additionalProperties": false,
          "required": ["protocol", "finalStatus"],
          "properties": {
            "protocol": {
              "type": "string",
              "description": "Protocol identifier. SSoT: PROTOCOL_ID (src/lib/finding-critic.mjs).",
              "const": "evidence-grounded-adversarial-v1"
            },
            "rounds": {
              "type": "integer",
              "description": "Number of Reviewer<->Critic rounds actually run. 0 when the Critic never answered.",
              "minimum": 0
            },
            "finalStatus": {
              "type": "string",
              "description": "Terminal state of the finding after validation. SSoT: FINAL_STATUS (src/lib/finding-critic.mjs); this enum is pinned to Object.values(FINAL_STATUS) by tests/finding-critic-stage.test.mjs and MUST NOT be extended independently.",
              "enum": [
                "confirmed",
                "withdrawn-by-reviewer",
                "dismissed-by-evidence",
                "dismissed-hallucination",
                "needs-human-judgment",
                "out-of-ask",
                "critic-timeout"
              ]
            },
            "askRelevance": {
              "type": "string",
              "description": "Relevance of the finding to the original ask. SSoT: ASK_RELEVANCE (src/lib/finding-critic.mjs). A separate axis from `scope`; the two are orthogonal.",
              "enum": ["in-ask", "uncertain", "out-of-ask"]
            },
            "humanReview": {
              "type": "boolean",
              "description": "True when the outcome is degraded or contested and a human must decide. Every fail-safe path (Critic timeout, unparseable response, deterministic contradiction, exhausted inner loop) sets this; a silent Critic never reads as an approval."
            },
            "reasons": {
              "type": "array",
              "description": "Reason codes and detail strings. Codes come from FAILSAFE_REASON (src/lib/finding-critic.mjs); free-text detail may follow them.",
              "items": { "type": "string", "minLength": 1 }
            }
          }
        }
      }
    }
  }
}
