{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://actum.run/spec/profile.schema.json",
  "title": "Compliance Profile Artifact",
  "description": "A versioned, immutable artifact that declares the audit/compliance controls a namespace enforces when this profile is assigned to it (via namespace.schema.json's `profile` field, name@version). Profiles are flat artifacts on the same discipline as compositions: PUT /v1/profiles/{name}/versions/{version}, cache-forever, name+version immutable. A profile declares WHICH controls are enforced (enforcement); the separate `capabilities` list on GET /v1/meta declares which controls an implementation SUPPORTS (capability). Changing a namespace's assignment is machine-diffed against the control partial order defined below: a tightening (or equal) change applies immediately, a loosening opens a profile-change proposal (see namespace.schema.json $defs/profileChange and access-control.md). See docs/regulated-audit-requirements.md.",
  "type": "object",
  "required": [
    "spec_version",
    "name",
    "version",
    "controls",
    "audit_failure"
  ],
  "properties": {
    "spec_version": {
      "type": "string",
      "description": "Version of the Actum spec this document conforms to. Unrelated to the profile's own version.",
      "examples": ["0.7.0"]
    },
    "name": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_-]*$",
      "description": "Identifier for the profile. Hyphens allowed (profile names are conventionally regime-named).",
      "examples": ["fedramp-moderate", "sox-prod", "finra-4511"]
    },
    "version": {
      "type": "string",
      "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z.-]+)?$",
      "description": "The profile's own semver. A name+version pair is immutable once published (409 on republish). Referenced from a namespace as name@version. Pre-release versions (e.g. 1.0.0-rc.1) may be assigned only to the dev namespace; assigning a pre-release profile to prod (or any gated namespace) is refused 422, consistent with the pre-release restriction on compositions — a production compliance posture must ride a released profile.",
      "examples": ["1.0.0", "2.1.0"]
    },
    "description": {
      "type": "string",
      "description": "What compliance posture this profile encodes, for the human assigning or loosening it."
    },
    "controls": {
      "type": "object",
      "additionalProperties": false,
      "required": ["audit_record", "chain", "signing", "trusted_time"],
      "description": "The tamper-evidence / non-repudiation / provenance / unwind-governance controls this profile enforces. Each is a boolean whose stricter direction is false < true (enabling a control is a tightening). See the control partial order in $defs/controlOrder. Capability tokens: an implementation advertises which of these it SUPPORTS as a hyphenated token at GET /v1/meta capabilities — audit_record -> 'audit-record', chain -> 'chain', signing -> 'signing', trusted_time -> 'trusted-time', and the retention block -> 'retention-policy'. (strict_unwind deliberately advertises NO capability token and gates no conformance band — plan decision D9 / SS-GOV-70: it is namespace governance over an always-present engine behavior, not an optional capability.) The document field names here are snake_case (consistent with every other spec field); the advertised tokens are the hyphenated forms.",
      "properties": {
        "audit_record": {
          "type": "boolean",
          "default": false,
          "description": "AU-2/AU-12: a retained, queryable audit record is kept for every auditable action (the pull system of record). Stricter direction: false < true."
        },
        "chain": {
          "type": "boolean",
          "default": false,
          "description": "Hash-chained tamper-evidence: audit events carry seq + prev_hash so a verifier can prove no interior record was altered or dropped. Requires audit_record. Stricter direction: false < true."
        },
        "signing": {
          "type": "boolean",
          "default": false,
          "description": "Non-repudiable signed records and periodic signed checkpoints (seq + head-hash) that prove the chain head against tail-truncation. Requires chain (a checkpoint signs a chain head). Stricter direction: false < true."
        },
        "trusted_time": {
          "type": "boolean",
          "default": false,
          "description": "AU-8: audit timestamps are bound to a trusted time source (e.g. RFC 3161). Requires audit_record. Stricter direction: false < true."
        },
        "strict_unwind": {
          "type": "boolean",
          "default": false,
          "description": "0.10.0 (preflight+unwind-policy epic, plan D9): when true, this namespace forces strict unwind behavior (halt-at-first-non-confirmed-undo, see composition.schema.json's unwind_mode) even for a composition that declares unwind_mode: best_effort — the namespace-level governance override for a regulated-prod posture that always wants a human to look at an incomplete unwind rather than let the engine skip past it (a dev namespace typically leaves this false so opportunistic best_effort cleanup runs as declared). Absent means false, the loosest/default setting: a composition's own declared unwind_mode governs unmodified. Stricter direction: false < true (this control only ever tightens a composition's declared mode, never loosens it — it cannot turn a strict-declared composition best_effort). Enforcing the override emits audit action execution.unwind.override. Existing profile documents that omit this field are unaffected and continue to validate — the control's absence is exactly its loosest value, per $defs/controlOrder's forward-compatibility rule."
        }
      }
    },
    "audit_failure": {
      "type": "string",
      "enum": ["fail_open", "fail_closed"],
      "description": "AU-5 response to an audit-write failure. Applies to ALL auditable actions, not only executions. fail_open: an action proceeds even if its audit event cannot be persisted. fail_closed: an action whose audit event cannot be persisted is refused — for an execution, 503 before it starts, and mid-execution the step fails into the normal failure contract (resumable once the audit store recovers, so no new terminal state); for an administrative action (publication, policy or profile change, principal creation), the request is refused 503 and no state changes. Stricter direction: fail_open < fail_closed."
    },
    "retention": {
      "type": "object",
      "additionalProperties": false,
      "required": ["execution_min_days"],
      "description": "Retention policy (AU-11). Optional as a whole; when the retention key is absent there is no enforced minimum retention (the loosest setting, equivalent to execution_min_days 0). When present it must carry execution_min_days (an empty retention object is not permitted, so the partial-order value is never undefined).",
      "properties": {
        "execution_min_days": {
          "type": "integer",
          "minimum": 0,
          "description": "Minimum number of days execution documents are retained before they may be purged. A higher floor is stricter (stricter direction: smaller < larger; an absent retention block is equivalent to 0). Purging an execution document never breaks chain verification: chain events reference executions by digest only and are not purged within the retention window."
        }
      }
    }
  },
  "allOf": [
    {
      "$comment": "chain requires audit_record — a hash chain is over the audit records.",
      "if": {
        "required": ["controls"],
        "properties": {
          "controls": {
            "required": ["chain"],
            "properties": { "chain": { "const": true } }
          }
        }
      },
      "then": {
        "properties": {
          "controls": {
            "required": ["audit_record"],
            "properties": { "audit_record": { "const": true } }
          }
        }
      }
    },
    {
      "$comment": "signing requires chain — a signed checkpoint proves a chain head.",
      "if": {
        "required": ["controls"],
        "properties": {
          "controls": {
            "required": ["signing"],
            "properties": { "signing": { "const": true } }
          }
        }
      },
      "then": {
        "properties": {
          "controls": {
            "required": ["chain"],
            "properties": { "chain": { "const": true } }
          }
        }
      }
    },
    {
      "$comment": "trusted_time requires audit_record — there is nothing to timestamp without a record.",
      "if": {
        "required": ["controls"],
        "properties": {
          "controls": {
            "required": ["trusted_time"],
            "properties": { "trusted_time": { "const": true } }
          }
        }
      },
      "then": {
        "properties": {
          "controls": {
            "required": ["audit_record"],
            "properties": { "audit_record": { "const": true } }
          }
        }
      }
    }
  ],
  "$defs": {
    "controlOrder": {
      "description": "NORMATIVE control partial order — the machine-decidable rule the namespace assignment ratchet applies. This is a documentation anchor, not a validated instance shape (nothing $refs it); the rule is stated here, in a normative annotation validators surface, so every implementation diffs identically. Each control has a total order on its own values: booleans false < true; audit_failure fail_open < fail_closed; retention.execution_min_days smaller < larger (an absent retention block == 0). The profile order is the componentwise PRODUCT order over the controls {audit_record, chain, signing, trusted_time, strict_unwind, audit_failure, retention.execution_min_days}: profile B is at-least-as-strict as profile A (B >= A) iff B's value is >= A's value for EVERY control. Assignment change A -> B is a TIGHTENING (applies immediately on PUT) iff B >= A; otherwise it is a LOOSENING (some control is weaker, or the two are incomparable) and must open a profile-change proposal. Equal profiles (B == A) are a no-op tightening. A first assignment from unassigned is a tightening from the bottom element (all controls at their loosest), hence always immediate. FORWARD-COMPATIBILITY: when comparing profiles that conform to different spec_versions, a control field absent from one profile because its schema predates that control is treated as the control's loosest value (false / fail_open / 0), so an upgrade that adds a control and enables it is a well-defined tightening (strict_unwind, added 0.10.0, is the newest instance of this rule).",
      "$comment": "Rule text lives in description (above) because JSON Schema 2020-12 requires validators to ignore $comment; the partial order is normative and must be visible to schema processors."
    },
    "controlPreconditions": {
      "description": "NORMATIVE identity precondition for the `signing` control — another documentation anchor, not a validated instance shape (nothing $refs it, same convention as $defs/controlOrder above). `signing`'s non-repudiation claim is only as strong as the identity behind each signature: spec/audit-event.schema.json's $defs/signatureEnvelope.signer field already states the normative MUST — an implementation enforcing 21 CFR Part 11 §11.50 or AU-10 non-repudiation MUST ensure `signer` maps unambiguously to a specific human signatory (a bare key fingerprint requires an external key-to-person registry the implementation may not have). This $def restates the consequence for the two moments a `signing`-enforcing profile actually governs an action: (1) ASSIGNMENT — a namespace-profile assignment (or a profile-change decision) that activates a profile with `controls.signing: true` MUST be refused, not silently applied, when the implementation cannot resolve a real per-human identity for the decision (e.g. access control is disabled entirely, so every caller is an anonymous superuser); (2) DECISION — any decision `signing` governs (an approval-gate decision, a profile-change decision, a publication decision, ...) MUST be refused rather than signed when the deciding principal itself carries no per-human identity (the bootstrap root/superuser principal is the reference implementation's own example of such a principal: it authenticates the caller as a trusted operator, but is not itself a human signatory record). A profile whose `signing` is `false` has no such precondition — the control simply does not activate, at any assignment or decision, regardless of identity resolution. This is additive normative text: it constrains implementations, not the wire shape of a profile document, and introduces no new required field."
    }
  }
}
