openapi: 3.1.0
info:
  title: PIT Fundamentals API
  version: 0.1.0
  summary: Point-in-time SEC fundamentals. Raw XBRL → standardized concepts → bitemporal PIT store.
  description: |
    ## Status

    This is the v0.1.0 contract for Phase B1/B2 of Version B. Endpoints marked
    `x-status: stable` are implemented and under regression coverage. Endpoints
    marked `x-status: reserved` have their shape frozen here but are not yet
    implemented; calling them returns `501 Not Implemented`.

    ## Versioning rules

    - `stable` endpoints: additive-only. New optional params and new response
      fields are allowed. Removing a field, changing a field type, or changing
      default values is a major version bump.
    - `reserved` endpoints: shape may still shift until the first implementing
      PR flips them to `stable`.

    ## PIT semantics

    See `docs/pit_rules.md`. Two timestamps are authoritative on every fact:
    `acceptedAt` (EDGAR acceptance) and `marketAvailableAt` (derived per the
    NYSE session policy). The `availability` query parameter selects which
    drives the `asOf` cutoff. Default is `market`.

    ## Derivation transparency

    `/v1/fundamentals` never silently returns a derived value. A caller
    requesting `period=TTM` gets a response that carries `derivationMethod`
    and `sourcePitFactIds`. A caller requesting `period=FY|Q|YTD` gets only
    directly reported facts.

  contact:
    name: CapraFeed
    email: sec@caprafeed.com
  license:
    name: Proprietary

servers:
  - url: https://api.pitdata.local/v1
    description: Local development
  - url: https://api.caprafeed.com/v1
    description: Production (reserved)

tags:
  - name: system
    description: Liveness, readiness, diagnostics.
  - name: fundamentals
    description: Canonical point-in-time numerical facts (Track A).
  - name: filings
    description: Filing index and per-filing detail.
  - name: disclosures
    description: Track B — 8-K items, non-reliance notices, guidance, auditor changes.
  - name: notes
    description: Track B — financial-statement notes.
  - name: reference
    description: Issuer and concept dictionaries.

