# RentRollIQ Validation API — public OpenAPI spec
# Generated from apps/api by `python -m scripts.generate_openapi`.
# Source of truth lives in apps/api/app/routes/validate.py and the
# Pydantic schemas under apps/api/app/schemas/. Do not edit this
# file by hand — regenerate after route or schema changes.
#
# The narrative spec is at docs/monetization/validation-api-spec.md.

openapi: 3.1.0
info:
  title: RentRollIQ Validation API
  version: 0.1.0
  description: Public validation surface for CRE document AI integration partners. POST extracted artifacts
    or a source PDF and receive a validation report driven by RentRollIQ's institutional-grade rules registry.
    Narrative spec at docs/monetization/validation-api-spec.md.
  contact:
    name: RentRollIQ integrations
    url: https://rentrolliq.com
paths:
  /v1/checks:
    get:
      tags:
      - validation
      summary: List validation checks in the current registry
      description: 'Registry catalog. Returns every check the validator knows about

        so a partner''s UI can pre-render the catalog before any inputs

        have been submitted.'
      operationId: list_checks_v1_checks_get
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChecksCatalogResponse'
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /v1/validate:
    post:
      tags:
      - validation
      summary: Validate an OM / rent roll, by PDF or by pre-extracted JSON
      description: 'Run the validation registry against caller-supplied input.


        See docs/monetization/validation-api-spec.md for the public schema;

        this handler dispatches to services/validation_api.py and renders

        the response.'
      operationId: validate_v1_validate_post
      parameters:
      - name: include_internal
        in: query
        required: false
        schema:
          type: boolean
          description: Include internal-audience checks (extraction / parser QA) in the response. Default
            false — client-audience only.
          default: false
          title: Include Internal
        description: Include internal-audience checks (extraction / parser QA) in the response. Default
          false — client-audience only.
      - name: async
        in: query
        required: false
        schema:
          type: boolean
          description: Reserved for v0.2 — async / webhook delivery. Setting true on v0.1 returns 501.
            See validation-api-spec.md §9.
          default: false
          title: Async
        description: Reserved for v0.2 — async / webhook delivery. Setting true on v0.1 returns 501. See
          validation-api-spec.md §9.
      - name: X-Registry-Version
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Registry-Version
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
        '400':
          description: Bad request — malformed JSON, missing field, or schema violation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: PDF payload exceeds the size ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '415':
          description: Content-Type is not application/pdf or application/json.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded; see Retry-After header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '501':
          description: Requested feature not implemented in this registry version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '500':
          description: Extraction or validator failure; correlate via validation_id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '503':
          description: Mode A extraction not configured on this deploy; Mode B remains available.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      requestBody:
        required: true
        description: 'Either a source PDF (Content-Type: application/pdf) or a pre-extracted JSON context
          (Content-Type: application/json).'
        content:
          application/pdf:
            schema:
              type: string
              format: binary
              description: Source PDF for Mode A. Up to 50 MB. Runs the RentRollIQ pipeline (section discovery
                → core extractors → validation registry).
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
            examples:
              deal_summary_only:
                summary: Minimal Mode B — deal summary only
                value:
                  extraction_context:
                    deal_summary:
                      property_name: Example Tower
                      property_type: multifamily
                      asking_price: '12500000'
                      broker_cap_rate: 0.055
                      unit_count: 24
