openapi: 3.0.3
info:
  title: Vitagri Vital Pulse Index API
  version: 0.1.0-draft
  description: |
    Public-read API exposing the UK Food System Vital Pulse Index. Implemented as Netlify
    Functions on the existing Vitagri stack. Read endpoints are unauthenticated and
    publicly cacheable. Write endpoints are gated through the existing Pulse admin
    role-based access (HTTP-only cookie session, magic-link authentication).

    Vital Pulse is a velocity-aware index. Every reading carries both a level (the score)
    and a velocity (smoothed 4-quarter rate of change). API responses surface both, plus
    a qualitative direction indicator (improving / stable / deteriorating) that makes the
    velocity easy to interpret for media and policy consumers.

    Designed to be embeddable, citable and AI-crawlable. Free academic and media use is
    explicit; commercial integrations move to Tier 2 Commercial Research Partner pricing
    once that tier opens.

  contact:
    name: Rob Ward
    email: rob.ward@vitagri.org
  license:
    name: Open data \u2014 CC-BY 4.0 with citation requirement
    url: https://creativecommons.org/licenses/by/4.0/

servers:
  - url: https://vitagri.org/api/vital-pulse
    description: Production
  - url: https://staging--meek-cascaron-f3c17a.netlify.app/api/vital-pulse
    description: Staging (Netlify preview deploy)

tags:
  - name: public-read
    description: Public unauthenticated read endpoints. Cacheable at the edge.
  - name: admin-write
    description: Admin endpoints. Require Pulse admin session.
  - name: embed
    description: Embeddable widget endpoints (HTML responses).

paths:
  /current:
    get:
      tags: [public-read]
      summary: Current Vital Pulse Index reading
      description: Returns the most recent published quarterly reading, including the composite VPI score, velocity, direction, and all six pillar readings with their own scores and velocities.
      responses:
        '200':
          description: Current reading
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Reading'
              example:
                version: "0.1.0"
                year: 2026
                quarter: "Q3"
                publishedAt: "2026-08-04T09:00:00Z"
                compositeScore: 47
                compositeStatus: "amber"
                compositeChangeFromPriorQuarter: 1.2
                compositeVelocity: 0.9
                compositeDirection: "improving"
                headlineFinding: "Vital Pulse holds at 47 for Q3, improving at +0.9 points/quarter; soil health and policy momentum are the strongest contributors."
                pillars:
                  - id: "soil-health-trajectory"
                    score: 52
                    status: "amber"
                    changeFromPriorQuarter: 0.8
                    velocity: 1.1
                    direction: "improving"
                    biAttributed: false
        '503':
          description: Service unavailable (no published reading yet)

  /timeseries:
    get:
      tags: [public-read]
      summary: Time series for the composite or a specific pillar
      description: Returns historical scores and velocities. Suitable for plotting both level and rate-of-change over time.
      parameters:
        - name: indicator
          in: query
          required: false
          schema:
            type: string
            enum: [vp, soil-health-trajectory, crop-nutrient-density, polyphenol-bioactive-trend, public-procurement-quality, diet-related-disease-burden, policy-market-momentum]
          description: Indicator to retrieve. Omit for composite VPI.
        - name: from
          in: query
          required: false
          schema:
            type: string
            example: "2026-Q1"
        - name: to
          in: query
          required: false
          schema:
            type: string
            example: "2027-Q4"
        - name: include
          in: query
          required: false
          schema:
            type: string
            enum: [score, velocity, both]
            default: both
          description: Whether the response includes scores, velocities, or both.
      responses:
        '200':
          description: Time series of readings
          content:
            application/json:
              schema:
                type: object
                properties:
                  indicator:
                    type: string
                  readings:
                    type: array
                    items:
                      type: object
                      properties:
                        year: { type: integer }
                        quarter: { type: string }
                        score: { type: number }
                        velocity: { type: number }
                        direction: { type: string }
                        status: { type: string }

  /methodology:
    get:
      tags: [public-read]
      summary: Current methodology version
      description: Returns the methodology version, weights, velocity-window length, direction thresholds, sources and panel approval state.
      responses:
        '200':
          description: Methodology
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Methodology'

  /methodology/{version}:
    get:
      tags: [public-read]
      summary: Specific methodology version (audit access)
      parameters:
        - name: version
          in: path
          required: true
          schema:
            type: string
            example: "0.1.0"
      responses:
        '200':
          description: Methodology at the requested version

  /annual/{year}:
    get:
      tags: [public-read]
      summary: Annual flagship report
      parameters:
        - name: year
          in: path
          required: true
          schema:
            type: integer
            example: 2026
      responses:
        '200':
          description: Annual report
          content:
            application/json:
              schema:
                type: object
                properties:
                  year: { type: integer }
                  publishedAt: { type: string }
                  title: { type: string }
                  headlineNumber: { type: number }
                  headlineVelocity: { type: number }
                  headlineDirection: { type: string }
                  headlineNarrative: { type: string }
                  downloadablePdfUrl: { type: string }
                  downloadableDataUrl: { type: string }

  /panel:
    get:
      tags: [public-read]
      summary: Current methodology panel composition
      responses:
        '200':
          description: Panel
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        title: { type: string }
                        affiliation: { type: string }
                        publicProfileUrl: { type: string }
                  chair: { type: string }
                  conflictRegistryUrl: { type: string }

  /ticker:
    get:
      tags: [public-read]
      summary: Live evidence ticker
      description: Returns the last 50 evidence-base events drawn from the Pulse Brain ingest pipeline.
      responses:
        '200':
          description: Ticker
          content:
            application/json:
              schema:
                type: object
                properties:
                  lastRefreshedAt: { type: string }
                  events:
                    type: array

  /embed/headline:
    get:
      tags: [embed]
      summary: Embeddable headline gauge widget (HTML)
      description: |
        Returns a self-contained HTML fragment displaying the composite Vital Pulse gauge,
        the headline number, the velocity badge and the direction. Suitable for partner
        sites and media embeds. Includes inline CSS, no external dependencies. Static SVG
        gauge rendering \u2014 no JavaScript required for first paint.
      parameters:
        - name: theme
          in: query
          required: false
          schema:
            type: string
            enum: [light, dark]
            default: light
        - name: variant
          in: query
          required: false
          schema:
            type: string
            enum: [composite, full]
            default: composite
          description: \u2018composite\u2019 returns just the headline gauge; \u2018full\u2019 returns the headline gauge plus the six pillar gauges in a 3\u00D72 grid.
      responses:
        '200':
          description: HTML embed
          content:
            text/html:
              schema:
                type: string

  /admin/draft:
    post:
      tags: [admin-write]
      summary: Save a draft reading
      description: Admin-only. Saves a draft quarterly reading (vp:draft:* key) without publishing. Velocity and direction fields are computed automatically by the publish flow at write time.
      security:
        - PulseAdminCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Reading'
      responses:
        '201':
          description: Draft saved
        '401':
          description: Unauthenticated
        '403':
          description: Insufficient role

  /admin/publish:
    post:
      tags: [admin-write]
      summary: Publish a draft reading
      description: |
        Admin-only. Promotes a draft to live. Requires both methodology-panel approval flag
        and a panel-chair authorisation header. Atomic publish-then-flip: writes all
        per-pillar Blobs first (with computed velocity and direction), then swaps vp:current.
      security:
        - PulseAdminCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                draftKey: { type: string, example: "vp:draft:rob:1714478400" }
                panelApprovalDate: { type: string, format: date }
                panelChairAuthorisation: { type: string, description: "Panel chair sign-off token (out-of-band)" }
      responses:
        '200':
          description: Published. Returns the new vp:current value.

