---
openapi: 3.0.3
info:
  title: TrustGrowth API
  version: v1
  description: TrustGrowth API for public proof data plus authenticated site, keyword,
    content, and audit operations.
servers:
- url: https://trustgrowth.ai
security:
- bearerAuth: []
tags:
- name: Public Proofs
  description: Public machine-readable proof endpoints for proof-eligible sites.
- name: Sites
  description: Site-scoped TrustGrowth API endpoints.
- name: Campaigns
  description: Read-only campaign and mission-control endpoints. Requires the `campaigns`
    plan feature (Growth/Admin).
- name: MCP
  description: MCP discovery endpoints for the TrustGrowth API.
paths:
  "/api/v1/proofs/{slug}":
    get:
      operationId: getPublicProof
      tags:
      - Public Proofs
      summary: Get public proof data
      description: Returns the canonical machine-readable proof payload for a proof-eligible
        public site.
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/PublicProofResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites":
    get:
      operationId: listSites
      tags:
      - Sites
      summary: List sites
      description: Returns the authenticated user's sites with current overall scores.
      security:
      - bearerAuth: []
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: Page number.
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
        description: Items per page.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/SitesIndexResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/score":
    get:
      operationId: getSiteScore
      tags:
      - Sites
      summary: Get site score
      description: Returns the raw current or historical daily-score breakdown for
        a site. Operator workflows should prefer the publication evidence packet's
        operator_summary contract when they need the operator-safe score snapshot.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: date
        in: query
        required: false
        schema:
          type: string
          format: date
        description: Optional score date in YYYY-MM-DD format.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ScoreResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/issues":
    get:
      operationId: listSiteIssues
      tags:
      - Sites
      summary: List site issues
      description: Returns site audit issues. Open issues default to the latest actionable
        scope used by summary and next_actions; pass scope=backlog to inspect the
        full open backlog.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - open
          - fixed
          - all
        description: Issue status filter.
      - name: scope
        in: query
        required: false
        schema:
          type: string
          enum:
          - actionable
          - backlog
        description: Issue scope. Open issues default to actionable; fixed/all default
          to backlog.
      - name: severity
        in: query
        required: false
        schema:
          type: string
          enum:
          - critical
          - warning
          - info
        description: Issue severity filter.
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: Page number.
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
        description: Items per page.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AuditIssue"
                  meta:
                    "$ref": "#/components/schemas/Meta"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/snapshots":
    get:
      operationId: listSiteSnapshots
      tags:
      - Sites
      summary: List site snapshots
      description: Currently returns not implemented while the snapshots endpoint
        is under development.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: from
        in: query
        required: false
        schema:
          type: string
          format: date
        description: Inclusive start date.
      - name: to
        in: query
        required: false
        schema:
          type: string
          format: date
        description: Inclusive end date.
      - name: granularity
        in: query
        required: false
        schema:
          type: string
          enum:
          - daily
          - weekly
          - monthly
        description: Optional time bucket size.
      responses:
        '501':
          description: Not implemented
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/keywords":
    get:
      operationId: listSiteKeywords
      tags:
      - Sites
      summary: List keyword opportunities
      description: Returns keyword opportunities for a site.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: type
        in: query
        required: false
        schema:
          type: string
          enum:
          - quick_win
          - striking_distance
          - content_gap
          - eeat_gap
        description: Opportunity type filter.
      - name: priority
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 3
        description: Priority filter.
      - name: sort
        in: query
        required: false
        schema:
          type: string
          enum:
          - volume
          - opportunity
        description: Sort order.
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: Page number.
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
        description: Items per page.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/KeywordListResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/competitors":
    get:
      operationId: listSiteCompetitors
      tags:
      - Sites
      summary: List competitors
      description: Returns known competitors ordered by overlap descending.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CompetitorListResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/campaigns":
    get:
      operationId: listSiteCampaigns
      tags:
      - Campaigns
      summary: List campaigns
      description: Returns a paginated index of the site's campaigns with summary
        fields and raw work-item planning-status counts. Requires the `campaigns`
        plan feature (Growth/Admin).
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: Page number.
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
        description: Items per page.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignsIndexResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    post:
      operationId: createSiteCampaign
      tags:
      - Campaigns
      summary: Create a campaign
      description: Creates a campaign directly (not through the commands endpoint)
        and writes its sequence-1 `create_campaign` event atomically with the row.
        Status always starts `draft`. The acting identity is always derived from the
        authenticated API key's owner, never from the request body. Requires the `campaigns`
        plan feature (Growth/Admin) and a `write`-scoped API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '201':
          description: created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignCreateResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CampaignCreateRequest"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}":
    get:
      operationId: getSiteCampaign
      tags:
      - Campaigns
      summary: Get campaign detail
      description: 'Returns full campaign fields (positioning + hypothesis block:
        audience, opportunity, intended_behavior, evidence_basis, hypothesis, baseline)
        plus every work item with its planning/hold/lock state and projected execution
        state. Campaigns are addressed by uid, matching the SiteAudit/ContentStrategy
        child-resource convention. Requires the `campaigns` plan feature (Growth/Admin).'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignDetailResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}/mission_control":
    get:
      operationId: getSiteCampaignMissionControl
      tags:
      - Campaigns
      summary: Get campaign mission control snapshot
      description: 'Returns the campaign''s mission-control read model: the single
        live gate (blocked item, pending review, or missing approved strategy — never
        more than one, never persisted), the waiting-on-you review queue, per-execution-state
        counts, the full annotated item list with resolved owner, `at_risk` — Campaigns::AtRiskPolicy''s
        findings (runway/overdue_artifact/held/unresolvable_near_end reasons, DEFAULT_RUNWAY_DAYS=2),
        `rules` — every campaign rule''s current enforced state, and `re_review_required`
        — Campaigns::ReReviewProjection''s live sweep of content artifacts whose most
        recent review snapshot predates a deterministic/reviewer rule''s current version
        (slice 7 task 2; V1 does not auto-enqueue re-reviews, this list is surfaced
        for an operator/planner to act on). All computed fresh on every call, never
        persisted. Requires the `campaigns` plan feature (Growth/Admin).'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignMissionControlResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}/commands":
    post:
      operationId: executeSiteCampaignCommand
      tags:
      - Campaigns
      summary: Execute a campaign command
      description: 'Thin envelope over Campaigns::Commands::Execute — see that class
        for the full precondition/locking/idempotency contract. `idempotency_key`
        identifies the request (replays return 200 with `meta.replayed=true`, without
        re-executing, even if the command string differs from the original call).
        `?preview=true` is honored for commands that declare preview support in the
        command registry (currently `reserve_capacity` and `reschedule`) — every other
        precondition gate still runs, but the handler''s read-only preview runs in
        place of the real mutation and nothing is written, so `idempotency_key` is
        not required. The response is a distinct preview envelope (`applied: false`,
        a `preview` payload shaped per command — for `reserve_capacity`: availability,
        requested_slots, feasible, displacement_proposals; for `reschedule`: the recomputed
        cascade of moves an anchor or constraint change would make) instead of the
        usual event resource. A command that doesn''t declare preview support returns
        422 `preview_not_supported`. Planning-lane amendment commands (design §3;
        campaign must be `draft`/`planning` unless noted): `propose_item` (params
        `title`, `item_type` required; optional `angle`/`layer`/`content_type`/owner
        fields) creates a new CampaignWorkItem — the only command that resolves no
        `work_item_uid` of its own. `approve_item` (`human_only`, optional `content_type`
        param) flips `planning_status` to `approved` independent of hold/lock state;
        a `content_piece` item with no `content_type` already on the row and none
        supplied fails `invalid_params`. `cancel_item` flips `planning_status` to
        `cancelled` (terminal) without touching hold/lock/owner fields. `hold` (param
        `hold_condition`) and `release_hold` toggle `hold_state`; `release_hold` re-resolves
        the item''s own `condition`-kind schedule constraints in the same transaction
        (§5). `lock_item` (optional `locked_by`, defaults to the acting actor) and
        `unlock_item` toggle `lock_state`. All four of hold/release_hold/lock_item/unlock_item
        reject a call that wouldn''t change state (`already_held`/`not_held`/`already_locked`/`not_locked`)
        rather than a silent no-op. `change_owner` accepts at most one of `assignee_user_id
        | agent_key | owner_label | owner_role`, always clearing all four owner columns
        first (never leaves a stale owner alongside a new one) — no owner param clears
        ownership entirely. `amend_plan_field` (`human_only`, params `field` + `value`)
        amends one whitelisted campaign field; `field` must be in `PLANNING_LANE_FIELDS`
        (identity/hypothesis/dates/baseline — rejected with `field_not_amendable_in_status`
        once the campaign leaves the planning lane; `submit_plan_revision` is the
        only path once approved) or `ACTIVE_FIELDS` (the learning-contract set `result`/`lessons`/`attribution_notes`/`product_findings`,
        amendable in both the planning lane and `active`). `reserve_capacity` (params
        `week_start`, `channel`, `content_type`, `slots`; preview payload described
        above) reserves a CampaignCapacityReservation via Site::CapacityLedger, subject
        to availability; `release_capacity` (param `reservation_uid`) returns a reservation
        to `released`, rejecting an already-released one (`not_active`) or an unknown
        uid (`reservation_not_found`). `grant_exception` (`human_only`, requires a
        non-blank `rationale`) is the operator-approved over-quota reservation path
        — same params/shape as `reserve_capacity` but skips the availability check
        (`enforce: false`), still rejecting a duplicate active reservation tuple.
        Not yet available this slice (return 422 `command_not_available` naming the
        intended future slice — see CampaignCommandRequest''s `command` enum description):
        `request_changes` and `regenerate_draft` already exist as dedicated REST actions
        (`POST .../review_queue/{review_uid}/request_changes`, `POST .../content/{entry_uid}/regenerate_draft`)
        rather than through this endpoint; `rebuild_package` and `split_lane` have
        no implementation or dedicated endpoint yet. Beyond the planning-lane amendment
        commands, this slice also implements `materialize` (resolves an approved work
        item''s `publish` milestone onto a real `ContentCalendarEntry.scheduled_date`,
        campaign must be `active`), `reschedule` (moves a campaign''s anchor date
        or a single work item''s schedule constraint, subject to lock state), and
        `lock_schedule`/`unlock_schedule` (locks or unlocks a work item''s schedule
        constraint against further cascade moves). Dependencies (design §3): `add_dependency`
        (params `prerequisite_uid`, `dependent_uid`, `dep_type` — one of `publication_of
        | evidence_from | asset_from | sequence`) creates a directed edge between
        two of this campaign''s work items, rejecting self-edges, cross-campaign edges,
        duplicate edges, and any edge that would create a cycle (`dependency_cycle`);
        `remove_dependency` (params `dependency_uid`) destroys an edge but rejects
        (`dependency_in_use`) while any `predecessor_event_offset` schedule constraint
        still references it — an edge is never left dangling and never silently cascade-deleted.
        Both run in the planning lane AND once the campaign is active (a deliberate
        deviation from the amendment commands above, which stop applying once a plan
        revision is approved — the plan graph must stay editable post-approval so
        §5''s re-resolution triggers keep working). `resolve_schedule` (no work item,
        campaign must be `active`) re-runs `Campaigns::ResolveSchedule` on demand,
        the same resolver an approved plan revision and a dependency''s publication
        wakeup already trigger automatically — idempotent, and a no-op for already-resolved
        or locked constraints. A dependent work item''s `predecessor_event_offset`
        constraint only ever resolves once the prerequisite''s content actually publishes
        (an `external_publication_recorded` system event, never issued through this
        endpoint — see CampaignCommandRequest''s `command` enum description); moving
        an unpublished prerequisite never manufactures a date for its dependents.
        Rules (design §3, slice 7 tasks 1-2): `add_rule` (params `rule_key`, `enforcement_mode`
        — one of `deterministic | reviewer | human | advisory`, `body`, and `lint_spec`
        — required only when `enforcement_mode` is `deterministic`, forbidden for
        every other mode, validated against Campaigns::LintSpecSchema''s closed check
        vocabulary of `character_count | required_link | forbidden_phrase`, each check
        stage-scoped to `submit_draft` or `package`) creates a new CampaignRule plus
        its version 1 atomically, rejecting a duplicate `rule_key` on the same campaign
        with a typed `rule_exists`. `amend_rule` (params `rule_key` plus at least
        one of `enforcement_mode`/`body`/`lint_spec`) appends version n+1 as a FULL
        SNAPSHOT — every field not explicitly passed is forward-carried verbatim from
        the current version, never diffed — against an unknown `rule_key` returns
        `rule_not_found`, and its response''s `new_value`/`cascade_preview` both carry
        `re_review_required`: the SAME `Campaigns::ReReviewProjection` findings (scoped
        to just the amended rule) that also surface campaign-wide on `GET .../mission_control`''s
        `re_review_required` — V1 never auto-enqueues those re-reviews; an operator/planner
        acts on the list. Both rule commands are campaign-level (no `work_item_uid`),
        agent-permitted (not `human_only`), and — like the dependency commands above
        — run in the planning lane AND once the campaign is active. Enforcement is
        mode-specific: `deterministic` rules are a hard lint gate at draft submission,
        before any LLM review ever runs (a failure returns `campaign_lint_failed`
        and never enqueues a `ReviewAgent` run — see `POST .../content/{entry_uid}/submit_draft`);
        `reviewer` rules are injected into the LLM review''s own prompt context; `human`
        rules surface as read-only gate context on the review-queue item (`campaign_human_rules`
        — see `GET /review_queue`); `advisory` rules are excluded from every automated
        surface (informational only). Assets (design §3, slice 9 tasks 2-3): `add_asset_requirement`
        (params `asset_type` — one of `chart | screenshot | terminal_recording | cover
        | og_card | link_artifact`, `stage` — `submit_draft | package`, `source_requirement`)
        creates a CampaignAssetRequirement on the given `work_item_uid` (must be a
        `content_piece` item); `remove_asset_requirement` (params `requirement_uid`,
        self-resolving — no `work_item_uid`) archives it (`archived_at` set, never
        destroyed — history including past fulfillments survives; a removed requirement''s
        derived status reports `archived` from then on, and any still-selected fulfillment''s
        asset stays exactly as approved/pending as it was). Creating a requirement
        carries no candidate/selection of its own — selecting a MediaAsset against
        it (and replacing that selection later) is `replace_evidence` (params `requirement_uid`,
        `media_asset_uid`, self-resolving — no `work_item_uid`): atomically supersedes
        the requirement''s currently selected fulfillment and selects the new one
        under a requirement row lock (`Campaigns::SelectFulfillment`), rejecting a
        `media_asset_uid` that isn''t site-scoped-and-assigned (`MediaAsset#site_scope_status
        == assigned`) to the campaign''s own site. Selecting (via `replace_evidence`,
        or the initial selection made when a requirement''s first fulfillment is proposed)
        opens a `campaign_asset_approval` review-queue item unless the MediaAsset
        is already approved (see `GET /review_queue` and the review-transition endpoints
        below) — approving that item is what actually marks the requirement `fulfilled`;
        this commands endpoint never sets asset approval itself. All three asset commands
        run in the planning lane AND once the campaign is active (assets keep changing
        throughout production, same rationale as the dependency/rule commands above).
        Metrics (design §3, slice 9 task 4): `add_metric_definition` (params `metric`,
        `source` — one of `gsc | social | visibility_index | proof_page | custom`,
        `value_type` — one of `count | rate | score | duration | snapshot`, optional
        `unit`/`windows` — a subset of `[3, 7, 14, 28]`/`baseline`/`exclusive`) creates
        a CampaignMetricDefinition declaring WHAT is measured, rejecting a duplicate
        `metric` name on the same campaign with a typed `metric_definition_exists`;
        no remove/amend command exists this slice (a wrong definition is a documented
        V1 gap). The actual observed values are written through the separate `POST
        .../metric_observations` endpoint below, never through this commands endpoint
        — an observation is measurement data, not a plan mutation, so no CampaignEvent
        is written for it and no `idempotency_key` is required there. Completion (design
        §3/§4, slice 9 task 4): `complete_campaign` (campaign-level, no `params`,
        `human_only` — an agent actor is rejected) transitions an `active` campaign
        to `completed`, gated on the learning contract''s `result` (non-empty jsonb)
        and `lessons` (non-blank text) already being set on the campaign row — both
        fields have no amend command of their own (`AmendPlanField`''s whitelist deliberately
        excludes them; they must be populated by a direct write before this command
        is called) — an incomplete contract returns `learning_contract_incomplete`.
        No item-state precondition: unfinished/held/blocked work items never block
        completion (an explicit V1 policy call, not inferred — mission control already
        surfaces that state for the human deciding whether to close). Provenance (design
        §13, slice 10 task 4): `link_existing_entry` (params `entry_uid`) attaches
        an ALREADY-EXISTING `ContentCalendarEntry` (content that predates Campaigns
        entirely — the operator-run dogfood migration''s primary write) to the given
        `work_item_uid` as pure provenance, mirroring `materialize`''s gates (campaign
        must be `active`, item must be `approved`) but creating no new entry and never
        touching `scheduled_date`; re-linking the same already-linked pair is an idempotent
        no-op success, while linking an entry already linked to a DIFFERENT work item
        returns `entry_already_linked`, and an unknown or cross-tenant `entry_uid`
        returns `entry_not_found`. `expected_lock_version` (optimistic lock on the
        target work item) and every command-specific field belong in `params`/at the
        top level per CampaignCommandRequest; the acting identity always comes from
        the authenticated API key''s owner, never the request body. Error codes are
        passed through verbatim from the executor; only the HTTP status (409 for stale_object/campaign_status_not_permitted/item_status_not_permitted,
        422 for everything else) is decided at this layer. Requires the `campaigns`
        plan feature (Growth/Admin) and a `write`-scoped API key.'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignCommandResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Conflict — stale optimistic lock or campaign/item state does
            not permit this command
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params, unknown/not-yet-available command, or preview_not_supported
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CampaignCommandRequest"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}/submit_plan":
    post:
      operationId: submitSiteCampaignPlan
      tags:
      - Campaigns
      summary: Submit the campaign's plan for review
      description: Sugar over the `submit_plan_revision` command — same idempotency
        contract as /commands. Freezes an immutable plan-revision snapshot, opens
        (or supersedes) a `campaign_plan_approval` review-queue item, and moves the
        campaign to `pending_review`. `next_step` points at the existing `POST /review_queue/{review_uid}/approve`
        endpoint — approving, requesting changes on, or rejecting the submission all
        go through that unchanged endpoint, not a campaigns-specific route. Requires
        the `campaigns` plan feature (Growth/Admin) and a `write`-scoped API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignSubmitPlanResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Conflict — campaign status does not permit submission
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params, or idempotency_key_reused — the idempotency_key
            was already used for a different command and does not name a submit_plan_revision
            event
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CampaignSubmitPlanRequest"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}/metric_observations":
    post:
      operationId: recordSiteCampaignMetricObservation
      tags:
      - Campaigns
      summary: Record a campaign metric observation
      description: 'Sole write path for CampaignMetricObservation (design §3 `campaign_metric_definitions`/`campaign_metric_observations`,
        slice-9 plan ruling P7 "metrics API-write-only V1"). NOT routed through `POST
        .../commands`/`Campaigns::Commands::Execute` — an observation is measurement
        data, not a plan mutation, so no `CampaignEvent` is written and no `idempotency_key`
        is required or accepted here. Idempotency instead comes from the write itself:
        `metric_definition_uid` + `window` + `source_ref` together identify one observation
        row (unique DB index) — POSTing the same triple again UPDATES that row (`Campaigns::RecordMetricObservation`''s
        upsert, row-locked on the definition to serialize concurrent writers) rather
        than creating a duplicate, returning 200 instead of 201 and never an error.
        `metric_definition_uid` must resolve a CampaignMetricDefinition belonging
        to THIS campaign (404-shaped `metric_definition_not_found` otherwise, created
        first via `add_metric_definition` on `POST .../commands`). Exactly one of
        `numeric_value`/`text_value`/`structured_value` must be populated, matching
        the definition''s `value_type` (`count`/`rate`/`score`/`duration` → `numeric_value`;
        `snapshot` → `structured_value`; `text_value` is permanently dark in V1 —
        no `value_type` maps to it) — a mismatch is a 422 `invalid_params` carrying
        the model''s own validation messages. `window` must be an integer from `CampaignMetricDefinition::ALLOWED_WINDOWS`
        (`3`/`7`/`14`/`28`) and, if the definition declares any `windows`, one of
        those specifically. `observed_at` must be an ISO8601 timestamp. `attribution_context`
        is a free-form jsonb bag a caller may stamp with the campaign''s own `attribution_key`
        (see `Campaign#attributed_entries`) for a future collector to join against
        — not itself validated or enforced as a join key in V1. Requires the `campaigns`
        plan feature (Growth/Admin) and a `write`-scoped API key.'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: Observation updated (idempotent upsert of an existing (definition,
            window, source_ref) triple)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignMetricObservationResponse"
        '201':
          description: Observation created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignMetricObservationResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params, or metric_definition_not_found — metric_definition_uid
            does not resolve a definition on this campaign
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CampaignMetricObservationRequest"
  "/api/v1/sites/{slug}/campaigns/{campaign_uid}/planning_sessions":
    post:
      operationId: startSiteCampaignPlanningSession
      tags:
      - Campaigns
      summary: Start or reopen a campaign planning session
      description: 'Creates a new active CampaignPlanningSession for the campaign,
        or — if one is already active — returns THAT session idempotently (`meta.existing:
        true`), never an error (design §3 policy 1; Codex amendment "one-active session").
        A session owns exactly one Chat, created alongside it — reuses the existing
        Chat/Message infrastructure wholesale (design §3 campaign_planning_sessions
        bullet: "no bespoke transcript store"); Chat itself stays campaign-agnostic,
        the linkage lives on the session row only. Reopening after a session has ended
        writes a NEW session row rather than mutating the ended one. Campaign must
        be `draft`, `planning`, or `active` — a campaign already `pending_review`/`completed`/`cancelled`
        cannot start a new planning conversation (409 `campaign_status_not_permitted`).
        The acting identity (`started_by_type`/`started_by_id`) is always derived
        from the authenticated API key''s owner, never from the request body. Requires
        the `campaigns` plan feature (Growth/Admin) and a `write`-scoped API key.'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: campaign_uid
        in: path
        required: true
        schema:
          type: string
        description: Campaign UID returned by the campaigns index endpoint.
      responses:
        '200':
          description: ok (existing active session returned idempotently)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignPlanningSessionResponse"
        '201':
          description: created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignPlanningSessionResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Conflict — campaign status does not permit starting a planning
            session
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/planning_sessions/{session_uid}":
    get:
      operationId: getSiteCampaignPlanningSession
      tags:
      - Campaigns
      summary: Get a campaign planning session and its transcript
      description: 'Returns the planning session plus its full transcript: every Message
        on the session''s Chat (role/content/created_at/kind), in chronological order.
        This is the complete audit history — never truncated; the bounded-send-to-the-LLM
        window and prompt-size bounds are a planner read-tool concern (Task 2/4),
        not a read-API concern here. `kind` (slice-10 task 1) is non-null only on
        a turn-status message CampaignPlanningAgent appended after a failed or skipped
        turn (`"campaign_planning_turn_failed"`/`"campaign_planning_turn_skipped"`)
        — null on every ordinary user/assistant message; a poller can use it to distinguish
        a real failure state from a normal reply without content-sniffing the human-readable
        text. Planning sessions carry no site_id of their own — this lookup is tenant-scoped
        through the owning campaign''s site. Requires the `campaigns` plan feature
        (Growth/Admin).'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: session_uid
        in: path
        required: true
        schema:
          type: string
        description: Planning session UID returned by the start-planning-session endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignPlanningSessionResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/planning_sessions/{session_uid}/end":
    post:
      operationId: endSiteCampaignPlanningSession
      tags:
      - Campaigns
      summary: End a campaign planning session
      description: Transitions an active planning session to `ended`. Idempotent —
        ending an already-ended session is a no-op that still returns 200 with the
        session's current state, never an error. Requires the `campaigns` plan feature
        (Growth/Admin) and a `write`-scoped API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: session_uid
        in: path
        required: true
        schema:
          type: string
        description: Planning session UID returned by the start-planning-session endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignPlanningSessionResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/planning_sessions/{session_uid}/messages":
    post:
      operationId: sendSiteCampaignPlanningSessionMessage
      tags:
      - Campaigns
      summary: Send a message to the campaign planner
      description: Persists the user's message on the session's own Chat and enqueues
        a CampaignPlanningAgent turn through the existing AgentJob infrastructure
        (async — never holds the request through the agent's tool loop; Codex P4 amendment).
        Returns 202 immediately with the persisted message id and the AgentRun's uid
        as the run reference. The planner's reply (or, on a gate/error, an idempotent
        skip/failure message) lands on the session's transcript asynchronously — poll
        `GET .../planning_sessions/{session_uid}` until it appears; this endpoint
        never fabricates assistant prose itself. Requires the session to be `active`
        (409 `planning_session_ended` otherwise) and the campaign to still be `draft`/`planning`/`active`
        (409 `campaign_status_not_permitted` otherwise). Only one turn may be in flight
        per session at a time — a second message sent before the first turn's reply
        lands returns 409 `turn_in_flight` without enqueueing a second job or persisting
        a second message. Consumes one `campaign_planning` credit action (quoted/checked
        synchronously here — 402 `insufficient_credits` if the balance is too low
        — and charged once per turn by the job itself, refunded automatically if the
        turn fails). Requires the `campaigns` plan feature (Growth/Admin) and a `write`-scoped
        API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: session_uid
        in: path
        required: true
        schema:
          type: string
        description: Planning session UID returned by the start-planning-session endpoint.
      responses:
        '202':
          description: accepted — turn queued
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CampaignPlanningSessionMessageResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '402':
          description: Payment required — insufficient credits
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Conflict — session ended, campaign status does not permit a
            turn, or a turn is already in flight for this session
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params — content is required
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CampaignPlanningSessionMessageRequest"
  "/api/v1/sites/{slug}/content":
    get:
      operationId: listSiteContent
      tags:
      - Sites
      summary: List content pipeline entries
      description: Returns active content pipeline entries by default with brief and
        publishing state. Use `status=all` for explicit inventory analysis that includes
        archived rows and legacy entries triaged out of the active pipeline.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - active
          - scheduled
          - draft
          - published
          - all
          default: active
        description: "`active` (default) excludes archived rows and rows triaged to
          `archive`/`delete_candidate`. `all` includes them for inventory analysis."
      - name: content_type
        in: query
        required: false
        schema:
          type: string
        description: Content type filter.
      - name: triage_state
        in: query
        required: false
        schema:
          type: string
          enum:
          - triage_needed
          - current_cycle
          - unreviewed
          - keep_publish
          - regenerate
          - repurpose
          - archive
          - delete_candidate
        description: Optional triage filter. `triage_needed` matches the effective
          stale/unreviewed queue, `current_cycle` matches automation-ready cycle rows,
          and explicit triage values match `effective_triage_state` (so `unreviewed`
          rows may still have `triage_state=null`). Matching semantics are echoed
          in `meta.filters.triage_state`.
      - name: from
        in: query
        required: false
        schema:
          type: string
          format: date
        description: Inclusive scheduled date lower bound.
      - name: to
        in: query
        required: false
        schema:
          type: string
          format: date
        description: Inclusive scheduled date upper bound.
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: Page number.
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
        description: Items per page.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ContentListResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/review_queue":
    get:
      operationId: listSiteReviewQueue
      tags:
      - Sites
      summary: List review queue items
      description: 'Returns the site''s active review queue items so operators can
        find the review UID and the linked content entry before approving, requesting
        changes, or rejecting. Each item''s `campaign_human_rules` lists the applicable
        human-mode campaign rules (rule_key, current version number, body) for campaign-linked
        content — empty for non-campaign content (slice 7 task 4, design §3 policy
        5: human rules map to review-queue gates, not a new review type). `review_type:
        "campaign_asset_approval"` items (slice 9 tasks 2-3, design §3 `campaign_asset_fulfillments`)
        are opened whenever a CampaignAssetFulfillment is selected against an as-yet-unapproved
        MediaAsset (`Campaigns::SelectFulfillment` → `ReviewQueueItem.sync_for!`)
        — for these, `reviewable.type` is `"CampaignAssetFulfillment"`, `reviewable.status`
        is the fulfillment''s own status (`candidate | selected | superseded | rejected`,
        not the MediaAsset''s `approval_status`), and `reviewable.entry_uid`/`reviewable.title`
        are always null (a fulfillment has neither). Slice-10 task 1 adds `campaign_asset`
        (present only for `campaign_asset_approval` items, null otherwise) so a review-canvas
        card never has to make a second round trip to the campaign detail endpoint
        just to render itself: `requirement` (uid/asset_type/stage), `work_item` (uid/title),
        `campaign` (uid/key), and `media_asset` (uid/kind/source/usage_context/alt_text/source_url
        — cheaply-available columns only, never the ActiveStorage file/blob). An asset
        already approved by the time it''s selected never opens an item at all (skips
        straight to fulfilled).'
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ReviewQueueListResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/content/{entry_uid}":
    get:
      operationId: getContentEntry
      tags:
      - Sites
      summary: Get a content entry and its current draft
      description: Returns one site-owned content entry by entry, brief, or draft
        UID. The nested brief includes its complete persisted outline, and the nullable
        nested draft contains the exact persisted current draft title and body plus
        its UID, version, status, and update timestamp.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: entry_uid
        in: path
        required: true
        schema:
          type: string
        description: Content entry UID returned by the content endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ContentDetailResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    patch:
      operationId: updateContentEntry
      tags:
      - Sites
      summary: Update campaign scheduling metadata
      description: Updates only `schedule_intent` and `owner_notes` on an existing
        content entry. Campaign entries use a `schedule_intent` beginning with `campaign:`.
        Requires a `write`-scoped API key and at least one field.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: entry_uid
        in: path
        required: true
        schema:
          type: string
        description: Content entry UID returned by the content endpoint.
      - name: schedule_intent
        in: query
        required: false
        schema:
          type: string
          nullable: true
        description: Scheduling intent; use `campaign:<key>` for cadence-exempt campaign
          content. Blank clears the value.
      - name: owner_notes
        in: query
        required: false
        schema:
          type: string
          nullable: true
        description: Operator notes for the entry. Blank clears the value.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ContentDetailResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/content/{entry_uid}/package_variant":
    get:
      operationId: getPackageContentVariant
      tags:
      - Sites
      summary: Get package content variant
      description: Returns the current packaged body for one content entry channel
        so operators can inspect distribution copy before whole-package or per-channel
        approval. Channel resolution follows the same legacy social-package fallback
        rules as the approved-variant endpoint.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: entry_uid
        in: path
        required: true
        schema:
          type: string
        description: Content calendar entry UID.
      - name: channel
        in: query
        required: true
        schema:
          type: string
        description: Target distribution channel, such as linkedin_post, linkedin,
          twitter_post, x, social, or blog.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/PackageContentVariantResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Invalid state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/content/{entry_uid}/approved_variant":
    get:
      operationId: getApprovedContentVariant
      tags:
      - Sites
      summary: Get approved content variant
      description: Returns the exact approved postable body for a packaged content
        entry channel. `linkedin_post` and `linkedin` resolve to the exact LinkedIn/manual-post
        snapshot for social packages, while `twitter_post`, `twitter`, and `x` resolve
        to the stored social variant when no channel-specific variant exists. Already-published
        rows whose approval state later drifted return a proof-only `already_published_content_variant`
        response instead of exposing current package copy.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: entry_uid
        in: path
        required: true
        schema:
          type: string
        description: Content calendar entry UID.
      - name: channel
        in: query
        required: true
        schema:
          type: string
        description: Target distribution channel, such as linkedin_post, linkedin,
          twitter_post, x, social, or blog.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ApprovedContentVariantResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Approval required
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/eeat":
    get:
      operationId: getSiteEeat
      tags:
      - Sites
      summary: Get E-E-A-T details
      description: Returns pillar totals and individual signal recommendations.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EeatResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/summary":
    get:
      operationId: getSiteSummary
      tags:
      - Sites
      summary: Get site summary
      description: Returns a composite summary of scores, issues, keyword movement,
        content pipeline, and the latest GSC snapshot.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/SummaryResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/publication_evidence_packet":
    get:
      operationId: getSitePublicationEvidencePacket
      tags:
      - Sites
      summary: Get publication evidence packet
      description: Returns a publication-safe operator packet that preserves public-proof
        attribution, operator-only evidence, content publication states, and machine-readable
        safety flags. This is the canonical operator score source via operator_summary
        and operator_summary.score_breakdown.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/PublicationEvidencePacketResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/next_actions":
    get:
      operationId: getSiteNextActions
      tags:
      - Sites
      summary: Get next best actions
      description: Returns prioritized, evidence-backed next actions for the current
        site growth cycle. Audit-issue actions are aggregated by issue_type — one
        action covers all affected pages, listed in evidence_source.pages with the
        total in evidence_source.affected_count.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/NextActionsResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/changes":
    get:
      operationId: getSiteChanges
      tags:
      - Sites
      summary: Get recent site changes
      description: Returns recent issue, score, keyword, content, and GSC deltas for
        a time window.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: since
        in: query
        required: false
        schema:
          type: string
          enum:
          - 1d
          - 7d
          - 14d
          - 30d
        description: Change window. Defaults to `7d`.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ChangesResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/trigger_audit":
    post:
      operationId: triggerSiteAudit
      tags:
      - Sites
      summary: Trigger a manual audit
      description: Queues a manual site audit for Starter or higher plans. Requires
        a `write`-scoped API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      responses:
        '202':
          description: accepted
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TriggerAuditResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/content/{entry_uid}/reschedule":
    patch:
      operationId: rescheduleContentEntry
      tags:
      - Sites
      summary: Reschedule content entry
      description: Moves an unpublished planned, brief-generating, approved-draft,
        or packaged content entry to an explicit calendar date without changing draft,
        package, review, claim, campaign, or publication state. A packaged entry remains
        reschedulable while a replacement draft is under review. Requires a `write`-scoped
        API key.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: entry_uid
        in: path
        required: true
        schema:
          type: string
        description: Content entry UID returned by the content endpoint.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WorkflowMutationResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Invalid state
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Invalid params
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - scheduled_date
              properties:
                scheduled_date:
                  type: string
                  format: date
                  description: The new scheduled date in YYYY-MM-DD format.
  "/api/v1/sites/{slug}/jobs/{job_id}":
    get:
      operationId: getAgentRunStatus
      tags:
      - Sites
      summary: Get agent run status
      description: Returns the current status for a queued or completed agent run.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: job_id
        in: path
        required: true
        schema:
          type: string
        description: Job ID returned by an async workflow response.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/AgentRunStatusResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/sites/{slug}/agent_runs/{job_id}":
    get:
      operationId: getLegacyAgentRunStatus
      tags:
      - Sites
      summary: Get agent run status (legacy alias)
      description: Legacy alias for `/api/v1/sites/{slug}/jobs/{job_id}`.
      security:
      - bearerAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Site slug.
      - name: job_id
        in: path
        required: true
        schema:
          type: string
        description: Job ID returned by an async workflow response.
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/AgentRunStatusResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/mcp/manifest":
    get:
      operationId: getMcpManifest
      tags:
      - MCP
      summary: Get MCP manifest
      description: Returns the TrustGrowth MCP discovery manifest.
      security:
      - bearerAuth: []
      parameters: []
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/McpManifest"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: 'Use a TrustGrowth API key in the Authorization header: `Bearer
        tg_live_...`.'
  schemas:
    ErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
            message:
              type: string
            retry_after:
              type: integer
              nullable: true
          additionalProperties: true
    ContentEntryCreateRequest:
      type: object
      required:
      - title
      - content_type
      - angle
      properties:
        title:
          type: string
        content_type:
          type: string
          enum:
          - blog_post
          - comparison
          - listicle
          - pillar_page
          - tutorial
          - case_study
          - product_update
          - changelog
          - linkedin_post
          - twitter_thread
          - twitter_post
        angle:
          type: string
        scheduled_date:
          type: string
          format: date
          nullable: true
        priority:
          type: integer
          nullable: true
    RecordPublicationRequest:
      type: object
      description: External publication attributes. Omit every field (empty body)
        to bridge-publish article content to the internal blog instead; social records
        must set channel and external_url.
      properties:
        channel:
          type: string
          description: Distribution channel, such as x, twitter_post, linkedin_post,
            linkedin, social, or blog.
        status:
          type: string
          description: Publication status; use published once the content is live.
        external_url:
          type: string
          description: Live URL of the publication. Required for social channels;
            the host must match the channel (for example x.com for channel x).
        external_post_id:
          type: string
          nullable: true
          description: Platform-native post identifier, when known.
        published_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          additionalProperties: true
          nullable: true
    ProofLinks:
      type: object
      required:
      - url
      - short_url
      - api_url
      properties:
        url:
          type: string
          format: uri
        short_url:
          type: string
          format: uri
        api_url:
          type: string
          format: uri
    PublicProofSite:
      type: object
      required:
      - slug
      - name
      - domain
      - language
      - story_mode
      properties:
        slug:
          type: string
        name:
          type: string
        domain:
          type: string
        language:
          type: string
        story_mode:
          type: boolean
        verified_since:
          type: string
          format: date-time
          nullable: true
        published_on:
          type: string
          format: date
          nullable: true
        updated_on:
          type: string
          format: date
          nullable: true
    PublicProofNarrative:
      type: object
      required:
      - title
      - description
      - author_name
      properties:
        title:
          type: string
        description:
          type: string
        author_name:
          type: string
    PublicProofSummary:
      type: object
      required:
      - overall_score
      - score_delta
      - click_growth_pct
      - keyword_count
      - keywords_in_top_10
      - published_content_count
      - geo_score
      - geo_eligible
      - total_clicks
      - total_impressions
      - latest_clicks
      - latest_impressions
      - latest_position
      properties:
        overall_score:
          type: integer
          nullable: true
        score_delta:
          type: integer
        click_growth_pct:
          type: integer
        keyword_count:
          type: integer
        keywords_in_top_10:
          type: integer
        published_content_count:
          type: integer
        geo_score:
          type: integer
          nullable: true
        geo_eligible:
          type: boolean
        total_clicks:
          type: integer
        total_impressions:
          type: integer
        latest_clicks:
          type: integer
        latest_impressions:
          type: integer
        latest_position:
          type: number
          nullable: true
    PublicProofLatestScore:
      type: object
      nullable: true
      properties:
        score_date:
          type: string
          format: date
        overall_score:
          type: integer
          nullable: true
        technical_score:
          type: integer
          nullable: true
        content_score:
          type: integer
          nullable: true
        performance_score:
          type: integer
          nullable: true
        growth_score:
          type: integer
          nullable: true
        eeat_score:
          type: integer
          nullable: true
        eeat_experience:
          type: integer
          nullable: true
        eeat_expertise:
          type: integer
          nullable: true
        eeat_authoritativeness:
          type: integer
          nullable: true
        eeat_trustworthiness:
          type: integer
          nullable: true
    PublicProofScores:
      type: object
      required:
      - latest
      - panels
      - eeat_breakdown
      - trajectory
      properties:
        latest:
          "$ref": "#/components/schemas/PublicProofLatestScore"
        panels:
          type: object
          properties:
            technical:
              type: integer
              nullable: true
            content:
              type: integer
              nullable: true
            performance:
              type: integer
              nullable: true
            growth:
              type: integer
              nullable: true
        eeat_breakdown:
          type: object
          properties:
            experience:
              type: integer
              nullable: true
            expertise:
              type: integer
              nullable: true
            authoritativeness:
              type: integer
              nullable: true
            trustworthiness:
              type: integer
              nullable: true
        trajectory:
          type: array
          items:
            type: object
            required:
            - score_date
            - overall_score
            - technical_score
            - content_score
            - performance_score
            - growth_score
            - eeat_score
            properties:
              score_date:
                type: string
                format: date
              overall_score:
                type: integer
                nullable: true
              technical_score:
                type: integer
                nullable: true
              content_score:
                type: integer
                nullable: true
              performance_score:
                type: integer
                nullable: true
              growth_score:
                type: integer
                nullable: true
              eeat_score:
                type: integer
                nullable: true
    PublicProofTraffic:
      type: object
      required:
      - latest_snapshot
      - weekly_snapshots
      properties:
        latest_snapshot:
          type: object
          nullable: true
          properties:
            snapshot_date:
              type: string
              format: date
            organic_clicks:
              type: integer
              nullable: true
            organic_impressions:
              type: integer
              nullable: true
            average_position:
              type: number
              nullable: true
            indexed_pages:
              type: integer
              nullable: true
        weekly_snapshots:
          type: array
          items:
            type: object
            required:
            - label
            - clicks
            - impressions
            properties:
              label:
                type: string
              clicks:
                type: integer
              impressions:
                type: integer
    PublicProofVisibilityMetric:
      type: object
      required:
      - value
      - raw_value
      properties:
        value:
          type: number
        raw_value:
          type: number
    PublicProofVerifiedRanking:
      type: object
      required:
      - keyword
      - position
      - clicks
      - impressions
      properties:
        keyword:
          type: string
        position:
          type: integer
        clicks:
          type: integer
        impressions:
          type: integer
    PublicProofVisibility:
      type: object
      nullable: true
      required:
      - captured_on
      - metrics
      - verified_rankings
      properties:
        captured_on:
          type: string
          format: date
          nullable: true
        metrics:
          type: object
          required:
          - visibility_index
          - share_of_voice_pct
          - estimated_traffic
          - estimated_traffic_value_usd
          - keywords_ranked
          - top3_count
          - top10_count
          properties:
            visibility_index:
              "$ref": "#/components/schemas/PublicProofVisibilityMetric"
            share_of_voice_pct:
              type: number
            estimated_traffic:
              type: integer
            estimated_traffic_value_usd:
              type: number
            keywords_ranked:
              type: integer
            top3_count:
              type: integer
            top10_count:
              type: integer
        verified_rankings:
          type: array
          items:
            "$ref": "#/components/schemas/PublicProofVerifiedRanking"
    PublicProofIssue:
      type: object
      required:
      - severity
      - issue_type
      - page_url
      - description
      properties:
        severity:
          type: string
        issue_type:
          type: string
        page_url:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
    PublicProofResponse:
      type: object
      required:
      - proof
      - site
      - narrative
      - summary
      - scores
      - traffic
      - visibility
      - issues
      properties:
        proof:
          "$ref": "#/components/schemas/ProofLinks"
        site:
          "$ref": "#/components/schemas/PublicProofSite"
        narrative:
          "$ref": "#/components/schemas/PublicProofNarrative"
        summary:
          "$ref": "#/components/schemas/PublicProofSummary"
        scores:
          "$ref": "#/components/schemas/PublicProofScores"
        traffic:
          "$ref": "#/components/schemas/PublicProofTraffic"
        visibility:
          "$ref": "#/components/schemas/PublicProofVisibility"
        issues:
          type: array
          items:
            "$ref": "#/components/schemas/PublicProofIssue"
    Meta:
      type: object
      required:
      - generated_at
      - plan
      - rate_limit
      properties:
        generated_at:
          type: string
          format: date-time
        plan:
          type: string
        rate_limit:
          type: object
          required:
          - limit
          - remaining
          - reset
          properties:
            limit:
              type: integer
            remaining:
              type: integer
              nullable: true
            reset:
              type: integer
        pagination:
          type: object
          nullable: true
          properties:
            page:
              type: integer
            per_page:
              type: integer
            total_count:
              type: integer
            total_pages:
              type: integer
        replayed:
          type: boolean
          nullable: true
          description: Present and true when a campaigns /commands or /submit_plan
            call was an idempotent replay of an already-recorded event.
    SiteSummary:
      type: object
      required:
      - slug
      - name
      - domain
      - overall_score
      - created_at
      properties:
        slug:
          type: string
        name:
          type: string
        domain:
          type: string
        overall_score:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
    SitesIndexResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/SiteSummary"
        meta:
          "$ref": "#/components/schemas/Meta"
    ScoreValue:
      type: object
      required:
      - value
      - delta_7d
      - delta_30d
      properties:
        value:
          type: integer
          nullable: true
        delta_7d:
          type: integer
          nullable: true
        delta_30d:
          type: integer
          nullable: true
    ScoreEvidence:
      type: object
      nullable: true
      properties:
        present:
          type: integer
          nullable: true
        total:
          type: integer
          nullable: true
        observed_weight:
          type: number
          nullable: true
        confidence_weighted_coverage:
          type: number
          nullable: true
        coverage:
          type: number
          nullable: true
        provisional:
          type: boolean
          nullable: true
        incomplete:
          type: boolean
          nullable: true
        score_state:
          type: string
          nullable: true
        rank_eligible:
          type: boolean
          nullable: true
        missing:
          type: array
          nullable: true
          items:
            type: object
            additionalProperties: true
        uncertainty:
          type: object
          nullable: true
          additionalProperties: true
      additionalProperties: false
    ScoreResponseData:
      type: object
      description: Values come from the published (champion-formula) score. Pillar
        fields answer from the champion's components and are null when the champion
        does not score that signal.
      required:
      - date
      - formula_version
      - formula_uid
      - formula_name
      - score_evidence
      - overall_score
      - eeat
      - content_score
      - growth_score
      - technical_score
      - performance_score
      - authority_score
      - visibility_score
      - geo_score
      properties:
        date:
          type: string
          format: date
        formula_version:
          type: integer
          nullable: true
          description: Version of the champion scoring formula that produced these
            values.
        formula_uid:
          type: string
          nullable: true
        formula_name:
          type: string
          nullable: true
        score_evidence:
          "$ref": "#/components/schemas/ScoreEvidence"
        overall_score:
          "$ref": "#/components/schemas/ScoreValue"
        eeat:
          type: object
          properties:
            score:
              "$ref": "#/components/schemas/ScoreValue"
            experience:
              "$ref": "#/components/schemas/ScoreValue"
            expertise:
              "$ref": "#/components/schemas/ScoreValue"
            authoritativeness:
              "$ref": "#/components/schemas/ScoreValue"
            trustworthiness:
              "$ref": "#/components/schemas/ScoreValue"
        content_score:
          "$ref": "#/components/schemas/ScoreValue"
        growth_score:
          "$ref": "#/components/schemas/ScoreValue"
        technical_score:
          "$ref": "#/components/schemas/ScoreValue"
        performance_score:
          "$ref": "#/components/schemas/ScoreValue"
        authority_score:
          "$ref": "#/components/schemas/ScoreValue"
        visibility_score:
          "$ref": "#/components/schemas/ScoreValue"
        geo_score:
          "$ref": "#/components/schemas/ScoreValue"
    ScoreResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ScoreResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    OperatorEeatBreakdown:
      type: object
      required:
      - score
      - experience
      - expertise
      - authoritativeness
      - trustworthiness
      properties:
        score:
          type: integer
          nullable: true
        experience:
          type: integer
          nullable: true
        expertise:
          type: integer
          nullable: true
        authoritativeness:
          type: integer
          nullable: true
        trustworthiness:
          type: integer
          nullable: true
    OperatorScoreBreakdown:
      type: object
      required:
      - overall_score
      - formula_version
      - formula_uid
      - formula_name
      - score_evidence
      - eeat
      - content_score
      - growth_score
      - technical_score
      - performance_score
      - authority_score
      - visibility_score
      - geo_score
      properties:
        overall_score:
          type: integer
          nullable: true
        formula_version:
          type: integer
          nullable: true
        formula_uid:
          type: string
          nullable: true
        formula_name:
          type: string
          nullable: true
        score_evidence:
          "$ref": "#/components/schemas/ScoreEvidence"
        eeat:
          "$ref": "#/components/schemas/OperatorEeatBreakdown"
        content_score:
          type: integer
          nullable: true
        growth_score:
          type: integer
          nullable: true
        technical_score:
          type: integer
          nullable: true
        performance_score:
          type: integer
          nullable: true
        authority_score:
          type: integer
          nullable: true
        visibility_score:
          type: integer
          nullable: true
        geo_score:
          type: integer
          nullable: true
    OperatorGscSnapshot:
      type: object
      required:
      - snapshot_date
      - clicks
      - impressions
      - ctr
      - position
      properties:
        snapshot_date:
          type: string
          format: date
          nullable: true
        clicks:
          type: integer
          nullable: true
        impressions:
          type: integer
          nullable: true
        ctr:
          type: number
          nullable: true
        position:
          type: number
          nullable: true
    OperatorContentPipeline:
      type: object
      required:
      - scheduled
      - in_progress
      - published
      - cycle_health
      properties:
        scheduled:
          type: integer
        in_progress:
          type: integer
        published:
          type: integer
        cycle_health:
          type: object
          required:
          - current_cycle
          - triage_needed
          - cleanup
          properties:
            current_cycle:
              type: integer
            triage_needed:
              type: integer
            cleanup:
              type: integer
    OperatorGeneratedTimestamps:
      type: object
      required:
      - packet_generated_at
      - latest_score_date
      - latest_snapshot_date
      - latest_audit_at
      properties:
        packet_generated_at:
          type: string
          format: date-time
        latest_score_date:
          type: string
          format: date
          nullable: true
        latest_snapshot_date:
          type: string
          format: date
          nullable: true
        latest_audit_at:
          type: string
          format: date-time
          nullable: true
    OperatorSummarySource:
      type: object
      required:
      - surface
      - sections
      properties:
        surface:
          type: string
        sections:
          type: array
          items:
            type: string
    OperatorSummary:
      type: object
      required:
      - overall_score
      - eeat
      - content_score
      - growth_score
      - technical_score
      - performance_score
      - score_date
      - score_breakdown
      - gsc_snapshot
      - content_pipeline
      - generated_timestamps
      - source
      properties:
        overall_score:
          type: integer
          nullable: true
        eeat:
          "$ref": "#/components/schemas/OperatorEeatBreakdown"
        content_score:
          type: integer
          nullable: true
        growth_score:
          type: integer
          nullable: true
        technical_score:
          type: integer
          nullable: true
        performance_score:
          type: integer
          nullable: true
        score_date:
          type: string
          format: date
          nullable: true
        score_breakdown:
          "$ref": "#/components/schemas/OperatorScoreBreakdown"
        gsc_snapshot:
          "$ref": "#/components/schemas/OperatorGscSnapshot"
        content_pipeline:
          "$ref": "#/components/schemas/OperatorContentPipeline"
        generated_timestamps:
          "$ref": "#/components/schemas/OperatorGeneratedTimestamps"
        source:
          "$ref": "#/components/schemas/OperatorSummarySource"
    KeywordOpportunity:
      type: object
      required:
      - keyword
      - volume
      - difficulty
      - current_position
      - opportunity_score
      - serp_features
      properties:
        keyword:
          type: string
        volume:
          type: integer
          nullable: true
        difficulty:
          type: integer
          nullable: true
        current_position:
          type: integer
          nullable: true
        opportunity_score:
          type: integer
          nullable: true
        serp_features:
          type: array
          items:
            type: string
    KeywordListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/KeywordOpportunity"
        meta:
          "$ref": "#/components/schemas/Meta"
    Competitor:
      type: object
      required:
      - domain
      - overlap_percentage
      - shared_keywords_count
      - their_avg_position
      - source
      properties:
        domain:
          type: string
        overlap_percentage:
          type: integer
        shared_keywords_count:
          type: integer
        their_avg_position:
          type: number
          nullable: true
        source:
          type: string
    CompetitorListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/Competitor"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignWorkItemCounts:
      type: object
      required:
      - total
      - by_planning_status
      properties:
        total:
          type: integer
        by_planning_status:
          type: object
          additionalProperties:
            type: integer
          description: Raw count per CampaignWorkItem::PLANNING_STATUSES value present
            on the campaign.
    CampaignSummary:
      type: object
      required:
      - uid
      - key
      - name
      - kind
      - status
      - anchor_on
      - starts_on
      - ends_on
      - timezone
      - work_item_counts
      properties:
        uid:
          type: string
        key:
          type: string
        name:
          type: string
        kind:
          type: string
          enum:
          - launch
          - event
          - seasonal
          - announcement
          - series
        status:
          type: string
          enum:
          - draft
          - planning
          - pending_review
          - active
          - completed
          - cancelled
        anchor_on:
          type: string
          format: date
          nullable: true
        starts_on:
          type: string
          format: date
          nullable: true
        ends_on:
          type: string
          format: date
          nullable: true
        timezone:
          type: string
        work_item_counts:
          "$ref": "#/components/schemas/CampaignWorkItemCounts"
    CampaignsIndexResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignSummary"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignExecutionState:
      type: object
      description: Projected read model from Campaigns::WorkItemExecutionState — never
        persisted.
      required:
      - state
      - blocked_reason
      - transient_activity
      properties:
        state:
          type: string
          enum:
          - planned
          - briefing
          - drafting
          - in_review
          - revision_needed
          - packaged
          - awaiting_gate
          - scheduled
          - publishing
          - published
          - strategy_required
          - blocked
          - cancelled
        blocked_reason:
          type: string
          nullable: true
        transient_activity:
          type: boolean
    CampaignWorkItem:
      type: object
      required:
      - uid
      - title
      - item_type
      - layer
      - planning_status
      - hold_state
      - lock_state
      - execution_state
      properties:
        uid:
          type: string
        title:
          type: string
        item_type:
          type: string
          enum:
          - content_piece
          - distribution_action
          - manual_action
          - operational_milestone
          - external_task_reference
        layer:
          type: string
          nullable: true
        planning_status:
          type: string
          enum:
          - proposed
          - discussing
          - approved
          - cancelled
        hold_state:
          type: string
          enum:
          - clear
          - held
        lock_state:
          type: string
          enum:
          - unlocked
          - locked
        execution_state:
          "$ref": "#/components/schemas/CampaignExecutionState"
    CampaignDetail:
      type: object
      description: CampaignSummary fields plus positioning and the hypothesis block
        (design doc §3).
      required:
      - uid
      - key
      - name
      - kind
      - status
      - anchor_on
      - starts_on
      - ends_on
      - timezone
      - work_item_counts
      - positioning
      - hypothesis
      - opportunity
      - intended_behavior
      - audience
      - evidence_basis
      - baseline
      - work_items
      properties:
        uid:
          type: string
        key:
          type: string
        name:
          type: string
        kind:
          type: string
          enum:
          - launch
          - event
          - seasonal
          - announcement
          - series
        status:
          type: string
          enum:
          - draft
          - planning
          - pending_review
          - active
          - completed
          - cancelled
        anchor_on:
          type: string
          format: date
          nullable: true
        starts_on:
          type: string
          format: date
          nullable: true
        ends_on:
          type: string
          format: date
          nullable: true
        timezone:
          type: string
        work_item_counts:
          "$ref": "#/components/schemas/CampaignWorkItemCounts"
        positioning:
          type: string
          nullable: true
        hypothesis:
          type: string
          nullable: true
          description: Falsifiable hypothesis statement.
        opportunity:
          type: string
          nullable: true
        intended_behavior:
          type: string
          nullable: true
        audience:
          type: string
          nullable: true
        evidence_basis:
          type: string
          nullable: true
        baseline:
          type: object
          additionalProperties: true
        work_items:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignWorkItem"
    CampaignDetailResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignDetail"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignOwner:
      type: object
      description: 'Resolution order: assignee_user_id -> agent_key -> owner_label
        -> owner_role.'
      required:
      - kind
      - value
      properties:
        kind:
          type: string
          nullable: true
          enum:
          - user
          - agent
          - label
          - role
        value:
          type: string
          nullable: true
    CampaignGate:
      type: object
      description: The single live gate — computed, never persisted (design principle
        5).
      required:
      - kind
      - item_uid
      - description
      - owner
      properties:
        kind:
          type: string
          enum:
          - blocked
          - review
          - strategy_required
        item_uid:
          type: string
        description:
          type: string
        owner:
          "$ref": "#/components/schemas/CampaignOwner"
    CampaignWaitingOnYouEntry:
      type: object
      required:
      - item_uid
      - review_type
      - requested_at
      properties:
        item_uid:
          type: string
        review_type:
          type: string
        requested_at:
          type: string
          format: date-time
    CampaignMissionControlItem:
      type: object
      required:
      - uid
      - title
      - item_type
      - layer
      - planning_status
      - hold_state
      - lock_state
      - execution_state
      - owner
      properties:
        uid:
          type: string
        title:
          type: string
        item_type:
          type: string
          enum:
          - content_piece
          - distribution_action
          - manual_action
          - operational_milestone
          - external_task_reference
        layer:
          type: string
          nullable: true
        planning_status:
          type: string
          enum:
          - proposed
          - discussing
          - approved
          - cancelled
        hold_state:
          type: string
          enum:
          - clear
          - held
        lock_state:
          type: string
          enum:
          - unlocked
          - locked
        execution_state:
          "$ref": "#/components/schemas/CampaignExecutionState"
        owner:
          "$ref": "#/components/schemas/CampaignOwner"
    CampaignAtRiskFinding:
      type: object
      description: One Campaigns::AtRiskPolicy finding (slice-6 policy 7) — pure,
        computed fresh on every mission-control read, never persisted.
      required:
      - constraint_uid
      - work_item_uid
      - milestone
      - reason
      - detail
      properties:
        constraint_uid:
          type: string
        work_item_uid:
          type: string
        milestone:
          type: string
          enum:
          - brief_due
          - draft_due
          - asset_due
          - package_due
          - publish
          - distribution
        reason:
          type: string
          enum:
          - runway
          - overdue_artifact
          - held
          - unresolvable_near_end
          description: '"runway" (unresolved, latest permissible date within DEFAULT_RUNWAY_DAYS),
            "overdue_artifact" (resolved past as_of with the milestone''s expected
            artifact absent), "held" (work item held with an unresolved condition
            constraint), or "unresolvable_near_end" (unresolved with no computable
            window at all — anchor_missing/predecessor_unpublished/condition_held
            — and the campaign''s own ends_on is within DEFAULT_RUNWAY_DAYS).'
        detail:
          type: string
    CampaignRuleSummary:
      type: object
      description: A campaign rule's current enforced state — CampaignRule#current_version
        projected to rule_key/enforcement_mode/version. Authority always lives on
        the versioned CampaignRuleVersion row (design §3, Codex amendment P7); this
        is the read-side "what does this rule enforce right now" convenience.
      required:
      - rule_key
      - enforcement_mode
      - version
      properties:
        rule_key:
          type: string
        enforcement_mode:
          type: string
          enum:
          - deterministic
          - reviewer
          - human
          - advisory
        version:
          type: integer
          description: The current version number (1-based, monotonically increasing
            per rule).
    CampaignHumanRule:
      type: object
      description: 'One human-mode campaign rule surfaced as review-queue gate context
        (slice 7 task 4, design §3 policy 5: "human rules map to review-queue gates"
        — V1 adds this as context on the existing review-queue item, not a new review
        type). Current version only — CampaignRuleVersion is the authority (Codex
        amendment P7).'
      required:
      - rule_key
      - version
      - body
      properties:
        rule_key:
          type: string
        version:
          type: integer
          description: The current version number (1-based, monotonically increasing
            per rule).
        body:
          type: string
    CampaignReReviewFinding:
      type: object
      description: One Campaigns::ReReviewProjection finding (Codex P8, slice-7 policy
        8) — a campaign content artifact (draft or package) whose most recent review
        snapshot does not yet include a deterministic/reviewer rule's current version.
        Computed fresh on every read, never persisted; resolves itself once a later
        approval snapshots the superseding version. V1 does not auto-enqueue re-reviews.
      required:
      - artifact_uid
      - artifact_type
      - reviewed_version_ids
      - superseding_version_id
      properties:
        artifact_uid:
          type: string
        artifact_type:
          type: string
          enum:
          - ContentDraft
          - ContentPackage
        reviewed_version_ids:
          type: array
          items:
            type: integer
          description: 'CampaignRuleVersion ids captured in this artifact''s most
            recent review snapshot. Empty when no snapshot exists yet (fails safe:
            an absent snapshot on a campaign with deterministic/reviewer rules always
            counts as affected).'
        superseding_version_id:
          type: integer
          description: The rule's current CampaignRuleVersion id, not yet present
            in reviewed_version_ids.
    CampaignMissionControl:
      type: object
      required:
      - single_live_gate
      - waiting_on_you
      - counts
      - items
      - at_risk
      - rules
      - re_review_required
      properties:
        single_live_gate:
          allOf:
          - "$ref": "#/components/schemas/CampaignGate"
          nullable: true
        waiting_on_you:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignWaitingOnYouEntry"
        counts:
          type: object
          additionalProperties:
            type: integer
          description: Item count per Campaigns::WorkItemExecutionState::STATES value
            present on the campaign.
        items:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignMissionControlItem"
        at_risk:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignAtRiskFinding"
        rules:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignRuleSummary"
          description: Every campaign rule with a current version, per Campaigns::MissionControlSnapshot#rules
            (slice 7 task 2).
        re_review_required:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignReReviewFinding"
          description: Live Campaigns::ReReviewProjection sweep across every deterministic/reviewer
            rule the campaign has (slice 7 task 2).
    CampaignMissionControlResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignMissionControl"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignCreateRequest:
      type: object
      required:
      - key
      - name
      - kind
      properties:
        key:
          type: string
          description: Lowercase slug, unique per site (letters, numbers, hyphens).
        name:
          type: string
        kind:
          type: string
          enum:
          - launch
          - event
          - seasonal
          - announcement
          - series
        anchor_on:
          type: string
          format: date
          nullable: true
        starts_on:
          type: string
          format: date
          nullable: true
        ends_on:
          type: string
          format: date
          nullable: true
        timezone:
          type: string
          description: IANA timezone identifier. Defaults to UTC.
        positioning:
          type: string
          nullable: true
        audience:
          type: string
          nullable: true
        opportunity:
          type: string
          nullable: true
        intended_behavior:
          type: string
          nullable: true
        evidence_basis:
          type: string
          nullable: true
        hypothesis:
          type: string
          nullable: true
        baseline:
          type: object
          additionalProperties: true
          nullable: true
    CampaignCreateResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - resource
      - next_step
      properties:
        operation:
          type: string
          enum:
          - create_campaign
        message:
          type: string
        site_slug:
          type: string
        resource:
          "$ref": "#/components/schemas/CampaignDetail"
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    CampaignCreateResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignCreateResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignCommandRequest:
      type: object
      description: See Campaigns::Commands::Execute (app/services/campaigns/commands/execute.rb)
        for the full precondition/locking/idempotency contract this endpoint is a
        thin envelope over.
      required:
      - command
      - idempotency_key
      properties:
        command:
          type: string
          enum:
          - propose_item
          - approve_item
          - cancel_item
          - hold
          - release_hold
          - lock_item
          - unlock_item
          - lock_schedule
          - unlock_schedule
          - reschedule
          - replace_evidence
          - request_changes
          - regenerate_draft
          - rebuild_package
          - change_owner
          - add_rule
          - amend_rule
          - split_lane
          - materialize
          - amend_plan_field
          - submit_plan_revision
          - reserve_capacity
          - release_capacity
          - grant_exception
          - add_dependency
          - remove_dependency
          - resolve_schedule
          - add_asset_requirement
          - remove_asset_requirement
          - complete_campaign
          - add_metric_definition
          - link_existing_entry
          description: create_campaign and external_publication_recorded are excluded
            — create_campaign is written directly by POST /campaigns, and external_publication_recorded
            is a system event written by the publication wakeup path; neither executes
            through this endpoint (both return not_executable_here if attempted).
        idempotency_key:
          type: string
          description: Identifies the request, not the command — replaying the same
            key (even with a different command) returns the original event with meta.replayed=true
            and does not re-execute.
        params:
          type: object
          additionalProperties: true
          nullable: true
          description: Command-specific fields (e.g. work_item_uid, title, item_type
            for propose_item). Validated per-command by the command's handler.
        rationale:
          type: string
          nullable: true
        expected_lock_version:
          type: integer
          nullable: true
          description: Optimistic-lock check against the target work item's lock_version.
            A mismatch (or an item modified since) returns 409 stale_object.
        source_ref:
          type: object
          additionalProperties: true
          nullable: true
    CampaignEventResource:
      type: object
      required:
      - sequence
      - command
      - actor_type
      - actor_id
      - idempotency_key
      - old_value
      - new_value
      - created_at
      properties:
        sequence:
          type: integer
        command:
          type: string
          enum:
          - propose_item
          - approve_item
          - cancel_item
          - hold
          - release_hold
          - lock_item
          - unlock_item
          - lock_schedule
          - unlock_schedule
          - reschedule
          - replace_evidence
          - request_changes
          - regenerate_draft
          - rebuild_package
          - change_owner
          - add_rule
          - amend_rule
          - split_lane
          - materialize
          - amend_plan_field
          - submit_plan_revision
          - reserve_capacity
          - release_capacity
          - grant_exception
          - add_dependency
          - remove_dependency
          - resolve_schedule
          - add_asset_requirement
          - remove_asset_requirement
          - complete_campaign
          - add_metric_definition
          - link_existing_entry
          - create_campaign
          - external_publication_recorded
        actor_type:
          type: string
          enum:
          - user
          - agent
        actor_id:
          type: string
        idempotency_key:
          type: string
        campaign_work_item_uid:
          type: string
          nullable: true
        old_value:
          type: object
          additionalProperties: true
        new_value:
          type: object
          additionalProperties: true
        rationale:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    CampaignCommandResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - resource
      - next_step
      properties:
        operation:
          type: string
          enum:
          - propose_item
          - approve_item
          - cancel_item
          - hold
          - release_hold
          - lock_item
          - unlock_item
          - lock_schedule
          - unlock_schedule
          - reschedule
          - replace_evidence
          - request_changes
          - regenerate_draft
          - rebuild_package
          - change_owner
          - add_rule
          - amend_rule
          - split_lane
          - materialize
          - amend_plan_field
          - submit_plan_revision
          - reserve_capacity
          - release_capacity
          - grant_exception
          - add_dependency
          - remove_dependency
          - resolve_schedule
          - add_asset_requirement
          - remove_asset_requirement
          - complete_campaign
          - add_metric_definition
          - link_existing_entry
          - create_campaign
          - external_publication_recorded
        message:
          type: string
        site_slug:
          type: string
        resource:
          "$ref": "#/components/schemas/CampaignEventResource"
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    CampaignCommandResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignCommandResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignSubmitPlanRequest:
      type: object
      required:
      - idempotency_key
      properties:
        idempotency_key:
          type: string
        rationale:
          type: string
          nullable: true
        source_ref:
          type: object
          additionalProperties: true
          nullable: true
    CampaignSubmitPlanResource:
      type: object
      required:
      - campaign_uid
      properties:
        campaign_uid:
          type: string
        revision_uid:
          type: string
          nullable: true
        revision_version:
          type: integer
          nullable: true
        review_queue_item_uid:
          type: string
          nullable: true
    CampaignSubmitPlanResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - resource
      - next_step
      properties:
        operation:
          type: string
          enum:
          - submit_plan_revision
        message:
          type: string
        site_slug:
          type: string
        resource:
          "$ref": "#/components/schemas/CampaignSubmitPlanResource"
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    CampaignSubmitPlanResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignSubmitPlanResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignMetricObservationRequest:
      type: object
      description: See Campaigns::RecordMetricObservation (app/services/campaigns/record_metric_observation.rb)
        for the full upsert/concurrency contract. NOT an idempotency_key-bearing command
        request — the (metric_definition_uid, window, source_ref) triple itself is
        the idempotency key.
      required:
      - metric_definition_uid
      - window
      - source_ref
      - observed_at
      properties:
        metric_definition_uid:
          type: string
          description: A CampaignMetricDefinition uid belonging to this campaign (created
            via add_metric_definition on POST .../commands).
        window:
          type: integer
          enum:
          - 3
          - 7
          - 14
          - 28
          description: Must be one of the definition's own configured windows when
            it declares any.
        source_ref:
          type: string
          description: Non-blank collector/source identifier — the third leg of the
            (definition, window, source_ref) idempotency key. Repeating the same triple
            upserts this same row.
        observed_at:
          type: string
          format: date-time
          description: ISO8601 timestamp of the underlying measurement.
        numeric_value:
          type: number
          nullable: true
          description: Required (and the ONLY populated value field) when the definition's
            value_type is count/rate/score/duration.
        text_value:
          type: string
          nullable: true
          description: Permanently unused in V1 — no CampaignMetricDefinition::VALUE_TYPES
            member maps to it; populating it is a 422 invalid_params.
        structured_value:
          type: object
          additionalProperties: true
          nullable: true
          description: Required (and the ONLY populated value field) when the definition's
            value_type is snapshot.
        unit:
          type: string
          nullable: true
        baseline_value:
          type: number
          nullable: true
        attribution_context:
          type: object
          additionalProperties: true
          nullable: true
          description: Free-form — a caller may stamp the campaign's own attribution_key
            here for a future collector join (no join is enforced in V1).
        collection_status:
          type: string
          enum:
          - recorded
          - partial
          - stale
          nullable: true
          description: Defaults to "recorded" when omitted.
    CampaignMetricObservationResource:
      type: object
      required:
      - uid
      - metric_definition_uid
      - window
      - observed_at
      - source_ref
      - collection_status
      properties:
        uid:
          type: string
        metric_definition_uid:
          type: string
        window:
          type: integer
        observed_at:
          type: string
          format: date-time
        numeric_value:
          type: number
          nullable: true
        text_value:
          type: string
          nullable: true
        structured_value:
          type: object
          additionalProperties: true
          nullable: true
        unit:
          type: string
          nullable: true
        source_ref:
          type: string
        baseline_value:
          type: number
          nullable: true
        attribution_context:
          type: object
          additionalProperties: true
        collection_status:
          type: string
          enum:
          - recorded
          - partial
          - stale
    CampaignMetricObservationResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - resource
      properties:
        operation:
          type: string
          enum:
          - record_metric_observation
        message:
          type: string
        site_slug:
          type: string
        resource:
          "$ref": "#/components/schemas/CampaignMetricObservationResource"
    CampaignMetricObservationResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignMetricObservationResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignPlanningSessionTranscriptMessage:
      type: object
      description: One Message row from the session's Chat (RubyLLM's Chat/Message
        infrastructure — no bespoke transcript store). A failed or skipped turn (CampaignPlanningAgent#append_turn_message!)
        appears here like any other assistant message — role "assistant", human-readable
        `content` — distinguishable from an ordinary reply via `kind`.
      required:
      - role
      - content
      - created_at
      - kind
      properties:
        role:
          type: string
        content:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        kind:
          type: string
          nullable: true
          enum:
          - campaign_planning_turn_failed
          - campaign_planning_turn_skipped
          -
          description: Non-null only for a turn-status message CampaignPlanningAgent
            appended on failure/skip — never set on a user message or an ordinary
            successful assistant reply.
    CampaignPlanningSession:
      type: object
      required:
      - uid
      - campaign_uid
      - status
      - started_by_type
      - started_by_id
      - created_at
      - updated_at
      properties:
        uid:
          type: string
        campaign_uid:
          type: string
        status:
          type: string
          enum:
          - active
          - ended
        started_by_type:
          type: string
          enum:
          - user
          - agent
        started_by_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        transcript:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignPlanningSessionTranscriptMessage"
          description: Only present on GET /planning_sessions/{session_uid} — every
            Message on the session's Chat, chronological, never truncated.
    CampaignPlanningSessionResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignPlanningSession"
        meta:
          "$ref": "#/components/schemas/Meta"
    CampaignPlanningSessionMessageRequest:
      type: object
      required:
      - content
      properties:
        content:
          type: string
          description: The user's message to the campaign planner. Must not be blank.
    CampaignPlanningSessionMessageResponseData:
      type: object
      required:
      - operation
      - message
      - session_uid
      - message_id
      - run_uid
      - next_step
      properties:
        operation:
          type: string
          enum:
          - planning_session_message
        message:
          type: string
        session_uid:
          type: string
        message_id:
          type: integer
        run_uid:
          type: string
          description: The queued CampaignPlanningAgent AgentRun's uid — the run reference
            for this turn.
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    CampaignPlanningSessionMessageResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/CampaignPlanningSessionMessageResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    ContentItem:
      type: object
      required:
      - uid
      - title
      - content_type
      - status
      - scheduled_date
      - published_date
      - brief_status
      - draft_status
      - word_count
      - triage_needed
      - stale_for_triage
      - automation_ready
      - current_cycle
      properties:
        uid:
          type: string
        title:
          type: string
        content_type:
          type: string
        status:
          type: string
        growth_cycle_uid:
          type: string
          nullable: true
        scheduled_date:
          type: string
          format: date
          nullable: true
        target_keyword:
          type: string
          nullable: true
        funnel_stage:
          type: string
          nullable: true
        pillar:
          type: string
          nullable: true
        target_word_count:
          type: integer
          nullable: true
        priority:
          type: integer
          nullable: true
        platform:
          type: string
          nullable: true
        topic:
          type: string
          nullable: true
        channel:
          type: string
          nullable: true
        schedule_intent:
          type: string
          nullable: true
        owner_notes:
          type: string
          nullable: true
        claim_safety_notes:
          type: string
          nullable: true
        import_batch_uid:
          type: string
          nullable: true
        published_date:
          type: string
          format: date-time
          nullable: true
        triage_state:
          type: string
          nullable: true
        triage_reason:
          type: string
          nullable: true
        effective_triage_state:
          type: string
          nullable: true
        triage_needed:
          type: boolean
        stale_for_triage:
          type: boolean
        triage_reference_at:
          type: string
          format: date-time
        automation_ready:
          type: boolean
        current_cycle:
          type: boolean
        brief_status:
          type: string
          nullable: true
        draft_status:
          type: string
          nullable: true
        schema_type_candidate:
          type: string
          nullable: true
          enum:
          - article
          - faq
          - how_to
        package_status:
          type: string
          nullable: true
        package_claim_risk_level:
          type: string
          nullable: true
          enum:
          - low
          - medium
          - high
        package_review_status:
          type: string
          nullable: true
        package_execution_state:
          type: string
          nullable: true
        draft_review:
          "$ref": "#/components/schemas/DraftReviewState"
        publication_state:
          type: string
          nullable: true
        publication_verified:
          type: boolean
          nullable: true
        legacy_unverified:
          type: boolean
          nullable: true
        external_url:
          type: string
          nullable: true
        social_engagement:
          "$ref": "#/components/schemas/SocialEngagement"
        word_count:
          type: integer
    SocialEngagementMetric:
      type: object
      required:
      - status
      - value
      - reason
      properties:
        status:
          type: string
          enum:
          - available
          - unavailable
        value:
          type: number
          nullable: true
          description: Persisted native value. Numeric zero is available evidence.
        reason:
          type: string
          nullable: true
          description: Non-empty machine-readable reason when status is unavailable.
      additionalProperties: false
    SocialEngagement:
      type: object
      nullable: true
      required:
      - refreshed_at
      - platform
      - metrics
      description: Normalized native engagement from the latest verified social publication.
        Stored likes/comments/shares aliases are returned as reactions/replies/reposts;
        values are never estimated.
      properties:
        refreshed_at:
          type: string
          format: date-time
          nullable: true
        platform:
          type: string
          enum:
          - x
          - linkedin
        metrics:
          type: object
          required:
          - impressions
          - reactions
          - replies
          - reposts
          - follower_trend
          properties:
            impressions:
              "$ref": "#/components/schemas/SocialEngagementMetric"
            reactions:
              "$ref": "#/components/schemas/SocialEngagementMetric"
            replies:
              "$ref": "#/components/schemas/SocialEngagementMetric"
            reposts:
              "$ref": "#/components/schemas/SocialEngagementMetric"
            follower_trend:
              "$ref": "#/components/schemas/SocialEngagementMetric"
          additionalProperties: false
      additionalProperties: false
    ContentItemResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ContentItem"
        meta:
          "$ref": "#/components/schemas/Meta"
    DraftReviewState:
      type: object
      required:
      - uid
      - review_type
      - status
      - approval_level
      - claim_risk_level
      - requested_at
      properties:
        uid:
          type: string
        review_type:
          type: string
          enum:
          - draft_review
        status:
          type: string
          enum:
          - pending
          - changes_requested
        approval_level:
          type: string
          enum:
          - standard
          - strict
        claim_risk_level:
          type: string
          enum:
          - low
          - medium
          - high
        reviewer_feedback:
          type: string
          nullable: true
        requested_at:
          type: string
          format: date-time
        reviewed_at:
          type: string
          format: date-time
          nullable: true
      additionalProperties: false
      nullable: true
    ContentListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/ContentItem"
        meta:
          "$ref": "#/components/schemas/Meta"
    ContentBriefReview:
      type: object
      required:
      - uid
      - entry_uid
      - title
      - target_keyword
      - target_word_count
      - outline
      - status
      properties:
        uid:
          type: string
        entry_uid:
          type: string
        title:
          type: string
        target_keyword:
          type: string
        target_word_count:
          type: integer
          nullable: true
        outline:
          type: string
          nullable: true
        status:
          type: string
          enum:
          - generating
          - pending_review
          - approved
          - rejected
      additionalProperties: false
    ContentDetail:
      allOf:
      - "$ref": "#/components/schemas/ContentItem"
      - type: object
        required:
        - brief
        - draft
        properties:
          brief:
            "$ref": "#/components/schemas/ContentBriefReview"
            nullable: true
          draft:
            "$ref": "#/components/schemas/ContentDraftSnapshot"
            nullable: true
    ContentDetailResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ContentDetail"
        meta:
          "$ref": "#/components/schemas/Meta"
    ContentDraftSnapshot:
      type: object
      required:
      - uid
      - title
      - body
      - version
      - status
      - updated_at
      properties:
        uid:
          type: string
        title:
          type: string
        body:
          type: string
        version:
          type: integer
        status:
          type: string
          enum:
          - generating
          - draft
          - regenerating
          - in_review
          - reviewing
          - ready_for_review
          - revision_needed
          - approved
          - published
          - archived
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    ContentImportEntryInput:
      type: object
      required:
      - title
      - content_type
      properties:
        title:
          type: string
        content_type:
          type: string
          enum:
          - blog_post
          - comparison
          - listicle
          - pillar_page
          - tutorial
          - case_study
          - product_update
          - changelog
          - linkedin_post
          - twitter_thread
          - twitter_post
        target_keyword:
          type: string
          nullable: true
        funnel_stage:
          type: string
          nullable: true
        pillar:
          type: string
          nullable: true
        scheduled_date:
          type: string
          format: date
          nullable: true
        target_word_count:
          type: integer
          nullable: true
        priority:
          type: integer
          nullable: true
        platform:
          type: string
          nullable: true
        channel:
          type: string
          nullable: true
        topic:
          type: string
          nullable: true
        schedule_intent:
          type: string
          nullable: true
        owner_notes:
          type: string
          nullable: true
        claim_safety_notes:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
    ContentImportRequest:
      type: object
      required:
      - entries
      properties:
        strategy_uid:
          type: string
          nullable: true
        replace_existing:
          type: boolean
          nullable: true
        entries:
          type: array
          items:
            "$ref": "#/components/schemas/ContentImportEntryInput"
    ContentImportResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - strategy_uid
      - growth_cycle_uid
      - import_batch_uid
      - imported_count
      - archived_count
      - entries
      - next_step
      properties:
        operation:
          type: string
          enum:
          - import_content
        message:
          type: string
        site_slug:
          type: string
        strategy_uid:
          type: string
        growth_cycle_uid:
          type: string
        import_batch_uid:
          type: string
        imported_count:
          type: integer
        archived_count:
          type: integer
        entries:
          type: array
          items:
            "$ref": "#/components/schemas/ContentItem"
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    ContentImportResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ContentImportResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    PackageContentVariant:
      type: object
      required:
      - type
      - entry_uid
      - draft_uid
      - package_uid
      - requested_channel
      - channel
      - channel_approval_state
      - package_review_status
      - package_execution_state
      - publication_state
      - title
      - body
      - source_draft_uid
      properties:
        type:
          type: string
          enum:
          - package_content_variant
        entry_uid:
          type: string
        draft_uid:
          type: string
        package_uid:
          type: string
        requested_channel:
          type: string
        channel:
          type: string
        channel_approval_state:
          type: string
          enum:
          - pending
          - approved
          - blocked
        package_review_status:
          type: string
        package_execution_state:
          type: string
        publication_state:
          type: string
        publication:
          type: object
          nullable: true
          additionalProperties: true
        title:
          type: string
        body:
          type: string
        source_draft_uid:
          type: string
    PackageContentVariantResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data: PackageContentVariant
        meta:
          "$ref": "#/components/schemas/Meta"
    ApprovedContentVariant:
      type: object
      required:
      - type
      - entry_uid
      - draft_uid
      - package_uid
      - requested_channel
      - channel
      - channel_approval_state
      - package_review_status
      - package_execution_state
      - publication_state
      - title
      - body
      - source_draft_uid
      properties:
        type:
          type: string
          enum:
          - approved_content_variant
        entry_uid:
          type: string
        draft_uid:
          type: string
        package_uid:
          type: string
        requested_channel:
          type: string
        channel:
          type: string
        channel_approval_state:
          type: string
          enum:
          - pending
          - approved
          - blocked
        package_review_status:
          type: string
        package_execution_state:
          type: string
        publication_state:
          type: string
        publication:
          type: object
          nullable: true
          additionalProperties: true
        title:
          type: string
        body:
          type: string
        source_draft_uid:
          type: string
    AlreadyPublishedContentVariant:
      type: object
      required:
      - type
      - entry_uid
      - draft_uid
      - package_uid
      - requested_channel
      - channel
      - channel_approval_state
      - package_review_status
      - package_execution_state
      - publication_state
      - already_published
      - publication
      properties:
        type:
          type: string
          enum:
          - already_published_content_variant
        entry_uid:
          type: string
        draft_uid:
          type: string
        package_uid:
          type: string
        requested_channel:
          type: string
        channel:
          type: string
        channel_approval_state:
          type: string
          enum:
          - pending
          - approved
          - blocked
        package_review_status:
          type: string
        package_execution_state:
          type: string
        publication_state:
          type: string
        already_published:
          type: boolean
          enum:
          - true
        publication:
          type: object
          additionalProperties: true
      additionalProperties: false
    ApprovedContentVariantResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          oneOf:
          - "$ref": "#/components/schemas/ApprovedContentVariant"
          - "$ref": "#/components/schemas/AlreadyPublishedContentVariant"
        meta:
          "$ref": "#/components/schemas/Meta"
    WorkflowResource:
      type: object
      properties:
        type:
          type: string
        uid:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
        entry_uid:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        content_type:
          type: string
          nullable: true
        entry_status:
          type: string
          nullable: true
        growth_cycle_uid:
          type: string
          nullable: true
        brief_status:
          type: string
          nullable: true
        draft_status:
          type: string
          nullable: true
        schema_type_candidate:
          type: string
          nullable: true
          enum:
          - article
          - faq
          - how_to
        package_status:
          type: string
          nullable: true
        package_claim_risk_level:
          type: string
          nullable: true
          enum:
          - low
          - medium
          - high
        package_review_status:
          type: string
          nullable: true
        package_execution_state:
          type: string
          nullable: true
        draft_review:
          "$ref": "#/components/schemas/DraftReviewState"
        publication_state:
          type: string
          nullable: true
        publication_verified:
          type: boolean
          nullable: true
        legacy_unverified:
          type: boolean
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true
        external_url:
          type: string
          nullable: true
        triage_state:
          type: string
          nullable: true
        triage_reason:
          type: string
          nullable: true
        effective_triage_state:
          type: string
          nullable: true
        triage_needed:
          type: boolean
          nullable: true
        stale_for_triage:
          type: boolean
          nullable: true
        automation_ready:
          type: boolean
          nullable: true
        current_cycle:
          type: boolean
          nullable: true
        triage_reference_at:
          type: string
          format: date-time
          nullable: true
      additionalProperties: false
    WorkflowNextStep:
      type: object
      required:
      - action
      - phase
      - message
      properties:
        action:
          type: string
        phase:
          type: string
        message:
          type: string
        endpoint:
          type: string
          nullable: true
      additionalProperties: false
    WorkflowMutationResponseData:
      type: object
      required:
      - operation
      - message
      - site_slug
      - resource
      - next_step
      properties:
        operation:
          type: string
        message:
          type: string
        site_slug:
          type: string
        job_id:
          type: string
          nullable: true
        resource:
          "$ref": "#/components/schemas/WorkflowResource"
        next_step:
          "$ref": "#/components/schemas/WorkflowNextStep"
    WorkflowMutationResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/WorkflowMutationResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    EeatSignal:
      type: object
      required:
      - name
      - pillar
      - points_earned
      - points_possible
      - status
      - recommendation
      properties:
        name:
          type: string
        pillar:
          type: string
        points_earned:
          type: integer
        points_possible:
          type: integer
        status:
          type: string
          enum:
          - present
          - absent
        recommendation:
          type: string
          nullable: true
    EeatPillar:
      type: object
      required:
      - pillar
      - points_earned
      - points_possible
      - signals
      properties:
        pillar:
          type: string
        points_earned:
          type: integer
        points_possible:
          type: integer
        signals:
          type: array
          items:
            "$ref": "#/components/schemas/EeatSignal"
    EeatResponseData:
      type: object
      required:
      - pillars
      - total_earned
      - total_possible
      properties:
        pillars:
          type: array
          items:
            "$ref": "#/components/schemas/EeatPillar"
        total_earned:
          type: integer
        total_possible:
          type: integer
    EeatResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/EeatResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    SummaryIssue:
      type: object
      required:
      - issue_type
      - severity
      - category
      - page_url
      - description
      properties:
        issue_type:
          type: string
        severity:
          type: string
        category:
          type: string
        page_url:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
    SummaryResponseData:
      type: object
      required:
      - scores
      - top_issues
      - keyword_movement
      - content_pipeline
      - gsc_snapshot
      properties:
        scores:
          type: object
          description: Published (champion-formula) score. Pillar fields are null
            when the champion does not score that signal.
          properties:
            overall_score:
              type: integer
              nullable: true
            formula_version:
              type: integer
              nullable: true
            eeat:
              type: object
              properties:
                score:
                  type: integer
                  nullable: true
                experience:
                  type: integer
                  nullable: true
                expertise:
                  type: integer
                  nullable: true
                authoritativeness:
                  type: integer
                  nullable: true
                trustworthiness:
                  type: integer
                  nullable: true
            content_score:
              type: integer
              nullable: true
            growth_score:
              type: integer
              nullable: true
            technical_score:
              type: integer
              nullable: true
            performance_score:
              type: integer
              nullable: true
        top_issues:
          type: array
          items:
            "$ref": "#/components/schemas/SummaryIssue"
        keyword_movement:
          type: object
          required:
          - up
          - down
          - new
          properties:
            up:
              type: integer
            down:
              type: integer
            new:
              type: integer
        content_pipeline:
          type: object
          required:
          - scheduled
          - in_progress
          - published
          properties:
            scheduled:
              type: integer
            in_progress:
              type: integer
            published:
              type: integer
        gsc_snapshot:
          type: object
          required:
          - snapshot_date
          - clicks
          - impressions
          - ctr
          - position
          properties:
            snapshot_date:
              type: string
              format: date
              nullable: true
            clicks:
              type: integer
              nullable: true
            impressions:
              type: integer
              nullable: true
            ctr:
              type: number
              nullable: true
            position:
              type: number
              nullable: true
    SummaryResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/SummaryResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    PublicationEvidencePacketResponseData:
      type: object
      required:
      - site
      - generated_at
      - proof_endpoints
      - public_proof_summary
      - operator_summary
      - issue_evidence
      - publication_state_summary
      - publication_safety
      properties:
        site:
          type: object
          required:
          - slug
          - name
          - domain
          properties:
            slug:
              type: string
            name:
              type: string
            domain:
              type: string
        generated_at:
          type: string
          format: date-time
        proof_endpoints:
          type: object
          additionalProperties: true
        public_proof_summary:
          type: object
          additionalProperties: true
        operator_summary:
          "$ref": "#/components/schemas/OperatorSummary"
        issue_evidence:
          type: object
          additionalProperties: true
        publication_state_summary:
          type: object
          additionalProperties: true
        publication_safety:
          type: object
          additionalProperties: true
    PublicationEvidencePacketResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/PublicationEvidencePacketResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    NextAction:
      type: object
      required:
      - priority
      - business_priority_score
      - action_type
      - title
      - reason
      - recommended_owner
      - capability
      - lane
      - labels
      - rationale
      - evidence_source
      properties:
        priority:
          type: integer
        business_priority_score:
          type: integer
        action_type:
          type: string
        title:
          type: string
        reason:
          type: string
        recommended_owner:
          type: string
        capability:
          type: string
          enum:
          - api
          - ui
          - external
        lane:
          type: string
        labels:
          type: array
          items:
            type: string
        rationale:
          type: object
          additionalProperties: true
        evidence_source:
          type: object
          description: 'Polymorphic evidence for the action, discriminated by `type`
            (audit_issue, content_strategy, content_brief, content_draft, keyword_opportunity,
            site_health). Audit-issue actions are AGGREGATED by issue_type: one action
            represents every page sharing that issue. `affected_count` is the true
            total; `pages` lists up to 10 affected pages (highest-value surface first).
            `page_url`/`primary_page_url` is the single highest-priority page and
            is preserved for backward compatibility — do not assume one action equals
            one page; iterate `pages` and read `affected_count`. Additional per-type
            fields may be present.'
          additionalProperties: true
          properties:
            type:
              type: string
              description: Evidence kind / discriminator.
            issue_type:
              type: string
              nullable: true
            severity:
              type: string
              nullable: true
            category:
              type: string
              nullable: true
            page_url:
              type: string
              nullable: true
              description: Primary (highest-priority) affected page; preserved for
                back-compat.
            primary_page_url:
              type: string
              nullable: true
              description: Explicit alias of page_url for aggregated actions.
            affected_count:
              type: integer
              nullable: true
              description: Total pages affected by this issue_type (may exceed pages.length).
            pages:
              type: array
              nullable: true
              description: Affected pages, capped at 10, highest-value surface first.
              items:
                type: object
                properties:
                  page_url:
                    type: string
                  id:
                    type: integer
                  surface_tier:
                    type: string
            corroborating_pages:
              type: integer
              nullable: true
            path:
              type: string
              nullable: true
              description: In-app path for content/site actions.
    NextActionsResponseData:
      type: object
      required:
      - site_slug
      - generated_at
      - actions
      properties:
        site_slug:
          type: string
        generated_at:
          type: string
          format: date-time
        actions:
          type: array
          items:
            "$ref": "#/components/schemas/NextAction"
    NextActionsResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/NextActionsResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    DeltaValue:
      type: object
      required:
      - before
      - after
      - delta
      properties:
        before:
          nullable: true
        after:
          nullable: true
        delta:
          nullable: true
      additionalProperties: false
    ChangeIssue:
      type: object
      required:
      - issue_type
      - severity
      - category
      - page_url
      - description
      properties:
        issue_type:
          type: string
        severity:
          type: string
        category:
          type: string
        page_url:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        first_seen_at:
          type: string
          format: date
          nullable: true
        fixed_at:
          type: string
          format: date-time
          nullable: true
    PublishedContentItem:
      type: object
      required:
      - uid
      - title
      - content_type
      - published_at
      properties:
        uid:
          type: string
        title:
          type: string
        content_type:
          type: string
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true
    ReviewQueueReviewable:
      type: object
      required:
      - type
      - uid
      - entry_uid
      - title
      - status
      properties:
        type:
          type: string
        uid:
          type: string
          nullable: true
        entry_uid:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
      additionalProperties: false
    CampaignAssetApprovalContext:
      type: object
      description: 'Slice-10 task 1: present only on `campaign_asset_approval` review-queue
        items (`reviewable.type == "CampaignAssetFulfillment"`), null on every other
        item — the requirement/work-item/campaign/media-asset context a review-canvas
        card needs to render itself without a second round trip to the campaign detail
        endpoint.'
      required:
      - requirement
      - work_item
      - campaign
      - media_asset
      properties:
        requirement:
          type: object
          required:
          - uid
          - asset_type
          - stage
          properties:
            uid:
              type: string
            asset_type:
              type: string
              enum:
              - chart
              - screenshot
              - terminal_recording
              - cover
              - og_card
              - link_artifact
            stage:
              type: string
              enum:
              - submit_draft
              - package
        work_item:
          type: object
          required:
          - uid
          - title
          properties:
            uid:
              type: string
            title:
              type: string
        campaign:
          type: object
          required:
          - uid
          - key
          properties:
            uid:
              type: string
            key:
              type: string
        media_asset:
          type: object
          description: Cheaply-available MediaAsset columns only — never `resolved_url`/the
            ActiveStorage file/blob (that would reintroduce an N+1 on the review-queue
            list path).
          required:
          - uid
          - kind
          - source
          - usage_context
          - alt_text
          - source_url
          properties:
            uid:
              type: string
            kind:
              type: string
              enum:
              - image
            source:
              type: string
              enum:
              - uploaded
              - generated
              - remote_url
              - screenshot
              - chart
              - unknown
            usage_context:
              type: string
              enum:
              - blog_cover
              - social_image
            alt_text:
              type: string
              nullable: true
            source_url:
              type: string
              nullable: true
              description: Thumbnail-ish reference — populated for remote_url/screenshot/chart-sourced
                assets, null for uploaded/generated assets that only have an attached
                file.
      nullable: true
    ReviewQueueItem:
      type: object
      required:
      - uid
      - review_type
      - status
      - approval_level
      - claim_risk_level
      - requested_at
      - reviewable
      - campaign_human_rules
      properties:
        uid:
          type: string
        review_type:
          type: string
          enum:
          - strategy_approval
          - brief_approval
          - draft_review
          - distribution_approval
          - campaign_plan_approval
          - campaign_asset_approval
        status:
          type: string
          enum:
          - pending
          - changes_requested
          - approved
          - rejected
          - cancelled
        approval_level:
          type: string
          enum:
          - standard
          - strict
        claim_risk_level:
          type: string
          enum:
          - low
          - medium
          - high
        claim_evidence:
          type: string
          nullable: true
        reviewer_feedback:
          type: string
          nullable: true
        requested_at:
          type: string
          format: date-time
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        reviewable:
          "$ref": "#/components/schemas/ReviewQueueReviewable"
        campaign_human_rules:
          type: array
          items:
            "$ref": "#/components/schemas/CampaignHumanRule"
          description: Applicable human-mode campaign rules at their current version.
            Empty for non-campaign content (slice 7 task 4).
        campaign_asset:
          "$ref": "#/components/schemas/CampaignAssetApprovalContext"
      additionalProperties: false
    ReviewQueueItemResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ReviewQueueItem"
        meta:
          "$ref": "#/components/schemas/Meta"
    ReviewQueueListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/ReviewQueueItem"
        meta:
          "$ref": "#/components/schemas/Meta"
    ChangesResponseData:
      type: object
      required:
      - since
      - new_issues
      - fixed_issues
      - score_deltas
      - keyword_movements
      - published_content
      - gsc_metric_changes
      properties:
        since:
          type: string
        new_issues:
          type: array
          items:
            "$ref": "#/components/schemas/ChangeIssue"
        fixed_issues:
          type: array
          items:
            "$ref": "#/components/schemas/ChangeIssue"
        score_deltas:
          type: object
          required:
          - overall_score
          - eeat
          - content_score
          - growth_score
          - technical_score
          - performance_score
          - authority_score
          - visibility_score
          - geo_score
          properties:
            overall_score:
              "$ref": "#/components/schemas/DeltaValue"
            eeat:
              type: object
              properties:
                score:
                  "$ref": "#/components/schemas/DeltaValue"
                experience:
                  "$ref": "#/components/schemas/DeltaValue"
                expertise:
                  "$ref": "#/components/schemas/DeltaValue"
                authoritativeness:
                  "$ref": "#/components/schemas/DeltaValue"
                trustworthiness:
                  "$ref": "#/components/schemas/DeltaValue"
            content_score:
              "$ref": "#/components/schemas/DeltaValue"
            growth_score:
              "$ref": "#/components/schemas/DeltaValue"
            technical_score:
              "$ref": "#/components/schemas/DeltaValue"
            performance_score:
              "$ref": "#/components/schemas/DeltaValue"
            authority_score:
              "$ref": "#/components/schemas/DeltaValue"
            visibility_score:
              "$ref": "#/components/schemas/DeltaValue"
            geo_score:
              "$ref": "#/components/schemas/DeltaValue"
        keyword_movements:
          type: object
          required:
          - gained
          - lost
          - moved_up
          - moved_down
          properties:
            gained:
              type: integer
            lost:
              type: integer
            moved_up:
              type: integer
            moved_down:
              type: integer
        published_content:
          type: array
          items:
            "$ref": "#/components/schemas/PublishedContentItem"
        gsc_metric_changes:
          type: object
          properties:
            clicks:
              "$ref": "#/components/schemas/DeltaValue"
            impressions:
              "$ref": "#/components/schemas/DeltaValue"
            ctr:
              "$ref": "#/components/schemas/DeltaValue"
            position:
              "$ref": "#/components/schemas/DeltaValue"
    ChangesResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/ChangesResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    TriggerAuditResponseData:
      type: object
      required:
      - job_id
      - site_slug
      - status
      - triggered_at
      properties:
        job_id:
          type: string
        site_slug:
          type: string
        status:
          type: string
        triggered_at:
          type: string
          format: date-time
    TriggerAuditResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/TriggerAuditResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    AgentRunStatusSite:
      type: object
      required:
      - slug
      - name
      - domain
      properties:
        slug:
          type: string
        name:
          type: string
        domain:
          type: string
    AgentRunStatusAction:
      type: object
      required:
      - name
      - endpoint
      properties:
        name:
          type: string
        endpoint:
          type: string
    AgentRunStatusResponseData:
      type: object
      required:
      - job_id
      - run_uid
      - status
      - agent_name
      - trigger
      - triggered_at
      - started_at
      - completed_at
      - error_summary
      - site
      - action
      properties:
        job_id:
          type: string
        run_uid:
          type: string
        status:
          type: string
          enum:
          - pending
          - running
          - completed
          - failed
          - skipped
        agent_name:
          type: string
        trigger:
          type: string
        triggered_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        error_summary:
          type: string
          nullable: true
        site:
          "$ref": "#/components/schemas/AgentRunStatusSite"
        action:
          "$ref": "#/components/schemas/AgentRunStatusAction"
    AgentRunStatusResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          "$ref": "#/components/schemas/AgentRunStatusResponseData"
        meta:
          "$ref": "#/components/schemas/Meta"
    McpTool:
      type: object
      required:
      - name
      - description
      - endpoint
      - inputSchema
      properties:
        name:
          type: string
        description:
          type: string
        endpoint:
          type: string
        inputSchema:
          type: object
    McpManifest:
      type: object
      required:
      - name
      - version
      - description
      - tools
      properties:
        name:
          type: string
        version:
          type: string
        description:
          type: string
        tools:
          type: array
          items:
            "$ref": "#/components/schemas/McpTool"
