{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://impact-manifest.org/schema/v0.1.0/impact-manifest.schema.json",
  "title": "Impact Manifest",
  "description": "An Impact Manifest is a machine-readable safety assertion about a proposed infrastructure mutation. A `terraform show -json` plan says WHAT will change; an Impact Manifest says whether the change is SAFE, and — critically — records what it could NOT determine. It is produced by consuming a plan plus (optionally) live cloud state, and is designed to be generated by any tool, for any provider. The governing rule of the whole format: a verdict of 'safe' is a positive claim that must be earned with evidence; the absence of a known problem is 'unknown', never 'safe'.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schema_version", "generated_at", "producer", "source", "changes", "verdict"],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of THIS format the document conforms to (semver). This IS the spec version. Compatibility is keyed off the MAJOR component: a consumer MUST refuse a major version it does not implement rather than risk misreading, and within a major it recognizes MUST ignore fields it does not know (which, for non-standard data, live under `extensions`). See the specification's Compatibility section."
    },
    "assertion_id": {
      "type": "string",
      "description": "Opaque unique id for this assertion, so it can be referenced from logs, PR comments, and audit trails."
    },
    "generated_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the assertion was produced. Safety verdicts about live state decay — a consumer may reject an assertion that is too old to trust."
    },
    "valid_until": {
      "type": "string",
      "format": "date-time",
      "description": "The moment after which this manifest MUST NOT be relied on for a gate decision. A 'safe' verdict is only true as of the state it was computed against, and state drifts — often fast. A consumer that receives an expired manifest MUST NOT treat it as authoritative; it MUST re-compute. Absent means the producer stated no window, and a strict consumer SHOULD supply its own maximum age."
    },
    "state_observed_at": {
      "type": "string",
      "format": "date-time",
      "description": "The timestamp of the FRESHEST live-state observation the verdicts rest on (which may be earlier than `generated_at`). Lets a consumer judge staleness against reality, not just against when the document was written."
    },
    "producer": {
      "$ref": "#/$defs/producer",
      "description": "The tool that produced this document AND what it was able to see. Provenance matters: two tools may disagree about the same change, and a reader must know which one spoke, what it queried, and what access it held — an unknown is only interpretable if you know whether the producer even had live access."
    },
    "integrity": {
      "$ref": "#/$defs/integrity",
      "description": "Optional detached-signature slot so a manifest can be attested and tamper-evidence established. A manifest without integrity is usable but NOT attestable; a consumer that requires attestation MUST reject an unsigned manifest. v0.1 defines the slot and a canonicalization rule but does not mandate a signing scheme — see the specification's Integrity section for the layering plan (in-toto/DSSE, Sigstore)."
    },
    "source": {
      "$ref": "#/$defs/source",
      "description": "The artifact this assertion was derived from (a Terraform plan today; the field is structured so other change sources can be added without breaking readers)."
    },
    "evidence": {
      "type": "array",
      "description": "The shared pool of evidence actually gathered to reach the verdicts below. Every verdict points into this pool by id. This is the auditable answer to 'how do you know?' — a verdict with no evidence is an opinion, and a reader can see that.",
      "items": { "$ref": "#/$defs/evidence" }
    },
    "changes": {
      "type": "array",
      "description": "One entry per resource change in the plan that this assertion covers. A plan is a set of changes applied atomically, so the safe/unsafe decision is made over the whole set, but each change is assessed on its own so a reader can see exactly which one is dangerous.",
      "minItems": 1,
      "items": { "$ref": "#/$defs/change" }
    },
    "verdict": {
      "$ref": "#/$defs/planVerdict",
      "description": "The single roll-up decision a CI gate acts on. It is a pure function of the per-change assessments and the applied policy; it exists so a machine does not have to re-derive intent from the details."
    },
    "extensions": {
      "type": "object",
      "description": "The ONLY place non-standard, producer- or consumer-specific fields may appear. Every core object in this schema is strict (additionalProperties:false) so a producer bug surfaces immediately during validation; anything outside the standard goes here instead. A consumer MUST ignore extension keys it does not recognize — that is what makes forward-compatible additions possible without breaking older readers.",
      "additionalProperties": true
    }
  },

  "$defs": {
    "confidence": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "How much the tool trusts the VALUE it just gave — orthogonal to the value itself. A verdict of 'value: none' with 'confidence: low' means 'I found no impact but I am not sure I looked everywhere', which a strict policy should treat differently from 'value: none, confidence: high'. Note that 'unknown' is a VALUE (I could not determine), not a confidence level (I determined it, but weakly)."
    },

    "evidenceRefs": {
      "type": "array",
      "description": "Ids of entries in the top-level `evidence` pool that support the verdict this appears on.",
      "items": { "type": "string" }
    },

    "evidence": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "source", "observation"],
      "properties": {
        "id": { "type": "string", "description": "Referenced by verdicts via their `evidence` arrays." },
        "source": {
          "type": "string",
          "enum": ["terraform_plan", "live_state", "policy", "heuristic", "human"],
          "description": "Where this fact came from. The distinction is the point: 'terraform_plan' is what the plan asserts (and the plan trusts state, which may be wrong); 'live_state' is what was observed by querying the provider directly; 'heuristic' is a rule of thumb, not a measurement. A reader weighs these differently."
        },
        "query": {
          "type": "string",
          "description": "What was actually asked to obtain this fact — a plan JSON pointer, an API/CLI call, a policy rule name. Makes the verdict reproducible."
        },
        "observation": {
          "type": "string",
          "description": "What came back. The raw finding, in human-readable form."
        },
        "observed_at": {
          "type": "string",
          "format": "date-time",
          "description": "When the observation was made. Live-state facts have a shelf life."
        },
        "provider": {
          "type": "string",
          "description": "The cloud/provider the fact pertains to, when relevant (e.g. a provider address). Optional and provider-agnostic."
        }
      }
    },

    "producer": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "properties": {
        "name": { "type": "string", "description": "Name of the tool that produced the document." },
        "version": { "type": "string", "description": "Version of that tool." },
        "homepage": { "type": "string", "description": "Optional URL for the tool." },
        "access": {
          "type": "object",
          "additionalProperties": false,
          "description": "What the producer was able to see when it ran. An 'unknown' verdict is only interpretable if you know whether the producer even had the access to determine otherwise — a delete assessed by a tool with no live-state access is unknown for a very different reason than one assessed by a fully-credentialed tool.",
          "properties": {
            "live_state": {
              "type": "string",
              "enum": ["queried", "unavailable", "not_attempted"],
              "description": "'queried' = the producer reached the provider and read live state. 'unavailable' = it tried and could not (no credentials, network). 'not_attempted' = it ran plan-only by choice. The difference explains why unknowns exist."
            },
            "scope": {
              "type": "string",
              "description": "Human description of the identity/permissions the producer held (e.g. 'read-only, Reader on subscription abc-123'). Bounds what it could have known — a reader can see that a narrow role explains a narrow assessment."
            }
          }
        }
      }
    },

    "integrity": {
      "type": "object",
      "additionalProperties": false,
      "description": "Detached integrity and signing metadata. The signature covers the CANONICAL form of the manifest with this `integrity` object removed (a signature cannot sign itself). Canonicalization is JCS — JSON Canonicalization Scheme, RFC 8785 — which fixes key ordering, whitespace, and number formatting so two conformant producers serializing the same logical document produce byte-identical input to the hash. A verifier reconstructs the signed payload by deleting `integrity`, applying JCS, and checking against `digest`/`signature`. v0.1 fixes this canonicalization rule and the slot; it does not yet mandate a single signing scheme.",
      "properties": {
        "digest": {
          "type": "object",
          "additionalProperties": false,
          "required": ["algorithm", "value"],
          "description": "Digest of the canonicalized payload (this `integrity` object removed). Lets a consumer detect tampering even before verifying a signature.",
          "properties": {
            "algorithm": { "type": "string", "description": "Hash algorithm, e.g. 'sha256'." },
            "value": { "type": "string", "description": "Hex or base64 digest value." }
          }
        },
        "signature": {
          "type": "object",
          "additionalProperties": false,
          "required": ["algorithm", "value"],
          "description": "A detached signature over the digested payload.",
          "properties": {
            "algorithm": { "type": "string", "description": "Signature algorithm, e.g. 'ed25519', 'ecdsa-p256'." },
            "value": { "type": "string", "description": "Base64 detached signature." },
            "key_id": { "type": "string", "description": "Identifier of the signing key, for verifier key lookup." },
            "certificate": { "type": "string", "description": "Optional PEM certificate or a reference to one (e.g. a Sigstore identity)." }
          }
        },
        "envelope": {
          "type": "object",
          "additionalProperties": false,
          "description": "Optional pointer to a full attestation carrying this manifest as its subject, for teams already running an attestation framework.",
          "properties": {
            "type": { "type": "string", "enum": ["dsse", "in-toto", "sigstore-bundle"], "description": "The attestation format wrapping this manifest." },
            "reference": { "type": "string", "description": "URI or storage reference to the envelope." }
          }
        }
      }
    },

    "source": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["terraform_plan"],
          "description": "The kind of change artifact consumed. An enum (not a free string) so a consumer can refuse a source it has no rules for. Additional sources are added by extending this enum in a future schema_version."
        },
        "tool_version": {
          "type": "string",
          "description": "Version of the change tool that produced the source artifact (e.g. the Terraform version). Behaviour of the plan format varies by version."
        },
        "format_version": {
          "type": "string",
          "description": "Version of the source artifact's own format (e.g. Terraform's plan JSON `format_version`)."
        },
        "reference": {
          "type": "string",
          "description": "An identifier for the specific artifact consumed — a filename, a content hash, a pipeline run id. Lets an audit tie the assertion back to the exact plan it judged."
        }
      }
    },

    "verdictBase": {
      "type": "object",
      "required": ["confidence", "rationale"],
      "properties": {
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": {
          "type": "string",
          "minLength": 1,
          "description": "A human-readable reason for the value. REQUIRED on every verdict, including 'unknown' — a bare 'unknown' with no reason is a bug, not an answer. This is what a reviewer reads when deciding whether to override the machine."
        },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "availabilityImpact": {
      "type": "object",
      "additionalProperties": false,
      "required": ["value", "confidence", "rationale"],
      "properties": {
        "value": {
          "type": "string",
          "enum": ["none", "at_risk", "interrupts", "unknown"],
          "description": "Whether applying the change interrupts something currently serving. 'none' = verified not to interrupt anything live. 'at_risk' = it may, depending on conditions the tool could not fully resolve. 'interrupts' = it will take a live thing offline. 'unknown' = the tool could not determine whether anything live depends on the target. 'none' and 'unknown' are deliberately distinct: the first is a finding, the second is the absence of one."
        },
        "serves_production_traffic": {
          "type": "string",
          "enum": ["yes", "no", "unknown"],
          "description": "Whether the affected resource is currently serving production traffic, as best the tool could establish by querying live state. The plan cannot know this; it is the enrichment the format exists to carry."
        },
        "expected_interruption": {
          "type": "string",
          "description": "Human description of the interruption if any — what goes down, and for how long."
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "reversibility": {
      "type": "object",
      "additionalProperties": false,
      "required": ["value", "confidence", "rationale"],
      "properties": {
        "value": {
          "type": "string",
          "enum": ["reversible", "reversible_with_data_loss", "irreversible", "unknown"],
          "description": "Whether the change can be undone. 'reversible' = fully, with no loss. 'reversible_with_data_loss' = the resource can be recreated but data written since is gone. 'irreversible' = there is no mechanism to get back to the prior state. 'unknown' = the tool could not establish a reversal path — which must be treated as at least as dangerous as irreversible, never as reversible."
        },
        "mechanism": {
          "type": "string",
          "description": "HOW the change would be reversed, concretely (e.g. 'restore from snapshot X', 're-apply the prior Terraform state'). A reversibility claim with no mechanism is not credible."
        },
        "window": {
          "type": "string",
          "description": "The time window in which reversal is possible before it stops being possible (e.g. 'until the resource is destroyed', 'within the 7-day soft-delete retention'). Reversibility often has an expiry."
        },
        "cost": {
          "type": "string",
          "description": "The OPERATIONAL cost of reversing — not whether it is possible, but what it costs. 'Reversible by restoring a snapshot taken 14 hours ago' is technically reversible and operationally a disaster: up to 14 hours of writes lost. Record the data-loss window and disruption here. If reversing loses data, `value` MUST be 'reversible_with_data_loss', not 'reversible', and this field quantifies the loss."
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "dataDurability": {
      "type": "object",
      "additionalProperties": false,
      "required": ["value", "confidence", "rationale"],
      "properties": {
        "value": {
          "type": "string",
          "enum": ["no_data_loss", "recoverable_loss", "unrecoverable_loss", "unknown"],
          "description": "What happens to data. 'no_data_loss' = no data is affected. 'recoverable_loss' = data is removed but a recovery path exists (a backup, a snapshot). 'unrecoverable_loss' = data becomes permanently unrecoverable. 'unknown' = the tool could not establish whether recoverable copies exist — treated as unrecoverable for safety, not as safe."
        },
        "at_risk": {
          "type": "array",
          "description": "What specifically becomes unrecoverable (or would, absent the recovery path named in the rationale).",
          "items": { "type": "string" }
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "securityPosture": {
      "type": "object",
      "additionalProperties": false,
      "required": ["value", "confidence", "rationale"],
      "properties": {
        "value": {
          "type": "string",
          "enum": ["unchanged", "improved", "widened", "unknown"],
          "description": "Direction of the change's effect on security posture. 'widened' = exposure increases, an identity is weakened, or an audit/logging control is broken. 'improved' = the opposite. 'unchanged' = verified no net effect. 'unknown' = could not determine — not to be read as 'unchanged'."
        },
        "concerns": {
          "type": "array",
          "description": "Specific posture changes found, each classed by kind so a policy can weight them.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["kind", "detail"],
            "properties": {
              "kind": {
                "type": "string",
                "enum": ["exposure", "identity", "audit"],
                "description": "'exposure' = network/public reachability widens; 'identity' = an authn/authz control weakens; 'audit' = a logging/monitoring/immutability control is removed or degraded."
              },
              "detail": { "type": "string", "description": "Human description of the specific concern." }
            }
          }
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "costDelta": {
      "type": "object",
      "additionalProperties": false,
      "required": ["direction", "confidence", "rationale"],
      "properties": {
        "direction": {
          "type": "string",
          "enum": ["increase", "decrease", "none", "unknown"],
          "description": "Direction of the change's effect on spend. Cost is a safety dimension too — a change that silently 100x's a bill is a production incident of a different kind."
        },
        "recurrence": {
          "type": "string",
          "enum": ["one_time", "recurring", "none", "unknown"],
          "description": "Whether the cost effect is a one-time charge or an ongoing rate change. A recurring increase compounds; a one-time charge does not."
        },
        "estimate": {
          "type": "object",
          "additionalProperties": false,
          "description": "Optional magnitude. Absent when the tool can state direction but not amount.",
          "properties": {
            "amount": { "type": "number", "description": "Estimated magnitude of the change, in `currency`." },
            "currency": { "type": "string", "description": "ISO currency code for `amount`." }
          }
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "stateConfidence": {
      "type": "object",
      "additionalProperties": false,
      "required": ["value", "confidence", "rationale"],
      "description": "How much the tool trusts that the recorded state the plan was computed against still matches reality. This dimension exists because a plan TRUSTS state, and state drifts: a plan can confidently describe a change to a resource whose real shape no longer matches what Terraform recorded. When state is not trustworthy, no downstream verdict about that resource can be trusted either.",
      "properties": {
        "value": {
          "type": "string",
          "enum": ["state_matches_reality", "drift_detected", "state_stale", "not_verified", "unknown"],
          "description": "'state_matches_reality' = live state was queried and agrees with recorded state. 'drift_detected' = live state was queried and DISAGREES (the plan is reasoning about a resource that no longer looks like this). 'state_stale' = recorded state is old enough or the resource volatile enough that it cannot be trusted. 'not_verified' = the tool did not check live state (distinct from checking and finding a match). 'unknown' = the tool tried to check and could not."
        },
        "verified_against_live": {
          "type": "boolean",
          "description": "True only when the tool actually queried the provider to compare. 'not_verified' with this false is honest; a match claim with this false is not."
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "rationale": { "type": "string", "minLength": 1 },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "precondition": {
      "type": "object",
      "additionalProperties": false,
      "required": ["description", "status"],
      "description": "An assertion that must hold for apply to be safe. Preconditions turn 'this is dangerous' into 'this is safe IF …', which is what actually unblocks a change: satisfy the precondition and re-run.",
      "properties": {
        "id": { "type": "string", "description": "Stable id for the precondition, so a re-run can report the same one as now satisfied." },
        "description": { "type": "string", "minLength": 1, "description": "Human statement of what must be true (e.g. 'a snapshot of the disk exists that is newer than the last write')." },
        "check": { "type": "string", "description": "Machine-checkable form of the assertion, when one exists — a query or expression a tool can evaluate to satisfied/unsatisfied." },
        "status": {
          "type": "string",
          "enum": ["satisfied", "unsatisfied", "unknown"],
          "description": "'satisfied' = verified true. 'unsatisfied' = verified false (this is what blocks). 'unknown' = could not verify — blocks under a strict policy, because an unverified precondition is not a met one."
        },
        "rationale": { "type": "string", "description": "Why the status is what it is." },
        "evidence": { "$ref": "#/$defs/evidenceRefs" }
      }
    },

    "change": {
      "type": "object",
      "additionalProperties": false,
      "required": ["address", "actions", "severity"],
      "properties": {
        "address": {
          "type": "string",
          "description": "The change tool's address for the resource (e.g. a Terraform resource address). The stable key a reader uses to find this exact change in the original plan."
        },
        "resource_type": { "type": "string", "description": "Provider resource type, verbatim from the plan (e.g. an azurerm/aws/google type). The format does not enumerate types — it is provider-agnostic." },
        "name": { "type": "string", "description": "The resource's local name within its module." },
        "provider": { "type": "string", "description": "Provider address the resource belongs to. Optional; provider-agnostic." },
        "actions": {
          "type": "array",
          "minItems": 1,
          "description": "The action(s) the plan takes on this resource, using the change tool's own vocabulary (Terraform: no-op | create | read | update | delete; a replace is the pair delete+create). Carried verbatim so the assertion cannot silently disagree with the plan about what is happening.",
          "items": {
            "type": "string",
            "enum": ["no-op", "create", "read", "update", "delete", "replace"]
          }
        },
        "availability_impact": { "$ref": "#/$defs/availabilityImpact" },
        "reversibility": { "$ref": "#/$defs/reversibility" },
        "data_durability": { "$ref": "#/$defs/dataDurability" },
        "security_posture": { "$ref": "#/$defs/securityPosture" },
        "cost_delta": { "$ref": "#/$defs/costDelta" },
        "state_confidence": { "$ref": "#/$defs/stateConfidence" },
        "preconditions": {
          "type": "array",
          "items": { "$ref": "#/$defs/precondition" }
        },
        "severity": {
          "type": "string",
          "enum": ["informational", "caution", "blocking", "unknown"],
          "description": "The per-change roll-up a reader scans first. 'blocking' = at least one dimension says this is dangerous. 'unknown' = at least one safety-critical dimension could not be determined, so this change cannot be certified safe. 'unknown' NEVER collapses to 'informational' — that collapse is the exact failure this format exists to prevent."
        },
        "rationale": { "type": "string", "description": "One-line human summary of why this change has the severity it does." }
      }
    },

    "planVerdict": {
      "type": "object",
      "additionalProperties": false,
      "required": ["decision", "rationale"],
      "properties": {
        "decision": {
          "type": "string",
          "enum": ["safe", "caution", "blocked", "unknown"],
          "description": "The whole-plan decision. 'safe' is a POSITIVE claim: every change was assessed and none is dangerous and nothing safety-critical is unknown. 'caution' = proceed with a human in the loop. 'blocked' = at least one change is dangerous under policy. 'unknown' = something safety-critical could not be determined; the plan is NOT safe, it is unproven. A conformant consumer must never map 'unknown' to 'safe'."
        },
        "rationale": { "type": "string", "minLength": 1, "description": "Human explanation of the decision, naming the change(s) that drove it." },
        "blocking_changes": {
          "type": "array",
          "description": "Addresses of the changes responsible for a 'blocked' or 'unknown' decision, so a gate can report them without re-deriving.",
          "items": { "type": "string" }
        },
        "unknowns_present": {
          "type": "boolean",
          "description": "True if any dimension anywhere in the document is 'unknown'. A loud, top-level flag so an unknown can never hide in the details and be skimmed past as safe."
        },
        "policy": {
          "type": "object",
          "additionalProperties": false,
          "description": "The policy applied to turn per-change assessments into this decision, when one was applied (e.g. by a CI gate). Absent when the document only describes and does not judge.",
          "properties": {
            "name": { "type": "string" },
            "version": { "type": "string" }
          }
        }
      }
    }
  }
}
