openapi: 3.1.0
info:
  title: Human-AI Chat Interaction Observatory
  version: 1.0.0
  description: |
    Analyses the interaction in a human-AI conversation rather than its topic.

    Layer 1 is deterministic and returns inside the ingest request. Layer 2
    (LCC and ICCC) is optional, never enabled implicitly, and runs
    asynchronously.

    These measures describe observable and computationally inferred interaction
    patterns. They do not, by themselves, establish learning, mastery, cognitive
    effort, engagement, trust, factual quality, intelligence, personality, or
    causal influence.
  license: { name: MIT }
servers:
  - url: https://observatory.example.org
    description: Production
  - url: http://localhost:8080
    description: Local mock mode

security:
  - bearerAuth: []

tags:
  - { name: health }
  - { name: threads }
  - { name: contributions }
  - { name: analysis }
  - { name: lcc }
  - { name: iccc }
  - { name: owner }
  - { name: research }
  - { name: events }

paths:
  /health:
    get:
      tags: [health]
      security: []
      summary: Liveness
      responses:
        '200': { description: Healthy }
        '503': { description: Draining }
  /ready:
    get:
      tags: [health]
      security: []
      summary: Readiness; validates configuration without disclosing secrets
      responses:
        '200': { description: Ready }
        '503': { description: Not ready }
  /api/v1/meta:
    get:
      tags: [health]
      security: []
      summary: Algorithm versions, semantic model version and active drivers
      responses:
        '200': { description: Metadata }

  /api/v1/threads:
    post:
      tags: [threads]
      summary: Create a thread (idempotent on threadId)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateThread' }
      responses:
        '201': { description: Created }
        '200': { description: Already existed }
        '400': { $ref: '#/components/responses/ValidationFailed' }
    get:
      tags: [threads]
      summary: List the caller's threads
      responses:
        '200': { description: Threads }

  /api/v1/threads/{threadId}:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [threads]
      summary: Read a thread and the caller's effective access
      responses:
        '200': { description: Thread }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [threads]
      summary: Delete, or record a deletion request when retention forbids deletion
      responses:
        '204': { description: Deleted }
        '202': { description: Deletion requested and recorded }

  /api/v1/threads/{threadId}/participants:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [threads]
      summary: Register a participant
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Participant' }
      responses: { '201': { description: Created } }
    get:
      tags: [threads]
      summary: List participants (pseudonymous for researchers)
      responses: { '200': { description: Participants } }

  /api/v1/threads/{threadId}/turns:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [contributions]
      summary: Ingest one contribution and return Layer 1 immediately
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateTurn' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IngestResult' }
        '200': { description: Idempotent replay }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Thread is pending deletion }
    get:
      tags: [contributions]
      summary: List contributions, filtered by grant scope
      responses: { '200': { description: Contributions } }

  /api/v1/threads/{threadId}/turns/batch:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [contributions]
      summary: Ingest up to 500 contributions in order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [turns]
              properties:
                turns:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { $ref: '#/components/schemas/CreateTurn' }
      responses: { '201': { description: Ingested } }

  /api/v1/threads/{threadId}/turns/{turnId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { $ref: '#/components/parameters/turnId' }
    get:
      tags: [contributions]
      summary: One contribution with its core analysis
      responses:
        '200': { description: Contribution }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/threads/{threadId}/analysis:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [analysis]
      summary: Thread-level Layer 1 analysis with its versioned envelope
      responses:
        '200': { description: Analysis }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/threads/{threadId}/analysis/turns/{turnId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { $ref: '#/components/parameters/turnId' }
    get:
      tags: [analysis]
      summary: Per-turn measures, plus speech acts under the speech_acts scope
      responses: { '200': { description: Analysis } }

  /api/v1/threads/{threadId}/reanalyze:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [analysis]
      summary: Recompute Layer 1 and re-enqueue Layer 2; safe to repeat
      responses: { '202': { description: Reanalysis started } }

  /api/v1/threads/{threadId}/semantic-extension:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [analysis]
      summary: Semantic configuration and per-engine applicability with reasons
      responses: { '200': { description: Configuration } }
    put:
      tags: [analysis]
      summary: Set the semantic configuration; requesting an engine requires its block
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SemanticConfiguration' }
      responses:
        '200': { description: Updated }
        '400': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/threads/{threadId}/semantic-analysis:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [analysis]
      summary: Shared semantic artifacts; units and embeddings are separately scoped
      responses: { '200': { description: Artifacts } }

  /api/v1/lcc/reference-models:
    post:
      tags: [lcc]
      summary: Publish a reference model version
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReferenceModel' }
      responses:
        '201': { description: Published }
        '409': { description: Version already exists }
    get:
      tags: [lcc]
      summary: List reference models
      responses: { '200': { description: Reference models } }

  /api/v1/lcc/reference-models/{referenceModelId}:
    parameters:
      - { name: referenceModelId, in: path, required: true, schema: { type: string } }
      - { name: version, in: query, schema: { type: string } }
    get:
      tags: [lcc]
      summary: Read a reference model, optionally at a specific version
      responses: { '200': { description: Reference model } }
    put:
      tags: [lcc]
      summary: Publish a new version (versions are immutable)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReferenceModel' }
      responses:
        '200': { description: Published }
        '409': { description: A new version string is required }

  /api/v1/threads/{threadId}/lcc:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [lcc]
      summary: LCC curves, or not_applicable with reasons
      responses:
        '200':
          description: Curves or a not_applicable result
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LccResult' }

  /api/v1/threads/{threadId}/lcc/learners/{learnerId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: learnerId, in: path, required: true, schema: { type: string } }
    get:
      tags: [lcc]
      summary: One learner's series
      responses: { '200': { description: Series } }

  /api/v1/threads/{threadId}/lcc/turns/{turnId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { $ref: '#/components/parameters/turnId' }
    get:
      tags: [lcc]
      summary: One LCC curve point
      responses: { '200': { description: Point } }

  /api/v1/threads/{threadId}/lcc/recalculate:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [lcc]
      summary: Queue an LCC recalculation
      responses: { '202': { description: Queued } }

  /api/v1/threads/{threadId}/iccc:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [iccc]
      summary: Six curves per participant, or not_applicable with reasons
      responses:
        '200':
          description: Curves
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IcccResult' }

  /api/v1/threads/{threadId}/iccc/participants:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get: { tags: [iccc], summary: Participant summaries, responses: { '200': { description: Participants } } }

  /api/v1/threads/{threadId}/iccc/participants/{participantId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: participantId, in: path, required: true, schema: { type: string } }
    get: { tags: [iccc], summary: One participant's series, responses: { '200': { description: Series } } }

  /api/v1/threads/{threadId}/iccc/contributions/{contributionId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: contributionId, in: path, required: true, schema: { type: string } }
    get: { tags: [iccc], summary: One ICCC curve point, responses: { '200': { description: Point } } }

  /api/v1/threads/{threadId}/iccc/configuration:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      {
        tags: [iccc],
        summary: Read the ICCC configuration,
        responses: { '200': { description: Configuration } },
      }
    put:
      tags: [iccc]
      summary: Set the ICCC configuration and queue a recalculation
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IcccConfiguration' }
      responses:
        '200':
          description: Updated; includes a comparabilityWarning when existing curves become incomparable

  /api/v1/threads/{threadId}/iccc/recalculate:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      { tags: [iccc], summary: Queue an ICCC recalculation, responses: { '202': { description: Queued } } }

  /api/v1/threads/{threadId}/iccc/comparability:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: a, in: query, required: true, schema: { type: string } }
      - { name: b, in: query, required: true, schema: { type: string } }
    get:
      tags: [iccc]
      summary: Whether two participants' curves may be compared
      responses: { '200': { description: Comparability verdict } }

  /api/v1/threads/{threadId}/events:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [events]
      summary: Server-Sent Events for this thread
      description: |
        Supports Last-Event-ID replay, reconnects and heartbeats. Authorization
        runs before the stream opens. Internal owner keys are stripped from
        every frame.
      responses:
        '200':
          description: Event stream
          content:
            text/event-stream:
              schema: { type: string }

  /api/v1/me/threads:
    get: { tags: [owner], summary: The owner's threads, responses: { '200': { description: Threads } } }

  /api/v1/me/threads/{threadId}/archive:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [owner]
      summary: Complete owner archive including access history
      responses: { '200': { description: Archive } }

  /api/v1/me/threads/{threadId}/exports:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [owner]
      summary: Start an asynchronous export
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                format: { type: string, enum: [json, jsonl, csv], default: json }
      responses: { '202': { description: Export queued } }

  /api/v1/me/threads/{threadId}/exports/inline:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: format, in: query, schema: { type: string, enum: [json, jsonl, csv] } }
    get: { tags: [owner], summary: Synchronous export, responses: { '200': { description: Export body } } }

  /api/v1/me/exports/{exportId}:
    parameters: [{ name: exportId, in: path, required: true, schema: { type: string } }]
    get:
      tags: [owner]
      summary: Export status and, once complete, a short-lived signed URL
      responses: { '200': { description: Export record } }

  /api/v1/threads/{threadId}/grants:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    post:
      tags: [research]
      summary: Grant scoped research access (owner only)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateGrant' }
      responses: { '201': { description: Granted } }
    get:
      { tags: [research], summary: List grants (owner only), responses: { '200': { description: Grants } } }

  /api/v1/threads/{threadId}/grants/{grantId}:
    parameters:
      - { $ref: '#/components/parameters/threadId' }
      - { name: grantId, in: path, required: true, schema: { type: string } }
    delete:
      tags: [research]
      summary: Revoke immediately
      responses: { '200': { description: Revoked } }

  /api/v1/threads/{threadId}/access-history:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      {
        tags: [research],
        summary: Audited access (owner only),
        responses: { '200': { description: Events } },
      }

  /api/v1/research/threads/{threadId}/analysis:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [research]
      summary: Grant-filtered thread analysis
      responses:
        '200': { description: Filtered analysis }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/research/threads/{threadId}/lcc:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [research]
      summary: LCC points; requires lcc_curves. Reference-model content is never disclosed.
      responses:
        '200': { description: Points }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/research/threads/{threadId}/iccc:
    parameters: [{ $ref: '#/components/parameters/threadId' }]
    get:
      tags: [research]
      summary: ICCC series; requires iccc_curves
      responses:
        '200': { description: Series }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/research/exports:
    post:
      tags: [research]
      summary: Export the permitted fields; requires the export scope per thread
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [threadIds]
              properties:
                format: { type: string, enum: [json, jsonl, csv] }
                threadIds: { type: array, items: { type: string } }
      responses:
        '200': { description: Export body }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }

  parameters:
    threadId:
      name: threadId
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Identifier' }
    turnId:
      name: turnId
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Identifier' }

  responses:
    ValidationFailed:
      description: Request validation failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Denied, or a required scope was not granted
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Not found or not visible to this principal
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Identifier:
      type: string
      pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$'
      description: Opaque identifier. Never an email address or a JWT subject.

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            requestId: { type: string }
            details: { type: array, items: { type: object } }

    InteractionType:
      type: string
      enum: [general_dyad, tutorial_dyad, multi_person_discussion, multi_agent_discussion, group_tutoring]

    QualityFlag:
      type: object
      required: [code, severity, message]
      properties:
        code: { type: string }
        severity: { type: string, enum: [info, warning, error] }
        message: { type: string }
        field: { type: string }

    TokenCount:
      type: object
      properties:
        value: { type: [integer, 'null'] }
        source: { type: string, enum: [provider_reported, locally_counted, estimated] }

    Participant:
      type: object
      required: [participantId, type, roles]
      properties:
        participantId: { $ref: '#/components/schemas/Identifier' }
        type: { type: string, enum: [human, assistant, agent, avatar, system, tool] }
        roles:
          type: array
          minItems: 1
          items: { type: string, enum: [learner, tutor, moderator, peer, assistant, other] }
        displayLabel: { type: string }
        model: { type: string }
        provider: { type: string }
        consentVersion: { type: string }

    LccConfiguration:
      type: object
      required: [referenceModelId, learnerParticipantIds, tutorParticipantIds]
      properties:
        referenceModelId: { $ref: '#/components/schemas/Identifier' }
        referenceModelVersion: { type: string }
        learnerParticipantIds:
          { type: array, minItems: 1, items: { $ref: '#/components/schemas/Identifier' } }
        tutorParticipantIds: { type: array, minItems: 1, items: { $ref: '#/components/schemas/Identifier' } }
        classificationMethod:
          type: string
          enum: [factor_product, direct_semantic_classification, hybrid]
          default: hybrid

    IcccConfiguration:
      type: object
      properties:
        calculationMode: { type: string, enum: [cumulative, rolling_window, exponential_decay] }
        rollingWindowContributions: { type: integer, minimum: 1 }
        rollingWindowMinutes: { type: number }
        decayLambda: { type: number }
        distanceUnit: { type: string, enum: [contributions, seconds, minutes] }
        smoothing: { type: string, enum: [none, moving_average, ewma] }
        smoothingParameter: { type: number }
        normalization:
          type: string
          enum:
            [
              none,
              theoretically_anchored,
              within_thread_minmax,
              within_participant_zscore,
              cohort_reference_zscore,
            ]

    SemanticConfiguration:
      type: object
      required: [interactionType]
      description: |
        Layer 2 is never enabled implicitly. Requesting an engine requires its
        configuration block, and mode "none" cannot be combined with enabled.
      properties:
        enabled: { type: boolean, default: false }
        mode: { type: string, enum: [none, lcc, iccc, lcc_and_iccc], default: none }
        interactionType: { $ref: '#/components/schemas/InteractionType' }
        lcc: { $ref: '#/components/schemas/LccConfiguration' }
        iccc: { $ref: '#/components/schemas/IcccConfiguration' }

    CreateThread:
      type: object
      required: [interactionType]
      properties:
        threadId: { $ref: '#/components/schemas/Identifier' }
        title: { type: string, maxLength: 300 }
        interactionType: { $ref: '#/components/schemas/InteractionType' }
        languages: { type: array, items: { type: string } }
        consentVersion: { type: string }
        sourceApplicationId: { $ref: '#/components/schemas/Identifier' }
        semanticConfiguration: { $ref: '#/components/schemas/SemanticConfiguration' }
        participants: { type: array, items: { $ref: '#/components/schemas/Participant' } }
        metadata: { type: object }

    Usage:
      type: object
      description: Provider-reported usage. A missing figure is never replaced by a local estimate.
      properties:
        newInputTokens: { type: integer, minimum: 0 }
        totalApiInputTokens: { type: integer, minimum: 0 }
        outputTokens: { type: integer, minimum: 0 }
        visibleOutputTokens: { type: integer, minimum: 0 }
        cachedInputTokens: { type: integer, minimum: 0 }
        reasoningTokens: { type: integer, minimum: 0 }

    CreateTurn:
      type: object
      required: [participantId, speakerType]
      properties:
        turnId: { $ref: '#/components/schemas/Identifier' }
        parentTurnId: { $ref: '#/components/schemas/Identifier' }
        sequence: { type: integer, minimum: 0 }
        participantId: { $ref: '#/components/schemas/Identifier' }
        speakerType: { type: string, enum: [human, assistant, agent, avatar, system, tool] }
        text: { type: string, maxLength: 200000 }
        explicitTargetParticipantIds: { type: array, items: { $ref: '#/components/schemas/Identifier' } }
        submittedAt: { type: string, format: date-time }
        firstTokenAt: { type: string, format: date-time }
        lastTokenAt: { type: string, format: date-time }
        model: { type: string }
        provider: { type: string }
        finishReason: { type: string }
        usage: { $ref: '#/components/schemas/Usage' }
        metadata: { type: object }

    IngestResult:
      type: object
      properties:
        created: { type: boolean }
        contribution: { type: object }
        analysis:
          type: object
          properties:
            layer1: { type: object }
            layer1Status: { type: string, enum: [completed] }
            layer2Status: { type: string, enum: [queued, not_applicable] }
            speechActStatus: { type: string }
        jobIds: { type: array, items: { type: string } }

    ReferenceModel:
      type: object
      required: [questionOrProblem, expectations]
      properties:
        referenceModelId: { $ref: '#/components/schemas/Identifier' }
        version: { type: string, default: 1.0.0 }
        learningObjective: { type: string }
        questionOrProblem: { type: string, minLength: 1 }
        language: { type: string, default: en }
        expectations:
          type: array
          minItems: 1
          items:
            type: object
            required: [expectationId, description]
            properties:
              expectationId: { $ref: '#/components/schemas/Identifier' }
              description: { type: string }
              weight: { type: number, default: 1 }
              required: { type: boolean }
        misconceptions:
          type: array
          items:
            type: object
            required: [misconceptionId, description]
            properties:
              misconceptionId: { $ref: '#/components/schemas/Identifier' }
              description: { type: string }
              weight: { type: number, default: 1 }

    LccCurvePoint:
      type: object
      description: RN + RO + IN + IO = 1. Null values carry a quality flag.
      properties:
        turnId: { type: string }
        learnerId: { type: string }
        sequence: { type: integer }
        timestamp: { type: string, format: date-time }
        relevance: { type: [number, 'null'] }
        novelty: { type: [number, 'null'] }
        rn: { type: [number, 'null'] }
        ro: { type: [number, 'null'] }
        in: { type: [number, 'null'] }
        io: { type: [number, 'null'] }
        unmappedIrrelevantNew: { type: [number, 'null'] }
        unmappedIrrelevantOld: { type: [number, 'null'] }
        overallScore: { type: [number, 'null'] }
        instructionalState: { type: string }
        candidateTutoringMoves: { type: array, items: { type: string } }
        referenceModelId: { type: string }
        referenceModelVersion: { type: string }
        semanticModelVersion: { type: string }
        algorithmVersion: { type: string }
        qualityFlags: { type: array, items: { type: string } }

    LccResult:
      type: object
      properties:
        status: { type: string, enum: [completed, pending, not_applicable] }
        reason: { type: string }
        learners:
          type: array
          items:
            type: object
            properties:
              learnerId: { type: string }
              points: { type: array, items: { $ref: '#/components/schemas/LccCurvePoint' } }
        curves: { type: array, items: { type: string } }
        prohibitedInterpretation: { type: string }

    IcccMetricValue:
      type: object
      properties:
        rawValue: { type: [number, 'null'] }
        normalizedValue: { type: [number, 'null'] }
        confidence: { type: number }
        sampleSize: { type: integer }
        status:
          type: string
          enum: [available, provisional, insufficient_history, not_applicable, failed]
        evidenceContributionIds: { type: array, items: { type: string } }
        qualityFlags: { type: array, items: { $ref: '#/components/schemas/QualityFlag' } }
        detail: { type: object }

    IcccCurvePoint:
      type: object
      properties:
        participantId: { type: string }
        contributionId: { type: string }
        sequence: { type: integer }
        timestamp: { type: string, format: date-time }
        values:
          type: object
          properties:
            participation: { $ref: '#/components/schemas/IcccMetricValue' }
            overallResponsivity: { $ref: '#/components/schemas/IcccMetricValue' }
            internalCohesion: { $ref: '#/components/schemas/IcccMetricValue' }
            socialImpact: { $ref: '#/components/schemas/IcccMetricValue' }
            newness: { $ref: '#/components/schemas/IcccMetricValue' }
            communicationDensity: { $ref: '#/components/schemas/IcccMetricValue' }
        provisional: { type: boolean }
        socialImpactState: { type: string, enum: [provisional, stabilizing, final] }
        revision: { type: integer }
        calculationMode: { type: string }
        algorithmVersion: { type: string }
        semanticModelVersion: { type: string }

    IcccResult:
      type: object
      properties:
        status: { type: string, enum: [completed, pending, not_applicable] }
        reason: { type: string }
        participants:
          type: array
          items:
            type: object
            properties:
              participantId: { type: string }
              points: { type: array, items: { $ref: '#/components/schemas/IcccCurvePoint' } }
              smoothed: { type: object }
        metrics: { type: array, items: { type: string } }
        provisionalPointCount: { type: integer }
        prohibitedInterpretation: { type: string }

    ResearchScope:
      type: string
      enum:
        - aggregate_metrics
        - turn_metrics
        - speech_acts
        - timestamps
        - token_usage
        - pseudonymized_text
        - raw_text
        - lcc_curves
        - lcc_expectation_matches
        - lcc_misconception_matches
        - iccc_curves
        - iccc_contribution_scores
        - semantic_units
        - semantic_representations
        - export

    CreateGrant:
      type: object
      required: [researcherId, scopes, consentVersion, purpose]
      properties:
        researcherId: { type: string }
        projectId: { $ref: '#/components/schemas/Identifier' }
        threadIds: { type: array, items: { $ref: '#/components/schemas/Identifier' } }
        scopes: { type: array, minItems: 1, items: { $ref: '#/components/schemas/ResearchScope' } }
        expiresAt: { type: string, format: date-time }
        consentVersion: { type: string }
        purpose: { type: string }
