openapi: 3.0.3
info:
  title: FOSA Smart Advisor API
  version: 0.2.0
  description: |
    # Overview

    FOSA Smart Advisor turns a restaurant owner's plain-language problem into a
    structured diagnosis: evidence, actions, and a verification window.

    Everything runs on **one host/port** (default `http://localhost:3040`):

    - **Docs (Docusaurus):** `/docu`
    - **API reference (Redoc):** `/redoc`
    - **OpenAPI YAML:** `/openapi/advisor.openapi.yaml`
    - **JSON APIs:** `/api/*`

    # Pipeline (test each step)

    | Step | Status | Endpoint |
    |------|--------|----------|
    | Auth | ready | `POST /api/auth/login`, `GET /api/auth/me` |
    | Catalog | ready | `GET /api/catalog/playbooks` |
    | Classify | ready | `POST /api/classify` |
    | Query | ready | `POST /api/query/ask` |
    | Agent + sessions | ready | `POST /api/advisor/sessions` |

    Live status map: `GET /api/steps`.

    ## Human docs (same host)

    | Guide | URL |
    |-------|-----|
    | Quick start | `/docu/docs/intro` |
    | Architecture | `/docu/docs/architecture` |
    | Pipeline (test each step) | `/docu/docs/pipeline` |
    | Sessions (full diagnosis) | `/docu/docs/sessions` |
    | Tweaking prompts / YAML / models | `/docu/docs/tweaking` |
    | Postman import order | `/docu/docs/postman` |

    # Offline mock

    ```
    AUTH_ADAPTER=mock
    CATALOG_ADAPTER=yaml
    LLM_ADAPTER=mock
    QUERY_ADAPTER=mock
    ```

    Token format after mock login: `mock.<restaurantId>` (e.g. `mock.30`).

    # Live QueryCraft + OpenAI

    ```
    AUTH_ADAPTER=querycraft
    QUERY_ADAPTER=querycraft
    QUERYCRAFT_ENABLED=true
    QUERYCRAFT_URL=https://querycraft.ncrts.pro
    LLM_ADAPTER=openai
    OPENAI_ENABLED=true
    OPENAI_API_KEY=sk-...
    ```

    Auth subject `principalId` = FOSA shop / restaurant id (SQL scope).
  contact:
    name: FOSA Smart Advisor
servers:
  - url: /
    description: Same host (API + docs + Redoc)
tags:
  - name: Pipeline
    description: |
      Discover which build steps are ready and how to test them.
      Prefer this before calling Advisor sessions.
  - name: Health
    description: Process health and which adapters are wired.
  - name: Auth
    description: |
      Login / token validate. Proxies QueryCraft Auth Gateway when
      `AUTH_ADAPTER=querycraft`, or issues `mock.<id>` tokens offline.
  - name: Catalog
    description: |
      Read-only knowledge pack from `diagnostics/**` (playbooks + recommendations).
      No LLM. No restaurant POS data.
  - name: Classify
    description: |
      Map owner problem text → 1–2 playbook ids using LLM `purpose=classify`
      with symptom-overlap fallback.
  - name: Query
    description: |
      Single analytics ask via QueryCraft `POST /query/nlp` (or mock rows).
      Used by the agent tool `query_data` and testable on its own.
  - name: Advisor
    description: |
      Full diagnosis sessions: classify → investigate tool loop →
      `awaiting_manager` or `concluded` with a Diagnosis object.