components:
  schemas:
    ApiError:
      properties:
        error:
          type: string
          title: Error
          description: Machine-readable error code, e.g. 'bad_request'.
        message:
          anyOf:
          - type: string
          - type: 'null'
          title: Message
        field_path:
          anyOf:
          - type: string
          - type: 'null'
          title: Field Path
          description: Dot-path to the offending field on 400/422 responses.
      type: object
      required:
      - error
      title: ApiError
      description: JSON error envelope for non-2xx responses on /v1/*.
    CheckCatalogEntry:
      properties:
        check_id:
          type: string
          title: Check Id
        check_name:
          type: string
          title: Check Name
        category:
          type: string
          title: Category
        audience:
          type: string
          enum:
          - client
          - internal
          title: Audience
        severity_default:
          type: string
          enum:
          - pass
          - info
          - warn
          - fail
          - skipped
          title: Severity Default
      type: object
      required:
      - check_id
      - check_name
      - category
      - audience
      - severity_default
      title: CheckCatalogEntry
    ChecksCatalogResponse:
      properties:
        registry_version:
          type: string
          title: Registry Version
        checks_catalog_size:
          type: integer
          title: Checks Catalog Size
        checks:
          items:
            $ref: '#/components/schemas/CheckCatalogEntry'
          type: array
          title: Checks
      type: object
      required:
      - registry_version
      - checks_catalog_size
      - checks
      title: ChecksCatalogResponse
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ValidateResponse:
      properties:
        validation_id:
          type: string
          title: Validation Id
          description: Opaque id; useful for support correlation.
        input_mode:
          type: string
          enum:
          - pdf
          - json
          title: Input Mode
        summary:
          $ref: '#/components/schemas/ValidationSummary'
        results:
          items:
            $ref: '#/components/schemas/ValidationCheckResult'
          type: array
          title: Results
        citations:
          items:
            $ref: '#/components/schemas/ValidationCitation'
          type: array
          title: Citations
        telemetry:
          $ref: '#/components/schemas/ValidationTelemetry'
      type: object
      required:
      - validation_id
      - input_mode
      - summary
      - results
      - telemetry
      title: ValidateResponse
    ValidationCheckResult:
      properties:
        check_id:
          type: string
          title: Check Id
          description: Stable identifier; see GET /v1/checks for the full catalog.
        check_name:
          type: string
          title: Check Name
        category:
          type: string
          title: Category
        status:
          type: string
          enum:
          - pass
          - info
          - warn
          - fail
          - skipped
          title: Status
        audience:
          type: string
          enum:
          - client
          - internal
          title: Audience
        message:
          type: string
          title: Message
        details:
          anyOf:
          - type: string
          - type: 'null'
          title: Details
        affected_fields:
          items:
            type: string
          type: array
          title: Affected Fields
        suggested_fix:
          anyOf:
          - type: string
          - type: 'null'
          title: Suggested Fix
        data:
          type: object
          title: Data
      type: object
      required:
      - check_id
      - check_name
      - category
      - status
      - audience
      - message
      title: ValidationCheckResult
      description: 'One row from the validator''s response. Mirrors

        `services/validation/base.py::ValidationResult` with `audience`

        surfaced so a buyer''s UI can route internal-audience checks to

        engineering and client-audience checks to the underwriter view.'
    ValidationCitation:
      properties:
        field:
          type: string
          title: Field
        source_page:
          type: integer
          title: Source Page
        source_label:
          anyOf:
          - type: string
          - type: 'null'
          title: Source Label
        bbox:
          anyOf:
          - items:
              type: number
            type: array
          - type: 'null'
          title: Bbox
          description: '[x0, y0, x1, y1] in PDF points when available.'
      type: object
      required:
      - field
      - source_page
      title: ValidationCitation
      description: 'Per-field source-page citation. Populated by Mode A on artifacts

        where the upstream extractor exposed page-level provenance; empty

        list in Mode B today.'
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    ValidationSummary:
      properties:
        checks_run:
          type: integer
          title: Checks Run
        checks_passed:
          type: integer
          title: Checks Passed
        checks_info:
          type: integer
          title: Checks Info
          description: Count of checks that returned 'info' — neutral observations the validator wants
            the reader to see but that aren't themselves problems. Kept separate from 'passed' so checks_run
            = passed + info + warned + failed + skipped is an arithmetic identity, not an approximation.
          default: 0
        checks_warned:
          type: integer
          title: Checks Warned
        checks_failed:
          type: integer
          title: Checks Failed
        checks_skipped:
          type: integer
          title: Checks Skipped
        overall_status:
          type: string
          enum:
          - pass
          - warn
          - fail
          title: Overall Status
        headline_concern:
          anyOf:
          - type: string
          - type: 'null'
          title: Headline Concern
          description: First fail message, else first warn, else null.
      type: object
      required:
      - checks_run
      - checks_passed
      - checks_warned
      - checks_failed
      - checks_skipped
      - overall_status
      title: ValidationSummary
    ValidationTelemetry:
      properties:
        latency_ms:
          type: integer
          title: Latency Ms
        registry_version:
          type: string
          title: Registry Version
        checks_catalog_size:
          type: integer
          title: Checks Catalog Size
      type: object
      required:
      - latency_ms
      - registry_version
      - checks_catalog_size
      title: ValidationTelemetry
    ValidateRequest:
      description: 'Top-level POST /v1/validate request body for Mode B (JSON in).

        Mode A (PDF in) does not use this shape — the body is the raw PDF.'
      properties:
        extraction_context:
          $ref: '#/components/schemas/PublicExtractionContext'
          description: Partial snapshot of extracted artifacts. Send whatever you have; missing artifacts
            skip the checks that need them.
      title: ValidateRequest
      type: object
    DealSummary:
      properties:
        property_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: The property's branded or street name (e.g. 'Royal Palms', 'Hartwell Tower').
          title: Property Name
        property_name_candidate:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: When the LLM-extracted property name fails the verbatim check and we fall back
            to an address-based name, the original (untrusted) candidate is preserved here so the analyst
            can see what was rejected. Null when the LLM's name passed.
          title: Property Name Candidate
        property_name_verbatim_status:
          anyOf:
          - enum:
            - pass
            - warn
            - fail
            type: string
          - type: 'null'
          default: null
          description: Result of the post-extraction verbatim check on property_name. 'pass' = appears
            verbatim in source; 'warn' = did not appear verbatim, fell back to address-based name; 'fail'
            = no name could be determined. Null when no extraction was attempted.
          title: Property Name Verbatim Status
        property_address:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Full street address. Single line; include city/state/zip when given.
          title: Property Address
        market:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: MSA or submarket (e.g. 'Dallas-Fort Worth', 'Phoenix', 'Brooklyn').
          title: Market
        property_type:
          anyOf:
          - enum:
            - multifamily
            - office
            - retail
            - industrial
            - mixed_use
            - hotel
            - self_storage
            - land
            - other
            type: string
          - type: 'null'
          default: null
          title: Property Type
        year_built:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Year Built
        year_renovated:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Year Renovated
        unit_count:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: Total units (apartments, suites, keys). Null for non-multifamily where not applicable.
          title: Unit Count
        rentable_sf:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: Total rentable / net leasable square feet, when stated. Null when not given.
          title: Rentable Sf
        stated_occupancy:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Occupancy rate as the BROKER states it on the OM cover or property summary pages,
            as a decimal (0.88 for 88%). Null if not stated. We surface this alongside our rent-roll-derived
            occupancy so an analyst can spot conflicts (broker says 88%, our roll-derived math says 100%)
            without having to flip back to the OM.
          title: Stated Occupancy
        asking_price:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Asking / list / offering price as stated in the OM. Null when 'price upon request'
            or unstated.
          title: Asking Price
        asking_price_needs_review:
          default: false
          description: Set by the post-extraction reconciliation step when the asking price disagrees
            with other extracted facts (e.g. broker cap rate × NOI implies a different price). Surfaced
            on the Deal Summary sheet so an analyst knows to verify against the OM cover.
          title: Asking Price Needs Review
          type: boolean
        price_candidates:
          description: Every distinct currency value seen on the cover / exec-summary pages near the asking-price
            block. Used to (a) filter out struck-through stale prices, and (b) propose a corrected price
            during cap-rate reconciliation when the originally selected asking_price doesn't tie to broker_cap_rate
            × NOI.
          items:
            $ref: '#/components/schemas/PriceCandidate'
          title: Price Candidates
          type: array
        property_tax_footnote_price:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Some OMs footnote the property-tax line with the price the tax assumes ('Property
            tax based on $3,350,000 asking price'). When present, this is the most reliable cross-check
            on asking price.
          title: Property Tax Footnote Price
        broker_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: The broker's stated NOI used to back into the cap rate. Annual.
          title: Broker Noi
        broker_cap_rate:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Broker's stated cap rate as a decimal (0.0468 for 4.68%). Convert any percent-of-100
            in the source (e.g. '4.68%') to decimal.
          title: Broker Cap Rate
        grm:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Broker's stated Gross Rent Multiplier (e.g. 8.75), when the OM headline discloses
            one. asking_price ÷ grm implies the broker's GPI — the single cheapest cross-check for a dropped
            income line on the operating statement (CLAUDE.md VIII.7). Null when not stated; never computed.
          title: Grm
        stabilized_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Year-1 stabilized / pro-forma NOI when broker shows it separately from current
            NOI.
          title: Stabilized Noi
        stabilized_cap_rate:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Stabilized / pro-forma cap rate as a decimal.
          title: Stabilized Cap Rate
        listing_broker:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Listing Broker
        offer_due_date:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Best-and-final / call-for-offers date if stated. Free-text — many brokers write
            'TBD'.
          title: Offer Due Date
        source_pages:
          description: 1-indexed pages the deal_summary was extracted from. Set by pipeline, not the model.
          items:
            type: integer
          title: Source Pages
          type: array
        asking_price_search_pages:
          description: 1-indexed pages we searched for the asking price. Useful for debugging when asking_price
            is None despite the OM clearly stating a price somewhere in the financial summary.
          items:
            type: integer
          title: Asking Price Search Pages
          type: array
        commercial_tenant_credit_flags:
          description: Distinct credit_flags surfaced across every extracted commercial lease abstract.
            Mirrors lease_abstracts[].credit_flags so a downstream IC packet can read deal-level risk
            from one field without iterating into per-tenant detail. Set by the pipeline.
          items:
            type: string
          title: Commercial Tenant Credit Flags
          type: array
        non_arms_length_leases_count:
          default: 0
          description: Count of leases (commercial OR residential) where the source discloses a relationship
            between tenant and ownership — e.g. 'Acquaintance of the owner', employee leases, family rentals.
            Common in seller-friendly small-deal rent rolls to inflate occupancy; useful underwriting
            signal because those rents may not reflect market.
          title: Non Arms Length Leases Count
          type: integer
        rent_roll_completeness_score:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: 0.0–1.0 score reflecting how thoroughly the broker disclosed the rent roll (sf,
            beds/baths, lease dates, deposits, market rent, tenant names). Mirrors rent_roll.rent_roll_completeness_score
            so the Deal Summary surfaces disclosure quality at a glance. Null when no rent roll was extracted,
            or when the rent roll is synthesized.
          title: Rent Roll Completeness Score
        material_disclosures:
          description: 'Structured disclosure findings surfaced by the post-extraction disclosure scanner.
            Each entry: {category, phrase, quote}. Categories: broker_affiliate_ownership, dual_agency,
            related_party. Set by the pipeline, not the LLM.'
          items:
            type: object
          title: Material Disclosures
          type: array
        deal_classification:
          default: stabilized
          description: Lifecycle bucket for the deal. Default 'stabilized' keeps existing institutional-OM
            behavior unchanged. Set by the pipeline, not the LLM.
          enum:
          - stabilized
          - value_add
          - distressed
          - lease_up
          - development
          - owner_user
          - land
          title: Deal Classification
          type: string
        classification_signals:
          description: Distinct heuristic signals that fed the classification (e.g. 'distressed_keyword',
            'occupancy_below_25pct', 'renovation_capex_present'). Surfaced on the Deal Summary sheet and
            the Audit sheet so analysts can see why the classifier landed where it did.
          items:
            type: string
          title: Classification Signals
          type: array
      title: DealSummary
      type: object
    LeaseAbstract:
      description: 'One per-tenant lease summary block.


        Optional fields stay null when the OM doesn''t disclose the value;

        we never invent. credit_flags drives the commercial_tenant_credit_review

        validator, so every flag should be backed by a verbatim quote in

        credit_flag_source_text.'
      properties:
        tenant_name:
          description: Tenant name as it appears on the lease abstract block. Use the legal entity name
            when given (e.g. 'Triple J Industries LLC'), not a shortened DBA.
          title: Tenant Name
          type: string
        space_id:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Suite / unit identifier. Use the same string the rent roll uses when the unit appears
            on both, so cross-validation can match rows.
          title: Space Id
        use:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Permitted use clause summary, e.g. 'Laundromat & coffee shop'. Free-text from the
            abstract.
          title: Use
        lease_commencement:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Lease Commencement
        lease_expiration:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Lease Expiration
        current_base_rent_monthly:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Current contractual base rent in $/month.
          title: Current Base Rent Monthly
        current_base_rent_annual:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Current contractual base rent in $/year. Compute as monthly × 12 when only monthly
            is shown, or vice versa, only if doing so does not disagree with another stated value in the
            abstract.
          title: Current Base Rent Annual
        escalation_type:
          anyOf:
          - enum:
            - fixed_pct
            - cpi
            - flat
            - step
            type: string
          - type: 'null'
          default: null
          title: Escalation Type
        escalation_rate:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: When escalation_type is fixed_pct, this is the per-year rate as a decimal (0.03
            for 3%). Null otherwise.
          title: Escalation Rate
        renewal_option_count:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: Number of renewal options the lease grants the tenant.
          title: Renewal Option Count
        renewal_term_years:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: Length of each renewal option in years.
          title: Renewal Term Years
        renewal_starting_rent:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Starting monthly rent at the first renewal, when stated.
          title: Renewal Starting Rent
        renewal_ending_rent:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Ending monthly rent at the last year of the renewal term, when the OM shows a renewal-period
            rent ramp ('renewal at $6,720→$7,565').
          title: Renewal Ending Rent
        holdover_rate_monthly:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Holdover rent in $/month (often 125-150% of expiring base).
          title: Holdover Rate Monthly
        tenant_pays:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Free-text summary of expense categories the tenant pays (e.g. 'utilities, interior
            maintenance, taxes').
          title: Tenant Pays
        landlord_pays:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Free-text summary of expense categories the landlord pays.
          title: Landlord Pays
        cam_reimbursement_pct:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Tenant's pro-rata share of CAM (common-area maintenance) expenses as a decimal
            (0.20 for 20%). Null when the lease abstract doesn't disclose a CAM reimbursement structure.
          title: Cam Reimbursement Pct
        expense_categories_reimbursed:
          description: 'Free-text categories the tenant reimburses, with a per-category share when the
            OM is specific. Examples: [''cam_general'', ''water_40pct''], [''cam_general'', ''water_partial''],
            [''taxes'', ''insurance'', ''cam'']. Empty when no reimbursement structure is disclosed.'
          items:
            type: string
          title: Expense Categories Reimbursed
          type: array
        credit_flags:
          description: Detected credit-risk markers. Populated from any disclosed concerns in the abstract
            or its footnotes. A bankruptcy disclosure must populate this list.
          items:
            enum:
            - bankruptcy_protection
            - in_default
            - deferred_rent
            - personal_guarantee
            - corporate_guarantee
            - subordinated
            - lease_guaranty
            - non_paying
            - holdover
            - non_arms_length
            type: string
          title: Credit Flags
          type: array
        credit_flag_source_text:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Verbatim quote from the OM that triggered each credit flag. Surfaced in the validation
            message so a reviewer can see exactly what the abstract said without flipping to source.
          title: Credit Flag Source Text
        notes:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Any remaining narrative detail not captured above.
          title: Notes
        source_page:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: 1-indexed page number where this abstract appears.
          title: Source Page
      required:
      - tenant_name
      title: LeaseAbstract
      type: object
    OSLineItem:
      properties:
        category:
          enum:
          - income
          - operating_expense
          - capex
          title: Category
          type: string
        gl_account:
          title: Gl Account
          type: string
        raw_label:
          title: Raw Label
          type: string
        annual_value:
          anyOf:
          - type: number
          - type: string
          title: Annual Value
        proforma_value:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Value
        note:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Footnote / annotation text from the source, carried verbatim through merge from
            OSRawLineItem.note. Surfaced on the Operating Statement sheet and consumed by the IC-memo
            scaffold (ancillary-income breakdown, property-tax basis reconciliation). Null when the source
            line carries no footnote.
          title: Note
        monthly_value:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Monthly value as printed in the source when the document exposes both a Monthly
            and an Annual column. Carried through merge so the os_monthly_annual_consistency check can
            run on the post-merge OperatingStatement. Null when the source didn't print a monthly column
            for this row.
          title: Monthly Value
        confidence:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          title: Confidence
        needs_review:
          default: false
          title: Needs Review
          type: boolean
        indent_level:
          default: 0
          title: Indent Level
          type: integer
        is_subtotal:
          default: false
          title: Is Subtotal
          type: boolean
        is_component:
          default: false
          title: Is Component
          type: boolean
        parent_label:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Label of the parent subtotal this line rolls up into, if any. E.g. 'Total Reimbursement
            Income' for CAM, Insurance, RE Taxes and Mgmt Fees on a commercial NNN OS.
          title: Parent Label
        contributes_to_total:
          default: true
          description: Gating flag for aggregation. True iff this line should be summed into Total Income
            / Total OpEx / NOI. False for components that roll up into a parent subtotal — the subtotal
            is the contributor, never both. Replaces the older subtotal-drop pattern that lost reimbursement
            income on NNN office OMs.
          title: Contributes To Total
          type: boolean
        row_type:
          default: line_item
          description: Renderer-facing classification, written to the Operating Statement sheet's 'Row
            Type' column. The aggregator only sums rows where row_type == 'line_item'. Subtotals stay
            visible (broker's stated total alongside our components) but never contribute. 'header' is
            reserved for label-only section rows; 'noi' for the synthetic NOI row written by the renderer.
          enum:
          - line_item
          - subtotal
          - header
          - noi
          title: Row Type
          type: string
        column_match_state:
          default: both
          description: When this row was detected as a subtotal, which columns qualified as a value-sum
            match? 'both' is the common case. 'current_only' / 'proforma_only' surface mixed-state roll-ups
            where only one column's components sum to the subtotal — rendered with a Note rather than
            silently dropped.
          enum:
          - both
          - current_only
          - proforma_only
          - none
          title: Column Match State
          type: string
        source_sign:
          default: positive
          description: Sign convention as the broker presented the value in the source. 'positive' = the
            source printed a positive number (or parens-style accounting that we already inverted); 'negative'
            = the source printed a negative or parenthesized value. We standardize the stored annual_value
            AFTER capturing this — OpEx is always stored positive and subtracted during aggregation regardless
            of how the broker presented it. Surfaced on the OS Mapping Audit sheet so analysts can see
            how the source presented each line without contaminating math.
          enum:
          - positive
          - negative
          title: Source Sign
          type: string
        is_recurring:
          default: true
          description: True when this line represents recurring operating spend, False when the line is
            a one-time / non-recurring cost mixed into the OpEx column (e.g. 'New Intercom Installation',
            'Appliance Purchase & Installation'). Capex lines are always is_recurring=False by construction.
            Drives steady_state_opex and steady_state_noi on the OperatingStatement so an underwriting
            model can isolate the in-place run-rate from trailing one-time costs.
          title: Is Recurring
          type: boolean
      required:
      - category
      - gl_account
      - raw_label
      - annual_value
      title: OSLineItem
      type: object
    OperatingStatement:
      properties:
        property_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Property Name
        source_property_label:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: 'The property name / address printed VERBATIM on the operating-statement / financial
            page header, captured before any canonicalization. Feeds the cross_property_consistency check:
            a financial page labeled with a DIFFERENT address than the subject (the 120 Storms Ave defect
            — its P&L page read ''904 Bergenline Avenue, Union City'') means the NOI/cap describe the
            wrong building. Null when the page carries no property header.'
          title: Source Property Label
        period_start:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Period Start
        period_end:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Period End
        period_description:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Period Description
        is_proforma:
          default: false
          title: Is Proforma
          type: boolean
        has_proforma_column:
          default: false
          title: Has Proforma Column
          type: boolean
        line_items:
          items:
            $ref: '#/components/schemas/OSLineItem'
          title: Line Items
          type: array
        total_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Aggregated total income. NULL when our aggregation disagrees with the broker's
            stated EGR by >1% AND the value-based subtotal detector did not flag any roll-up — better
            to ship a missing number than a wrong number. See section_summary.os_aggregation.fallback_to_broker.
          title: Total Income
        total_operating_expenses:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Total Operating Expenses
        noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Standardized NOI. NULL when total_income is NULL (the self-check fired) — never
            compute NOI on top of an unreliable Total Income.
          title: Noi
        expense_ratio:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          title: Expense Ratio
        proforma_total_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Total Income
        proforma_total_operating_expenses:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Total Operating Expenses
        proforma_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Noi
        broker_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Broker Noi
        capex_reclassified:
          anyOf:
          - type: number
          - type: string
          default: '0'
          title: Capex Reclassified
        broker_egr:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Broker Egr
        broker_stated_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Broker Stated Noi
        broker_stated_noi_proforma:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Broker Stated Noi Proforma
        proforma_noi_standardized:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Noi Standardized
        proforma_noi_broker_stated:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Proforma Noi Broker Stated
        proforma_capex_reclassified:
          anyOf:
          - type: number
          - type: string
          default: '0'
          title: Proforma Capex Reclassified
        proforma_noi_convention_delta:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: proforma_noi_standardized - proforma_noi_broker_stated. Equal to proforma_capex_reclassified
            by construction; duplicated here so the analyst can read the reconciliation without re-doing
            the math.
          title: Proforma Noi Convention Delta
        noi_standardized:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: 'Our standardized NOI: total_income - sum(operating_expense). Capex / reserves
            are excluded by construction. Equal to `noi` in the common case; surfaced as its own field
            so an analyst reading the OS Summary doesn''t have to recompute when the broker uses a different
            convention.'
          title: Noi Standardized
        noi_broker_stated:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: The broker-stated NOI for the current period. Prefers the OS source's printed NOI
            line (broker_stated_noi). Falls back to DealSummary.broker_noi (cover / exec-summary page)
            when the OS doesn't print an NOI subtotal. Null when neither source discloses an NOI.
          title: Noi Broker Stated
        noi_convention_note:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: 'Human-readable derivation note explaining the gap between noi_standardized and
            noi_broker_stated when the gap is explained by a capex/reserves convention difference. Example:
            ''Broker NOI includes $5,242 of capex (Intercom, Appliance) in OpEx; standardized NOI excludes.''
            Null when the two views agree or when no broker NOI is available.'
          title: Noi Convention Note
        steady_state_opex:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Sum of operating_expense line items where is_recurring=True. Equal to total_operating_expenses
            when no non-recurring items are present. Null when total_operating_expenses is null.
          title: Steady State Opex
        steady_state_noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: total_income - steady_state_opex. Excludes one-time costs the broker bundled into
            OpEx so underwriting can isolate the in-place run-rate from trailing non-recurring spend.
            Null when total_income or steady_state_opex is null.
          title: Steady State Noi
        one_time_costs:
          anyOf:
          - type: number
          - type: string
          default: '0'
          description: Sum of operating_expense annual_value where is_recurring=False. Used to render
            the 'One-Time Costs (Excluded from NOI)' line with its component breakdown on the OS Summary
            sheet.
          title: One Time Costs
        proforma_period_description:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Header of the forward column (carried through from the raw extraction) — 'Year
            1 Adjusted', 'Current Budget', etc. Null when the source had no second column or it was unlabeled.
          title: Proforma Period Description
        opex_basis_columns:
          anyOf:
          - items:
              $ref: '#/components/schemas/OpexBasisColumn'
            type: array
          - type: 'null'
          default: null
          description: Per-column opex-basis classification. None when the classifier never ran (legacy
            direct construction).
          title: Opex Basis Columns
        noi_by_basis:
          anyOf:
          - additionalProperties:
              anyOf:
              - type: number
              - type: string
            type: object
          - type: 'null'
          default: null
          description: 'One standardized NOI per detected opex basis: {''actuals'': ..., ''buyer_budget'':
            ..., ''pro_forma'': ...}. The broker headline NOI is matched against these to decide whether
            a gap is the new-buyer cost load (INFO) or a real defect (WARN).'
          title: Noi By Basis
        current_opex_basis:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Basis our standardized rebuild (the Current column) sits on — almost always 'actuals'.
            Used to decide whether a broker-vs-rebuild gap is cross-basis (new-buyer cost load) or same-basis
            (genuine reconciliation defect).
          title: Current Opex Basis
        opex_basis_unknown:
          default: false
          description: True when at least one labeled column could not be classified deterministically
            — drives a WARN so the basis is reviewed manually rather than silently defaulted.
          title: Opex Basis Unknown
          type: boolean
      required:
      - line_items
      title: OperatingStatement
      type: object
    OpexBasisColumn:
      description: 'One operating-statement column classified to an opex basis.


        Populated post-merge by app.services.opex_basis. Surfaced on the OS

        Mapping Audit sheet and fed to the IC-memo contract so the broker-vs-

        standardized NOI gap can be reframed as the new-buyer cost load

        (CLAUDE.md VIII.2) instead of flagged as a defect.'
      properties:
        header:
          description: Column header as classified (verbatim or 'Current'/'Pro Forma' fallback).
          title: Header
          type: string
        basis:
          enum:
          - actuals
          - buyer_budget
          - pro_forma
          - unknown
          title: Basis
          type: string
        matched_pattern:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: The regex that matched the header, or null when the basis was defaulted.
          title: Matched Pattern
        source:
          description: Which OS column this row came from (annual_value vs proforma_value).
          enum:
          - current
          - proforma
          title: Source
          type: string
        noi:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Standardized NOI built from this column, when available.
          title: Noi
      required:
      - header
      - basis
      - source
      title: OpexBasisColumn
      type: object
    PriceCandidate:
      description: 'One currency value seen near the OM''s asking-price block.


        Carries enough metadata to distinguish a struck-through original price

        from the active asking price (the 2226 Whittier Blvd bug class) and to

        do a cross-page consensus check when multiple candidates appear.'
      properties:
        value:
          anyOf:
          - type: number
          - type: string
          description: The currency value with no symbols/commas (e.g. 3350000).
          title: Value
        page:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: 1-indexed PDF page where the value appears.
          title: Page
        is_struck_through:
          default: false
          description: True if the value is rendered with strikethrough / line-through styling, an HTML
            <s>/<strike>/<del> tag, or an explicit visual cross-out. A struck-through price is almost
            always a stale previous asking price the broker dropped from.
          title: Is Struck Through
          type: boolean
        context:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Short label for where the candidate was found (e.g. 'cover', 'exec_summary', 'property_tax_footnote').
          title: Context
      required:
      - value
      title: PriceCandidate
      type: object
    ProFormaRentRoll:
      properties:
        property_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Property Name
        as_of_date:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: Effective / projection-base date of the pro forma.
          title: As Of Date
        units:
          items:
            $ref: '#/components/schemas/ProFormaUnit'
          title: Units
          type: array
        total_base_rent_annual:
          anyOf:
          - type: number
          - type: string
          description: Sum of base_rent_annual across all units.
          title: Total Base Rent Annual
        total_reimbursements_annual:
          anyOf:
          - type: number
          - type: string
          description: Sum of all reimbursement amounts across all units.
          title: Total Reimbursements Annual
        total_annual_revenue:
          anyOf:
          - type: number
          - type: string
          description: Total stabilized annual revenue = total_base_rent_annual + total_reimbursements_annual.
            The headline pro forma number a buyer pastes into IC.
          title: Total Annual Revenue
        signed_unit_count:
          default: 0
          description: Count of units with lease_status in {signed_renewal, signed_new}.
          title: Signed Unit Count
          type: integer
        assumed_unit_count:
          default: 0
          description: Count of units with lease_status == assumed_new.
          title: Assumed Unit Count
          type: integer
      required:
      - units
      - total_base_rent_annual
      - total_reimbursements_annual
      - total_annual_revenue
      title: ProFormaRentRoll
      type: object
    ProFormaSummary:
      description: Single-period stabilized projection with value-add underwriting.
      properties:
        gross_rental_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Gross Rental Income / Gross Scheduled Rent / GPR — annual base rent at market for
            every unit, before vacancy. Strip $ and commas.
          title: Gross Rental Income
        other_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Other income (parking, laundry, application fees, RUBS not separately reimbursed,
            pet rent, etc.). Annual.
          title: Other Income
        reimbursements:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: RUBS / CAM / expense pass-throughs. Annual.
          title: Reimbursements
        gross_potential_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Gross Potential Income (GPI) = gross_rental_income + other_income + reimbursements.
            The broker's stated total before vacancy.
          title: Gross Potential Income
        vacancy_credit_loss:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Vacancy + credit loss as a NEGATIVE Decimal value (e.g. -129760) OR the broker's
            stated absolute deduction amount; extractor normalizes to positive-deduction below.
          title: Vacancy Credit Loss
        vacancy_credit_loss_pct:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Vacancy / credit loss as a decimal (0.10 for 10%).
          title: Vacancy Credit Loss Pct
        effective_gross_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: EGI = GPI − vacancy_credit_loss.
          title: Effective Gross Income
        operating_expenses_detail:
          additionalProperties:
            anyOf:
            - type: number
            - type: string
          description: Free-form expense line items keyed by the broker's label (e.g. 'Property Taxes',
            'Insurance', 'Utilities — Electric'). Annual amounts. Captured verbatim so analysts can trace
            back to source.
          title: Operating Expenses Detail
          type: object
        property_taxes:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Property Taxes
        insurance:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Insurance
        utilities:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Utilities
        repairs_maintenance:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Repairs Maintenance
        contract_services:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Contract Services
        payroll:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Payroll
        management_fee:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Management Fee
        management_fee_pct:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Management fee as a decimal of EGI (0.035 for 3.5%).
          title: Management Fee Pct
        general_admin:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: General Admin
        replacement_reserves:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Replacement Reserves
        total_operating_expenses:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Total annual OpEx. Should equal sum of the detail bag.
          title: Total Operating Expenses
        net_operating_income:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: NOI = EGI − total_operating_expenses.
          title: Net Operating Income
        purchase_price:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Purchase / asking / list price — when the pro forma page explicitly footnotes the
            price used to compute going-in cap. May duplicate DealSummary.asking_price; left here so the
            Pro Forma sheet stands alone.
          title: Purchase Price
        renovation_capex:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: 'Total renovation / value-add capital budget. Label variants: ''CapEx Budget'',
            ''Renovation Budget'', ''Estimated Renovation/Capital Expenditures'', ''Value-Add Budget''.
            All map here.'
          title: Renovation Capex
        renovation_capex_per_unit:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Renovation capex divided by unit count, when stated.
          title: Renovation Capex Per Unit
        holding_costs:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: 'Carry / interest reserve / construction-period operating shortfall. Labels: ''Holding
            Costs'', ''Construction Period Carry'', ''Interest Reserve''.'
          title: Holding Costs
        total_project_cost:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: 'Total all-in basis: purchase_price + renovation_capex + holding_costs. Labels:
            ''Total Project Cost'', ''Total Investment'', ''All-in Basis''.'
          title: Total Project Cost
        going_in_cap_rate:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Broker's stated going-in cap rate, as a decimal (0.087 for 8.7%). For value-add
            deals this is typically computed on total_project_cost, NOT purchase_price — see the cap_rate_basis_reconciliation
            validator.
          title: Going In Cap Rate
        exit_cap_rate:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Stabilized exit / disposition cap rate as a decimal.
          title: Exit Cap Rate
        projected_roi:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Total projected return as a decimal (0.238 for 23.8%).
          title: Projected Roi
        projected_irr:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Projected IRR as a decimal (0.18 for 18%). Often present alongside ROI on value-add
            OMs.
          title: Projected Irr
        purchase_price_per_unit:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Purchase Price Per Unit
        total_cost_per_unit:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Total Cost Per Unit
        noi_per_unit:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Noi Per Unit
        source_pages:
          items:
            type: integer
          title: Source Pages
          type: array
        extraction_confidence:
          default: 0.0
          description: 0.0–1.0 — fraction of headline fields the LLM could populate. Computed post-extraction;
            default 0.0 so missing extractions read as low confidence.
          title: Extraction Confidence
          type: number
        is_stabilized_projection:
          default: true
          description: True for value-add / distressed deals where the pro forma is a stabilized projection
            rather than a trailing actual.
          title: Is Stabilized Projection
          type: boolean
      title: ProFormaSummary
      type: object
    ProFormaUnit:
      description: 'One row of the pro forma rent roll. Keep the schema close to the

        OM''s tabular shape — base rent + a list of reimbursements per tenant —

        rather than coercing into a single ''rent'' number. Buyers price each

        component differently.'
      properties:
        unit_number:
          description: Unit / suite identifier as it appears on the OM. Use the same string the in-place
            rent roll uses when the unit appears on both, so cross-validation can match rows.
          title: Unit Number
          type: string
        tenant_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Tenant name. Use 'New Tenant' or similar when the OM marks the row as a placeholder
            for assumed lease-up.
          title: Tenant Name
        square_feet:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Square Feet
        lease_status:
          description: Status of this row's lease. See LeaseStatus docstring for the five options. Infer
            from color bands when present, or from the tenant name + date pattern.
          enum:
          - in_place
          - signed_renewal
          - signed_new
          - assumed_new
          - expiring_holdover
          title: Lease Status
          type: string
        base_rent_annual:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Annual contractual base rent in dollars (excludes reimbursements).
          title: Base Rent Annual
        reimbursements:
          description: Per-line reimbursement amounts. The OM typically itemizes tax pass-throughs and
            CAM contributions as separate rows under each tenant — capture them as separate entries, not
            folded into base_rent_annual.
          items:
            $ref: '#/components/schemas/ReimbursementLine'
          title: Reimbursements
          type: array
        total_annual_revenue:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Total annual revenue for this row = base_rent_annual + sum of reimbursements. Compute
            it explicitly so the workbook can be reconciled without re-summing reimbursements.
          title: Total Annual Revenue
        lease_start:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: Lease commencement date in ISO YYYY-MM-DD.
          title: Lease Start
        lease_end:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: Lease expiration date in ISO YYYY-MM-DD.
          title: Lease End
        effective_date:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: When the pro-forma rent begins. For signed_renewal / signed_new, this is the lease
            commencement date; for assumed_new, it's the broker's assumed lease-up date.
          title: Effective Date
        notes:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Notes
        source_page:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: 1-indexed page number where this row appears in the source PDF.
          title: Source Page
      required:
      - unit_number
      - lease_status
      title: ProFormaUnit
      type: object
    PublicExtractionContext:
      description: 'A partial snapshot of extracted artifacts. Every field is optional;

        checks whose prerequisites aren''t supplied return `skipped` results.


        Field names mirror `services/validation/base.py::ExtractionContext`

        so a buyer who reads the internal source isn''t surprised by a

        different shape on the wire.'
      properties:
        deal_summary:
          anyOf:
          - $ref: '#/components/schemas/DealSummary'
          - type: 'null'
          default: null
        rent_roll:
          anyOf:
          - $ref: '#/components/schemas/RentRoll'
          - type: 'null'
          default: null
        pro_forma_rent_roll:
          anyOf:
          - $ref: '#/components/schemas/ProFormaRentRoll'
          - type: 'null'
          default: null
        pro_forma_summary:
          anyOf:
          - $ref: '#/components/schemas/ProFormaSummary'
          - type: 'null'
          default: null
        operating_statement:
          anyOf:
          - $ref: '#/components/schemas/OperatingStatement'
          - type: 'null'
          default: null
        t12:
          anyOf:
          - $ref: '#/components/schemas/T12'
          - type: 'null'
          default: null
        lease_abstracts:
          anyOf:
          - items:
              $ref: '#/components/schemas/LeaseAbstract'
            type: array
          - type: 'null'
          default: null
          title: Lease Abstracts
        document_type:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Document Type
        section_summary:
          anyOf:
          - type: object
          - type: 'null'
          default: null
          title: Section Summary
      title: PublicExtractionContext
      type: object
    ReimbursementLine:
      properties:
        reimbursement_type:
          description: 'What''s being reimbursed: ''Real Estate Tax Reimbursement'', ''CAM Contribution'',
            ''Operating Expense Pass-Through'', etc. Use the OM''s label verbatim.'
          title: Reimbursement Type
          type: string
        annual_amount:
          anyOf:
          - type: number
          - type: string
          description: Annual reimbursement amount in dollars.
          title: Annual Amount
      required:
      - reimbursement_type
      - annual_amount
      title: ReimbursementLine
      type: object
    RentRoll:
      properties:
        property_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Property Name
        source_property_label:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: 'The property name / address printed VERBATIM on the rent-roll''s own page header,
            captured before any canonicalization. Used by the cross_property_consistency check to detect
            when the rent roll belongs to a DIFFERENT building than the subject (the 120 Storms Ave defect:
            a 101-unit development OM with an unrelated 8-unit building''s rent roll stapled in, labeled
            ''904 Bergenline Ave''). Null when the page carries no property header.'
          title: Source Property Label
        as_of_date:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: Effective date of the rent roll
          title: As Of Date
        units:
          items:
            $ref: '#/components/schemas/RentRollUnit'
          title: Units
          type: array
        total_units:
          title: Total Units
          type: integer
        occupied_units:
          title: Occupied Units
          type: integer
        vacant_units:
          title: Vacant Units
          type: integer
        total_monthly_rent:
          anyOf:
          - type: number
          - type: string
          title: Total Monthly Rent
        average_rent:
          anyOf:
          - type: number
          - type: string
          title: Average Rent
        occupancy_rate:
          description: occupied_units / total_units, 0.0 to 1.0
          title: Occupancy Rate
          type: number
        total_banked_monthly:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Sum of banked_monthly across units. Null when no unit disclosed banked rent — distinguishes
            'building has no banked rent' from 'we summed across nothing'.
          title: Total Banked Monthly
        total_banked_annual:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Sum of banked_annual across units. Null when none disclosed.
          title: Total Banked Annual
        units_with_banked_rent:
          default: 0
          description: Count of units carrying any disclosed banked rent.
          title: Units With Banked Rent
          type: integer
        banked_rent_pct_of_gpi:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: total_banked_annual / (total_monthly_rent × 12). Null when either side is missing
            or zero. Reflects how much potential upside the building's banked rent represents as a share
            of current GPI — useful sizing signal for SFRB-constrained deals.
          title: Banked Rent Pct Of Gpi
        banked_rent_recovery_constrained:
          default: false
          description: True when the rent roll is in a jurisdiction with a per-unit annual recovery cap
            (e.g. SF Rent Board §1.21 caps banked rent recovery at 10% per unit per year). Surfaces the
            constraint so downstream models don't treat banked rent as immediately recoverable.
          title: Banked Rent Recovery Constrained
          type: boolean
        tenant_names_redacted_in_source:
          default: false
          description: True when the source explicitly states tenant names are withheld pre-NDA / CA execution
            ('Tenant names available upon CA execution'). Lets downstream consumers tell 'no tenant data'
            apart from 'tenant data redacted by design'.
          title: Tenant Names Redacted In Source
          type: boolean
        non_arms_length_leases_count:
          default: 0
          description: Count of units in this rent roll where the source footnote discloses a tenant relationship
            to ownership (acquaintance, family, employee, etc.). Aggregated by the pipeline from the per-unit
            non_arms_length flag.
          title: Non Arms Length Leases Count
          type: integer
        residential_units:
          default: 0
          description: Count of units in this rent roll classified as residential. Set by the post-extraction
            reclassification pass; defaults to 0 until the pass runs. On a pure-residential rent roll
            this equals total_units.
          title: Residential Units
          type: integer
        commercial_units:
          default: 0
          description: Count of units classified as commercial (retail / office / ground-floor tenancy
            on a mixed-use roll). Driven by the post-extraction commercial-classification heuristic so
            the Operating Statement can split residential vs commercial income downstream.
          title: Commercial Units
          type: integer
        commercial_income_pct_of_gpi:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Sum of commercial monthly_rent × 12 ÷ (sum of all unit monthly_rent × 12). Null
            when there are no commercial units or no in-place rent. Surfaces how much of the building's
            gross potential income comes from commercial tenancy.
          title: Commercial Income Pct Of Gpi
        rent_roll_completeness_score:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: 0.0–1.0 score reflecting how many of the 'standard' rent-roll fields (sf, beds,
            baths, lease_start, lease_end, monthly_rent, market_rent, security_deposit, tenant) are populated
            across every unit. Buyers use this to triage disclosure quality before committing to LOI.
            Null on synthesized rolls — the score is meaningless when the units themselves are derived.
          title: Rent Roll Completeness Score
        source:
          default: per_unit
          description: Where the per-row data came from. 'per_unit' is a real broker rent roll. 'synthesized'
            is one row per unit type aggregated from the OM's unit-mix table when no per-unit roll was
            given.
          enum:
          - per_unit
          - synthesized
          title: Source
          type: string
        synthesis_signals:
          description: 'When source=''synthesized'', the names of the heuristic signals that contributed:
            ''unit_mix_table'', ''kpi_callouts'', ''comp_subject_row''. Empty for ''per_unit''.'
          items:
            type: string
          title: Synthesis Signals
          type: array
      required:
      - units
      - total_units
      - occupied_units
      - vacant_units
      - total_monthly_rent
      - average_rent
      - occupancy_rate
      title: RentRoll
      type: object
    RentRollUnit:
      properties:
        unit_number:
          description: Unit number / suite identifier as it appears on the doc. For type-aggregated rolls
            (e.g. '1 Bed, 1 Bath × 37'), use the type label as the unit_number and set unit_count=37.
          title: Unit Number
          type: string
        unit_count:
          default: 1
          description: Number of units this row represents. 1 for unit-by-unit rolls (default). >1 only
            when the source aggregates multiple units of the same type into one row, in which case the
            rent values are averages.
          title: Unit Count
          type: integer
        tenant_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Tenant or lessee name. Use null for vacant or 'VACANT' rows or aggregated rows.
          title: Tenant Name
        square_feet:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Square Feet
        bedrooms:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Bedrooms
        bathrooms:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          title: Bathrooms
        lease_start:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          description: Lease start in ISO YYYY-MM-DD. Infer year from context if ambiguous.
          title: Lease Start
        lease_end:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Lease End
        monthly_rent:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Current monthly rent. Strip currency symbols/commas.
          title: Monthly Rent
        market_rent:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Market Rent
        security_deposit:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          title: Security Deposit
        is_occupied:
          default: true
          description: false if VACANT, no tenant, or zero rent
          title: Is Occupied
          type: boolean
        notes:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Notes
        banked_pct:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          description: Banked rent percentage as a decimal (0.115 for 11.5%). The share of allowable rent
            the landlord has not yet imposed. Null when the source doesn't disclose.
          title: Banked Pct
        banked_monthly:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Banked rent in $/month for this unit when disclosed.
          title: Banked Monthly
        banked_annual:
          anyOf:
          - type: number
          - type: string
          - type: 'null'
          default: null
          description: Banked rent in $/year for this unit. Compute as banked_monthly × 12 only when the
            source is silent on annual but explicit on monthly. Null when the source discloses neither.
          title: Banked Annual
        unit_designation:
          anyOf:
          - enum:
            - resident_manager
            - model
            - office
            - down
            - common_area
            - renovation
            - commercial
            - address_unit
            - garage
            - parking
            - storage
            - laundry_room
            type: string
          - type: 'null'
          default: null
          description: Special designation explaining a footnote symbol on the unit label (e.g. 'Unit
            121*' marked as the resident manager's unit). Null when the unit has no special designation.
            Drives the unit_label_sanity check's footnote-aware suppression. 'commercial' covers retail
            / office tenancy on a mixed-use rent roll; 'address_unit' covers corner-property units that
            carry their own street number (e.g. '139 8th' on a building addressed 135 8th). 'garage' /
            'parking' / 'storage' / 'laundry_room' mark non-residential income lines that must be excluded
            from the residential unit count (CLAUDE.md VIII.8) — their rent flows to Ancillary Income.
          title: Unit Designation
        commercial_address_distinct:
          default: false
          description: True when the unit_number is itself a street-address-style identifier separate
            from the building's primary address ('139 8th' on a 135 8th Street building). Suppresses the
            OCR-collision false-positive on the unit_label_sanity check.
          title: Commercial Address Distinct
          type: boolean
        non_arms_length:
          default: false
          description: True when a source footnote describes the tenant as related to ownership in a way
            that makes the lease non-arm's-length (e.g. 'Acquaintance of the owner', employee, family,
            related party). Tenant rent on these units may not reflect market and should be discounted
            in underwriting.
          title: Non Arms Length
          type: boolean
        non_arms_length_source_text:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          description: Verbatim source footnote that triggered non_arms_length. Surfaced in validation
            messaging so a reviewer can see the OM's exact wording without flipping to source.
          title: Non Arms Length Source Text
        source_page:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          description: 1-indexed page number where this unit appears in the source PDF
          title: Source Page
      required:
      - unit_number
      title: RentRollUnit
      type: object
    T12:
      properties:
        property_name:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Property Name
        period_start:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Period Start
        period_end:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          default: null
          title: Period End
        line_items:
          items:
            $ref: '#/components/schemas/T12LineItem'
          title: Line Items
          type: array
        total_income:
          anyOf:
          - type: number
          - type: string
          title: Total Income
        total_operating_expenses:
          anyOf:
          - type: number
          - type: string
          title: Total Operating Expenses
        noi:
          anyOf:
          - type: number
          - type: string
          title: Noi
        expense_ratio:
          title: Expense Ratio
          type: number
      required:
      - line_items
      - total_income
      - total_operating_expenses
      - noi
      - expense_ratio
      title: T12
      type: object
    T12LineItem:
      properties:
        category:
          enum:
          - income
          - operating_expense
          - capex
          title: Category
          type: string
        gl_account:
          title: Gl Account
          type: string
        raw_label:
          title: Raw Label
          type: string
        monthly_values:
          items:
            anyOf:
            - type: number
            - type: string
            - type: 'null'
          title: Monthly Values
          type: array
        total:
          anyOf:
          - type: number
          - type: string
          title: Total
        confidence:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          title: Confidence
        needs_review:
          default: false
          title: Needs Review
          type: boolean
      required:
      - category
      - gl_account
      - raw_label
      - monthly_values
      - total
      title: T12LineItem
      type: object
  securitySchemes:
    HTTPBearer:
      type: http
      scheme: bearer
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Issued out-of-band per integration partner. See docs/monetization/validation-api-spec.md
        §2.
servers:
- url: https://api.rentrolliq.com
  description: Production
security:
- ApiKeyAuth: []
