{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://actum.run/spec/evidence-pack.schema.json",
  "title": "Evidence Pack",
  "description": "An exportable compliance-evidence bundle: a derived, re-derivable read over a namespace's existing audit chain, publication, execution, and approval records for one or more control frameworks over a time window. NOT a stored resource — an implementation assembles it on read (GET /v1/audit/{chain}/evidence-pack?framework=&since=&until=, see evidence-packs.md) from records that already exist at spec 0.7.0+; the pack itself introduces no new persisted state. DETERMINISM IS NORMATIVE: the same (chain, framework, since, until) input MUST produce a byte-identical pack (content_hash equal) on every regeneration, so an auditor or a conformance suite can regenerate and compare rather than trust a single export. BY-DIGEST, NOT INLINED: every evidence entry references the underlying chain segment or record by digest/identifier (content_hash, chain+seq range, head checkpoint), never by copying the record's full body into the pack — the pack is a manifest over the system of record, not a second copy of it. See evidence-packs.md for the normative control-mapping catalog (control ID -> record/field/query) this schema's evidence entries instantiate.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "spec_version",
    "manifest",
    "evidence",
    "content_hash"
  ],
  "properties": {
    "spec_version": {
      "type": "string",
      "description": "The spec version this pack conforms to."
    },
    "manifest": {
      "$ref": "#/$defs/manifest"
    },
    "evidence": {
      "type": "array",
      "description": "One entry per control ID resolved for this pack's framework(s) and window. Order is the catalog order in evidence-packs.md for the requested framework(s), so byte-identical regeneration does not depend on an unstable sort.",
      "items": {
        "$ref": "#/$defs/evidenceEntry"
      }
    },
    "content_hash": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$",
      "description": "Lowercase hex SHA-256 over the JCS canonical JSON (RFC 8785) of this pack excluding content_hash and signature themselves. The determinism contract's verifiable anchor: two regenerations of the same (chain, framework, since, until) over an unchanged audit trail MUST produce equal content_hash."
    },
    "signature": {
      "$ref": "https://actum.run/spec/audit-event.schema.json#/$defs/signatureEnvelope",
      "description": "Present when the namespace's profile enforces signing: a non-repudiable signature over the pack (meaning 'evidence.pack', binds.content_hash equal to this pack's content_hash field). Reuses the existing content_hash binds form on signatureEnvelope — no new binds branch. One envelope shape shared with publication decisions, profile-change decisions, and chain checkpoints."
    }
  },
  "$defs": {
    "manifest": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "server_id",
        "namespace",
        "chain",
        "framework",
        "control_ids",
        "since",
        "until",
        "generated_at"
      ],
      "description": "Identifies what this pack is evidence of: which server, which namespace/chain, which compliance profile was in force over the window, which framework and control IDs were requested, and the exact time range covered.",
      "properties": {
        "server_id": {
          "type": "string",
          "description": "Stable identity of the server instance that generated this pack (e.g. a deployment id or root-key fingerprint), so a multi-server deployment's packs are individually attributable."
        },
        "namespace": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_-]{0,31}$",
          "description": "The namespace this pack's chain is scoped to. Matches the audit chain label (namespace.schema.json#/properties/name); the reserved 'server' chain is also a valid value for server-scoped controls (e.g. principal/policy administration).",
          "examples": ["prod", "server"]
        },
        "chain": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_-]{0,31}$",
          "description": "The audit chain this pack was derived from, per audit-event.schema.json#/properties/chain. Equal to manifest.namespace for a namespace-scoped pack."
        },
        "profile": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_-]*@.+$",
          "description": "The compliance profile assigned to the namespace and in force for the full [since, until) window, as name@version (profile.schema.json semantic key). Absent when the namespace was unassigned for any part of the window (an evidence pack over a window with a profile change mid-window is refused -- see evidence-packs.md's window-stability rule -- so when present this value is unambiguous for the whole window)."
        },
        "framework": {
          "type": "string",
          "description": "The control framework this pack's evidence entries were resolved against, as a catalog key from evidence-packs.md.",
          "examples": ["iso-42001", "eu-ai-act", "nist-800-53", "sox-404", "21-cfr-part-11", "fedramp-20x-ksi"]
        },
        "control_ids": {
          "type": "array",
          "description": "The specific control IDs resolved into this pack's evidence array, in the same order. A framework query with no control_ids filter resolves every catalog row registered for that framework.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "examples": [["AU-9", "AU-10"], ["A.6", "A.9"]]
        },
        "since": {
          "type": "string",
          "format": "date-time",
          "description": "Inclusive start of the covered window, UTC ISO-8601."
        },
        "until": {
          "type": "string",
          "format": "date-time",
          "description": "Exclusive end of the covered window, UTC ISO-8601."
        },
        "generated_at": {
          "type": "string",
          "format": "date-time",
          "description": "When this pack was assembled. Distinct from since/until: generated_at moves on every regeneration even though a re-derivation over an unchanged trail MUST still produce the same content_hash (generated_at itself is excluded from the content_hash computation as noted on content_hash's own description -- restated here because it is the one manifest field that is expected to vary across otherwise-identical regenerations)."
        }
      }
    },
    "evidenceEntry": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "control_id",
        "description",
        "source"
      ],
      "description": "One resolved control: the catalog row (control_id + description, copied from evidence-packs.md at generation time so the pack is self-describing without a live join back to the catalog doc) plus the concrete source references that evidence it for this window.",
      "properties": {
        "control_id": {
          "type": "string",
          "description": "The control ID, as named in evidence-packs.md's catalog table for manifest.framework.",
          "examples": ["AU-9", "AU-5", "404", "11.10(e)"]
        },
        "description": {
          "type": "string",
          "description": "The control's short description, copied verbatim from the evidence-packs.md catalog row at generation time."
        },
        "source": {
          "$ref": "#/$defs/evidenceSource"
        },
        "notes": {
          "type": "string",
          "description": "Optional implementation notes on how this control resolved for this specific window (e.g. 'no approval-gate records in window' when the referenced record set is legitimately empty)."
        }
      }
    },
    "evidenceSource": {
      "type": "object",
      "additionalProperties": false,
      "description": "The concrete evidence for one control, as references BY DIGEST into the existing system of record -- never inlined record bodies. At least one of chain_segment, records, or checkpoint MUST be present (a control with none of the three has nothing to point at, which is a catalog-authoring error, not a valid pack).",
      "anyOf": [
        { "required": ["chain_segment"] },
        { "required": ["records"] },
        { "required": ["checkpoint"] }
      ],
      "properties": {
        "chain_segment": {
          "type": "object",
          "additionalProperties": false,
          "required": ["chain", "from_seq", "to_seq"],
          "description": "A contiguous range of audit-event.schema.json events on one chain that evidences this control (e.g. every execution.approval.* event in the window). An auditor re-walks [from_seq, to_seq] on the live chain via GET /v1/audit/{chain} and recomputes prev_hash linkage independently -- the pack does not inline the events themselves.",
          "properties": {
            "chain": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_-]{0,31}$"
            },
            "from_seq": {
              "type": "integer",
              "minimum": 0
            },
            "to_seq": {
              "type": "integer",
              "minimum": 0
            },
            "action_filter": {
              "type": "string",
              "description": "Optional dotted-verb pattern (per audit-event.schema.json#/properties/action) narrowing which events in the range this control's evidence actually is, when the range also contains unrelated events.",
              "examples": ["execution.approval.*", "publication.approve"]
            }
          }
        },
        "records": {
          "type": "array",
          "description": "References to specific publication / execution / approval / profile-change records by digest -- id plus content_hash, never the record body. A verifier fetches each by id (e.g. GET /v1/publications/{id}) and confirms its content_hash matches before trusting it as this control's evidence.",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["type", "id", "content_hash"],
            "properties": {
              "type": {
                "type": "string",
                "enum": ["publication", "execution", "approval", "profile_change"],
                "description": "The record kind. 'approval' and 'profile_change' are decision records nested under an execution/namespace respectively, not top-level collections -- id disambiguates within that parent (see id's description)."
              },
              "id": {
                "type": "string",
                "description": "The record's id. For a top-level record (publication, execution) this is its own resource id. For a nested decision record (approval, profile_change) this is '<parent_id>:<step_or_key>' (e.g. an approval is scoped to its execution and step)."
              },
              "content_hash": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "The record's own content_hash field at the time it was decided (publication.content_hash, or the equivalent decided-record hash for an approval/profile_change), so a verifier can prove this exact decided version -- not merely the current state of a possibly-since-changed resource -- is what the pack cites."
              }
            }
          }
        },
        "checkpoint": {
          "type": "object",
          "additionalProperties": false,
          "required": ["chain", "seq", "head_hash"],
          "description": "A reference to a signed chain checkpoint (audit-event.schema.json#/$defs/checkpoint) that proves the chain head at or after this pack's window -- used by controls that evidence tail-truncation resistance (e.g. AU-9) rather than a specific event range.",
          "properties": {
            "chain": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_-]{0,31}$"
            },
            "seq": {
              "type": "integer",
              "minimum": 0
            },
            "head_hash": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        }
      }
    }
  }
}
