{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://actum.run/spec/publication.schema.json",
  "title": "Publication Resource",
  "description": "An auditable record that makes one immutable composition version runnable in one namespace. Subsumes draft <= 0.6's promotion record, the version-level yank verb, and per-namespace visibility. Created by proposing; a human decision (approve/reject) resolves it when policy requires sign-off, otherwise it activates immediately (dev auto-activate). Retained after it reaches a terminal state so the publication history of a name in a namespace is enumerable. See access-control.md.",
  "type": "object",
  "required": [
    "spec_version",
    "id",
    "namespace",
    "composition",
    "state",
    "content_hash",
    "created_at"
  ],
  "properties": {
    "spec_version": {
      "type": "string",
      "description": "The spec version this record conforms to."
    },
    "id": {
      "type": "string"
    },
    "namespace": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_-]{0,31}$",
      "description": "The target namespace this publication makes the version runnable in (a semantic key; the URL is rendered under links.namespace). In 0.x this is 'dev' or 'prod'.",
      "examples": ["prod"]
    },
    "composition": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_]*@.+$",
      "description": "The composition being published, as name@version (a semantic key; the URL is rendered under links.composition). A pre-release version may be published only to the authoring namespace (dev); publishing it elsewhere is refused 422 prerelease_not_publishable.",
      "examples": ["create_issue_with_labels@1.2.0"]
    },
    "state": {
      "enum": ["proposed", "active", "rejected", "withdrawn", "yanked"],
      "description": "proposed -> active (approved; the version is now runnable in the namespace) | rejected (declined) | withdrawn (proposer cancelled). active -> yanked (taken out of service; no new executions, in-flight resume/unwind unaffected). rejected, withdrawn, and yanked are terminal; active is terminal except for yank. When no sign-off is required the record is created directly in active (dev auto-activate)."
    },
    "content_hash": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$",
      "description": "The content hash of the exact bytes being published, as lowercase hex SHA-256. Approval binds to this hash; because publication is content-hash-verified (build-once-publish-many), the bytes a human approved are byte-for-byte the bytes that run in the namespace. Re-proposing a content_hash already active in the namespace is idempotent and returns the existing record."
    },
    "diff_class": {
      "enum": ["major", "minor", "patch", "initial"],
      "description": "Informational (audit) only, never a gate input: the interface diff class of this version against the most-recently-activated non-yanked publication for this name in this namespace, or 'initial' when none. Computed at propose time. A diff-class gate is deliberately not offered: semver is enforced against the interface only, so a patch version can still change behavior; only a human (or, later, a full behavioral diff) may waive sign-off, never diff_class. See access-control.md."
    },
    "proposed_by": {
      "type": "string",
      "description": "The principal that proposed the publication. Present when the implementation enforces access control."
    },
    "approved_by": {
      "type": "string",
      "description": "The principal that approved the publication. Present once state is active. On a decision (state was proposed, activated via decision: approve), MUST differ from proposed_by unconditionally — a proposing principal can never be its own approver, regardless of four_eyes (see the publicationPolicy.four_eyes note below). Equals proposed_by only via the dev auto-activate path (no sign-off required, so no human decision is recorded at all)."
    },
    "decided_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the approve/reject decision was recorded."
    },
    "reason": {
      "type": "string",
      "description": "The decider's note. Required on a reject decision."
    },
    "revision": {
      "type": "string",
      "description": "Opaque concurrency token (ETag). A decision, withdraw, or yank must supply the current revision (If-Match) to guard against lost updates on concurrent transitions."
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "signature": {
      "$ref": "https://actum.run/spec/audit-event.schema.json#/$defs/signatureEnvelope",
      "description": "Present when the target namespace's profile enforces signing: a non-repudiable signature over the approve/reject decision (meaning 'publication.approve'/'publication.reject', binds this record's content_hash). One envelope shape shared with profile-change decisions and chain checkpoints."
    },
    "links": {
      "type": "object",
      "description": "Server-rendered navigation URLs (path-absolute, opaque to clients). NOT part of the persisted record: derived on read from the semantic keys, so stored state survives an API-major change. Typically carries self, composition, and namespace.",
      "additionalProperties": {
        "type": "string",
        "format": "uri-reference"
      }
    }
  },
  "if": {
    "properties": { "state": { "const": "rejected" } },
    "required": ["state"]
  },
  "then": {
    "required": ["reason"]
  },
  "$defs": {
    "publicationPolicy": {
      "type": "object",
      "additionalProperties": false,
      "description": "The publication sign-off policy. Lives on the namespace policy (applies to every publication into that namespace) and, additively, on a per-composition policy: the effective policy is the per-flag OR of the two, so a composition may only strengthen the namespace gate, never relax it. Inert in local single-trusted-caller mode. This is the sign-off object carried on the policy documents in access-control.md.",
      "properties": {
        "requires_signoff": {
          "type": "boolean",
          "default": false,
          "description": "When true, a proposed publication activates only after an approve decision. When false (or the object is absent), a proposed publication resolves immediately to active."
        },
        "four_eyes": {
          "type": "boolean",
          "default": false,
          "description": "Historically gated the segregation-of-duties refusal (approver must differ from proposer) on this flag being true. As of the agent-authorship epic, that refusal is unconditional (see approved_by): a proposing principal can never decide its own proposal whenever a human decision is required, whether or not this flag is set, and regardless of how the deciding principal acquired its role (including the admin grant a first-publisher receives). This field is retained for schema/input compatibility with existing policy documents but no longer changes that outcome; only meaningful when requires_signoff is true and access control is enforced."
        }
      }
    }
  }
}
