openapi: '3.1.0'
info:
  title: Motionworks API - Workspace (Projects, Scenarios & Sharing)
  version: 1.0.0
  description: >
    MA-55 — v1 of the Workspace feature: Projects (named containers for
    Scenarios), Scenarios (the atomic unit of analytical work, including
    sandbox / project-less scenarios), and org-level sharing. Every
    operation here is a control-plane / management-plane operation: 0 credit
    cost and no `meta.provenance` (spec §8's explicit carve-out — ADR-7/TF-93
    provenance applies to data-product endpoints only). Auth (revised
    2026-08-19): Projects/Scenarios operations accept EITHER a JWT OR an
    X-API-Key (never both — `400 AMBIGUOUS_AUTH`); a JWT-created resource is
    owned by that user, an X-API-Key-created resource is owned by the key's
    org. Org-management operations (`listWorkspaceOrgs`, `activateWorkspaceOrg`,
    `listWorkspaceSharingOrgs`) stay JWT-only. See
    `docs/products/workspace-projects-scenarios.md` § "Auth mode per endpoint"
    for the full per-operation table and `docs/architecture/0036-tag-based-resource-permissions.md`
    for the underlying tag-based ownership/sharing model (MA-64).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com

servers:
  - url: https://api.mworks.com/v2
    description: Production

components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    OrgSummary:
      type: object
      required: [id, name]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    Project:
      type: object
      required: [id, name, description, created_at, updated_at, archived_at, access]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        archived_at: { type: string, format: date-time, nullable: true }
        access: { type: string, enum: [owner, recipient] }

    CreateProjectRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 2000 }

    UpdateProjectRequest:
      type: object
      description: At least one of `name`/`description` must be provided.
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 2000, nullable: true }

    ProjectSharing:
      type: object
      required: [shared_org_ids, shared_by_user_id, shared_at]
      properties:
        shared_org_ids:
          type: array
          items: { type: string, format: uuid }
        shared_by_user_id: { type: string, format: uuid, nullable: true }
        shared_at: { type: string, format: date-time, nullable: true }

    UpdateSharingRequest:
      type: object
      required: [shared_org_ids]
      description: PUT semantics — replaces the full shared-org list. Empty array = fully private.
      properties:
        shared_org_ids:
          type: array
          items: { type: string, format: uuid }

    ScenarioType:
      type: string
      description: >
        'marketplace' is deliberately excluded from v1 — no worker or
        OpenAPI spec exists yet for that product (spec §2.5).
      enum: [inventory_plan, place_select, set_dynamics]

    Scenario:
      type: object
      required:
        - id
        - project_id
        - name
        - type
        - description
        - payload
        - is_sharing_private
        - created_at
        - updated_at
        - archived_at
        - access
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid, nullable: true, description: "null = sandbox scenario" }
        name: { type: string }
        type: { $ref: '#/components/schemas/ScenarioType' }
        description: { type: string, nullable: true }
        payload: { type: object }
        is_sharing_private: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        archived_at: { type: string, format: date-time, nullable: true }
        access: { type: string, enum: [owner, recipient] }

    CreateScenarioRequest:
      type: object
      required: [name, type]
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        type: { $ref: '#/components/schemas/ScenarioType' }
        description: { type: string, maxLength: 2000 }
        payload: { type: object, default: {} }

    UpdateScenarioRequest:
      type: object
      description: At least one field must be provided.
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 2000, nullable: true }
        payload: { type: object }
        is_sharing_private:
          type: boolean
          description: Project-scoped scenarios only — 400 NOT_PROJECT_SCENARIO on a sandbox scenario.

    CloneScenarioRequest:
      type: object
      properties:
        target_project_id:
          type: string
          format: uuid
          description: Omit to create the clone as a sandbox scenario. Caller must own the target project.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: "Defaults to \"Copy of <original name>\" when omitted."

    MoveScenarioRequest:
      type: object
      required: [target_project_id]
      properties:
        target_project_id:
          type: string
          format: uuid
          nullable: true
          description: null moves the scenario to sandbox. Must be a project owned by the caller otherwise.

