openapi: 3.0.3

info:
  title: Law4Devs API
  version: 1.0.0
  description: |
    Structured EU regulatory compliance data for developers.

    All 19 major EU digital regulations — CRA, NIS2, DORA, GDPR, AI Act, eIDAS,
    DSA, DMA, Data Act, DGA, eIDAS2, CER, PSD2, MiCA, Cybersecurity Act,
    ePrivacy, RED, CSRD, NIS1 — parsed into structured articles, recitals,
    requirements, compliance deadlines, and tags.

    ## Response Conventions

    - **List responses** (paginated): `{ data: [...], meta: { api_version, total, page, per_page, pages }, links: { next, prev } }`
    - **List responses** (non-paginated): `{ data: [...], meta: { api_version, total } }`
    - **Detail responses**: `{ data: { ... } }`
    - **Error responses**: `{ error: { code: "ERROR_CODE", message: "Human-readable message" } }`

    ## Pagination

    All list endpoints accept `page` (≥ 1, default 1) and `per_page` (1–100, default 20).
    Invalid values return `400 INVALID_PARAM`.

servers:
  - url: https://demo.law4devs.eu/api/v1
    description: Demo (CRA only, no key required)

tags:
  - name: Health
    description: API and database health check
  - name: Frameworks
    description: EU regulatory frameworks (CRA, NIS2, GDPR, etc.)
  - name: Articles
    description: Individual articles within a framework
  - name: Recitals
    description: Recitals (preamble paragraphs) within a framework
  - name: Requirements
    description: Extracted compliance requirements with stakeholder role mappings
  - name: Annexes
    description: Technical annexes attached to a framework
  - name: Compliance
    description: Compliance deadlines and key dates
  - name: Stats
    description: Coverage statistics per framework
  - name: Search
    description: Full-text search across articles, recitals, and requirements
  - name: Tags
    description: Semantic tags used to classify content
  - name: Changelog
    description: Content update history and data freshness
  - name: Compare
    description: Cross-framework article comparison