paths:

  /health:
    get:
      tags: [system]
      summary: Liveness and table-count diagnostics.
      operationId: getHealth
      x-status: stable
      responses:
        '200':
          description: Service healthy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /fundamentals:
    get:
      tags: [fundamentals]
      summary: Point-in-time numerical facts for one or more issuers.
      description: |
        Executes the canonical PIT selection query documented in
        `docs/pit_rules.md` §3. For each `(issuer, metric)` pair, returns the
        single most recent fact whose chosen availability timestamp is
        `≤ asOf`. Restatements return the restated row; the prior row is still
        reachable by querying with an earlier `asOf`.
      operationId: getFundamentals
      x-status: stable
      parameters:
        - $ref: '#/components/parameters/IdsParam'
        - $ref: '#/components/parameters/MetricsParam'
        - $ref: '#/components/parameters/PeriodParam'
        - name: fiscalYear
          in: query
          required: true
          description: Required for reported periods (FY, Q1..Q4, YTD). For TTM, selects the year the TTM window ends in.
          schema:
            type: integer
            minimum: 2009
            maximum: 2100
        - $ref: '#/components/parameters/AsOfParam'
        - $ref: '#/components/parameters/AvailabilityParam'
        - name: versioning
          in: query
          schema:
            $ref: '#/components/schemas/VersioningMode'
          description: Select restatement-aware `latest` (default) or as-filed `original`.
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [json, csv]
            default: json
          description: >
            Output format. `json` (default) returns a `FundamentalsResponse`.
            `csv` returns a flat text/csv table with one row per
            FundamentalRecord — lineage and nested metadata are dropped.
            Equivalent to `Accept: text/csv`.
        - name: includeRawTag
          in: query
          schema:
            type: boolean
            default: true
          description: Include the XBRL tag that sourced each fact.
        - name: includeLineage
          in: query
          schema:
            type: boolean
            default: false
          description: Include `lineage` block with accession URL, primary doc URL, XBRL instance URL.
        - name: minMappingConfidence
          in: query
          x-status: reserved
          schema:
            type: number
            minimum: 0
            maximum: 1
            default: 0.8
          description: Exclude facts below this mapping confidence.
        - name: includeNonStandard
          in: query
          x-status: reserved
          schema:
            type: boolean
            default: false
          description: Include Layer 5 `unmapped` passthroughs as raw tags.
        - name: includeAttachments
          in: query
          x-status: reserved
          schema:
            type: boolean
            default: false
          description: Fan out to Track B attachments (disclosures + notes).
        - name: includeNotes
          in: query
          x-status: reserved
          schema:
            type: boolean
            default: false
        - name: includeDisclosures
          in: query
          x-status: reserved
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: PIT-selected facts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FundamentalsResponse'
            text/csv:
              schema:
                type: string
                description: >
                  Comma-separated flat table. One row per FundamentalRecord.
                  Header row is fixed: requestId, cik, metric, value, unit,
                  currency, fiscalYear, fiscalPeriod, periodEndDate,
                  acceptedAt, marketAvailableAt, accessionNumber, formType,
                  isRestated, rawTag, mappingMethod, mappingConfidence,
                  pitFactId.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /fundamentals/{pitFactId}/attachments:
    get:
      tags: [fundamentals]
      summary: Track B attachments for a specific PIT fact.
      operationId: getFactAttachments
      x-status: reserved
      parameters:
        - $ref: '#/components/parameters/PitFactIdParam'
        - $ref: '#/components/parameters/AsOfParam'
      responses:
        '200':
          description: Disclosures and notes attached to this fact (filtered by `asOf`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachmentsResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /filings:
    get:
      tags: [filings]
      summary: Filings index for one or more issuers.
      operationId: listFilings
      x-status: reserved
      parameters:
        - $ref: '#/components/parameters/IdsParam'
        - name: formTypes
          in: query
          schema:
            type: string
            example: "10-K,10-Q,8-K"
          description: Comma-separated form types.
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
        - $ref: '#/components/parameters/AsOfParam'
      responses:
        '200':
          description: Filing list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FilingsResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /filings/{accessionNumber}:
    get:
      tags: [filings]
      summary: Filing detail.
      operationId: getFiling
      x-status: reserved
      parameters:
        - $ref: '#/components/parameters/AccessionNumberParam'
      responses:
        '200':
          description: Filing record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Filing'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /filings/{accessionNumber}/disclosures:
    get:
      tags: [disclosures, filings]
      summary: Disclosures extracted from a filing.
      operationId: getFilingDisclosures
      x-status: reserved
      parameters:
        - $ref: '#/components/parameters/AccessionNumberParam'
      responses:
        '200':
          description: Disclosure list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisclosuresResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /filings/{accessionNumber}/notes:
    get:
      tags: [notes, filings]
      summary: Notes extracted from a filing.
      operationId: getFilingNotes
      x-status: reserved
      parameters:
        - $ref: '#/components/parameters/AccessionNumberParam'
      responses:
        '200':
          description: Note list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotesResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /disclosures:
    get:
      tags: [disclosures]
      summary: Direct disclosure search.
      operationId: searchDisclosures
      x-status: reserved
      parameters:
        - name: disclosureTypes
          in: query
          schema:
            type: string
            example: "non_reliance_8k_item_402,auditor_change_8k_item_401"
        - name: ids
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/AsOfParam'
      responses:
        '200':
          description: Matching disclosures.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisclosuresResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /notes:
    get:
      tags: [notes]
      summary: Direct note search.
      operationId: searchNotes
      x-status: reserved
      parameters:
        - name: noteTypes
          in: query
          schema:
            type: string
            example: "revenue_recognition,debt,segment"
        - name: ids
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/AsOfParam'
      responses:
        '200':
          description: Matching notes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotesResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /point-in-time:
    get:
      tags: [fundamentals]
      summary: Snapshot of all core metrics for an issuer as of a date.
      operationId: getPointInTimeSnapshot
      x-status: reserved
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: Single issuer ID (ticker or CIK).
        - $ref: '#/components/parameters/AsOfParam'
        - $ref: '#/components/parameters/AvailabilityParam'
      responses:
        '200':
          description: Multi-metric snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PointInTimeSnapshot'
        '501':
          $ref: '#/components/responses/NotImplemented'

  /periods:
    get:
      tags: [reference]
      summary: Fiscal-period metadata (filings) for one or more issuers.
      description: >
        Returns one record per filing that has reached the chosen
        `availability` watermark by `asOf`. Each record exposes the fiscal
        year / period / period-end, the form type, the accession number,
        amendment linkage, and PIT ticker. Use this to discover what
        periods exist for an issuer, to resolve "FY 2023 of AAPL" to a
        concrete (periodEnd, accessionNumber, marketAvailableAt) tuple, or
        to walk amendment chains.
      operationId: listPeriods
      x-status: stable
      parameters:
        - name: ids
          in: query
          required: true
          schema:
            type: string
          description: Comma-separated issuer identifiers (ticker or CIK).
        - $ref: '#/components/parameters/AsOfParam'
        - $ref: '#/components/parameters/AvailabilityParam'
        - name: fiscalYear
          in: query
          required: false
          schema:
            type: integer
          description: Filter to a single fiscal year.
        - name: formType
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated SEC form types (e.g. `10-K,10-Q`).
      responses:
        '200':
          description: Periods matching the filter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PeriodsResponse'
        '400':
          description: Invalid parameter (e.g. unknown formType).
        '404':
          description: Unknown issuer identifier.

  /concepts:
    get:
      tags: [reference]
      summary: Standardized concept dictionary.
      description: >
        Returns the full set of standardized concepts the system supports,
        including aliases, derivation rules, category/subcategory metadata,
        units, descriptions, and links to the US-GAAP taxonomy. Helper
        concepts (no user-facing metric) are included so that the full
        derivation graph is visible. Use `?userFacing=true` to filter to
        only concepts that can be queried via `/v1/fundamentals`.
      operationId: listConcepts
      x-status: stable
      parameters:
        - name: userFacing
          in: query
          required: false
          description: If true, return only concepts exposed as `metric=` on `/v1/fundamentals` (excludes helpers).
          schema:
            type: boolean
            default: false
        - name: category
          in: query
          required: false
          description: Filter by category (income_statement, balance_sheet, cash_flow, ratio).
          schema:
            type: string
            enum: [income_statement, balance_sheet, cash_flow, ratio]
      responses:
        '200':
          description: Concept dictionary with aliases, derivations, applicability, and metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConceptsResponse'

  /issuers:
    get:
      tags: [reference]
      summary: Issuer directory with ticker history.
      operationId: listIssuers
      x-status: reserved
      parameters:
        - name: id
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/AsOfParam'
      responses:
        '200':
          description: Issuers matching the filter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuersResponse'
        '501':
          $ref: '#/components/responses/NotImplemented'

components:

  parameters:

    IdsParam:
      name: ids
      in: query
      required: true
      description: Comma-separated issuer identifiers (ticker or CIK). Tickers resolved as of `asOf`.
      schema:
        type: string
        example: "AAPL,MSFT"

    MetricsParam:
      name: metrics
      in: query
      required: true
      description: Comma-separated metric names. See `/v1/concepts` (reserved) or `mapping_spec.md` §1.
      schema:
        type: string
        example: "revenue,netincome,totalassets"

    PeriodParam:
      name: period
      in: query
      schema:
        $ref: '#/components/schemas/PeriodValue'

    AsOfParam:
      name: asOf
      in: query
      description: ISO-8601 UTC timestamp. Default is now.
      schema:
        type: string
        format: date-time

    AvailabilityParam:
      name: availability
      in: query
      schema:
        $ref: '#/components/schemas/AvailabilityMode'

    AccessionNumberParam:
      name: accessionNumber
      in: path
      required: true
      schema:
        type: string
        pattern: '^[0-9]{10}-[0-9]{2}-[0-9]{6}$'

    PitFactIdParam:
      name: pitFactId
      in: path
      required: true
      schema:
        type: string

  responses:
    BadRequest:
      description: Malformed or missing required parameter.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Issuer, concept, or fact not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotImplemented:
      description: Endpoint reserved but not yet implemented.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:

    PeriodValue:
      type: string
      enum: [FY, Q, Q1, Q2, Q3, Q4, YTD, TTM]
      default: FY
      description: |
        `FY`, `Q1`..`Q4`, and `YTD` are reported periods sourced directly from XBRL.
        `TTM` is derived; the response carries `derivationMethod` and `sourcePitFactIds`.
        `Q` is an alias for "whichever quarter the response reports" and is resolved
        against `fiscalYear` + `fiscalPeriod` on the matching filing.

    AvailabilityMode:
      type: string
      enum: [accepted, market]
      default: market
      description: |
        `market` — default. Uses `marketAvailableAt`, derived per the NYSE session
        policy. Safe for backtests that trade regular session.
        `accepted` — uses `acceptedAt_utc`. "What did EDGAR know at this moment?"
        Safe only for callers that understand the pre-market / after-hours implications.

    VersioningMode:
      type: string
      enum: [latest, original]
      default: latest
      description: |
        Selects which point on the fact's restatement timeline to return.
        `latest` — the most recently accepted fact for (cik, concept, fiscalYear,
        fiscalPeriod) whose availability is ≤ asOf. Reflects the issuer's
        current best knowledge of that period (restatement-aware). This is the
        correct default for research and most backtests.
        `original` — the earliest accepted fact matching the same key. Reflects
        the value as first reported, before any later 10-K/A or 10-Q/A
        restatements. Use for strict as-filed backtests that reject hindsight
        from restatements.

    MappingMethod:
      type: string
      enum: [standard, calc_linkbase_anchor, curated_yaml, semantic_label, derived_formula, unmapped]
      description: Which layer of the 5-layer pipeline produced this fact. See `mapping_spec.md` §2.

    HealthResponse:
      type: object
      required: [status, storage, tables]
      properties:
        status:
          type: string
          enum: [ok, degraded, down]
        storage:
          type: object
          additionalProperties:
            type: string
          example:
            engine: duckdb
            databasePath: /var/lib/pitdata/pitdata.duckdb
        tables:
          type: object
          additionalProperties:
            type: integer

    FundamentalsResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/FundamentalRecord'
        meta:
          $ref: '#/components/schemas/FundamentalsMeta'

    FundamentalsMeta:
      type: object
      required: [asOf, availability, versioning, source, pointInTime]
      properties:
        asOf:
          type: string
          format: date-time
        availability:
          $ref: '#/components/schemas/AvailabilityMode'
        versioning:
          $ref: '#/components/schemas/VersioningMode'
        source:
          type: string
          default: SEC EDGAR
        pointInTime:
          type: boolean

    FundamentalRecord:
      type: object
      required:
        - requestId
        - cik
        - metric
        - value
        - unit
        - fiscalYear
        - fiscalPeriod
        - periodEndDate
        - acceptedAt
        - marketAvailableAt
        - accessionNumber
        - formType
        - isRestated
        - mappingMethod
        - mappingConfidence
        - pitFactId
      properties:
        requestId:
          type: string
          description: Echoes the `ids` token the caller used (ticker or CIK).
        cik:
          type: string
          description: 10-digit zero-padded.
        metric:
          type: string
        value:
          type: number
        unit:
          type: string
          description: XBRL unit. Includes compound units for per-share facts.
          enum: [USD, "USD/shares", shares, pure]
        currency:
          type: string
          nullable: true
          description: >
            ISO-4217 currency code (e.g. "USD") when the value is monetary,
            or null for non-monetary units (shares, pure ratios). For per-share
            units like "USD/shares" the leading currency is returned.
          example: USD
        fiscalYear:
          type: integer
        fiscalPeriod:
          type: string
        periodEndDate:
          type: string
          format: date
        acceptedAt:
          type: string
          format: date-time
        marketAvailableAt:
          type: string
          format: date-time
        accessionNumber:
          type: string
        formType:
          type: string
        isRestated:
          type: boolean
        rawTag:
          type: string
          description: Present iff `includeRawTag=true`.
        mappingMethod:
          $ref: '#/components/schemas/MappingMethod'
        mappingConfidence:
          type: number
          minimum: 0
          maximum: 1
        pitFactId:
          type: string
        supersedesPitFactId:
          type: string
          nullable: true
          description: If restated, the `pitFactId` this row replaces.
        derivationMethod:
          type: string
          description: Present for derived facts only.
          example: sum_of_quarters
        sourcePitFactIds:
          type: array
          items:
            type: string
          description: Present for derived facts only.
        lineage:
          $ref: '#/components/schemas/LineageInfo'
        attachments:
          $ref: '#/components/schemas/FactAttachments'

    LineageInfo:
      type: object
      required: [rawFactId, accessionNumber, filingUrl, primaryDocUrl]
      properties:
        rawFactId:
          type: string
        accessionNumber:
          type: string
        filingUrl:
          type: string
          format: uri
        primaryDocUrl:
          type: string
          format: uri
        xbrlInstanceUrl:
          type: string
          format: uri
          nullable: true

    FactAttachments:
      type: object
      x-status: reserved
      description: Populated only when `includeAttachments=true`; filtered by `asOf`.
      properties:
        disclosures:
          type: array
          items:
            $ref: '#/components/schemas/Disclosure'
        notes:
          type: array
          items:
            $ref: '#/components/schemas/Note'

    Filing:
      type: object
      required:
        - accessionNumber
        - cik
        - formType
        - acceptedAt
        - marketAvailableAt
        - periodEnd
        - fiscalYear
        - fiscalPeriod
        - isAmendment
        - filingUrl
        - primaryDocUrl
        - status
      properties:
        accessionNumber:
          type: string
        cik:
          type: string
        formType:
          type: string
        filedAt:
          type: string
          format: date-time
        acceptedAt:
          type: string
          format: date-time
        marketAvailableAt:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date
        fiscalYear:
          type: integer
        fiscalPeriod:
          type: string
        amendsAccessionNumber:
          type: string
          nullable: true
        isAmendment:
          type: boolean
        filingUrl:
          type: string
          format: uri
        primaryDocUrl:
          type: string
          format: uri
        xbrlInstanceUrl:
          type: string
          format: uri
          nullable: true
        status:
          type: string
          enum: [ingested, parsed, quarantined, failed_validation]

    FilingsResponse:
      type: object
      x-status: reserved
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Filing'

    Disclosure:
      type: object
      x-status: reserved
      required:
        - disclosureId
        - accessionNumber
        - cik
        - disclosureType
        - acceptedAt
        - marketAvailableAt
        - extractionMethod
        - extractionConfidence
      properties:
        disclosureId:
          type: string
        accessionNumber:
          type: string
        cik:
          type: string
        disclosureType:
          type: string
        sourceSection:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        bodyText:
          type: string
        bodyMarkdown:
          type: string
          nullable: true
        eventDate:
          type: string
          format: date
          nullable: true
        acceptedAt:
          type: string
          format: date-time
        marketAvailableAt:
          type: string
          format: date-time
        extractionMethod:
          type: string
          enum: [rule_based, classifier, llm_assisted]
        extractionConfidence:
          type: number
          minimum: 0
          maximum: 1
        isMaterialFlag:
          type: boolean
          nullable: true

    DisclosuresResponse:
      type: object
      x-status: reserved
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Disclosure'

    Note:
      type: object
      x-status: reserved
      required:
        - noteId
        - accessionNumber
        - cik
        - noteType
        - noteOrder
        - acceptedAt
        - marketAvailableAt
        - extractionMethod
        - extractionConfidence
      properties:
        noteId:
          type: string
        accessionNumber:
          type: string
        cik:
          type: string
        noteType:
          type: string
        noteTitle:
          type: string
          nullable: true
        noteText:
          type: string
        noteOrder:
          type: integer
        acceptedAt:
          type: string
          format: date-time
        marketAvailableAt:
          type: string
          format: date-time
        sourceAnchor:
          type: string
          nullable: true
        extractionMethod:
          type: string
          enum: [rule_based, classifier, llm_assisted]
        extractionConfidence:
          type: number
          minimum: 0
          maximum: 1

    NotesResponse:
      type: object
      x-status: reserved
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Note'

    AttachmentsResponse:
      type: object
      x-status: reserved
      required: [disclosures, notes]
      properties:
        disclosures:
          type: array
          items:
            $ref: '#/components/schemas/Disclosure'
        notes:
          type: array
          items:
            $ref: '#/components/schemas/Note'

    PointInTimeSnapshot:
      type: object
      x-status: reserved
      required: [cik, asOf, metrics]
      properties:
        cik:
          type: string
        asOf:
          type: string
          format: date-time
        metrics:
          type: array
          items:
            $ref: '#/components/schemas/FundamentalRecord'

    PeriodRecord:
      type: object
      x-status: stable
      required:
        [requestId, cik, fiscalYear, fiscalPeriod, periodEnd, formType,
         isAmendment, accessionNumber, filedAt, acceptedAt, marketAvailableAt,
         filingStatus, filingUrl, primaryDocUrl]
      properties:
        requestId:
          type: string
        cik:
          type: string
          example: "0000320193"
        ticker:
          type: string
          nullable: true
          description: Ticker-of-record at the filing's acceptedAt (PIT).
        fiscalYear:
          type: integer
        fiscalPeriod:
          type: string
          description: FY, Q1, Q2, Q3.
        periodStart:
          type: string
          format: date
          nullable: true
        periodEnd:
          type: string
          format: date
        formType:
          type: string
          example: 10-K
        isAmendment:
          type: boolean
        amendsAccessionNo:
          type: string
          nullable: true
          description: Accession number of the filing this one amends, or null for originals.
        accessionNumber:
          type: string
        filedAt:
          type: string
          format: date-time
        acceptedAt:
          type: string
          format: date-time
        marketAvailableAt:
          type: string
          format: date-time
        filingStatus:
          type: string
          description: Pipeline status (seeded, validated, failed_validation, superseded, ...).
        filingUrl:
          type: string
          format: uri
        primaryDocUrl:
          type: string
          format: uri
        xbrlInstanceUrl:
          type: string
          format: uri
          nullable: true

    PeriodsResponse:
      type: object
      x-status: stable
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PeriodRecord'
        meta:
          type: object
          required: [asOf, availability, totalCount]
          properties:
            asOf:
              type: string
              format: date-time
            availability:
              $ref: '#/components/schemas/AvailabilityMode'
            totalCount:
              type: integer

    Concept:
      type: object
      x-status: stable
      required: [concept, primaryRawTag, periodType, category, unit]
      properties:
        concept:
          type: string
          description: Canonical concept name (PascalCase), used in the mapping catalog and derivation rules.
          example: NetIncome
        metric:
          type: string
          nullable: true
          description: Lowercase metric key used on `/v1/fundamentals?metrics=...`. Null for helper concepts.
          example: netincome
        primaryRawTag:
          type: string
          description: Preferred XBRL tag. Sentinel `derived:<Concept>` marks purely derived non-GAAP concepts.
          example: "us-gaap:NetIncomeLoss"
        aliases:
          type: array
          items:
            type: string
          description: Additional XBRL local names (no namespace) accepted as equivalents. Order is priority.
        periodType:
          type: string
          enum: [duration, instant]
        category:
          type: string
          enum: [income_statement, balance_sheet, cash_flow, ratio]
        subcategory:
          type: string
          nullable: true
          description: Finer grouping (e.g. revenue, profit, margin, return, leverage, liquidity, non_gaap, helper, per_share).
        unit:
          type: string
          description: XBRL unit of the concept's values. Monetary = currency code, per-share = compound, ratios = pure, shares = shares.
          example: USD
        description:
          type: string
          nullable: true
          description: One-line description suitable for analyst-facing docs.
        derivations:
          type: array
          description: Ordered list of derivation rules; the first that fully resolves from already-mapped concepts wins.
          items:
            $ref: '#/components/schemas/DerivationRule'
        notApplicableSectors:
          type: array
          items:
            type: string
          description: GICS sectors for which this concept is intentionally skipped (reported as `not_applicable`, not `missing`).
        sourceUrl:
          type: string
          format: uri
          nullable: true
          description: URL to the authoritative GAAP taxonomy entry. Null for purely derived concepts.

    DerivationRule:
      type: object
      x-status: stable
      required: [operator, operands]
      properties:
        operator:
          type: string
          enum: [add, subtract, divide]
          description: Binary operator; divide returns `pure` unit and relaxes operand period_type matching.
        operands:
          type: array
          items:
            type: string
          minItems: 2
          maxItems: 2
          description: Concept names of the two operands (not XBRL tags).
        optionalOperands:
          type: array
          items:
            type: string
          description: Operands treated as zero if missing (only honored for add/subtract).

    ConceptsResponse:
      type: object
      x-status: stable
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Concept'
        meta:
          type: object
          required: [totalCount, version]
          properties:
            totalCount:
              type: integer
            userFacingCount:
              type: integer
            version:
              type: string
              description: Mapping-catalog version (matches mapping_spec.md).
              example: "0.2.0"

    Issuer:
      type: object
      x-status: reserved
      required: [cik, name]
      properties:
        cik:
          type: string
        name:
          type: string
        tickerHistory:
          type: array
          items:
            $ref: '#/components/schemas/TickerHistoryEntry'

    TickerHistoryEntry:
      type: object
      x-status: reserved
      required: [ticker, validFrom]
      properties:
        ticker:
          type: string
        validFrom:
          type: string
          format: date-time
        validTo:
          type: string
          format: date-time
          nullable: true
        exchange:
          type: string
          nullable: true

    IssuersResponse:
      type: object
      x-status: reserved
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Issuer'

    Error:
      type: object
      required: [detail]
      properties:
        detail:
          type: string
        code:
          type: string
          nullable: true