paths:
  /workspace/orgs:
    get:
      operationId: listWorkspaceOrgs
      summary: List orgs the calling user belongs to
      description: Returns every organization the caller is currently a member of.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Org memberships
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/OrgSummary' }
                  meta: { type: object }
        '400':
          description: Both Authorization and X-API-Key present (AMBIGUOUS_AUTH)
        '401':
          description: Missing/invalid Authorization, or X-API-Key/X-MW-Anon-Session used instead of a JWT

  /workspace/orgs/{org_id}/activate:
    post:
      operationId: activateWorkspaceOrg
      summary: Switch the caller's active organization
      description: >
        Validates membership, then atomically sets `is_active=true` for
        `org_id` and `false` for every other org the caller belongs to
        (`activate_org` RPC, migration 0050).
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ bearerAuth: [] }]
      parameters:
        - name: org_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204':
          description: Active org switched
        '400':
          description: AMBIGUOUS_AUTH
        '401':
          description: Auth failure
        '404':
          description: Caller is not a member of org_id (ORG_NOT_FOUND)

  /workspace/sharing/orgs:
    get:
      operationId: listWorkspaceSharingOrgs
      summary: List orgs available as sharing targets
      description: >
        Composition (spec §12 Q1): orgs whose plan != 'free', excluding the
        caller's own org memberships. Exposes only org id + name — never
        members.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Shareable orgs
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/OrgSummary' }
                  meta: { type: object }
        '400':
          description: AMBIGUOUS_AUTH
        '401':
          description: Auth failure

  /workspace/projects:
    get:
      operationId: listWorkspaceProjects
      summary: List projects visible to the caller
      description: >
        Owned projects (any) plus projects shared with the caller's active
        org (owned by other users). Each item carries `access`.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: owned_only, in: query, schema: { type: boolean, default: false } }
        - { name: shared_only, in: query, schema: { type: boolean, default: false } }
        - { name: archived, in: query, schema: { type: boolean, default: false } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Visible projects
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Project' }
                  meta: { type: object }
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure

    post:
      operationId: createWorkspaceProject
      summary: Create a project (private by default)
      description: Creates a project owned by the caller. Private until explicitly shared.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateProjectRequest' }
      responses:
        '201':
          description: Project created
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Project' }
                  meta: { type: object }
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure

  /workspace/projects/{id}:
    get:
      operationId: getWorkspaceProject
      summary: Get project detail
      description: Returns the project if the caller is its owner or a recipient of an active-org share.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Project
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Project' }
                  meta: { type: object }
        '404':
          description: PROJECT_NOT_FOUND

    patch:
      operationId: updateWorkspaceProject
      summary: Update a project (owner only)
      description: Updates name/description. Only the project owner may call this.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateProjectRequest' }
      responses:
        '200':
          description: Updated project
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Project' }
                  meta: { type: object }
        '400':
          description: Validation error
        '403':
          description: FORBIDDEN — caller is a recipient, not the owner
        '404':
          description: PROJECT_NOT_FOUND

    delete:
      operationId: archiveWorkspaceProject
      summary: Archive a project (owner only)
      description: Soft-deletes the project (sets archived_at). Only the owner may call this.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204':
          description: Archived
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND

  /workspace/projects/{id}/sharing:
    get:
      operationId: getWorkspaceProjectSharing
      summary: Get project sharing config (owner only)
      description: Returns the project's current shared-org list. Only the owner may call this.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Sharing config
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/ProjectSharing' }
                  meta: { type: object }
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND

    patch:
      operationId: updateWorkspaceProjectSharing
      summary: Set the project's shared org list (owner only, PUT semantics)
      description: Replaces the full shared-org list. Empty array makes the project private again.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateSharingRequest' }
      responses:
        '200':
          description: Updated sharing config
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/ProjectSharing' }
                  meta: { type: object }
        '400':
          description: SELF_SHARE / ORG_NOT_SHAREABLE / validation error
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND

  /workspace/projects/{project_id}/scenarios:
    get:
      operationId: listProjectScenarios
      summary: List scenarios in a project
      description: Returns scenarios visible to the caller within the given project.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: project_id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: archived, in: query, schema: { type: boolean, default: false } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Scenarios in the project
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '404':
          description: PROJECT_NOT_FOUND

    post:
      operationId: createProjectScenario
      summary: Create a scenario in a project (project owner only)
      description: New scenario's is_sharing_private defaults to false (inherits project sharing).
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: project_id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateScenarioRequest' }
      responses:
        '201':
          description: Scenario created
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: Validation error / INVALID_SCENARIO_TYPE / PROJECT_ARCHIVED
        '403':
          description: FORBIDDEN — caller is not the project owner
        '404':
          description: PROJECT_NOT_FOUND

  /workspace/scenarios:
    get:
      operationId: listWorkspaceScenarios
      summary: List all scenarios visible to the caller (incl. sandbox)
      description: Owned scenarios (any project or sandbox) plus scenarios in shared, non-private projects.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, schema: { type: string, format: uuid } }
        - { name: sandbox_only, in: query, schema: { type: boolean, default: false } }
        - { name: owned_only, in: query, schema: { type: boolean, default: false } }
        - { name: archived, in: query, schema: { type: boolean, default: false } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Visible scenarios
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure

    post:
      operationId: createSandboxScenario
      summary: Create a sandbox (project-less) scenario
      description: Creates with project_id=null. Private until explicitly shared via the scenario sharing endpoint.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateScenarioRequest' }
      responses:
        '201':
          description: Sandbox scenario created
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: Validation error / INVALID_SCENARIO_TYPE

  /workspace/scenarios/{id}:
    get:
      operationId: getWorkspaceScenario
      summary: Get scenario detail
      description: Returns the scenario if the caller is its owner or a recipient with visibility into it.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Scenario
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '404':
          description: SCENARIO_NOT_FOUND

    patch:
      operationId: updateWorkspaceScenario
      summary: Update a scenario (owner only)
      description: Updates name/description/payload/is_sharing_private. Only the scenario owner may call this.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateScenarioRequest' }
      responses:
        '200':
          description: Updated scenario
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: Validation error / NOT_PROJECT_SCENARIO
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND

    delete:
      operationId: archiveWorkspaceScenario
      summary: Archive a scenario (owner only)
      description: Soft-deletes the scenario (sets archived_at). Only the owner may call this.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204':
          description: Archived
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND

  /workspace/scenarios/{id}/clone:
    post:
      operationId: cloneWorkspaceScenario
      summary: Clone a scenario (owner or shared recipient)
      description: >
        The clone is always private on creation and owned by the caller —
        it inherits no sharing from the original.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CloneScenarioRequest' }
      responses:
        '201':
          description: Cloned scenario
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: TARGET_PROJECT_NOT_OWNED / PROJECT_ARCHIVED / validation error
        '403':
          description: FORBIDDEN — caller has neither owner nor recipient access
        '404':
          description: SCENARIO_NOT_FOUND

  /workspace/scenarios/{id}/move:
    post:
      operationId: moveWorkspaceScenario
      summary: Move a scenario between a project and sandbox (owner only)
      description: Moving into a project resets is_sharing_private to false and drops direct org shares; moving to sandbox clears is_sharing_private and starts private.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MoveScenarioRequest' }
      responses:
        '200':
          description: Moved scenario
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/Scenario' }
                  meta: { type: object }
        '400':
          description: TARGET_PROJECT_NOT_OWNED / PROJECT_ARCHIVED / validation error
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND

  /workspace/scenarios/{id}/sharing:
    get:
      operationId: getWorkspaceScenarioSharing
      summary: Get sandbox scenario sharing config
      description: Sandbox scenarios only — 400 NOT_SANDBOX_SCENARIO for project-scoped scenarios.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Sharing config
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/ProjectSharing' }
                  meta: { type: object }
        '400':
          description: NOT_SANDBOX_SCENARIO
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND

    patch:
      operationId: updateWorkspaceScenarioSharing
      summary: Set shared orgs for a sandbox scenario (PUT semantics)
      description: Sandbox scenarios only — 400 NOT_SANDBOX_SCENARIO for project-scoped scenarios.
      tags: [workspace]
      x-credit-cost: 0
      x-motionworks-status: production
      security: [{ apiKey: [] }, { bearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateSharingRequest' }
      responses:
        '200':
          description: Updated sharing config
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/ProjectSharing' }
                  meta: { type: object }
        '400':
          description: NOT_SANDBOX_SCENARIO / SELF_SHARE / ORG_NOT_SHAREABLE
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