components:
  securitySchemes:
    PulseAdminCookie:
      type: apiKey
      in: cookie
      name: pulse_session
      description: Existing Pulse admin session cookie. Magic-link authenticated, HTTP-only.

  schemas:
    Reading:
      type: object
      properties:
        version: { type: string }
        year: { type: integer }
        quarter: { type: string, enum: ["Q1", "Q2", "Q3", "Q4"] }
        publishedAt: { type: string, format: date-time }
        compositeScore: { type: number, minimum: 0, maximum: 100 }
        compositeStatus: { type: string, enum: ["red", "amber", "green"] }
        compositeChangeFromPriorQuarter: { type: number }
        compositeVelocity:
          type: number
          description: Smoothed 4-quarter rolling change in points-per-quarter. Positive = improving.
        compositeAcceleration:
          type: number
          nullable: true
          description: Second-order \u2014 change in velocity. Null until methodology v1.0.
        compositeDirection: { type: string, enum: ["improving", "stable", "deteriorating"] }
        headlineFinding: { type: string }
        pillars:
          type: array
          items:
            $ref: '#/components/schemas/PillarReading'
        methodologyVersion: { type: string }

    PillarReading:
      type: object
      properties:
        id: { type: string }
        score: { type: number, minimum: 0, maximum: 100 }
        status: { type: string, enum: ["red", "amber", "green"] }
        changeFromPriorQuarter: { type: number }
        velocity:
          type: number
          description: Smoothed 4-quarter rolling change in points-per-quarter for this pillar.
        acceleration:
          type: number
          nullable: true
        direction: { type: string, enum: ["improving", "stable", "deteriorating"] }
        ingestedAt: { type: string, format: date-time }
        ingestedSource: { type: string }
        biAttributed: { type: boolean }
        narrative: { type: string }

    Methodology:
      type: object
      properties:
        version: { type: string }
        publishedAt: { type: string, format: date-time }
        approvedByPanel: { type: boolean }
        summary: { type: string }
        fullPaperUrl: { type: string }
        weights:
          type: object
          additionalProperties:
            type: number
        velocityWindow: { type: integer, description: "Rolling-window length in quarters. Default 4." }
        directionThresholds: { type: object }
        thresholds:
          type: object
        sources:
          type: object

x-vitagri-implementation-notes:
  - All read endpoints emit Cache-Control headers tuned to the publishing cadence. /current is cached for 1 hour at edge; /timeseries for 6 hours; /methodology for 24 hours; /ticker for 30 minutes.
  - All read endpoints emit CORS headers permitting cross-origin embed and academic API consumption.
  - All read responses include a Source attribution header (X-Source) and a Methodology-Version header.
  - Velocity and direction fields are computed by the publish flow at write time \u2014 not by the read endpoints. This ensures consistency between the dashboard, the API and any cached embeds.
  - Admin endpoints log every action to the existing Pulse Brain admin activity log.
  - Endpoint URLs follow the existing /.netlify/functions/{name} routing under the hood; the human-readable /api/vital-pulse/* paths are configured in netlify.toml redirects.