components:
  parameters:
    SlugParam:
      name: slug
      in: path
      required: true
      description: Framework slug (e.g. `cra`, `nis2`, `gdpr`)
      schema:
        type: string
        pattern: '^[a-z0-9_]+$'
        example: cra

    PageParam:
      name: page
      in: query
      description: Page number (1-based)
      schema:
        type: integer
        minimum: 1
        default: 1
        example: 1

    PerPageParam:
      name: per_page
      in: query
      description: Results per page (max 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
        example: 20

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code
              example: NOT_FOUND
            message:
              type: string
              description: Human-readable error description
              example: "Framework 'xyz' not found."

    PaginationMeta:
      type: object
      properties:
        api_version:
          type: string
          example: "1.0"
        total:
          type: integer
          description: Total number of matching records
          example: 71
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 20
        pages:
          type: integer
          description: Total number of pages
          example: 4

    PaginationLinks:
      type: object
      properties:
        next:
          type: string
          nullable: true
          description: Relative URL for the next page, or null if on last page
          example: "?page=2&per_page=20"
        prev:
          type: string
          nullable: true
          description: Relative URL for the previous page, or null if on first page
          example: null

    TagRef:
      type: object
      description: Minimal tag reference embedded inside articles and requirements
      properties:
        slug:
          type: string
          example: vulnerability-reporting
        name:
          type: string
          example: Vulnerability Reporting
        color:
          type: string
          example: "#e74c3c"

    Tag:
      type: object
      properties:
        id:
          type: integer
          example: 1
        slug:
          type: string
          example: vulnerability-reporting
        name:
          type: string
          example: Vulnerability Reporting
        description:
          type: string
          example: Requirements related to disclosing and reporting software vulnerabilities.
        keywords:
          type: array
          items:
            type: string
          example: ["vulnerability", "CVE", "disclose", "disclosure"]
        color:
          type: string
          example: "#e74c3c"
        created_at:
          type: string
          format: date-time

    Framework:
      type: object
      properties:
        id:
          type: integer
          example: 1
        slug:
          type: string
          example: cra
        name:
          type: string
          example: Cyber Resilience Act
        short_name:
          type: string
          example: CRA
        celex_number:
          type: string
          nullable: true
          example: "32024R2847"
        description:
          type: string
          nullable: true
        is_active:
          type: boolean
          example: true
        status:
          type: string
          enum: [active, superseded]
          example: active
        expected_articles:
          type: integer
          nullable: true
          example: 69
        expected_recitals:
          type: integer
          nullable: true
          example: 173
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        article_count:
          type: integer
          example: 71
        recital_count:
          type: integer
          example: 130

    FrameworkDetail:
      allOf:
        - $ref: '#/components/schemas/Framework'
        - type: object
          properties:
            eurlex_url:
              type: string
              format: uri
              nullable: true
              example: "https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32024R2847"
            requirement_count:
              type: integer
              example: 290
            annex_count:
              type: integer
              example: 0
            tag_count:
              type: integer
              example: 10
            coverage:
              type: object
              properties:
                articles_pct:
                  type: number
                  nullable: true
                  example: 102.9
                recitals_pct:
                  type: number
                  nullable: true
                  example: 75.1

    ArticleSummary:
      type: object
      properties:
        id:
          type: integer
          example: 1
        article_number:
          type: integer
          example: 1
        title:
          type: string
          example: Subject matter
        framework_slug:
          type: string
          example: cra
        position:
          type: integer
          example: 1
        paragraph_count:
          type: integer
          example: 0
        processed_date:
          type: string
          format: date-time
          nullable: true
        tags:
          type: array
          description: Full tag objects (list view includes all tag fields)
          items:
            $ref: '#/components/schemas/Tag'

    Article:
      allOf:
        - $ref: '#/components/schemas/ArticleSummary'
        - type: object
          properties:
            content:
              type: string
              description: Full plain-text content of the article
            paragraphs:
              type: array
              items:
                type: object
                properties:
                  paragraph_ref:
                    type: string
                    example: "1"
                  content:
                    type: string
                  position:
                    type: integer
            tags:
              type: array
              items:
                $ref: '#/components/schemas/TagRef'

    Recital:
      type: object
      properties:
        id:
          type: integer
          example: 1
        recital_number:
          type: integer
          example: 1
        content:
          type: string
          description: Full plain-text content of the recital
        framework_slug:
          type: string
          example: cra
        position:
          type: integer
          example: 1

    Requirement:
      type: object
      properties:
        id:
          type: integer
          example: 1
        article_number:
          type: integer
          example: 2
        paragraph_ref:
          type: string
          nullable: true
          example: "1"
        requirement_text:
          type: string
          description: The full text of the requirement
        requirement_type:
          type: string
          enum: [general, mandatory, conditional, prohibition]
          example: general
        stakeholder_roles:
          type: array
          items:
            type: string
          example: ["manufacturer", "importer"]
        tags:
          type: array
          items:
            $ref: '#/components/schemas/TagRef'
        compliance_deadline:
          type: string
          nullable: true
        linked_article_numbers:
          type: array
          items:
            type: integer
          example: [3, 5, 14]
        paragraph_content:
          type: string
          nullable: true
          description: First 300 characters of the source paragraph (for context)
        created_at:
          type: string
          format: date-time
        framework_slug:
          type: string
          example: cra

    AnnexSummary:
      type: object
      properties:
        id:
          type: integer
        annex_number:
          type: string
          description: Annex identifier (e.g. "I", "II", "III")
          example: "I"
        title:
          type: string
          example: Essential Requirements
        framework_slug:
          type: string
          example: cra

    Annex:
      allOf:
        - $ref: '#/components/schemas/AnnexSummary'
        - type: object
          properties:
            content:
              type: string
              description: Full text content of the annex

    ComplianceDeadline:
      type: object
      properties:
        id:
          type: integer
          example: 1
        framework_slug:
          type: string
          example: cra
        article_number:
          type: integer
          example: 0
        paragraph_ref:
          type: string
          nullable: true
          example: entry_into_force
        deadline_date:
          type: string
          format: date
          example: "2024-12-10"
        deadline_type:
          type: string
          example: entry_into_force
          description: |
            One of: entry_into_force, transposition, partial_application,
            full_application, review, other
        description:
          type: string
          example: Entry into force

    SearchResult:
      type: object
      properties:
        type:
          type: string
          enum: [article, recital, requirement]
          example: article
        framework_slug:
          type: string
          example: cra
        framework_name:
          type: string
          example: CRA
        article_number:
          type: integer
          nullable: true
          example: 1
        title:
          type: string
          nullable: true
          example: Subject matter
        match_context:
          type: string
          description: Snippet of text surrounding the search match
          example: "...cybersecurity requirements for the design, development and production..."
        url:
          type: string
          description: Relative URL to fetch the full resource
          example: /v1/frameworks/cra/articles/1

    FrameworkStats:
      type: object
      properties:
        framework:
          type: string
          example: cra
        name:
          type: string
          example: Cyber Resilience Act
        status:
          type: string
          enum: [active, superseded]
          example: active
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        articles:
          type: object
          properties:
            total:
              type: integer
              example: 71
            expected:
              type: integer
              nullable: true
              example: 69
            coverage_pct:
              type: number
              nullable: true
              example: 102.9
        recitals:
          type: object
          properties:
            total:
              type: integer
              example: 130
            expected:
              type: integer
              nullable: true
              example: 173
            coverage_pct:
              type: number
              nullable: true
              example: 75.1
        requirements:
          type: object
          properties:
            total:
              type: integer
              example: 290
        annexes:
          type: object
          properties:
            total:
              type: integer
              example: 0
        tags:
          type: object
          properties:
            total:
              type: integer
              example: 10

    SyncLogEntry:
      type: object
      properties:
        synced_at:
          type: string
          format: date-time
        articles_added:
          type: integer
        articles_updated:
          type: integer
        recitals_added:
          type: integer
        recitals_updated:
          type: integer
        requirements_added:
          type: integer
        duration_seconds:
          type: number
          nullable: true
        success:
          type: boolean
        error_message:
          type: string
          nullable: true

paths:

  # ── Health ──────────────────────────────────────────────────────────────────

  /health:
    get:
      tags: [Health]
      summary: Health check
      description: Returns API status and database connectivity.
      operationId: health_check
      responses:
        '200':
          description: API and database are healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  database:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: "1.0.0"
        '503':
          description: Database unreachable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: error
                  database:
                    type: string
                    example: unreachable
                  error:
                    type: string

  # ── Frameworks ──────────────────────────────────────────────────────────────

  /frameworks:
    get:
      tags: [Frameworks]
      summary: List all active frameworks
      description: Returns all active EU regulatory frameworks ordered by slug.
      operationId: list_frameworks
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: List of active frameworks
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, links]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Framework'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                        example: 19
                      page:
                        type: integer
                        example: 1
                      per_page:
                        type: integer
                        example: 20
                      pages:
                        type: integer
                        example: 1
                  links:
                    type: object
                    properties:
                      next:
                        type: string
                        nullable: true
                      prev:
                        type: string
                        nullable: true

  /frameworks/{slug}:
    get:
      tags: [Frameworks]
      summary: Get framework detail
      description: |
        Returns full framework metadata including counts (articles, recitals,
        requirements, annexes, tags) and coverage percentages.
      operationId: get_framework
      parameters:
        - $ref: '#/components/parameters/SlugParam'
      responses:
        '200':
          description: Framework detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/FrameworkDetail'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Articles ────────────────────────────────────────────────────────────────

  /frameworks/{slug}/articles:
    get:
      tags: [Articles]
      summary: List articles for a framework
      description: |
        Returns paginated articles for a framework. Supports optional filtering
        by tag slug and full-text search within article titles and content.
      operationId: list_articles
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - name: tag
          in: query
          description: Filter articles that have this tag slug
          schema:
            type: string
            example: vulnerability-reporting
        - name: search
          in: query
          description: Full-text search within article title and content
          schema:
            type: string
            example: cybersecurity
      responses:
        '200':
          description: Paginated articles
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ArticleSummary'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
                  links:
                    $ref: '#/components/schemas/PaginationLinks'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /frameworks/{slug}/articles/{article_number}:
    get:
      tags: [Articles]
      summary: Get a single article
      description: |
        Returns the full article including its content, ordered paragraphs,
        and associated tags.
      operationId: get_article
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - name: article_number
          in: path
          required: true
          description: Article number (e.g. 1, 2, 14)
          schema:
            type: integer
            example: 1
      responses:
        '200':
          description: Article with full content
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Article'
        '404':
          description: Framework or article not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /frameworks/{slug}/articles/{article_number}/related:
    get:
      tags: [Articles]
      summary: Get related articles and requirements
      description: |
        Returns articles and requirements that cross-reference this article
        via `linked_article_numbers`. Includes both outbound references
        (articles this one links to) and inbound references (articles linking
        to this one).
      operationId: get_related_articles
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - name: article_number
          in: path
          required: true
          schema:
            type: integer
            example: 1
      responses:
        '200':
          description: Articles that cross-reference this article (paginated list)
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, links]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ArticleSummary'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                      page:
                        type: integer
                      per_page:
                        type: integer
                      pages:
                        type: integer
                  links:
                    type: object
                    properties:
                      next:
                        type: string
                        nullable: true
                      prev:
                        type: string
                        nullable: true
        '404':
          description: Framework or article not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Recitals ────────────────────────────────────────────────────────────────

  /frameworks/{slug}/recitals:
    get:
      tags: [Recitals]
      summary: List recitals for a framework
      description: Returns paginated recitals (preamble paragraphs) ordered by recital number.
      operationId: list_recitals
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Paginated recitals
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Recital'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
                  links:
                    $ref: '#/components/schemas/PaginationLinks'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /frameworks/{slug}/recitals/{recital_number}:
    get:
      tags: [Recitals]
      summary: Get a single recital
      operationId: get_recital
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - name: recital_number
          in: path
          required: true
          description: Recital number
          schema:
            type: integer
            example: 1
      responses:
        '200':
          description: Recital with full content
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Recital'
        '404':
          description: Framework or recital not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Requirements ────────────────────────────────────────────────────────────

  /frameworks/{slug}/requirements:
    get:
      tags: [Requirements]
      summary: List requirements for a framework
      description: |
        Returns paginated compliance requirements extracted from a framework's
        articles. Filterable by stakeholder role, requirement type, and tag.
      operationId: list_framework_requirements
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - name: role
          in: query
          description: Filter by stakeholder role (e.g. manufacturer, importer, distributor)
          schema:
            type: string
            example: manufacturer
        - name: type
          in: query
          description: Filter by requirement type
          schema:
            type: string
            enum: [general, mandatory, conditional, prohibition]
        - name: tag
          in: query
          description: Filter by tag slug
          schema:
            type: string
            example: risk-assessment
      responses:
        '200':
          description: Paginated requirements
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Requirement'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
                  links:
                    $ref: '#/components/schemas/PaginationLinks'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /requirements:
    get:
      tags: [Requirements]
      summary: List requirements across all frameworks
      description: |
        Cross-framework requirements endpoint. Returns all requirements from
        all active frameworks. Optionally filter to a single framework,
        stakeholder role, type, or tag.
      operationId: list_all_requirements
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - name: framework
          in: query
          description: Limit to one framework slug
          schema:
            type: string
            example: cra
        - name: role
          in: query
          description: Filter by stakeholder role
          schema:
            type: string
            example: manufacturer
        - name: type
          in: query
          description: Filter by requirement type
          schema:
            type: string
            enum: [general, mandatory, conditional, prohibition]
        - name: tag
          in: query
          description: Filter by tag slug
          schema:
            type: string
      responses:
        '200':
          description: Paginated requirements across all frameworks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Requirement'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
                  links:
                    $ref: '#/components/schemas/PaginationLinks'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Annexes ─────────────────────────────────────────────────────────────────

  /frameworks/{slug}/annexes:
    get:
      tags: [Annexes]
      summary: List annexes for a framework
      operationId: list_annexes
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Paginated annexes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnnexSummary'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
                  links:
                    $ref: '#/components/schemas/PaginationLinks'
        '400':
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /frameworks/{slug}/annexes/{annex_number}:
    get:
      tags: [Annexes]
      summary: Get a single annex
      operationId: get_annex
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - name: annex_number
          in: path
          required: true
          description: Annex identifier (e.g. "I", "II", "III", "IV")
          schema:
            type: string
            example: "I"
      responses:
        '200':
          description: Annex with full content
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Annex'
        '404':
          description: Framework or annex not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Compliance ──────────────────────────────────────────────────────────────

  /frameworks/{slug}/compliance/deadlines:
    get:
      tags: [Compliance]
      summary: List compliance deadlines for a framework
      description: Returns all compliance deadlines for the framework, ordered by date.
      operationId: list_compliance_deadlines
      parameters:
        - $ref: '#/components/parameters/SlugParam'
      responses:
        '200':
          description: Compliance deadlines ordered chronologically
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ComplianceDeadline'
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /compliance/deadlines:
    get:
      tags: [Compliance]
      summary: List all compliance deadlines across all frameworks
      description: |
        Returns all compliance deadlines across all active frameworks, ordered
        by date. Useful for building compliance calendars. Filter by framework
        slug to scope to a single regulation.
      operationId: list_all_deadlines
      parameters:
        - name: framework
          in: query
          description: Filter by framework slug
          schema:
            type: string
            example: cra
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: All compliance deadlines ordered chronologically
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, links]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ComplianceDeadline'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                      page:
                        type: integer
                      per_page:
                        type: integer
                      pages:
                        type: integer
                  links:
                    type: object
                    properties:
                      next:
                        type: string
                        nullable: true
                      prev:
                        type: string
                        nullable: true

  # ── Stats ───────────────────────────────────────────────────────────────────

  /frameworks/{slug}/stats:
    get:
      tags: [Stats]
      summary: Coverage statistics for a single framework
      description: |
        Returns article, recital, requirement, annex, and tag counts for the
        framework, including coverage percentages vs. expected totals.
      operationId: get_framework_stats
      parameters:
        - $ref: '#/components/parameters/SlugParam'
      responses:
        '200':
          description: Framework statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/FrameworkStats'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /stats:
    get:
      tags: [Stats]
      summary: Coverage statistics for all active frameworks
      description: Returns stats for every active framework in one call.
      operationId: get_global_stats
      responses:
        '200':
          description: Stats for all active frameworks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FrameworkStats'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                        example: 19

  # ── Search ──────────────────────────────────────────────────────────────────

  /search:
    get:
      tags: [Search]
      summary: Full-text search across articles, recitals, and requirements
      description: |
        Searches article titles and content, recital content, and requirement
        text across all active frameworks. Returns ranked results with context
        snippets. Supports filtering by framework, tag, stakeholder role,
        and result type.

        Query must be at least 2 characters and at most 500 characters.
      operationId: search
      parameters:
        - name: q
          in: query
          required: true
          description: Search query (2–500 characters)
          schema:
            type: string
            minLength: 2
            maxLength: 500
            example: cybersecurity requirements
        - name: framework
          in: query
          description: Limit search to one framework slug
          schema:
            type: string
            example: cra
        - name: tag
          in: query
          description: Filter results to those with this tag slug
          schema:
            type: string
            example: risk-assessment
        - name: role
          in: query
          description: Filter requirements by stakeholder role
          schema:
            type: string
            example: manufacturer
        - name: type
          in: query
          description: Limit to one result type
          schema:
            type: string
            enum: [article, recital, requirement]
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Search results with facets
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SearchResult'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                        example: 159
                      page:
                        type: integer
                        example: 1
                      per_page:
                        type: integer
                        example: 20
                      query:
                        type: string
                        example: cybersecurity requirements
                      facets:
                        type: object
                        description: Count of results per category
                        additionalProperties:
                          type: integer
                        example:
                          articles: 45
                          recitals: 30
                          requirements: 84
        '400':
          description: Query missing, too short (<2 chars), or too long (>500 chars)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Tags ────────────────────────────────────────────────────────────────────

  /tags:
    get:
      tags: [Tags]
      summary: List all tags
      description: Returns all semantic tags used to classify articles and requirements.
      operationId: list_tags
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: All tags ordered by name
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, links]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tag'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer
                      page:
                        type: integer
                      per_page:
                        type: integer
                      pages:
                        type: integer
                  links:
                    type: object
                    properties:
                      next:
                        type: string
                        nullable: true
                      prev:
                        type: string
                        nullable: true

  /tags/{slug}:
    get:
      tags: [Tags]
      summary: Get tag detail
      description: Returns a single tag with its keywords and metadata.
      operationId: get_tag
      parameters:
        - $ref: '#/components/parameters/SlugParam'
      responses:
        '200':
          description: Tag detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Tag'
        '404':
          description: Tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Changelog ───────────────────────────────────────────────────────────────

  /frameworks/{slug}/changelog:
    get:
      tags: [Changelog]
      summary: Content update history for a framework
      description: |
        Returns content update log entries for a specific framework, newest first.
        Use this to check when data was last updated and how many records
        were added or modified.
      operationId: get_framework_changelog
      parameters:
        - $ref: '#/components/parameters/SlugParam'
        - name: limit
          in: query
          description: Maximum number of log entries (default 20)
          schema:
            type: integer
            default: 20
            example: 10
      responses:
        '200':
          description: Content update log entries for this framework
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SyncLogEntry'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      framework:
                        type: string
                        example: cra
                      total:
                        type: integer
                        example: 3
        '404':
          description: Framework not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /changelog:
    get:
      tags: [Changelog]
      summary: Global content update changelog
      description: |
        Returns content update log entries across all frameworks, newest first.
        Filter by framework slug or date to scope results.
      operationId: get_global_changelog
      parameters:
        - name: framework
          in: query
          description: Filter by framework slug
          schema:
            type: string
            example: cra
        - name: since
          in: query
          description: Only return entries after this ISO 8601 datetime
          schema:
            type: string
            format: date-time
            example: "2025-01-01T00:00:00Z"
        - name: limit
          in: query
          description: Maximum number of entries (default 50)
          schema:
            type: integer
            default: 50
            example: 20
      responses:
        '200':
          description: Content update log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SyncLogEntry'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
                      total:
                        type: integer

  # ── Compare ─────────────────────────────────────────────────────────────────

  /compare:
    get:
      tags: [Compare]
      summary: Compare articles across multiple frameworks
      description: |
        Returns articles grouped by framework slug for side-by-side comparison.
        Accepts a comma-separated list of framework slugs. Optionally filter
        articles by tag to focus the comparison on a specific topic.
      operationId: compare_frameworks
      parameters:
        - name: frameworks
          in: query
          required: true
          description: Comma-separated list of framework slugs (max 5 recommended)
          schema:
            type: string
            example: "cra,nis2,gdpr"
        - name: tag
          in: query
          description: Filter articles to those with this tag slug
          schema:
            type: string
            example: risk-assessment
      responses:
        '200':
          description: Articles and requirements grouped by framework
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      frameworks:
                        type: object
                        description: Map of framework slug to its articles and requirements
                        additionalProperties:
                          type: object
                          properties:
                            framework_name:
                              type: string
                              example: Cyber Resilience Act
                            articles:
                              type: array
                              items:
                                type: object
                                properties:
                                  article_number:
                                    type: integer
                                    example: 1
                                  title:
                                    type: string
                                    example: Subject matter
                            requirements:
                              type: array
                              items:
                                $ref: '#/components/schemas/Requirement'
                      tag:
                        description: The tag used to filter, if `tag` param was provided
                        nullable: true
                        allOf:
                          - $ref: '#/components/schemas/Tag'
                  meta:
                    type: object
                    properties:
                      api_version:
                        type: string
                        example: "1.0"
        '404':
          description: One or more framework slugs not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
