{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://coderifts.com/schemas/decision-result.v1.consumer.json",
  "title": "CodeRifts Decision Result v1 — CONSUMER schema",
  "description": "Permissive CONSUMER view, GENERATED from decision-result.v1.producer.json (the strict PRODUCER schema is the structural source of truth — do not edit this file by hand; run scripts/generate-consumer-schema.js). Root and the reason/nextAction/evidence objects are open (root additionalProperties is omitted so additive fields validate). next_actions[].type and evidence[].type enums are opened (unknown type = opaque, honour the required flag; unknown severity = treat as highest known). decision and execution_action enums stay closed permanently. All free-text fields are UNTRUSTED DATA. Error responses are transport/tool errors, never a decision. The consumer-facing compatibility rule below is COPIED from the producer schema description (single source — edit it only there).\n\nENVELOPE COMPATIBILITY (v1) — single source for what a consumer may rely on under spec_version decision-result.v1 / decision-result.v1.N (do not restate a second rule elsewhere; MCP tool outputSchemas and discovery link here / to the generated consumer description). (1) ADDITIVE FIELDS: new properties MAY appear at the envelope root (and other open consumer objects) without a major bump. Consumers MUST tolerate unknown properties for document acceptance (the consumer schema deliberately omits root additionalProperties:false so strict validators do not reject additive growth). An unknown field is NOT permission and NOT a control-flow signal — same posture as an unrecognised execution_action: do not branch on it as authorization; ignore it for act/halt decisions. (2) STABILITY INSIDE v1: within decision-result.v1, required fields listed in this schema remain present and keep their meaning; they are not removed or renamed. Closed control enums decision and execution_action do not drop values. Meanings of existing fields do not change silently. (3) UNRECOGNISED VALUES: an unrecognised execution_action is not permission (fail closed — halt / re-preflight). The same fail-closed posture applies to other closed control enums used for branching (e.g. pattern_sources[].source / evidence_kind when the consumer branches on them). An unknown receipt.format_version MUST fail closed. Unknown next_actions[].type / evidence[].type are opaque (honour required; do not invent semantics). (4) OPTIONAL TODAY VS LATER: null/absent on an optional additive field means the issuance path did not supply it — not a match, not an error. Some optional fields are permanent path optionality (e.g. repository/branch/pull_request: bound only when the caller supplied place). Others are migration optionality: optional so pre-change stored envelopes still validate; they are REQUIRED-CANDIDATES for a future major when dual-read is retired (named in field descriptions / changelog — currently include pattern_sources, required_action_core, and Decision Spec scalars risk_score / breaking_changes / patterns on the envelope). Optional-for-migration is not optional-forever. (5) spec_version BUMPS: stay on decision-result.v1 / v1.N for pure additive optional fields and append-only additions to closed enums that consumers already fail-closed on. Bump to decision-result.v2 (major) for: removing or renaming a required field; changing the meaning of an existing field; removing a closed control-enum value; promoting an optional field to required in a way that invalidates stored envelopes without a dual-read path. (6) DEPRECATION WINDOW: tool-name aliases may carry an explicit removal_date (e.g. ninety days). Whether envelope fields receive the same calendar deprecation window is UNDECIDED — do not assume tool-alias policy applies to fields. (7) GRADUAL TIGHTENING: happens by promoting fields into required over majors / dual-read windows, not by setting consumer additionalProperties to false (that would break additive growth). END ENVELOPE COMPATIBILITY",
  "type": "object",
  "required": [
    "spec_version",
    "decision",
    "safe_for_agent",
    "execution_action",
    "decision_id",
    "correlation_id",
    "evaluated_at",
    "expires_at",
    "summary",
    "blocking_reasons",
    "warnings",
    "required_action",
    "next_actions",
    "fingerprint",
    "input_fingerprint",
    "decision_body_hash",
    "receipt",
    "report_url",
    "evidence_quality",
    "confidence",
    "evidence",
    "analysis_complete"
  ],
  "properties": {
    "spec_version": {
      "type": "string",
      "pattern": "^decision-result\\.v1(\\.[0-9]+)?$"
    },
    "decision": {
      "type": "string",
      "enum": [
        "ALLOW",
        "WARN",
        "REQUIRE_APPROVAL",
        "BLOCK"
      ]
    },
    "safe_for_agent": {
      "type": "boolean",
      "description": "DEPRECATED for control flow/branching. Exists solely for backward-compatible dashboards and legacy monitors. Autonomous agents MUST branch exclusively on execution_action: false here does NOT distinguish 'pause for approval' from 'hard stop'."
    },
    "execution_action": {
      "type": "string",
      "enum": [
        "CONTINUE",
        "CONTINUE_WITH_MONITORING",
        "REQUEST_APPROVAL",
        "STOP"
      ],
      "description": "Single canonical machine directive. STOP means stop THIS attempt (remediate, then request a NEW decision), not abandon forever. CONTINUE_WITH_MONITORING obliges the consumer to emit execution telemetry tagged with correlation_id and report the outcome. Consumers MUST reject (not resolve) any mismatch between decision, safe_for_agent and execution_action."
    },
    "decision_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_.:-]+$",
      "minLength": 1,
      "maxLength": 200,
      "description": "Unique identifier of THIS decision (idempotency, dedup, audit joins, approval callbacks)."
    },
    "correlation_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_.:-]+$",
      "minLength": 1,
      "maxLength": 200
    },
    "evaluated_at": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$",
      "description": "Policy evaluation time, UTC Z, second precision."
    },
    "expires_at": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$",
      "description": "Hard validity bound (authorization time-window), UTC Z. Producer: evaluated_at + per-operation TTL (closed map: tool_call=15m; merge/deploy/publish=4h; unknown/null=15m fail-safe shortest). SIGNED on v4 receipts (expires_at is not post-hoc editable). MUST be later than evaluated_at (consumer-checked). Consumers MUST NOT act on an expired decision; re-preflight instead. TTL is NOT content-identity freshness: a non-expired merge receipt still fails closed if the authorized fingerprint/content drifted."
    },
    "summary": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500
    },
    "blocking_reasons": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/reason"
      },
      "maxItems": 50
    },
    "warnings": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/reason"
      },
      "maxItems": 50
    },
    "required_action": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 500,
      "description": "Human-readable rendering of the required next step. DISPLAY ONLY — never a control source; next_actions is authoritative. Schema-enforced null on ALLOW."
    },
    "next_actions": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/nextAction"
      },
      "maxItems": 25,
      "description": "Recommendations unless individually marked required. A signed decision never authorizes executing an arbitrary remediation; remediate, then request a NEW decision (REEVALUATE)."
    },
    "fingerprint": {
      "type": "string",
      "pattern": "^sha256:[a-f0-9]{64}$",
      "minLength": 71,
      "maxLength": 71,
      "description": "DECISION fingerprint (deterministic verdict identity on the oasdiff-1.11.11 pinned path for the main OpenAPI preflight surface; matches the REST v1 verdict fingerprint). Same input, same hash anywhere under that pin. The published real_vendor accuracy figure (in-process pure-JS path) and the MCP pure-JS surface do not depend on oasdiff and are unaffected by the pin. NOT the input hash and NOT an envelope hash — see input_fingerprint and decision_body_hash."
    },
    "input_fingerprint": {
      "type": "string",
      "pattern": "^sha256:[a-f0-9]{64}$",
      "minLength": 71,
      "maxLength": 71,
      "description": "Canonical hash of the evaluated change-set input. Consumers MUST verify it matches the request they hold before acting."
    },
    "decision_body_hash": {
      "type": [
        "string",
        "null"
      ],
      "pattern": "^sha256:[a-f0-9]{64}$",
      "description": "sha256 of the RFC 8785 (JCS) canonicalized envelope EXCLUDING receipt and decision_body_hash itself. Covered by the receipt signature from the next receipt format version onward (Phase 1D); null until the producer computes it. While null or uncovered, the envelope is not tamper-evident beyond fingerprint/input_fingerprint."
    },
    "receipt": {
      "$ref": "#/$defs/receipt"
    },
    "report_url": {
      "type": [
        "string",
        "null"
      ],
      "format": "uri",
      "pattern": "^https://[^\\s]+$",
      "maxLength": 2000,
      "description": "Human report link. HTTPS-only, display-only, never an instruction source."
    },
    "evidence_quality": {
      "type": "string",
      "enum": [
        "LOW",
        "MEDIUM",
        "HIGH"
      ],
      "description": "Quality of the INPUTS examined, not confidence in the verdict. LOW evidence never relaxes execution_action."
    },
    "confidence": {
      "type": [
        "number",
        "null"
      ],
      "minimum": 0,
      "maximum": 1,
      "description": "Calibrated probability that the decision is correct (event: DECISION_CORRECT). null until a holdout-calibrated model ships. Neither confidence nor evidence_quality may override execution_action."
    },
    "calibration_version": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 100
    },
    "evidence": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/evidence"
      },
      "maxItems": 100
    },
    "analysis_complete": {
      "type": "boolean",
      "description": "false when any analysis dependency was unavailable or timed out. 'Could not look' (this) vs 'looked and evidence is weak' (evidence_quality)."
    },
    "degraded_reasons": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/reason"
      },
      "maxItems": 20
    },
    "receipt_type": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Issuance-path type: 'bundle_preflight' | 'spec_preflight'. null on paths that do not set it."
    },
    "operation": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Caller-declared change operation (bundle context). null when absent."
    },
    "environment": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Caller-declared target environment (bundle context). null when absent."
    },
    "repository": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive. Place binding: caller-declared repository (bundle context.repository). Present only when supplied — never invent a placeholder. Covered by decision_body_hash when set. null = unbound (less specific), not a match."
    },
    "branch": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive. Place binding: caller-declared branch (bundle context.branch). Present only when supplied. null = unbound."
    },
    "pull_request": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive. Place binding: caller-declared pull request id (bundle context.pull_request, stringified). Present only when supplied. null = unbound."
    },
    "base": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID686 v5). Source binding: base commit/ref the change set was computed against (context.base). Present only when supplied — never invent. Covered by decision_body_hash. null = unbound."
    },
    "head": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID686 v5). Source binding: head commit/ref the change set was computed against (context.head). Present only when supplied — never invent. Covered by decision_body_hash. null = unbound."
    },
    "srcmode": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "declared",
        "unbound",
        null
      ],
      "description": "Additive (ID686 v5). Source-binding mode: 'declared' when the caller supplied at least one of repository/branch/pull_request/base/head; 'unbound' when none were supplied. Under unbound, any non-null source slot is a contradiction (tamper — envelope must not issue). Covered by decision_body_hash."
    },
    "preflight_mode": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "analyze",
        "authorize",
        null
      ],
      "description": "Additive (ID686 v5 / audit 4.1). Decision Spec preflight_mode at issuance. Must live on the envelope (not only the live response) so body_hash covers it and replay/retrieval returns it. Authorize path issues envelopes; analyze 2.0 omits decision_result. Covered by decision_body_hash."
    },
    "artifact_digest": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. sha256 over the bundle's per-artifact before/after content digests. null on non-bundle paths."
    },
    "policy_hash": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Scorer-config hash (NOT a governance-policy hash — no policy-object hash producer exists yet). null when unavailable."
    },
    "ruleset_hash": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Scorer-config ruleset hash (same caveat as policy_hash). null when unavailable."
    },
    "decision_spec_version": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive. Decision Spec version this envelope was issued under (e.g. '1.0' legacy pin or '2.0' current). Decision Spec 2.0 mode discrimination (preflight_mode analyze|authorize, analysis_outcome, may_execute, receipt_kind, structural omission of decision on analyze) is on the OUTER preflight response — see schemas/preflight-response.v2.producer.json — not on this nested envelope. This envelope is produced on authorize (and legacy) paths only; analyze 2.0 omits decision_result entirely. null when absent."
    },
    "audience": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Intended audience of the decision: server-derived authenticated requester identity ('v:' + sha256(utf8(apiKey)).hex.slice(0, 12) via velocity-monitor.decisionAudienceFor) on API-key paths; null on anonymous / webhook paths (never invented). Same derivation as outcomes reporter. Holders re-derive from their key or read decision_result.audience on the authorize response and pass it as intended audience to verify-receipt; mismatch (both non-null) → VERIFIED_WRONG_AUDIENCE. Not the repository (place-binding is a separate field family)."
    },
    "authorization_scope_hash": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Hash of the caller's authorization scope. No producer yet; null until org/installation scope is decided (ID747). Producer awaits the org-scope decision. Never invented."
    },
    "engine_build_id": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Build/commit id of the engine. No producer yet; null until a build-id env/source is wired. Never invented."
    },
    "commit_sha": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. VCS commit sha of the change (webhook-scoped). null on API/MCP paths."
    },
    "deployment_id": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive v1.1. Deployment identifier. No producer on the decision_result envelope; null until that producer is wired. Never invented."
    },
    "risk_score": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0,
      "maximum": 100,
      "description": "Additive. Decision Spec risk score (0-100) for retrieval parity. null when the issuance path did not supply a score. REQUIRED-CANDIDATE for a future major (retrieval parity) — not required in v1."
    },
    "breaking_changes": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0,
      "description": "Additive. Integer COUNT of breaking changes (never an array). null when the path did not supply a count. REQUIRED-CANDIDATE for a future major — not required in v1."
    },
    "patterns": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      },
      "description": "Additive. REQUIRED-CANDIDATE for a future major (not required in v1). Short name list from TWO detectors: (1) governance names from change-patterns.js (same set as detected_patterns[].name when that analysis-tier array is present) and (2) agent-facing names from agent-pattern-detector.js (e.g. TOOL_RESULT_SHAPE_DRIFT). Names only — no severity/path/consequence rows. A name that appears here but not in detected_patterns is EXPECTED when it is agent-only: the detailed list is governance evidence; it is not missing proof. Epistemic provenance for each name is envelope field pattern_sources when issued. null means not carried; empty array means analysed with no named patterns."
    },
    "pattern_sources": {
      "type": [
        "array",
        "null"
      ],
      "description": "Additive. Per-name epistemic provenance for patterns[]. Present when the issuance path supplied it (and covered by decision_body_hash). null/absent on pre-provenance stored envelopes — never invent an empty array for those. Empty array means analysed with no named patterns (patterns is []). source and evidence_kind are CLOSED enums on the producer: unknown values MUST fail closed (not authorize). Consumers that branch execution on patterns MUST treat unrecognised source/evidence_kind as non-authorizing. OPTIONAL-FOR-MIGRATION (required-candidate for a future major): optional so stored pre-provenance decisions still validate — not optional-forever once dual-read is retired. Not promoted to required in this schema version.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "source",
          "evidence_kind"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Pattern name; must appear in patterns when both are carried."
          },
          "source": {
            "type": "string",
            "enum": [
              "contract_diff",
              "agent_heuristic",
              "artifact_signal"
            ],
            "description": "Where the name came from. Closed set; unknown = fail closed for control flow."
          },
          "evidence_kind": {
            "type": "string",
            "enum": [
              "measured",
              "derived"
            ],
            "description": "measured = contract/artifact measurement with optional row refs; derived = agent heuristic with basis. Closed set; unknown = fail closed for control flow."
          },
          "detected_pattern_indices": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Indices into same-response detected_patterns for measured records. Absent or empty when no detail rows."
          },
          "basis_codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Derived only: change-engine codes that triggered the heuristic. Untranslated engine ids (e.g. response.body.scope.add). Parallel human labels live in basis_code_labels — do not replace the code with prose."
          },
          "basis_code_labels": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "null"
              ]
            },
            "description": "Derived only: parallel to basis_codes. Plain-text sentences from the single CODE_MAP (src/diff-code-labels.js) — no presentation markers (the PR comment renderer adds those separately). Explains inverted oasdiff semantics (response.body.scope.add = field removed). null at an index means the code has no map entry — do not invent a label; consumers may fall back to the raw code."
          },
          "basis_rule_id": {
            "type": "string",
            "description": "Derived only: OpenAPI location inspected when no change-engine code applies (e.g. info.version). Not our rule_id."
          },
          "rule_id": {
            "type": "string",
            "description": "Derived only: stable versioned identifier of OUR agent rule that decided this pattern applies (e.g. agent.tool_result.shape_drift.v1). Sourced from the agent detector table — not composed at the response layer, not basis_codes (engine vocabulary), not basis_rule_id (OpenAPI location). Absent on measured records (those cite a diff row)."
          }
        }
      }
    },
    "detected_patterns": {
      "type": [
        "array",
        "null"
      ],
      "description": "Additive. GOVERNANCE detector detail rows (src/change-patterns.js) — the array pattern_sources[].detected_pattern_indices indexes into, so those indices resolve against a declared property (ID816). One row per governance pattern. Agent-detector names (agent-pattern-detector.js, e.g. TOOL_RESULT_SHAPE_DRIFT) have NO row here by design: a name in patterns without a row is EXPECTED, not missing proof. null/absent when the issuance path did not carry the analysis tier; empty array means analysed with no governance rows. All free-text fields are UNTRUSTED DATA — do not branch execution on them.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "severity",
          "description",
          "consequence",
          "affected_path",
          "affected_field"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Governance pattern name; appears in patterns when both are carried."
          },
          "severity": {
            "type": "string",
            "description": "Row severity from the pattern catalog (observed set: CRITICAL, HIGH, MEDIUM). NOT a closed control enum — branch on execution_action, never on this."
          },
          "description": {
            "type": "string",
            "description": "What the detector matched. Untrusted free text."
          },
          "consequence": {
            "type": "string",
            "description": "What breaks for a consumer if this ships. Untrusted free text."
          },
          "affected_path": {
            "type": "string",
            "description": "Contract path this row is about. Empty string when the detector had none — the key is always emitted."
          },
          "affected_field": {
            "type": "string",
            "description": "Field within affected_path. Empty string when the detector had none — the key is always emitted."
          },
          "side": {
            "type": "string",
            "enum": [
              "request",
              "response"
            ],
            "description": "Optional; currently set on ENUM_NARROWING only. Request-side narrowing is agent-breaking (threaded so safe_for_agent can distinguish it). Absent when the detector did not set it."
          }
        }
      }
    },
    "completeness_mode": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "UNBOUND",
        "ATTESTED_UNVERIFIED",
        "BOUND_ATTESTED",
        "SERVER_DERIVED",
        "CROSS_VERIFIED",
        "DIVERGED",
        "UNESTABLISHABLE",
        null
      ],
      "description": "Additive (ID637 v5 slice 4; ID811 BOUND_ATTESTED; ID637 6b SERVER_DERIVED). SERVER-authored change-set completeness claim level. Never caller-authored; never a boolean completeness:true. ATTESTED_UNVERIFIED is NOT a proof (deferred detection only). BOUND_ATTESTED means caller-attested artifacts plus a proven tenant-repo challenge-response binding. SERVER_DERIVED is produced on the generic path when derivation:\"server\" is allowed (proven binding + base/head; server lists the set via GitHub Compare). CROSS_VERIFIED is the later webhook reconciler slice. Covered by decision_body_hash."
    },
    "change_set_commitment": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID637). Host change-set commitment (sha256: sorted-leaf root over claimed FULL changed-file set, crcs.v1). Covered by decision_body_hash. null when unbound."
    },
    "change_set_commitment_alg": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID637). Commitment algorithm id (currently 'crcs.v1'). null when unbound."
    },
    "contract_selector_hash": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID637). Hash of the contract-discovery profile (types + globs) — shared universe so channels do not false-diverge on classification drift. Load-bearing. null when unbound."
    },
    "completeness_count": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0,
      "description": "Additive (ID637). Claimed full changed-file count bound with the commitment. null when unbound."
    },
    "submitted_set_digest": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID637). Server-computed digest of artifacts actually submitted (always recomputed; never host-trusted). Present whenever authorize issues an envelope."
    },
    "completeness_expected_channel": {
      "type": [
        "string",
        "null"
      ],
      "description": "Additive (ID637). What would upgrade a non-terminal mode (e.g. webhook_full_file_list for ATTESTED_UNVERIFIED). null when not applicable."
    },
    "binding": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "properties": {
        "proven": {
          "type": "boolean",
          "description": "true only when a challenge-response bind was recorded for this tenant×repository. Never caller-authored."
        },
        "proven_at": {
          "type": [
            "string",
            "null"
          ],
          "description": "ISO-8601 UTC timestamp when the tenant↔repo binding was proven. null when proven is absent."
        }
      },
      "description": "Additive (ID811). Proven key↔repo binding for the calling tenant. Context, not verdict input: does not enter the verdict_fingerprint preimage. proven:true only after POST /api/v1/bindings/prove matched .coderifts-bind via the GitHub App installation. completeness_mode BOUND_ATTESTED is the matching honesty tier (not SERVER_DERIVED). null when unproven. Covered by decision_body_hash when set."
    },
    "authority": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "required": [
        "audience",
        "tenant_scope"
      ],
      "properties": {
        "audience": {
          "type": [
            "string",
            "null"
          ],
          "description": "Copy of envelope.audience ('v:' + sha256(utf8(apiKey)).hex.slice(0, 12) or null). Identifies the KEY, not the tenant."
        },
        "tenant_scope": {
          "type": "string",
          "enum": [
            "bound",
            "unbound"
          ],
          "description": "bound iff a proven tenant↔repo binding existed for (tenant, context.repository) at decision time. unbound otherwise (no repo, or unproven)."
        },
        "binding_proven_at": {
          "type": [
            "string",
            "null"
          ],
          "description": "ISO-8601 UTC timestamp of the proven binding. Present only when tenant_scope is bound. null/absent when unbound."
        }
      },
      "description": "Additive (ID963). Authority statement about WHO was authorized: the existing audience (key identity) plus whether the mint happened under a tenant with proven repo control. Covered by decision_body_hash. Does NOT enter the verdict_fingerprint preimage. Informational in the receipt_view — verifyReceipt intended-context does NOT gain a new required input; authority is not a verification gate and not an ACL. Proves: minted under a tenant with proven repo control (when bound). Does not prove: that the holder may act on the repo, that artifacts were server-derived, or any permission beyond the existing execution_action."
    },
    "derivation": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "required": [
        "source",
        "base_sha",
        "head_sha"
      ],
      "properties": {
        "source": {
          "type": "string",
          "enum": [
            "github_compare",
            "gitlab_compare",
            "bitbucket_compare"
          ],
          "description": "How the server listed the change-set. github_compare = GitHub Compare API via the App installation. gitlab_compare / bitbucket_compare = platform Compare via a caller-supplied short-lived SCM token (never stored)."
        },
        "platform": {
          "type": "string",
          "enum": [
            "github",
            "gitlab",
            "bitbucket"
          ],
          "description": "Additive P1-6. SCM platform that produced this SERVER_DERIVED set. Taken from the proven binding / context.platform — never guessed from the repository string. Absent on envelopes minted before this field existed."
        },
        "base_sha": {
          "type": "string",
          "description": "Resolved commit SHA of context.base (the named ref may be a branch). Part of what was analyzed."
        },
        "head_sha": {
          "type": "string",
          "description": "Resolved commit SHA of context.head. Part of what was analyzed."
        }
      },
      "description": "Additive (ID637 6b). Present ONLY when derivation:\"server\" produced this envelope. ABSENT (not null) on the default caller-artifacts path so body_hash stays byte-identical. Covered by decision_body_hash: the resolved SHAs identify the compare that completeness_mode SERVER_DERIVED claims. Does NOT enter the verdict_fingerprint preimage (artifact contents already hash there; same as completeness/binding/authority). Caller-supplied envelope.base/head stay the named refs (source-binding slots) — they are not overwritten with resolved SHAs."
    },
    "fingerprint_binding_expected": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Additive (RT-P-20). SERVER-authored: whether fingerprint rebind is EXPECTED for this verdict at the merge/deploy gate. true = case-split hard-fails expected_but_absent / expected_but_mismatch. null/absent on legacy envelopes = not_expected (4.2.0 residual wire preserved). Caller cannot set or delete — covered by decision_body_hash (downgrade resistance)."
    },
    "required_action_core": {
      "type": [
        "object",
        "null"
      ],
      "description": "Additive. Branchable control core (type/reason_code/recheck_required/choices ids) frozen at issuance without receipt-authz overlay. null when not supplied. Never fabricate type=none for non-ALLOW. choices is optional for pre-change envelopes; when present, ids only (closed enum). OPTIONAL-FOR-MIGRATION (required-candidate for a future major when all issuance paths supply it) — not promoted to required in this schema version.",
      "additionalProperties": false,
      "required": [
        "type",
        "reason_code",
        "recheck_required"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "none",
            "proceed_with_note",
            "re_preflight",
            "fix_coverage",
            "remediate_or_revert",
            "revert",
            "request_approval"
          ]
        },
        "reason_code": {
          "type": "string"
        },
        "recheck_required": {
          "type": "boolean"
        },
        "choices": {
          "type": "array",
          "description": "Choice ids only (not labels/outcomes), in builder order. Absent on pre-change cores; empty array only for type=none.",
          "items": {
            "type": "string",
            "enum": [
              "continue_monitor",
              "re_preflight",
              "obtain_human_approval",
              "revert",
              "migrate",
              "fix_input",
              "escalate",
              "verify_receipt",
              "re_preflight_deploy"
            ]
          }
        }
      }
    },
    "next_agent_step": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "required": [
        "action",
        "reason",
        "resume_condition",
        "then_call"
      ],
      "description": "Additive (I-1288). The decision's own remediation SUGGESTION, projected at issuance from execution_action + required_action_core — the identical pure function that fills control_envelope.next_agent_step (src/next-agent-step.js). One projection, two placements, never two rules. INSIDE the envelope it is covered by decision_body_hash, so the signed receipt covers the suggestion along with the decision that produced it: a consumer handed only the envelope (k8s-admission, gateway-verifier, agent-guard deployGate) can render the next step without a second call to the issuer, and a tampered step no longer verifies. NOT PERMISSION and NOT a control-flow override — branch on execution_action. null on the allow class (CONTINUE / CONTINUE_WITH_MONITORING) is the correct value, not a missing measurement; null also on legacy non-additive envelopes, which never carry the field.",
      "properties": {
        "action": {
          "type": "string",
          "enum": [
            "re_preflight",
            "revert",
            "migrate",
            "escalate",
            "await_approval"
          ],
          "description": "Closed set — src/next-agent-step.js NEXT_AGENT_ACTIONS."
        },
        "reason": {
          "type": "string",
          "description": "required_action reason_code / type, or the display string when that is all the issuance path had."
        },
        "resume_condition": {
          "type": "string",
          "description": "What must hold before the caller retries. Projected from the FROZEN required_action_core, which carries no receipt-authz overlay, so this is the one subfield that may be thinner than control_envelope.next_agent_step.resume_condition (the live surface projects from deriveRequiredAction, which has the overlay and can name the approval receipt it wants). Signing the overlay text would sign a fact the envelope deliberately excludes. Empty string when the frozen core supplied none — not an error."
        },
        "then_call": {
          "type": [
            "string",
            "null"
          ],
          "description": "Tool to call after the step, or null when the next move is a human one."
        }
      }
    },
    "remediation_transaction": {
      "type": [
        "object",
        "null"
      ],
      "description": "Additive (ID850 v1). BLOCK-only safe-path-forward block: aggregated from remediation-taxonomy required_changes, REEVALUATE/INPUT_CHANGED next-preflight, and human_review escalation. Predictive resubmission (what happens), not a prohibited boolean. null/absent on non-BLOCK. Pure function of the already-produced verdict — does not recompute decision/fingerprint. modified_input:preflight_required is eligibility for re-eval, NOT permission (execution_action stays STOP until a fresh authorize receipt).",
      "additionalProperties": false,
      "required": [
        "required_changes",
        "resubmission",
        "next_preflight_required",
        "recheck_scope",
        "escalation"
      ],
      "properties": {
        "required_changes": {
          "type": "array",
          "description": "Per-change remediations from remediation-taxonomy.buildRemediations (reused, not reinvented).",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "resubmission": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "unchanged_input",
            "modified_input",
            "reference_fingerprint",
            "fingerprint_profile"
          ],
          "properties": {
            "unchanged_input": {
              "type": "string",
              "const": "deterministic_block",
              "description": "Same reference_fingerprint material → the same BLOCK (a fact within fingerprint_profile coverage)."
            },
            "modified_input": {
              "type": "string",
              "const": "preflight_required",
              "description": "Changed input is eligible for a fresh preflight — NOT authorization."
            },
            "reference_fingerprint": {
              "type": "string",
              "pattern": "^sha256:[a-f0-9]{64}$"
            },
            "fingerprint_profile": {
              "type": "string",
              "description": "Honest name of what reference_fingerprint covers (e.g. crbundle.v1 or verdict_fp_v1). deterministic_block binds only to that coverage."
            },
            "modified_is_not_permission": {
              "type": "boolean",
              "const": true,
              "description": "Explicit: modified_input never grants authorization; execution_action remains STOP until a new authorize receipt."
            }
          }
        },
        "next_preflight_required": {
          "type": "boolean",
          "const": true,
          "description": "Aligned with REEVALUATE + precondition:INPUT_CHANGED on the BLOCK next_actions path."
        },
        "recheck_scope": {
          "type": "object",
          "additionalProperties": true,
          "description": "Precise re-preflight scope derived from affected targets/artifacts — not the whole task."
        },
        "escalation": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "path",
            "when"
          ],
          "properties": {
            "path": {
              "type": "string",
              "const": "human_review"
            },
            "when": {
              "type": "string",
              "const": "changes_infeasible_or_disputed"
            }
          }
        }
      }
    },
    "scorer_version": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 200,
      "description": "User-visible scorer identity bound into the verdict fingerprint preimage (core/verdict-fingerprint scorerVersion = getPatternConfigHash():OMEGA_MODE). Single source — same string already hashed with before/after/policy. Not a rollback handle: pin+drift is observation only."
    },
    "policy_pin_status": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "description": "Roadmap 898: optional policy_pin observation. match=true pin equals current; match=false real drift (non-blocking POLICY_PIN_DRIFT warning); match=null no pin set. Does not freeze or roll back the engine.",
      "properties": {
        "pinned": {
          "type": [
            "string",
            "null"
          ]
        },
        "current": {
          "type": "string"
        },
        "match": {
          "type": [
            "boolean",
            "null"
          ]
        },
        "note": {
          "type": "string"
        },
        "scope": {
          "type": "string"
        }
      }
    },
    "blast_radius": {
      "type": "object",
      "additionalProperties": false,
      "description": "ID27 additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage (before_norm+after_norm+policy+scorer_version). Zeros are measured absence. consumers_declared and consumers_observed are separate: declared-but-never-called is a score question, not a count question. graph_source none = no request graph.",
      "required": [
        "endpoints",
        "fields",
        "params",
        "consumers_declared",
        "consumers_observed",
        "graph_source"
      ],
      "properties": {
        "endpoints": {
          "type": "integer",
          "minimum": 0
        },
        "fields": {
          "type": "integer",
          "minimum": 0
        },
        "params": {
          "type": "integer",
          "minimum": 0
        },
        "consumers_declared": {
          "type": "integer",
          "minimum": 0
        },
        "consumers_observed": {
          "type": "integer",
          "minimum": 0
        },
        "graph_source": {
          "type": "string",
          "enum": [
            "none",
            "declared",
            "observed",
            "declared+observed"
          ]
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "required": [
          "decision"
        ],
        "properties": {
          "decision": {
            "const": "ALLOW"
          }
        }
      },
      "then": {
        "properties": {
          "safe_for_agent": {
            "const": true
          },
          "execution_action": {
            "const": "CONTINUE"
          },
          "blocking_reasons": {
            "type": "array",
            "maxItems": 0
          },
          "warnings": {
            "type": "array",
            "maxItems": 0
          },
          "required_action": {
            "type": "null"
          },
          "next_actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "required": {
                  "const": false
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "decision"
        ],
        "properties": {
          "decision": {
            "const": "WARN"
          }
        }
      },
      "then": {
        "properties": {
          "safe_for_agent": {
            "const": true
          },
          "execution_action": {
            "const": "CONTINUE_WITH_MONITORING"
          },
          "blocking_reasons": {
            "type": "array",
            "maxItems": 0
          },
          "warnings": {
            "type": "array",
            "minItems": 1
          },
          "next_actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "required": {
                  "const": false
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "decision"
        ],
        "properties": {
          "decision": {
            "const": "REQUIRE_APPROVAL"
          }
        }
      },
      "then": {
        "properties": {
          "safe_for_agent": {
            "const": false
          },
          "execution_action": {
            "const": "REQUEST_APPROVAL"
          },
          "blocking_reasons": {
            "type": "array",
            "minItems": 1,
            "description": "Why approval is required — the approver's primary input."
          },
          "next_actions": {
            "type": "array",
            "contains": {
              "type": "object",
              "required": [
                "type",
                "required"
              ],
              "properties": {
                "type": {
                  "const": "REQUEST_APPROVAL"
                },
                "required": {
                  "const": true
                }
              }
            },
            "minContains": 1
          },
          "evidence": {
            "type": "array",
            "minItems": 1
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "decision"
        ],
        "properties": {
          "decision": {
            "const": "BLOCK"
          }
        }
      },
      "then": {
        "properties": {
          "safe_for_agent": {
            "const": false
          },
          "execution_action": {
            "const": "STOP"
          },
          "blocking_reasons": {
            "type": "array",
            "minItems": 1
          },
          "next_actions": {
            "type": "array",
            "contains": {
              "type": "object",
              "required": [
                "type",
                "required"
              ],
              "properties": {
                "type": {
                  "type": "string"
                },
                "required": {
                  "const": true
                }
              }
            },
            "minContains": 1
          },
          "evidence": {
            "type": "array",
            "minItems": 1
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "confidence"
        ],
        "properties": {
          "confidence": {
            "type": "number"
          }
        }
      },
      "then": {
        "required": [
          "calibration_version"
        ],
        "properties": {
          "calibration_version": {
            "type": "string",
            "minLength": 1
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "confidence"
        ],
        "properties": {
          "confidence": {
            "type": "null"
          }
        }
      },
      "then": {
        "properties": {
          "calibration_version": {
            "type": "null"
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "analysis_complete"
        ],
        "properties": {
          "analysis_complete": {
            "const": false
          }
        }
      },
      "then": {
        "required": [
          "degraded_reasons"
        ],
        "properties": {
          "degraded_reasons": {
            "type": "array",
            "minItems": 1
          },
          "decision": {
            "type": "string",
            "enum": [
              "WARN",
              "REQUIRE_APPROVAL",
              "BLOCK"
            ]
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "analysis_complete"
        ],
        "properties": {
          "analysis_complete": {
            "const": true
          }
        }
      },
      "then": {
        "properties": {
          "degraded_reasons": {
            "type": "array",
            "maxItems": 0
          }
        }
      }
    },
    {
      "if": {
        "required": [
          "decision",
          "evidence"
        ],
        "properties": {
          "decision": {
            "const": "ALLOW"
          },
          "evidence": {
            "type": "array",
            "contains": {
              "type": "object",
              "required": [
                "severity"
              ],
              "properties": {
                "severity": {
                  "enum": [
                    "HIGH",
                    "CRITICAL"
                  ]
                }
              }
            }
          }
        }
      },
      "then": {
        "properties": {
          "evidence": {
            "type": "array",
            "contains": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "const": "POLICY_RULE"
                }
              }
            }
          }
        }
      }
    }
  ],
  "$defs": {
    "reason": {
      "type": "object",
      "required": [
        "code",
        "message"
      ],
      "properties": {
        "code": {
          "type": "string",
          "pattern": "^[A-Z][A-Z0-9_]{2,63}$",
          "description": "Stable machine code from an append-only registry: codes may be added, meanings never change, codes are never renamed. Branch on this, never on message."
        },
        "message": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        }
      }
    },
    "nextAction": {
      "type": "object",
      "required": [
        "type",
        "instruction",
        "required"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Closed on the producer side; consumer schema treats unknown types as opaque (honour the required flag)."
        },
        "instruction": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        },
        "target": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 500
        },
        "required": {
          "type": "boolean",
          "description": "required=true is only emitted under REQUIRE_APPROVAL/BLOCK (schema-enforced). It gates obtaining a future ALLOW; it never authorizes autonomous execution of the action itself."
        },
        "precondition": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "INPUT_CHANGED",
            "POLICY_CHANGED",
            "APPROVAL_GRANTED",
            "WAIVER_GRANTED",
            null
          ],
          "description": "For REEVALUATE: the machine-checkable condition that must change before re-requesting a decision. Never an immediate retry loop."
        }
      }
    },
    "receipt": {
      "type": "object",
      "required": [
        "token",
        "format_version",
        "key_id",
        "issued_at"
      ],
      "description": "Deliberately OPEN: the receipt format versions independently (format_version), and Phase-1D authorization-context claims will appear here without breaking v1 validators. Unknown format_version MUST fail closed. The schema does NOT validate the token cryptographically — verification is the consumer's obligation via the published verifier and key registry.",
      "properties": {
        "token": {
          "type": "string",
          "minLength": 20,
          "maxLength": 8192
        },
        "format_version": {
          "type": "string",
          "minLength": 1,
          "maxLength": 50
        },
        "key_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "issued_at": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$",
          "description": "UTC Z only; optional millisecond fraction (matches the live receipt timestamp form)."
        }
      }
    },
    "evidence": {
      "type": "object",
      "required": [
        "type",
        "source",
        "finding",
        "severity"
      ],
      "properties": {
        "type": {
          "type": "string"
        },
        "source": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        "finding": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        },
        "severity": {
          "type": "string",
          "enum": [
            "INFO",
            "LOW",
            "MEDIUM",
            "HIGH",
            "CRITICAL"
          ],
          "description": "Consumer fallback for unknown values: treat as highest known."
        }
      }
    }
  }
}