paths:
  /api/steps:
    get:
      tags: [Pipeline]
      summary: List build steps + how to test each
      description: |
        Returns the pipeline map used by docs and Postman.
        Each step includes `status`, `summary`, `endpoints`, and mock notes.
      operationId: listSteps
      responses:
        '200':
          description: Step map
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepsResponse'
              example:
                success: true
                data:
                  note: Prefer mock adapters for offline step tests
                  steps:
                    - id: auth
                      status: ready
                    - id: catalog
                      status: ready
                    - id: classify
                      status: ready
                    - id: query
                      status: ready
                    - id: agent
                      status: ready
                    - id: advisor_sessions
                      status: ready
  /api/health:
    get:
      tags: [Health]
      summary: Health + adapter status
      description: |
        Confirms the process is up and shows which plugins are selected
        (`auth`, `catalog`, `llm`, `query`, …) plus non-secret third-party flags.
      operationId: getHealth
      responses:
        '200':
          description: Service is up
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /api/auth/login:
    post:
      tags: [Auth]
      summary: Login (QueryCraft or mock)
      description: |
        **Step = Auth.**

        - Live: proxies `POST {QUERYCRAFT_URL}/auth/login`
        - Mock: any email/password → token `mock.<principalId>` (default shop `30`)

        Response includes `subject.principalId` (restaurant scope) and resolved `tenant`.
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              mock:
                summary: Mock login
                value:
                  email: owner@example.com
                  password: anything
              live:
                summary: QueryCraft / FOSA
                value:
                  email: user@example.com
                  password: your-password
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '400':
          description: Missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/me:
    get:
      tags: [Auth]
      summary: Validate bearer token
      description: |
        Validates the token (QueryCraft `GET /auth/me` or mock parse) and
        returns subject + tenant context.
      operationId: me
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Token valid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/catalog/playbooks:
    get:
      tags: [Catalog]
      summary: List playbook summaries
      description: |
        **Step = Catalog.**

        Returns lightweight summaries used by the classifier (id, title, symptoms,
        negative_symptoms, requires_channels). Full hypotheses/probes are on
        `GET /api/catalog/playbooks/{id}`.

        No auth. Requires `CATALOG_ADAPTER=yaml` and `DIAGNOSTICS_DIR=./diagnostics`.
      operationId: listPlaybooks
      responses:
        '200':
          description: Summaries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaybookListResponse'
              example:
                success: true
                data:
                  adapter: yaml
                  count: 5
                  playbooks:
                    - playbook_id: demand-decline
                      version: 1.0.0
                      title: Overall orders/revenue declining
                      symptoms:
                        - sales are down
                      negative_symptoms:
                        - lunch tapers off
                      requires_channels: []
  /api/catalog/playbooks/{id}:
    get:
      tags: [Catalog]
      summary: Full playbook by id
      description: |
        Returns the full YAML playbook body (hypotheses, quant/qual probes,
        recommendation refs, follow_up). This is what the agent sees in Block C.
      operationId: getPlaybook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: demand-decline
      responses:
        '200':
          description: Playbook document
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaybookGetResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/catalog/recommendations/{recId}:
    get:
      tags: [Catalog]
      summary: Shared recommendation by rec_id
      description: |
        Actions live in `diagnostics/recommendations/catalog.yaml` and are
        referenced from playbooks by `rec_id` (e.g. `lapsed-customer-winback`).
      operationId: getRecommendation
      parameters:
        - name: recId
          in: path
          required: true
          schema:
            type: string
            example: lapsed-customer-winback
      responses:
        '200':
          description: Recommendation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecommendationGetResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/classify:
    post:
      tags: [Classify]
      summary: Classify problem → playbook_ids
      description: |
        **Step = Classify.**

        Input: owner `problem_statement`.
        Output: up to 2 `playbook_ids` plus `source`:

        - `llm` — model returned valid ids (`purpose=classify`)
        - `symptom_fallback` — phrase overlap against playbook symptoms
        - `empty` — nothing available

        Optional channel filter uses tenant `active_channels` vs playbook
        `requires_channels`.
      operationId: classifyProblem
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [problem_statement]
              properties:
                problem_statement:
                  type: string
                  example: sales are down across the board
            example:
              problem_statement: sales are down across the board
      responses:
        '200':
          description: Classification result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClassifyResponse'
              example:
                success: true
                data:
                  playbook_ids: [demand-decline]
                  source: llm
                  adapters:
                    catalog: yaml
                    llm: mock
        '400':
          description: Missing problem_statement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/query/ask:
    post:
      tags: [Query]
      summary: Ask restaurant data (QueryPort)
      description: |
        **Step = Query.**

        Thin wrapper over QueryCraft `POST /query/nlp` or `MockQueryAdapter`.

        Normalized `result.kind`:

        - `data` — rows/sql available
        - `clarification` — QueryCraft needs a follow-up (`clarification` field)
        - `empty` / `unknown`

        Full upstream blob is in `result.provider_data` for the agent.
      operationId: queryAsk
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryAskRequest'
            example:
              question: Weekly order count last 8 weeks vs prior 8 weeks
              probe_id: dd-01
      responses:
        '200':
          description: Normalized QueryResult
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryAskResponse'
        '400':
          description: Missing question
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/advisor/sessions:
    post:
      tags: [Advisor]
      summary: Start diagnosis session
      description: |
        **Step = Advisor sessions (full path).**

        1. Validate bearer → resolve tenant  
        2. Classify problem → playbook ids  
        3. Create session  
        4. Run investigate tool loop until:

           - `status=concluded` + `diagnosis` (done), or  
           - `status=awaiting_manager` + `pending_questions` (resume with answers)

        Agent tools: `query_data`, `ask_manager`, `update_hypothesis`,
        `select_playbook`, `conclude_diagnosis`.

        Caps (defaults): 8 queries, 3 asks, 24 LLM turns — see
        `src/application/agent/constants.ts`.

        Mock LLM usually concludes in one start without asking the manager.
      operationId: startSession
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [problem_statement]
              properties:
                problem_statement:
                  type: string
                  example: Lunch covers are down 20% vs last month
            example:
              problem_statement: sales are down across the board
      responses:
        '201':
          description: Session created and loop ran
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionEnvelope'
              example:
                success: true
                data:
                  session_id: abc123
                  status: concluded
                  playbook_ids: [demand-decline]
                  stop: concluded
                  diagnosis:
                    finding: Mock diagnosis example
                    confidence: likely_unconfirmed
                    evidence: ['…']
                    actions:
                      - rec_id: lapsed-customer-winback
                        summary: Run a win-back
                        effort: low
                    verification:
                      check_after_days: 14
                      probe_ids: [mock-01]
        '400':
          description: Missing problem_statement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/advisor/sessions/{id}:
    get:
      tags: [Advisor]
      summary: Get session snapshot
      description: |
        Returns the current session view (status, board, pending questions,
        diagnosis). Does not re-run the agent.
      operationId: getSession
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Session view
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionEnvelope'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/advisor/sessions/{id}/answers:
    post:
      tags: [Advisor]
      summary: Resume after ask_manager
      description: |
        When `status=awaiting_manager`, submit answers for `pending_questions`,
        then the investigate loop continues until conclude or another ask.

        Body `answers[]`: `{ probe_id|question_id, answer }`.
      operationId: resumeSession
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResumeAnswersRequest'
            example:
              answers:
                - probe_id: q1
                  answer: We launched a DoorDash promo last month
      responses:
        '200':
          description: Continued session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionEnvelope'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: Session not awaiting_manager
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Token from `POST /api/auth/login` (QueryCraft JWT or `mock.<restaurantId>`).
  schemas:
    ErrorEnvelope:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
    StepsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            note:
              type: string
            steps:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
                    enum: [ready, stub, not_implemented]
                  summary:
                    type: string
                  endpoints:
                    type: array
                    items:
                      type: object
                  mock:
                    type: string
                    nullable: true
    AuthSubject:
      type: object
      properties:
        principalType:
          type: string
          example: restaurant
        principalId:
          type: string
          description: FOSA shop / restaurant id
          example: '30'
        tenantId:
          type: string
          example: fosa
        roles:
          type: array
          items:
            type: string
        appRole:
          type: string
          example: user
        email:
          type: string
        displayName:
          type: string
    TenantContext:
      type: object
      additionalProperties: true
      description: Resolved restaurant context injected into agent Block B
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password
        device_type:
          type: string
        device_id:
          type: string
        push_key:
          type: string
    LoginResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
        data:
          type: object
          properties:
            token:
              type: string
            expiresAt:
              nullable: true
            subject:
              $ref: '#/components/schemas/AuthSubject'
            restaurant_id:
              type: string
            tenant:
              $ref: '#/components/schemas/TenantContext'
    MeResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            token:
              type: string
            subject:
              $ref: '#/components/schemas/AuthSubject'
            restaurant_id:
              type: string
            tenant:
              $ref: '#/components/schemas/TenantContext'
    HealthResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            ok:
              type: boolean
            service:
              type: string
            adapters:
              type: object
              additionalProperties:
                type: string
            thirdParty:
              type: object
              additionalProperties: true
    PlaybookSummary:
      type: object
      properties:
        playbook_id:
          type: string
        version:
          type: string
        title:
          type: string
        symptoms:
          type: array
          items:
            type: string
        negative_symptoms:
          type: array
          items:
            type: string
        requires_channels:
          type: array
          items:
            type: string
    PlaybookListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            adapter:
              type: string
            count:
              type: integer
            playbooks:
              type: array
              items:
                $ref: '#/components/schemas/PlaybookSummary'
    PlaybookGetResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            adapter:
              type: string
            playbook:
              type: object
              additionalProperties: true
    Recommendation:
      type: object
      properties:
        rec_id:
          type: string
        action:
          type: string
        effort:
          type: string
          enum: [low, medium, high]
        expected_impact:
          type: string
        verify_with:
          type: string
        fosa_module:
          type: string
    RecommendationGetResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            adapter:
              type: string
            recommendation:
              $ref: '#/components/schemas/Recommendation'
    ClassifyResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            playbook_ids:
              type: array
              items:
                type: string
            source:
              type: string
              enum: [llm, symptom_fallback, empty]
            reason:
              type: string
            adapters:
              type: object
              properties:
                catalog:
                  type: string
                llm:
                  type: string
    QueryAskRequest:
      type: object
      properties:
        question:
          type: string
        clarification:
          type: string
          description: Follow-up when prior result kind was clarification
        probe_id:
          type: string
        max_rows:
          type: integer
    QueryResult:
      type: object
      properties:
        ok:
          type: boolean
        kind:
          type: string
          enum: [data, clarification, empty, unknown]
        question:
          type: string
        overview:
          type: string
        sql:
          type: string
        columns:
          type: array
          items:
            type: string
        rows:
          type: array
        row_count:
          type: integer
        truncated:
          type: boolean
        clarification_text:
          type: string
        provider_data:
          type: object
          additionalProperties: true
    QueryAskResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            adapter:
              type: string
            result:
              $ref: '#/components/schemas/QueryResult'
    Diagnosis:
      type: object
      properties:
        finding:
          type: string
        confidence:
          type: string
          enum: [confirmed, likely_unconfirmed]
        evidence:
          type: array
          items:
            type: string
        actions:
          type: array
          items:
            type: object
            properties:
              rec_id:
                type: string
              summary:
                type: string
              effort:
                type: string
                enum: [low, medium, high]
              expected_impact:
                type: string
        verification:
          type: object
          properties:
            check_after_days:
              type: integer
            probe_ids:
              type: array
              items:
                type: string
        confirmed_hypothesis_ids:
          type: array
          items:
            type: string
        playbook_gap_flag:
          type: boolean
    SessionView:
      type: object
      properties:
        session_id:
          type: string
        status:
          type: string
          enum:
            - intake
            - diagnosing
            - awaiting_manager
            - concluded
            - abandoned
            - verification_pending
            - verified
        problem_statement:
          type: string
        playbook_ids:
          type: array
          items:
            type: string
        hypothesis_board:
          type: array
          items:
            type: object
            properties:
              hypothesis_id:
                type: string
              status:
                type: string
              evidence_note:
                type: string
        pending_questions:
          nullable: true
          type: object
          properties:
            context:
              type: string
            questions:
              type: array
              items:
                type: object
                properties:
                  probe_id:
                    type: string
                  text:
                    type: string
                  options:
                    type: array
                    items:
                      type: string
        diagnosis:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Diagnosis'
        verify_after:
          type: string
          nullable: true
          description: YYYY-MM-DD when verification is due
        query_count:
          type: integer
        ask_count:
          type: integer
        stop:
          type: string
          enum: [awaiting_manager, concluded, max_turns, model_stop]
        classify:
          type: object
          properties:
            playbook_ids:
              type: array
              items:
                type: string
            source:
              type: string
    SessionEnvelope:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SessionView'
    ResumeAnswersRequest:
      type: object
      properties:
        answers:
          type: array
          items:
            type: object
            properties:
              probe_id:
                type: string
              question_id:
                type: string
              answer:
                type: string
