openapi: 3.1.0

# The Brain Memory API — the one contract the local service (`memory_api.serve`) publishes and the
# Mac app's generated Swift client (packages/brain-memory-api) calls: the Brain lifecycle plus the
# product reads (Search, Inspect, Answer, Journal, People, Quality, Gold inspect). The private Mac
# source admissions are deliberately outside this document.

info:
  title: Brain Memory API
  version: 0.3.0
  summary: Sourced history for changing facts — cited or honestly absent.
  description: |
    Read your Brain: Search and Inspect cited evidence, ask for an evidence-backed Answer, browse
    the Journal and People, and check Foundation Quality. Every returned assertion carries
    provenance to an exact source version. A miss means no eligible result was found — never that
    the fact is false.

servers:
  - url: http://localhost:8000/v1
    description: Brain Local — the loopback Memory API service.

security:
  - apiKey: []

tags:
  - name: brains
  - name: context

paths:
  /native/read-enrollment:
    post:
      operationId: enrollNativeProductRead
      summary: Enroll native product reads after explicit owner authority
      description: |
        Version 1 is available only on the protected owner Unix listener with exact configured
        Serve owner identity; ordinary TCP always refuses v1. Version 2 local_owner_approval
        mode permits bounded anonymous registration and returns 202 until the Mac owner approves
        the exact request fingerprint, policy hash and immutable expiry through private local
        files. Identity headers and matching codes alone never authorize issuance. Both modes
        use the same issuer, durable once-only reservation and native-only encrypted reply.
        No requester selects a Brain, project or scope. Ordinary route authentication is unchanged.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [version, operation_id, public_key, challenge]
              properties:
                version: {type: integer, enum: [1, 2]}
                operation_id: {type: string, format: uuid}
                public_key: {type: string, description: Canonical standard-base64 uncompressed P256 X9.63 65 bytes}
                challenge: {type: string, description: Canonical standard-base64 random 32 bytes}
      responses:
        '200':
          description: Sealed native-only credential; exact same operation retrieves identical ciphertext
          headers:
            Cache-Control: {schema: {type: string, const: no-store}}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [version, aad, salt, nonce, ciphertext]
                properties:
                  version: {type: integer, enum: [1, 2]}
                  aad: {type: string}
                  salt: {type: string}
                  nonce: {type: string}
                  ciphertext: {type: string}
        '202':
          description: Awaiting exact local Mac owner approval; never a product-read grant
          headers:
            Cache-Control: {schema: {type: string, const: no-store}}
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [version, state, operation_id, request_fingerprint, matching_code, created_at, expires_at, retry_after_seconds]
                properties:
                  version: {type: integer, const: 2}
                  state: {type: string, const: waiting_mac_approval}
                  operation_id: {type: string, format: uuid}
                  request_fingerprint: {type: string, pattern: '^[0-9a-f]{64}$'}
                  matching_code: {type: string, pattern: '^[0-9]{6}$'}
                  created_at: {type: string, format: date-time}
                  expires_at: {type: string, format: date-time}
                  retry_after_seconds: {type: integer, const: 2}
        default: {$ref: '#/components/responses/Error'}

  /brains:
    post:
      tags: [brains]
      operationId: createBrain
      summary: Create a Brain
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BrainCreate' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Brain' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [brains]
      operationId: getBrain
      summary: Inspect a Brain
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Brain' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [brains]
      operationId: forgetBrain
      summary: Delete a Brain and everything derived from it
      responses:
        '200':
          description: Deleted, with a receipt
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletionReceipt' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/search:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainSearch
      summary: Find cited source evidence through the existing Brain retrieval view
      description: |
        A view over the Brain's existing cited retrieval substrate, not a sixth resource or a
        second search system. Every returned result names both its immutable evidence chunk and a
        supporting source event; a candidate whose custody lineage cannot be named is withheld.

        This is deliberately a bounded, pre-answer evidence read. It neither writes data nor
        synthesizes an answer. It is a compatibility view over existing retrieval, not the future
        deterministic Foundation gold layer. Use the citation target to inspect the source-backed
        result. The caller must present a scoped Bearer credential issued for this exact Brain;
        changing the path cannot select another Brain. Legacy user/device headers are not
        product-read authorization. The only accepted query shape is all currently eligible
        evidence; source and time filtering wait for deterministic Foundation coverage.
      parameters:
        - name: q
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, minLength: 1 }
        - name: limit
          in: query
          x-brain-view-param: true
          schema: { type: integer, default: 8, minimum: 1, maximum: 50 }
      responses:
        '200':
          description: Cited evidence, or an explicit empty result set
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductSearchResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/search/inspect:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainSearchInspect
      summary: Inspect the cited source lineage for one search result
      description: |
        A view over the same existing retrieval substrate as `getBrainSearch`. It resolves the
        cited search chunk under the caller's credential-scoped Brain and returns its supporting
        source-event ids. A missing chunk is absent; a chunk without source-event lineage is a
        conflict, never a fabricated detail response. The scoped Bearer credential must be issued
        for the exact path Brain; legacy user/device headers are not product-read authorization.
      parameters:
        - name: target_kind
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, const: search_chunk }
        - name: target_id
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, minLength: 1 }
      responses:
        '200':
          description: Source-backed evidence detail
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductSearchInspectResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/answer:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainAnswer
      summary: Ask for a grounded answer or an honest abstention
      description: |
        A view over the existing Brain answer spine. The route imports the one route → retrieve →
        synthesize → judge implementation; it does not create a second answer engine, retrieval
        system, model policy, or cache. A served answer includes only citations that resolve through
        the same Search Inspect lineage view. If the answer engine abstains, fails, or loses
        inspectable source-event lineage, the result is an explicit abstention with no citations.

        When the deployment has no generation route (generation not activated, or the model
        transport is off), the engine is never invoked and the result is evidence only:
        `mode: evidence_only`, `abstain_reason: generation_unavailable`, no citations, and up to
        five `evidence` items. They come from the same retrieval as `getBrainSearch`, and each is
        served only while the Search Inspect view still resolves it with lineage. Nothing is
        synthesized or judged; `answer_text` is a fixed status sentence. A response without `mode`
        came from the answer engine.

        When the local generation lane is on (off by default; not served until its board passes an
        independent checker), the same inspected evidence is written by the pinned local model:
        `mode: local_generation`. Every served sentence quotes its cited evidence and carries a
        citation that resolves through Search Inspect; otherwise the result is an abstention with
        reason `no_evidence`. No judge runs, so `faithful` is null. `evidence` carries the items the
        model was shown. If the local model cannot serve, the result is the evidence-only one.

        The scoped Bearer credential must be issued for the exact path Brain. The query is the only
        public input: retrieval, generation, and judge budgets remain server policy rather than
        caller-tunable knobs.
      parameters:
        - name: q
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, minLength: 1 }
      responses:
        '200':
          description: A cited grounded answer, a typed honest abstention, or cited evidence only
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductAnswerResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/quality:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    head:
      tags: [context]
      operationId: checkBrainReadAccess
      summary: Verify permission to read this Brain without computing its quality report
      description: |
        Requires the same ordinary Bearer credential, context:read scope, exact Brain binding
        and tenant membership as the aggregate quality read. Returns no body and performs no
        warehouse aggregation or model call. Success proves read authorization only; it does
        not establish source coverage, quality, data population or device installation.
      responses:
        '200':
          description: Exact-Brain context-read authorization verified; no response body
          headers:
            Cache-Control:
              schema: { type: string, const: no-store }
        default:
          description: Read authorization refused; HEAD responses carry no body
    get:
      tags: [context]
      operationId: getBrainQuality
      summary: Read aggregate Foundation quality for this Brain's installed source cohort
      description: |
        A zero-generation view over the canonical Foundation quality board. It reports aggregate
        custody, deterministic typing, Gold lineage, Context-unit coverage, freshness, settlement,
        timestamp-lifecycle gaps, and—when the downstream-AI receipt schema is installed—aggregate
        AI run/output validation and spend custody. It returns no source or generated content,
        prompts, structured model payloads, evidence hashes, fingerprints, or record, version,
        candidate, run, output, outbox, connection, credential, or device identifiers. The cohort
        is server-owned; callers cannot hide a weak source with a query parameter. The scoped
        Bearer credential must be issued for this exact Brain. Reading this route never calls a
        model or changes activation.
      responses:
        '200':
          description: One stable, aggregate-only Foundation quality receipt
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FoundationQualityResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/journal:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainJournal
      summary: List deterministic Context-unit Journal entries for a bounded local-date range
      description: |
        A zero-generation, keyset-paginated projection of existing deterministic Context units.
        Entry text is the exact bounded unit text, local dates use the effective tenant time-zone
        authority, and each source link names the immutable Foundation version at its exact unit
        ordinal. Day completeness remains `unknown` until separately proven by source receipts.
        Optional start_at and end_at_exclusive must be paired, timezone-aware and at most ten
        days apart. They intersect the local-date range. Whole entries with unknown or
        out-of-window source occurrence times are excluded; text and evidence are never clipped.
        Day counts and cursor membership respect this filter. Omission preserves lifetime reads.
      parameters:
        - name: start_at
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Inclusive UTC-window start; requires end_at_exclusive.
        - name: end_at_exclusive
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Exclusive UTC-window end; requires start_at.
        - name: start_date
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, format: date }
        - name: end_date
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, format: date }
        - name: limit
          in: query
          x-brain-view-param: true
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
        - name: cursor
          in: query
          x-brain-view-param: true
          schema: { type: string, minLength: 1, maxLength: 1024 }
          description: Opaque cursor bound to the requested date range.
        - name: released_only
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
          description: Select only entries with a current explicitly released source-moment reading before pagination.
        - name: include_conversation_shape
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
          description: Opt into optional exact-parent observed conversation kind and source-label citation. Omission or false preserves the original conversation_context fields.
        - name: include_insights
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
          description: Include complete current saved-message aggregates before pagination for exactly 10 or 30 inclusive calendar dates. Cannot be combined with a UTC read_window. No real-life coverage or relationship inference.
      responses:
        '200':
          description: Literal unit-backed Journal entries and an optional next-page cursor
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/journal/entries/{entry_id}:
    parameters:
      - { $ref: '#/components/parameters/BrainId' }
      - name: entry_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [context]
      operationId: getBrainJournalEntry
      summary: Inspect one deterministic Journal entry and its exact source links
      parameters:
        - name: start_at
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Optional inclusive whole-evidence window start; requires end_at_exclusive.
        - name: end_at_exclusive
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Optional exclusive window end; paired window is at most ten days.
        - name: include_conversation_shape
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
          description: Opt into optional exact-parent observed conversation kind and source-label citation. Omission or false preserves the original conversation_context fields.
        - name: require_current_context
          in: query
          schema: { type: boolean, default: false }
          description: Require a freshly proved complete current message-window Context and Journal identity. Refuses stale, held or non-message entries; preserves the UTC read window.
        - name: include_conversation
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
        - name: conversation_cursor
          in: query
          x-brain-view-param: true
          schema: { type: string, minLength: 1, maxLength: 1024 }
        - name: include_browser_session
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
        - name: browser_cursor
          in: query
          x-brain-view-param: true
          schema: { type: string, minLength: 1, maxLength: 1024 }
      responses:
        '200':
          description: One literal Context-unit Journal entry
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntry' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/people:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainPeople
      summary: Browse canonical people by recent observation or bounded daily relevance
      description: |
        The same corpus every other read addresses by time, addressed by canonical person. A
        directory row is deliberately thin — who, and when they were last observed — because the
        full person page costs six rollup branches and a list row needs one aggregate. Supplying the
        paired UTC bounds returns only people with exact supported activity in that interval,
        ordered by daily message, call, and shared-calendar touchpoints. Raw volume and AI scores
        never control the order.

        Optional `q` filters canonical display names and currently valid identifier values using
        literal case-insensitive substring matching before pagination. Leading and trailing
        whitespace is ignored; an omitted or blank query keeps the unfiltered directory. Punctuation
        such as `%` and `_` is literal. Matching does not create or merge identities.

        Assistants are excluded by default; `include_assistants` asks for them. `person_count` and
        `assistant_count` remain the authorized tenant's lifetime census, unaffected by the query,
        window, or assistant filter. `result_count` counts only this page; a non-null `next_cursor`
        signals another page. Stamps are exact UTC; elapsed time is the client's to render because
        only the client knows the tenant time zone.
      parameters:
        - name: q
          in: query
          x-brain-view-param: true
          schema: { type: string, maxLength: 200 }
          description: |
            Optional literal name or current identifier filter; at most 200 Unicode code points.
            NUL characters return people_query_invalid (400). Excessive HTTP parameter length
            returns invalid_request_body (400). Remove NUL or shorten q before retrying.
        - name: start_at
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Optional inclusive UTC relevance bound; requires end_at_exclusive.
        - name: end_at_exclusive
          in: query
          x-brain-view-param: true
          schema: { type: string, format: date-time }
          description: Optional exclusive UTC relevance bound; paired interval is at most 240 hours.
        - name: limit
          in: query
          x-brain-view-param: true
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
        - name: cursor
          in: query
          x-brain-view-param: true
          schema: { type: string, minLength: 1, maxLength: 1024 }
          description: |
            Opaque keyset cursor bound to the selected ordering and exact UTC window. Filtered
            cursors also bind the trimmed query (including casing), tenant, and assistant setting;
            changing those or switching filtered/unfiltered mode returns people_cursor_invalid.
            Start a fresh page when the filter changes. Legacy unfiltered cursors remain supported.
        - name: include_assistants
          in: query
          x-brain-view-param: true
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Canonical person index and an optional next-page cursor
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PeopleResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/people/{person_id}:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainPerson
      summary: Read one canonical person with observed detail and interaction stamps
      description: |
        Identity, observed address-book detail, and exact first/last authored message and resolved
        call stamps for one person, assembled from deterministic projections with zero generation.

        `details` is null when a person has no address-book card, which is normal rather than an
        error: message and call handles resolve people the contacts app never named. Image entries
        carry presence and digest only — the native reader never transmits image bytes. Birthdays
        and custom dates stay raw source numbers, because normalizing a calendar date needs a
        time-zone authority and would put a guess inside custody.
      parameters:
        - name: person_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: One canonical person page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PersonResponse' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/gold/inspect:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: getBrainGoldInspect
      summary: Inspect one deterministic gold row and its source-version lineage
      description: |
        Resolves an activity, object, relationship, or current source version directly from the
        pre-AI Foundation views.
        Every successful response names immutable source-record/version evidence. A missing row is
        absent; lineage is never manufactured from a title or a similar name.
      parameters:
        - name: target_kind
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, enum: [activity, object, relationship, source_version] }
        - name: target_id
          in: query
          required: true
          x-brain-view-param: true
          schema: { type: string, format: uuid }
        - name: start_at
          in: query
          x-brain-view-param: true
          description: Paired UTC start; source_version only. Suppresses nearby-message reads.
          schema: { type: string, format: date-time }
        - name: end_at_exclusive
          in: query
          x-brain-view-param: true
          description: Paired exclusive UTC end, at most ten days after start_at.
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: One Foundation-backed deterministic row and its lineage
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreAIGoldInspectResponse' }
        default: { $ref: '#/components/responses/Error' }

components:

  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: '`sk_live_…` or `sk_test_…`. Hashed at rest; test mode never touches live data.'

  headers:

  parameters:
    BrainId:
      name: brain_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Slug' }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Plumbing, not a knob. Replays return the original response body.
      schema: { type: string }

  responses:
    Error:
      description: Typed, stable, documented
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    Identifier:
      type: string
      minLength: 1
      maxLength: 200
      pattern: '^[A-Za-z0-9._:-]+(?![\s\S])'

    # ─── THE RESOURCE ──────────────────────────────────────────────────────────
    # x-brain-resource marks a top-level product noun. A Brain is the only one.

    Brain:
      x-brain-resource: true
      type: object
      description: One durable, isolated memory boundary.
      required: [id, object, self, created_at]
      properties:
        id: { type: string }
        object: { type: string, const: brain }
        name: { type: string }
        self:
          $ref: '#/components/schemas/SelfEntity'
        created_at: { type: string, format: date-time }

    # ─── VIEWS: responses that are NOT resources ───────────────────────────────
    # A view is a projection of data that already exists, so it carries no x-brain-resource and
    # has no write path.

    FoundationQualityResponse:
      type: object
      description: A redacted product projection of one repeatable-read Foundation quality board.
      additionalProperties: false
      required: [schema_version, brain_id, evaluated_at, status, ready, source_count,
                 ready_source_count, current_record_count, current_version_count,
                 typed_current_version_count, gold_row_count, input_unit_count, sources,
                 settlement, time_zone_authority, ai_input_lifecycle, blocked_reason_counts,
                 model_calls, embedding_calls]
      properties:
        schema_version: { type: string, const: brain-record-quality-v1 }
        brain_id: { $ref: '#/components/schemas/Slug' }
        evaluated_at: { type: string, format: date-time }
        status: { type: string, enum: [ready, attention] }
        ready: { type: boolean }
        source_count: { type: integer, minimum: 0 }
        ready_source_count: { type: integer, minimum: 0 }
        current_record_count: { type: integer, minimum: 0 }
        current_version_count: { type: integer, minimum: 0 }
        typed_current_version_count: { type: integer, minimum: 0 }
        gold_row_count: { type: integer, minimum: 0 }
        input_unit_count: { type: integer, minimum: 0 }
        sources:
          type: array
          items: { $ref: '#/components/schemas/FoundationQualitySource' }
        settlement: { $ref: '#/components/schemas/FoundationQualitySettlement' }
        time_zone_authority: { $ref: '#/components/schemas/FoundationQualityTimeZoneAuthority' }
        ai_input_lifecycle: { $ref: '#/components/schemas/FoundationQualityInputLifecycle' }
        progress:
          type: [object, 'null']
          description: |
            Optional content-free progress over the existing Search, interpretation, and exact
            release authorities. Null means the additive read dependencies are not installed yet.
            It contains counts only: no chunk, input-unit, source-record, invocation, or person
            identity.
          additionalProperties: false
          required: [semantic_coverage, interpretation]
          properties:
            semantic_coverage: { $ref: '#/components/schemas/FoundationSemanticCoverage' }
            interpretation: { $ref: '#/components/schemas/FoundationInterpretationProgress' }
        serving:
          type: object
          description: |
            Optional configuration of this API's existing product-read gates. These flags report
            whether a lane is enabled by policy; they do not verify model availability, prove
            semantic quality, or grant source access. Missing means unknown on an older server.
          additionalProperties: false
          required: [semantic_query_enabled, answer_generation_enabled, accepted_memory_read_enabled]
          properties:
            semantic_query_enabled: { type: boolean }
            answer_generation_enabled: { type: boolean }
            accepted_memory_read_enabled: { type: boolean }
        ai_outputs:
          type: [object, 'null']
          description: |
            Optional additive downstream-AI receipt. `null` means the receipt schema is not yet
            installed on that server; `activation: inactive` means it is installed but no run has
            been observed. Counts and accounting only—never generated content or private identity.
          additionalProperties: false
          required: [contract_version, activation, status, totals, latest_receipt,
                     latest_invocation]
          properties:
            contract_version: { type: string, const: ai-quality-read-v2 }
            activation:
              type: string
              enum: [inactive, shadow_observed, materialize_observed]
            status:
              type: string
              enum: [idle, completed, receipt_pending, failed, ambiguous, reserved, call_started,
                     failed_pre_call, failed_after_call]
            totals:
              type: object
              additionalProperties: false
              required: [receipts, runs, outputs, evidence_rows, candidates, accepted,
                         rejected, insufficient, failed, ambiguous, cached, model_calls,
                         input_tokens, output_tokens, unpriced_runs, cost_microusd]
              properties:
                receipts: { type: integer, minimum: 0 }
                runs: { type: integer, minimum: 0 }
                outputs: { type: integer, minimum: 0 }
                evidence_rows: { type: integer, minimum: 0 }
                candidates: { type: integer, minimum: 0 }
                accepted: { type: integer, minimum: 0 }
                rejected: { type: integer, minimum: 0 }
                insufficient: { type: integer, minimum: 0 }
                failed: { type: integer, minimum: 0 }
                ambiguous: { type: integer, minimum: 0 }
                cached: { type: integer, minimum: 0 }
                model_calls: { type: integer, minimum: 0 }
                input_tokens: { type: integer, minimum: 0 }
                output_tokens: { type: integer, minimum: 0 }
                unpriced_runs: { type: integer, minimum: 0 }
                cost_microusd: { type: [integer, 'null'], minimum: 0 }
            latest_receipt:
              type: [object, 'null']
              additionalProperties: false
              required: [status, input_units, candidates, accepted, rejected, insufficient,
                         failed, ambiguous, cached, model_calls, input_tokens, output_tokens,
                         pricing_state, cost_microusd, latency_ms, error_code, started_at,
                         completed_at]
              properties:
                status: { type: string, enum: [completed, failed, ambiguous] }
                input_units: { type: integer, minimum: 0, maximum: 1200 }
                candidates: { type: integer, minimum: 0, maximum: 50 }
                accepted: { type: integer, minimum: 0 }
                rejected: { type: integer, minimum: 0 }
                insufficient: { type: integer, minimum: 0 }
                failed: { type: integer, minimum: 0 }
                ambiguous: { type: integer, minimum: 0 }
                cached: { type: integer, minimum: 0 }
                model_calls: { type: integer, minimum: 0 }
                input_tokens: { type: integer, minimum: 0 }
                output_tokens: { type: integer, minimum: 0 }
                pricing_state: { type: string, enum: [priced, unpriced] }
                cost_microusd: { type: [integer, 'null'], minimum: 0 }
                latency_ms: { type: integer, minimum: 0 }
                error_code:
                  type: [string, 'null']
                  enum: [candidate_call_ambiguous, candidate_generation_failed,
                         unknown_ai_error, null]
                started_at: { type: string, format: date-time }
                completed_at: { type: string, format: date-time }
            latest_invocation:
              type: [object, 'null']
              additionalProperties: false
              required: [state, updated_at]
              properties:
                state:
                  type: string
                  enum: [reserved, call_started, completed, failed_pre_call,
                         failed_after_call, ambiguous]
                updated_at: { type: string, format: date-time }
        blocked_reason_counts:
          type: object
          additionalProperties: { type: integer, minimum: 0 }
        model_calls: { type: integer, const: 0 }
        embedding_calls: { type: integer, const: 0 }

    FoundationSemanticCoverage:
      type: object
      additionalProperties: false
      required: [retrievable_chunk_count, active_embedding_chunk_count,
                 missing_embedding_chunk_count]
      properties:
        retrievable_chunk_count:
          type: integer
          minimum: 0
          description: Chunks with non-empty semantic text passing current source and release authority.
        active_embedding_chunk_count:
          type: integer
          minimum: 0
          description: Those chunks with a current active vector matching the selected read space, model, dimension and content version.
        missing_embedding_chunk_count:
          type: integer
          minimum: 0
          description: Eligible semantic chunks missing a current active vector in the selected read space.

    FoundationInterpretationProgress:
      type: object
      additionalProperties: false
      required: [eligible_unit_count, settled_unit_count, remaining_upper_bound_count,
                 active_unit_count, attention_unit_count, authorized_unit_count,
                 authorization_held_unit_count, authorized_remaining_unit_count,
                 authorized_active_unit_count, authorized_attention_unit_count,
                 released_unit_count, released_moment_count]
      properties:
        eligible_unit_count:
          type: integer
          minimum: 0
          description: Current deterministic R1/R2 units from supported interpretation sources.
        settled_unit_count:
          type: integer
          minimum: 0
          description: Current unit IDs whose latest observed invocation is terminal.
        remaining_upper_bound_count:
          type: integer
          minimum: 0
          description: |
            Eligible units without observed terminal settlement. This is an upper bound because
            later request, model, prompt, schema, authorization, or content changes may create new
            work for a previously terminal unit.
        active_unit_count:
          type: integer
          minimum: 0
          description: Current unit IDs whose latest observed invocation is reserved or in flight.
        attention_unit_count:
          type: integer
          minimum: 0
          description: Current unit IDs whose latest observed terminal invocation failed or is ambiguous.
        authorized_unit_count:
          type: integer
          minimum: 0
          description: Structurally eligible units whose complete current lineage has exact comprehension authority.
        authorization_held_unit_count:
          type: integer
          minimum: 0
          description: Structurally eligible units held because exact current comprehension authority is absent.
        authorized_remaining_unit_count:
          type: integer
          minimum: 0
          description: Authorized units without an observed terminal invocation.
        authorized_active_unit_count:
          type: integer
          minimum: 0
          description: Authorized units whose latest invocation is reserved or in flight.
        authorized_attention_unit_count:
          type: integer
          minimum: 0
          description: Authorized units whose latest terminal invocation failed or is ambiguous.
        released_unit_count:
          type: integer
          minimum: 0
          description: Current unit IDs with at least one source moment visible through the release authority.
        released_moment_count:
          type: integer
          minimum: 0
          description: Source moments currently visible through the exact release authority.

    FoundationQualitySource:
      type: object
      additionalProperties: false
      required: [source, status, ready, current_record_count, current_version_count,
                 typed_current_version_count, gold_row_count, input_unit_count,
                 missing_typed_current_version_count, missing_gold_lineage_count,
                 missing_input_lineage_count, freshness_status, freshness_age_seconds,
                 freshness_max_lag_seconds, latest_captured_at, blocked_reasons]
      properties:
        source: { $ref: '#/components/schemas/Identifier' }
        status: { type: string, enum: [ready, attention] }
        ready: { type: boolean }
        current_record_count: { type: integer, minimum: 0 }
        current_version_count: { type: integer, minimum: 0 }
        typed_current_version_count: { type: integer, minimum: 0 }
        gold_row_count: { type: integer, minimum: 0 }
        input_unit_count: { type: integer, minimum: 0 }
        missing_typed_current_version_count: { type: integer, minimum: 0 }
        missing_gold_lineage_count: { type: integer, minimum: 0 }
        missing_input_lineage_count: { type: integer, minimum: 0 }
        freshness_status: { type: string, enum: [fresh, stale, missing, invalid, future] }
        freshness_age_seconds: { type: [integer, 'null'] }
        freshness_max_lag_seconds: { type: integer, minimum: 0 }
        latest_captured_at: { type: [string, 'null'], format: date-time }
        blocked_reasons: { type: array, items: { type: string } }

    FoundationQualitySettlement:
      type: object
      additionalProperties: false
      required: [pending_or_claimed_outbox_count, failed_outbox_count,
                 acknowledged_stage_set_mismatch_count, unsettled_sweep_count]
      properties:
        pending_or_claimed_outbox_count: { type: integer, minimum: 0 }
        failed_outbox_count: { type: integer, minimum: 0 }
        acknowledged_stage_set_mismatch_count: { type: integer, minimum: 0 }
        unsettled_sweep_count: { type: integer, minimum: 0 }

    FoundationQualityTimeZoneAuthority:
      type: object
      additionalProperties: false
      required: [ready, day_unit_count, eligible_input_missing_day_unit_count,
                 eligible_input_missing_financial_day_unit_count, missing_day_units_by_source,
                 missing_financial_day_units_by_source, blocked_reasons]
      properties:
        ready: { type: boolean }
        day_unit_count: { type: integer, minimum: 0 }
        eligible_input_missing_day_unit_count: { type: integer, minimum: 0 }
        eligible_input_missing_financial_day_unit_count: { type: integer, minimum: 0 }
        missing_day_units_by_source:
          type: object
          additionalProperties: { type: integer, minimum: 0 }
        missing_financial_day_units_by_source:
          type: object
          additionalProperties: { type: integer, minimum: 0 }
        blocked_reasons: { type: array, items: { type: string } }

    FoundationQualityInputLifecycle:
      type: object
      additionalProperties: false
      required: [ready, coverage, blocked_reasons]
      properties:
        ready: { type: boolean }
        coverage:
          type: object
          additionalProperties: { type: integer, minimum: 0 }
        blocked_reasons: { type: array, items: { type: string } }

    ProductSearchResponse:
      type: object
      description: A bounded, cited evidence view over the existing retrieval substrate.
      additionalProperties: false
      required: [query, user_id, status, strategy, result_count, results]
      properties:
        query: { type: string }
        user_id: { type: string }
        status: { type: string, enum: [ok, empty] }
        strategy: { type: string, const: existing_retrieval_spine }
        result_count: { type: integer, minimum: 0 }
        results:
          type: array
          items: { $ref: '#/components/schemas/ProductSearchResult' }

    ProductSearchResult:
      type: object
      description: >
        One cited retrieval result. Missing source lineage is a reason to withhold a result, never
        to invent an id. Lineage is an event id OR a source-record-version id: sources compiled by
        the direct-version foundation path write no `events` row, and version lineage is the deeper
        root because an event is derived from a version. This matches PreAIGoldCitation,
        JournalSource and JournalCall, which already require source_record_version_id.
      additionalProperties: false
      required: [chunk_id, event_type, chunk_kind, title, snippet, body_text,
                 citation, metadata_json]
      properties:
        chunk_id: { type: string }
        source_event_id: { type: string }
        source_record_version_id: { type: string, format: uuid }
        source_event_ids: { type: array, items: { type: string } }
        source_record_version_ids: { type: array, items: { type: string } }
        source: { type: string }
        source_plane: { type: string, enum: [native, customer] }
        event_type: { type: string }
        occurred_at: { type: string, format: date-time }
        chunk_kind: { type: string }
        title: { type: string }
        subtitle: { type: string }
        snippet: { type: string }
        body_text: { type: string }
        body_projection:
          $ref: '#/components/schemas/SearchBodyProjection'
        citation: { $ref: '#/components/schemas/ProductSearchCitation' }
        metadata_json: { type: object, additionalProperties: true }

    ProductSearchCitation:
      type: object
      description: >
        The evidence pointer required to inspect a product-search result. Exactly one of event_id
        or source_record_version_id is present — whichever anchor the chunk actually carries. An
        absent key is honest; a null or a cross-labelled id would be a fabrication.
      additionalProperties: false
      required: [target_kind, target_id, target_label]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string }
        target_label: { type: string }
        event_id: { type: string }
        source_record_version_id: { type: string, format: uuid }
        source: { type: string }
        source_record_id: { type: string }
        thread_id: { type: string }
        person_id: { type: string }

    ProductSearchInspectResponse:
      type: object
      description: One source-backed search chunk resolved through its explicit lineage — source
        events, source-record versions, or both. At least one anchor is always present (the
        storage layer forbids an uncited chunk), so a version-only Foundation chunk resolves here
        instead of being rejected.
      additionalProperties: false
      required: [target_kind, target_id, title, summary, event_count, key_facts, source_event_ids,
                 source_record_version_ids, recent_events]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string }
        title: { type: string }
        subtitle: { type: string }
        summary: { type: string }
        body_projection:
          $ref: '#/components/schemas/SearchBodyProjection'
        source: { type: string }
        occurred_at: { type: string, format: date-time }
        event_count: { type: integer, minimum: 0 }
        key_facts: { type: array, items: {} }
        source_event_ids: { type: array, items: { type: string } }
        source_record_version_ids: { type: array, items: { type: string } }
        journal_context:
          $ref: '#/components/schemas/SearchJournalContext'
        message_anchors:
          $ref: '#/components/schemas/SourceMessageAnchors'
        content_authority: { type: string, enum: [source_evidence, accepted_current_memory] }
        recent_events: { type: array, items: { type: object, additionalProperties: true } }

    SearchBodyProjection:
      type: object
      additionalProperties: false
      description: Exact code-point body span and UTF-8 hashes for this response's presentation text. This binds rendering only; source custody and current authority still require the cited lineage.
      required: [contract, offset_unit, body_start, body_end, body_sha256, rendered_sha256]
      properties:
        contract: { type: string, const: brain-search-body-projection-v1 }
        offset_unit: { type: string, const: unicode_code_points }
        body_start: { type: integer, minimum: 0 }
        body_end: { type: integer, minimum: 0 }
        body_sha256: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
        rendered_sha256: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }

    SearchJournalContext:
      type: object
      additionalProperties: false
      description: Navigation hint to one freshly validated saved conversation or browser session. A later reader must opt into require_current_context and recheck the complete parent lineage; this descriptor grants no continuing authority.
      required: [contract_version, entry_id, context_unit_id, source_record_version_ids]
      properties:
        contract_version: { type: string, const: search-journal-context-v1 }
        entry_id: { type: string, format: uuid }
        context_unit_id: { type: string, format: uuid }
        source_record_version_ids:
          type: array
          minItems: 1
          uniqueItems: true
          items: { type: string, format: uuid }

    SourceMessageAnchors:
      type: object
      additionalProperties: false
      description: Exact displayed message ranges in this chunk, never a claim about which
        message matched the query. Every target belongs to the complete parent lineage and is
        revalidated against current ordered facts. Absent for older or unmapped chunks.
      required: [contract_version, offset_unit, body_sha256, messages]
      properties:
        contract_version: { type: string, const: source-message-anchors-v1 }
        offset_unit: { type: string, const: unicode_code_points }
        body_sha256: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
        messages:
          type: array
          minItems: 1
          maxItems: 4096
          items:
            type: object
            additionalProperties: false
            required: [source_record_version_id, body_start, body_end]
            properties:
              source_record_version_id: { type: string, format: uuid }
              body_start: { type: integer, minimum: 0 }
              body_end: { type: integer, minimum: 1 }

    ProductAnswerResponse:
      type: object
      description: >
        The existing grounded answer verdict, exposed only with inspectable citations. With no
        generation route it is evidence only instead: `mode` is `evidence_only`, the status is an
        abstention with reason `generation_unavailable` and no citations, and the inspected source
        items are in `evidence`. With the local generation lane on, `mode` is `local_generation`:
        a quoted, cited answer or an abstention over the same `evidence`, with `faithful` null;
        its abstention reason is `no_evidence` when the model abstained, `citation_gate` when
        the citation gate refused the written answer, and `judge_no_answer` when the local judge
        found that the gated answer does not answer the question or that one of its sentences
        says something is missing.
        `mode` and `evidence` are absent on an answer-engine response.
      additionalProperties: false
      required: [query, user_id, status, strategy, answer_text, is_abstain, abstain_reason,
                 faithful, citations, ownership_claims, entities, latency_ms]
      properties:
        query: { type: string, minLength: 1 }
        user_id: { type: string, minLength: 1 }
        status: { type: string, enum: [answered, abstained] }
        strategy: { type: string, enum: [existing_grounded_answer_spine, existing_retrieval_spine] }
        mode: { type: string, enum: [evidence_only, local_generation] }
        answer_text: { type: string, minLength: 1 }
        is_abstain: { type: boolean }
        abstain_reason:
          type: [string, 'null']
          enum: [no_evidence, conflicting_evidence, expired_evidence, no_attribution, not_self_event,
                 generation_unavailable, citation_gate, judge_no_answer, null]
        faithful: { type: [boolean, 'null'] }
        citations:
          type: array
          items: { $ref: '#/components/schemas/ProductAnswerCitation' }
        ownership_claims:
          type: array
          items: { $ref: '#/components/schemas/ProductAnswerOwnershipClaim' }
        entities:
          type: array
          items: { $ref: '#/components/schemas/ProductAnswerEntity' }
        latency_ms: { type: [integer, 'null'], minimum: 0 }
        evidence:
          type: array
          maxItems: 5
          items: { $ref: '#/components/schemas/ProductAnswerEvidence' }

    ProductAnswerEvidence:
      type: object
      description: >
        One source item served when there is no generation route: a result of the same retrieval as
        getBrainSearch, re-resolved through the Search Inspect view. At least one lineage anchor is
        always present. It is what a source says, not a claim Brain makes.
      additionalProperties: false
      required: [target_kind, target_id, target_label, snippet, source_event_ids,
                 source_record_version_ids]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string, minLength: 1 }
        target_label: { type: string, minLength: 1 }
        snippet: { type: string }
        source_event_ids: { type: array, items: { type: string, minLength: 1 } }
        source_record_version_ids: { type: array, items: { type: string, minLength: 1 } }
        source: { type: string }
        subtitle: { type: string }
        occurred_at: { type: string, format: date-time }

    ProductAnswerCitation:
      type: object
      description: A cited answer target resolved through the existing Search Inspect view. Lineage
        is source events, source-record versions, or both; at least one anchor is always present.
      additionalProperties: false
      required: [target_kind, target_id, target_label, snippet, source_event_ids,
                 source_record_version_ids]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string, minLength: 1 }
        target_label: { type: string, minLength: 1 }
        snippet: { type: string }
        source_event_ids: { type: array, items: { type: string, minLength: 1 } }
        source_record_version_ids: { type: array, items: { type: string, minLength: 1 } }

    ProductAnswerOwnershipClaim:
      type: object
      additionalProperties: false
      required: [claim, owner, owner_named, grounded_chunk_id, grounded_chunk_kind,
                 grounded_owner_ref]
      properties:
        claim: { type: string }
        owner: { type: string, enum: [mine, not_mine, unknown] }
        owner_named: { type: string }
        grounded_chunk_id: { type: [string, 'null'] }
        grounded_chunk_kind: { type: [string, 'null'] }
        grounded_owner_ref: { type: [string, 'null'] }

    ProductAnswerEntity:
      type: object
      additionalProperties: false
      required: [name, id]
      properties:
        name: { type: string, minLength: 1 }
        id: { type: string, minLength: 1 }

    JournalSource:
      type: object
      additionalProperties: false
      required: [ordinal, source_kind, record_uuid, source_record_version_id, source_content_hash]
      properties:
        ordinal: { type: integer, minimum: 0 }
        source_kind: { type: string }
        record_uuid: { type: string, format: uuid }
        source_record_version_id: { type: string, format: uuid }
        source_content_hash: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }

    JournalMomentEntity:
      type: object
      additionalProperties: false
      required: [kind, text, source_record_version_ids]
      properties:
        kind:
          type: string
          enum: [person, place, organization, project, product, document, event, other]
        text: { type: string, minLength: 1, maxLength: 256, description: Limited to 256 UTF-8 bytes by the service. }
        source_record_version_ids:
          type: array
          minItems: 1
          maxItems: 16
          uniqueItems: true
          items: { type: string, format: uuid }

    JournalMomentAction:
      type: object
      additionalProperties: false
      required: [kind, text, source_record_version_ids]
      properties:
        kind: { type: string, enum: [decision, commitment, request, task, question, plan] }
        text: { type: string, minLength: 1, maxLength: 256, description: Limited to 256 UTF-8 bytes by the service. }
        source_record_version_ids:
          type: array
          minItems: 1
          maxItems: 16
          uniqueItems: true
          items: { type: string, format: uuid }

    JournalMomentTemporalMention:
      type: object
      additionalProperties: false
      required: [kind, text, source_record_version_ids]
      properties:
        kind: { type: string, enum: [date, time, deadline, range, recurrence] }
        text: { type: string, minLength: 1, maxLength: 256, description: Literal supported wording, limited to 256 UTF-8 bytes by the service. }
        source_record_version_ids:
          type: array
          minItems: 1
          maxItems: 16
          uniqueItems: true
          items: { type: string, format: uuid }

    JournalMomentDetails:
      type: [object, 'null']
      description: Optional released details. Every item references only evidence of its owning moment.
      additionalProperties: false
      required: [entities, actions, temporal_mentions]
      properties:
        entities:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/JournalMomentEntity' }
        actions:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/JournalMomentAction' }
        temporal_mentions:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/JournalMomentTemporalMention' }

    JournalSourceMoment:
      type: object
      description: An explicitly owner-released AI reading of one deterministic Journal unit.
      additionalProperties: false
      required: [moment_id, source_kind, title, summary, category, source_type, valid_from,
                 valid_to, duration_seconds, evidence_count, source_record_version_ids]
      properties:
        details: { $ref: '#/components/schemas/JournalMomentDetails' }
        placement_unit_id: { type: string, format: uuid }
        placement_marker: { type: [string, 'null'], enum: ['earlier context not interpreted', null] }
        moment_id: { type: string, format: uuid }
        source_kind: { type: string }
        title: { type: string, minLength: 1, maxLength: 160 }
        summary: { type: string, minLength: 1, maxLength: 1200 }
        category:
          type: string
          enum: [communication, work, planning, travel, health, finance, learning,
                 entertainment, shopping, home, social, creative, administrative, other]
        source_type: { type: string }
        valid_from: { type: [string, 'null'], format: date-time }
        valid_to: { type: [string, 'null'], format: date-time }
        duration_seconds: { type: [integer, 'null'], minimum: 0 }
        evidence_count: { type: integer, minimum: 1 }
        source_record_version_ids:
          type: array
          minItems: 1
          items: { type: string, format: uuid }

    JournalMessageMetadata:
      type: object
      additionalProperties: false
      required: [source_record_version_id, attachments, attachment_total, attachments_omitted, links, link_total, links_omitted]
      properties:
        source_record_version_id: { type: string, format: uuid }
        attachment_total: { type: integer, minimum: 0 }
        attachments_omitted: { type: integer, minimum: 0 }
        link_total: { type: integer, minimum: 0 }
        links_omitted: { type: integer, minimum: 0 }
        attachments:
          type: array
          maxItems: 8
          items:
            $ref: '#/components/schemas/JournalMessageAttachmentMetadata'
        links:
          type: array
          maxItems: 8
          items:
            $ref: '#/components/schemas/JournalMessageLinkMetadata'
    JournalMessageAttachmentMetadata:
      type: object
      additionalProperties: false
      required: [ordinal, kind, status, declared_bytes]
      properties:
        ordinal: { type: integer, minimum: 0 }
        kind: { type: string, enum: [image, audio, video, document, sticker, other] }
        status: { type: string, enum: [pending, available, unavailable, rejected, failed] }
        declared_bytes: { type: [integer, 'null'], minimum: 0 }
    JournalMessageLinkMetadata:
      type: object
      additionalProperties: false
      required: [ordinal, source_field, host]
      properties:
        ordinal: { type: integer, minimum: 0 }
        source_field: { type: string, enum: [resolved_text, subject, typed_body_run] }
        host: { type: string, maxLength: 255 }
    JournalConversationMessage:
      type: object
      additionalProperties: false
      required: [source_record_version_id, occurred_at, text, text_truncated, sender_label, direction, conversation_kind, attachment_count, availability]
      properties:
        source_record_version_id: { type: string, format: uuid }
        occurred_at: { type: [string, 'null'], format: date-time }
        text: { type: [string, 'null'] }
        text_truncated: { type: boolean }
        sender_label: { type: [string, 'null'] }
        direction: { type: [string, 'null'] }
        conversation_kind: { type: [string, 'null'] }
        attachment_count: { type: [integer, 'null'], minimum: 0 }
        metadata:
          $ref: '#/components/schemas/JournalMessageMetadata'
        availability: { type: string, enum: [available, unavailable] }
    JournalConversationPage:
      type: object
      additionalProperties: false
      required: [contract_version, entry_id, messages, next_cursor]
      properties:
        contract_version: { type: string, const: journal-conversation-v1 }
        entry_id: { type: string, format: uuid }
        read_window:
          $ref: '#/components/schemas/JournalReadWindow'
        messages:
          type: array
          maxItems: 40
          items: { $ref: '#/components/schemas/JournalConversationMessage' }
        next_cursor: { type: [string, 'null'], minLength: 1, maxLength: 1024 }

    JournalBrowserVisit:
      type: object
      additionalProperties: false
      required: [source_record_version_id, occurred_at, availability]
      properties:
        source_record_version_id: { type: string, format: uuid }
        occurred_at: { type: [string, 'null'], format: date-time }
        title: { type: [string, 'null'], maxLength: 1024 }
        domain: { type: [string, 'null'], maxLength: 253 }
        availability: { type: string, enum: [available, unavailable] }

    JournalBrowserSessionPage:
      type: object
      additionalProperties: false
      required: [contract_version, entry_id, visits]
      properties:
        contract_version: { type: string, const: journal-browser-session-v1 }
        entry_id: { type: string, format: uuid }
        read_window:
          $ref: '#/components/schemas/JournalReadWindow'
        visits:
          type: array
          minItems: 1
          maxItems: 40
          items: { $ref: '#/components/schemas/JournalBrowserVisit' }
        next_cursor: { type: [string, 'null'], minLength: 1, maxLength: 1024 }

    JournalEntry:
      type: object
      additionalProperties: false
      required: [entry_id, day_id, context_unit_id, entry_kind, local_date, sort_at, time_zone,
                 time_zone_authority_fingerprint, day_completeness, title, body_text, content_hash,
                 contract_version, end_at, sources, participants, calls]
      properties:
        conversation_context:
          $ref: '#/components/schemas/JournalConversationContext'
          description: Exact opaque retained conversation identity for a message window; omitted or null for every other entry kind.
        conversation:
          $ref: '#/components/schemas/JournalConversationPage'
        browser_session:
          $ref: '#/components/schemas/JournalBrowserSessionPage'

        read_window:
          $ref: '#/components/schemas/JournalReadWindow'
        entry_id: { type: string, format: uuid }
        day_id: { type: string, format: uuid }
        context_unit_id: { type: string, format: uuid }
        entry_kind:
          type: string
          enum: [message_window, note_section, call_session, browser_session, asset_envelope,
                 calendar_event, reminder, health_metric, health_workout, moment]
        local_date: { type: string, format: date }
        sort_at: { type: string, format: date-time }
        end_at: { type: [string, 'null'], format: date-time }
        time_zone: { type: string }
        time_zone_authority_fingerprint:
          type: string
          pattern: '^(sha256:[0-9a-f]{64}|md5:[0-9a-f]{32})$'
        day_completeness: { type: string, enum: [complete, partial, empty, unknown] }
        title: { type: string, minLength: 1, maxLength: 512 }
        body_text: { type: string, minLength: 1, maxLength: 65536 }
        content_hash: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
        contract_version: { type: string, const: journal-v1 }
        sources:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/JournalSource' }
        participants:
          type: array
          items: { $ref: '#/components/schemas/JournalParticipant' }
        calls:
          type: array
          items: { $ref: '#/components/schemas/JournalCall' }
        source_moments:
          type: array
          description: Zero or more exact owner-released readings; empty preserves literal Journal truth.
          items: { $ref: '#/components/schemas/JournalSourceMoment' }

    JournalConversationContext:
      type: object
      additionalProperties: false
      required: [conversation_key, source_kind, message_count, first_observed_at,
                 last_observed_at]
      properties:
        conversation_key:
          type: string
          pattern: '^md5:[0-9a-f]{32}$'
          description: Opaque stable key; the source thread identifier is never exposed.
        source_kind: { type: string, minLength: 1, maxLength: 128 }
        message_count: { type: integer, minimum: 1 }
        first_observed_at: { type: string, format: date-time }
        last_observed_at: { type: string, format: date-time }

        conversation_kind:
          type: string
          enum: [direct, group, unknown, conflicting]
          description: Observed flags across this complete current parent; no roster inference. Omitted when whole-parent shape is unavailable.
        source_title:
          type: [string, 'null']
          description: Latest nonblank source-authored label in this parent, with existing privacy redaction; never a participant-derived title.
        source_title_source_record_version_id:
          type: [string, 'null']
          format: uuid
          description: Exact immutable source version in this parent that supplied source_title; null when no clock-ranked source label is available.

    JournalParticipant:
      type: object
      additionalProperties: false
      required: [participant_key, person_id, display_name, opaque_source_identifier_key, resolution_state,
                 participant_evidence_kind, coverage_state, roster_completeness,
                 first_observed_at, last_observed_at, observed_message_count, evidence_count]
      properties:
        participant_key: { type: string }
        person_id: { type: [string, 'null'], format: uuid }
        display_name:
          type: [string, 'null']
          description: Canonical person label only after exact person resolution; null otherwise.
        opaque_source_identifier_key: { type: [string, 'null'] }
        resolution_state: { type: string, enum: [exact_person, unresolved, ambiguous] }
        participant_evidence_kind: { type: string, enum: [sender_observed, self_observed] }
        coverage_state: { type: string, const: observed_only }
        roster_completeness: { type: string, const: unknown }
        first_observed_at: { type: string, format: date-time }
        last_observed_at: { type: string, format: date-time }
        observed_message_count: { type: integer, minimum: 1 }
        evidence_count: { type: integer, minimum: 1 }

    JournalCall:
      type: object
      additionalProperties: false
      required: [call_fact_id, source_record_version_id, service, direction, started_at,
                 ended_at, duration_seconds, remote_handle, counterparty_kind]
      properties:
        call_fact_id: { type: string, format: uuid }
        source_record_version_id: { type: string, format: uuid }
        service: { type: string, enum: [phone, facetime_audio, facetime_video] }
        direction: { type: string, enum: [incoming, outgoing, missed, declined] }
        started_at: { type: string, format: date-time }
        ended_at: { type: [string, 'null'], format: date-time }
        duration_seconds: { type: [integer, 'null'], minimum: 0 }
        remote_handle: { type: [string, 'null'], maxLength: 512 }
        counterparty_kind: { type: string, const: observed_handle }

    JournalDay:
      type: object
      additionalProperties: false
      required: [day_id, local_date, time_zone, time_zone_authority_fingerprint,
                 completeness_state, entry_count, kind_counts, source_counts, first_sort_at,
                 last_sort_at, content_hash, contract_version]
      properties:
        day_id: { type: string, format: uuid }
        local_date: { type: string, format: date }
        time_zone: { type: string }
        time_zone_authority_fingerprint:
          type: string
          pattern: '^(sha256:[0-9a-f]{64}|md5:[0-9a-f]{32})$'
        completeness_state: { type: string, enum: [complete, partial, empty, unknown] }
        entry_count: { type: integer, minimum: 1 }
        kind_counts:
          type: array
          minItems: 1
          description: Exact zero-model counts for the effective day scope, ordered by count then kind.
          items: { $ref: '#/components/schemas/JournalKindCount' }
        source_counts:
          type: array
          minItems: 1
          description: Exact observed-source counts for the effective day scope; observation does not claim completeness.
          items: { $ref: '#/components/schemas/JournalSourceCount' }
        source_coverage:
          type: array
          description: Exact zero-model source/day coverage proof. Omitted only by older compatible servers.
          items: { $ref: '#/components/schemas/JournalSourceCoverage' }
        first_sort_at: { type: string, format: date-time }
        last_sort_at: { type: string, format: date-time }
        content_hash: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
        contract_version: { type: string, const: journal-v1 }

    JournalKindCount:
      type: object
      additionalProperties: false
      required: [entry_kind, entry_count, source_version_count]
      properties:
        entry_kind:
          type: string
          enum: [message_window, note_section, call_session, browser_session, asset_envelope,
                 calendar_event, reminder, health_metric, health_workout, moment]
        entry_count: { type: integer, minimum: 1 }
        source_version_count: { type: integer, minimum: 1 }

    JournalSourceCount:
      type: object
      additionalProperties: false
      required: [source_kind, entry_count, source_version_count]
      properties:
        source_kind: { type: string, minLength: 1, maxLength: 128 }
        entry_count: { type: integer, minimum: 1 }
        source_version_count: { type: integer, minimum: 1 }

    JournalSourceCoverage:
      type: object
      additionalProperties: false
      required: [source_kind, coverage_state, reason_code, observed_entry_count,
                 observed_source_version_count, connection_count,
                 authoritative_connection_count, proven_connection_count]
      properties:
        source_kind: { type: string, minLength: 1, maxLength: 128 }
        coverage_state: { type: string, enum: [complete, observed, unknown] }
        reason_code:
          type: string
          enum: [proven_complete, no_active_connection, authority_not_active,
                 connection_unavailable, permission_not_granted, no_sync_run,
                 sync_not_committed, rejected_records, decode_failures,
                 day_outside_selection, snapshot_incomplete, journal_unsettled,
                 coverage_unknown]
        observed_entry_count: { type: integer, minimum: 0 }
        observed_source_version_count: { type: integer, minimum: 0 }
        connection_count: { type: integer, minimum: 0 }
        authoritative_connection_count: { type: integer, minimum: 0 }
        proven_connection_count: { type: integer, minimum: 0 }

    PersonSummary:
      type: object
      additionalProperties: false
      required: [person_id, display_name, entity_type, is_self, is_assistant, first_observed_at,
                 last_observed_at, evidence_count]
      properties:
        first_seen_at: { type: [string, 'null'], format: date-time, description: Compatibility canonical identity creation stamp. }
        last_seen_at: { type: [string, 'null'], format: date-time, description: Compatibility canonical identity observation stamp. }
        person_id: { type: string, format: uuid }
        display_name: { type: string, maxLength: 512 }
        entity_type: { type: string, enum: [person, self, group, org] }
        is_self: { type: boolean }
        is_assistant: { type: boolean, description: Flagged, never hidden; a filtered row stays countable. }
        first_observed_at: { type: [string, 'null'], format: date-time }
        last_observed_at: { type: [string, 'null'], format: date-time }
        evidence_count: { type: integer, minimum: 0 }

    PeopleResponse:
      type: object
      additionalProperties: false
      required: [contract_version, status, result_count, person_count, assistant_count,
                 assistants_included, people, next_cursor]
      properties:
        contract_version: { type: string, const: people-v1 }
        status: { type: string, enum: [ok, empty] }
        result_count: { type: integer, minimum: 0, maximum: 200 }
        person_count: { type: integer, minimum: 0 }
        assistant_count: { type: integer, minimum: 0, description: How many rows the assistant filter is holding back. }
        assistants_included: { type: boolean }
        people:
          type: array
          maxItems: 200
          items: { $ref: '#/components/schemas/PersonSummary' }
        next_cursor: { type: [string, 'null'], minLength: 1, maxLength: 1024 }

    PersonIdentifier:
      type: object
      additionalProperties: true
      required: [attribute, value, normalized_value]
      properties:
        attribute: { type: string, enum: [email, phone, handle, address, social] }
        value: { type: string }
        normalized_value: { type: string, description: The join key; names are never join keys. }
        is_primary: { type: boolean }
        verification: { type: string, enum: [observed, confirmed, inferred] }

    PersonDetailValue:
      type: object
      additionalProperties: true

    PersonDetailDateComponentsV2:
      type: [object, 'null']
      additionalProperties: false
      description: Gregorian calendar date retained from a current native Contacts v4 source version. The Apple raw number is excluded from details_v2 and the visible typed UI; the unchanged people-v1 details compatibility object can still expose birthday_raw until a future breaking removal.
      required: [month, day, year, calendar, yearless]
      properties:
        month: { type: integer, minimum: 1, maximum: 12 }
        day: { type: integer, minimum: 1, maximum: 31 }
        year: { type: [integer, 'null'], minimum: 1, maximum: 9999 }
        calendar: { type: string, const: gregorian }
        yearless: { type: boolean }

    PersonDetailValueV2:
      type: object
      additionalProperties: false
      description: One currently cited Contact value. Exactly one of value or components is non-null; only birthday currently uses components.
      required: [kind, value, components, label, status, source_kind, source_record_version_id, observed_at, conflict_group]
      properties:
        kind:
          type: string
          enum: [name, address, organization, messaging_handle, related_name, image, social, url, birthday]
        value: { type: [string, 'null'], minLength: 1 }
        components: { $ref: '#/components/schemas/PersonDetailDateComponentsV2' }
        label: { type: [string, 'null'] }
        status: { type: string, enum: [saved, conflicting] }
        source_kind: { type: string, minLength: 1 }
        source_record_version_id: { type: string, format: uuid }
        observed_at:
          type: string
          format: date-time
          description: Immutable capture time of the cited source record version, serialized in UTC.
        conflict_group:
          type: [string, 'null']
          pattern: '^sha256:[0-9a-f]{64}$'
          description: Shared opaque group for disagreeing current variants; null for a saved value.

    PersonDetailsV2:
      type: [object, 'null']
      additionalProperties: false
      description: Per-value current Contact detail with exact immutable source-version provenance. Null when no current Contact card names this person. Historical v2/v3 birthdays remain available only through details until re-admitted under native Contacts v4; while any current birthday-bearing card is still raw-only, v2 withholds all typed birthday values rather than claiming a settled date.
      required: [contract_version, values]
      properties:
        contract_version: { type: string, const: person-details-v2 }
        values:
          type: array
          items: { $ref: '#/components/schemas/PersonDetailValueV2' }

    PersonDetails:
      type: [object, 'null']
      additionalProperties: false
      required: [contact_fact_ids, names, addresses, organizations, messaging_handles,
                 related_names, images, socials, urls, birthday_raw, birthday_is_ambiguous,
                 custom_dates]
      properties:
        contact_fact_ids:
          type: array
          items: { type: string, format: uuid }
          description: Every address-book card this merged person owns.
        names: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        addresses: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        organizations: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        messaging_handles: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        related_names:
          type: array
          items: { $ref: '#/components/schemas/PersonDetailValue' }
          description: Card text about people related to THIS person, never a relationship to the reader.
        images:
          type: array
          items: { $ref: '#/components/schemas/PersonDetailValue' }
          description: Presence and digest only; the native reader never transmits image bytes.
        socials: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        urls: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }
        birthday_raw: { type: [number, 'null'], description: Raw source number; two cards that disagree resolve to null. }
        birthday_is_ambiguous: { type: boolean }
        custom_dates: { type: array, items: { $ref: '#/components/schemas/PersonDetailValue' } }

    PersonInteractions:
      type: object
      additionalProperties: true
      required: [source_kinds, evidence_count, authored_message_count, authored_thread_count,
                 call_count]
      properties:
        source_kinds:
          type: array
          items: { type: string }
          description: Which sources know this human at all — the cross-source join the axis exists for.
        evidence_count: { type: integer, minimum: 0 }
        authored_message_count: { type: integer, minimum: 0 }
        authored_thread_count: { type: integer, minimum: 0 }
        last_authored_message_at: { type: [string, 'null'], format: date-time }
        last_authored_message_thread_key: { type: [string, 'null'] }
        last_authored_message_source_kind: { type: [string, 'null'] }
        call_count: { type: integer, minimum: 0 }
        last_call_at: { type: [string, 'null'], format: date-time }
        last_call_service: { type: [string, 'null'] }
        last_call_direction: { type: [string, 'null'] }
        last_call_duration_seconds: { type: [integer, 'null'], minimum: 0 }

    PersonHistorySource:
      type: object
      additionalProperties: false
      required: [source_kind, source_record_version_id]
      properties:
        source_kind: { type: string }
        source_record_version_id: { type: string, format: uuid }

    PersonHistoryInteraction:
      type: object
      additionalProperties: false
      required: [kind, source_kind, source_record_version_id, occurred_at, direction, duration_seconds]
      properties:
        kind: { type: string, enum: [message, call] }
        source_kind: { type: string }
        source_record_version_id: { type: string, format: uuid }
        occurred_at: { type: string, format: date-time }
        direction: { type: [string, 'null'] }
        duration_seconds: { type: [number, 'null'], minimum: 0 }

    PersonHistory:
      type: object
      additionalProperties: false
      description: Current attributed evidence through as_of; not lifetime completeness or relationship age. Active days count direct-message UTC dates today and the preceding 29 or 89 dates. Imported records may overlap across sources.
      required: [as_of, time_zone, scope, coverage_note, source_kinds, first_direct_exchange_at,
                 last_direct_exchange_at, incoming_message_count, outgoing_message_count,
                 direct_message_count, active_days_30, active_days_90, call_count,
                 contact_sources, recent_interactions, recent_limit, recent_has_more]
      properties:
        as_of: { type: string, format: date-time }
        time_zone: { type: string, const: UTC }
        scope: { type: string, const: current_attributed_evidence }
        coverage_note: { type: string }
        source_kinds: { type: array, items: { type: string } }
        first_direct_exchange_at: { type: [string, 'null'], format: date-time }
        last_direct_exchange_at: { type: [string, 'null'], format: date-time }
        first_direct_exchange_source: { $ref: '#/components/schemas/PersonHistorySource' }
        last_direct_exchange_source: { $ref: '#/components/schemas/PersonHistorySource' }
        incoming_message_count: { type: integer, minimum: 0 }
        outgoing_message_count: { type: integer, minimum: 0 }
        direct_message_count: { type: integer, minimum: 0 }
        active_days_30: { type: integer, minimum: 0, maximum: 30 }
        active_days_90: { type: integer, minimum: 0, maximum: 90 }
        call_count: { type: integer, minimum: 0 }
        contact_sources: { type: array, items: { $ref: '#/components/schemas/PersonHistorySource' } }
        recent_interactions:
          type: array
          maxItems: 50
          items: { $ref: '#/components/schemas/PersonHistoryInteraction' }
        recent_limit: { type: integer, const: 50 }
        recent_has_more: { type: boolean, description: True when only the newest 50 supported interactions are returned. }

    PersonMemories:
      type: object
      additionalProperties: false
      description: Newest explicitly released readings naming this exact person, across available history. Sources and attribution are rechecked at read time. Recorded requests/plans do not imply current open status.
      required: [scope, items, limit, has_more]
      properties:
        scope: { type: string, const: current_released_person_memories }
        items: { type: array, maxItems: 12, items: { $ref: '#/components/schemas/JournalSourceMoment' } }
        limit: { type: integer, const: 12 }
        has_more: { type: boolean }

    PersonResponse:
      type: object
      additionalProperties: false
      required: [contract_version, person, identifiers, details, interactions]
      properties:
        contract_version: { type: string, const: people-v1 }
        person: { $ref: '#/components/schemas/PersonSummary' }
        identifiers:
          type: array
          items: { $ref: '#/components/schemas/PersonIdentifier' }
        details:
          $ref: '#/components/schemas/PersonDetails'
          description: Null when no current address-book card names this person, which is normal.
        details_v2:
          $ref: '#/components/schemas/PersonDetailsV2'
          description: Additive typed per-value detail with current source-version provenance.
        interactions: { $ref: '#/components/schemas/PersonInteractions' }
        history: { $ref: '#/components/schemas/PersonHistory' }
        memories: { $ref: '#/components/schemas/PersonMemories' }

    JournalReadWindow:
      type: object
      additionalProperties: false
      required: [start_at, end_at_exclusive]
      description: Effective paired UTC read filter; positive duration at most 240 hours.
      properties:
        start_at: { type: string, format: date-time }
        end_at_exclusive: { type: string, format: date-time }

    JournalResponse:
      type: object
      additionalProperties: false
      required: [data_layer, strategy, status, start_date, end_date, result_count, days, entries,
                 next_cursor]
      properties:
        read_window:
          $ref: '#/components/schemas/JournalReadWindow'
        compact_preview:
          $ref: '#/components/schemas/CompactJournalPreview'
        accepted_selected_claims:
          type: array
          maxItems: 12
          description: Nonexhaustive selected accounts supplementing exact complete literal parents.
          items: { $ref: '#/components/schemas/JournalSelectedClaimMemory' }
        accepted_memories:
          type: array
          maxItems: 12
          description: Accepted current conversation targets intersecting this page, with full historical spans. Literal entries and their counts remain intact; only represented_entry_ids prove complete cited parent coverage.
          items: { $ref: '#/components/schemas/JournalAcceptedMemory' }
        insights:
          $ref: '#/components/schemas/JournalInsights'
        data_layer: { type: string, const: journal_v1 }
        strategy: { type: string, const: deterministic_context_units }
        status: { type: string, enum: [ok, empty] }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        result_count: { type: integer, minimum: 0, maximum: 200 }
        days:
          type: array
          maxItems: 200
          description: Day aggregates for the effective read window, or the stored whole day when unscoped. A keyset page may contain only part of a day's entries.
          items: { $ref: '#/components/schemas/JournalDay' }
        entries:
          type: array
          maxItems: 200
          items: { $ref: '#/components/schemas/JournalEntry' }
        next_cursor: { type: [string, 'null'], minLength: 1, maxLength: 1024 }

    JournalInsights:
      type: object
      additionalProperties: false
      required: [contract_version, start_date, end_date, as_of, scope, coverage_state, totals,
                 last_observed_at, people, people_omitted_count, conversations,
                 conversations_omitted_count, holds]
      properties:
        contract_version: { type: string, const: journal-insights-v1 }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        as_of: { type: string, format: date-time }
        scope: { type: string, const: current_authorized_saved_journal }
        coverage_state: { type: string, const: unknown }
        totals: { $ref: '#/components/schemas/JournalInsightTotals' }
        last_observed_at: { type: [string, 'null'], format: date-time }
        people:
          type: array
          maxItems: 100
          description: Exact non-self observed authors, ordered by their own authored messages. Never all group messages attributed to every member.
          items: { $ref: '#/components/schemas/JournalInsightPerson' }
        people_omitted_count: { type: integer, minimum: 0 }
        conversations:
          type: array
          maxItems: 100
          description: Source-scoped conversations ordered by saved message count. Totals include omitted rows too. Pointers must be freshly reacquired before opening.
          items: { $ref: '#/components/schemas/JournalInsightConversation' }
        conversations_omitted_count: { type: integer, minimum: 0 }
        holds:
          type: array
          maxItems: 7
          items: { $ref: '#/components/schemas/JournalInsightHold' }

    JournalInsightTotals:
      type: object
      additionalProperties: false
      required: [message_count, sent_count, received_count, conversation_count, direct_count,
                 group_count, unknown_count, conflicting_count]
      properties:
        message_count: { type: integer, minimum: 0 }
        sent_count: { type: integer, minimum: 0 }
        received_count: { type: integer, minimum: 0 }
        conversation_count: { type: integer, minimum: 0 }
        direct_count: { type: integer, minimum: 0 }
        group_count: { type: integer, minimum: 0 }
        unknown_count: { type: integer, minimum: 0 }
        conflicting_count: { type: integer, minimum: 0 }

    JournalInsightPerson:
      type: object
      additionalProperties: false
      required: [person_id, display_name, authored_message_count]
      properties:
        person_id: { type: string, format: uuid }
        display_name: { type: string }
        authored_message_count: { type: integer, minimum: 1 }

    JournalInsightObservedPerson:
      type: object
      additionalProperties: false
      required: [participant_key, person_id, display_name, evidence_kind, resolution_state]
      properties:
        participant_key: { type: string }
        person_id: { type: [string, 'null'], format: uuid }
        display_name: { type: [string, 'null'] }
        evidence_kind: { type: string, enum: [sender_observed, self_observed] }
        resolution_state: { type: string, enum: [exact_person, unresolved, ambiguous] }

    JournalInsightConversation:
      type: object
      additionalProperties: false
      required: [conversation_key, source_kind, conversation_kind, title, message_count,
                 sent_count, received_count, last_observed_at, latest_entry_id,
                 latest_source_record_version_id, observed_people, observed_people_omitted_count]
      properties:
        conversation_key: { type: string, pattern: '^md5:[0-9a-f]{32}$' }
        source_kind: { type: string }
        conversation_kind: { type: string, enum: [direct, group, unknown, conflicting] }
        title: { type: [string, 'null'] }
        message_count: { type: integer, minimum: 1 }
        sent_count: { type: integer, minimum: 0 }
        received_count: { type: integer, minimum: 0 }
        last_observed_at: { type: string, format: date-time }
        latest_entry_id: { type: string, format: uuid }
        latest_source_record_version_id: { type: string, format: uuid }
        observed_people:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/JournalInsightObservedPerson' }
        observed_people_omitted_count: { type: integer, minimum: 0 }

    JournalInsightHold:
      type: object
      additionalProperties: false
      required: [reason_code, count]
      properties:
        reason_code:
          type: string
          enum: [current_parent_unavailable, source_time_unavailable, time_authority_unavailable,
                 source_metadata_unavailable, sender_identity_conflicting, sender_identity_unavailable]
        count:
          type: integer
          minimum: 1
          description: Distinct parent IDs for current_parent_unavailable; distinct original versions for other reasons. Boundary metadata holds may precede exact date attribution.

    JournalAcceptedMemory:
      type: object
      additionalProperties: false
      required: [target_kind, target_id, memory_id, revision, output_id, group_ordinal, source_kind, conversation_kind,
                 title, summary, valid_from, valid_to, input_unit_ids, source_record_version_ids, represented_entry_ids]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string, format: uuid }
        memory_id: { type: string, format: uuid }
        revision: { type: integer, minimum: 1, maximum: 9007199254740991 }
        output_id: { type: string, format: uuid }
        group_ordinal: { type: integer, minimum: 0, maximum: 7 }
        source_kind: { type: string, enum: [imessage, whatsapp_personal] }
        conversation_kind: { type: string, enum: [direct, group, unknown] }
        title: { type: string, minLength: 1, maxLength: 160 }
        summary: { type: string, minLength: 1, maxLength: 6415 }
        valid_from: { type: string, format: date-time }
        valid_to: { type: string, format: date-time }
        input_unit_ids: { type: array, minItems: 1, maxItems: 8, uniqueItems: true, items: { type: string, format: uuid } }
        source_record_version_ids: { type: array, minItems: 1, maxItems: 256, uniqueItems: true, items: { type: string, format: uuid } }
        represented_entry_ids: { type: array, maxItems: 200, uniqueItems: true, items: { type: string, format: uuid } }

    JournalSelectedClaimMemory:
      type: object
      additionalProperties: false
      required: [target_kind, target_id, memory_id, revision, output_id, memory_kind, input_unit_id, parent_entry_id, conversation_coverage, source_kind, conversation_kind,
                 title, statement, valid_from, valid_to, source_record_version_ids]
      properties:
        target_kind: { type: string, const: search_chunk }
        target_id: { type: string, format: uuid }
        memory_id: { type: string, format: uuid }
        revision: { type: integer, minimum: 1, maximum: 9007199254740991 }
        output_id: { type: string, format: uuid }
        memory_kind: { type: string, const: selected_claim_v1 }
        input_unit_id: { type: string, format: uuid }
        parent_entry_id: { type: string, format: uuid }
        conversation_coverage: { type: string, const: nonexhaustive }
        source_kind: { type: string, enum: [imessage, whatsapp_personal] }
        conversation_kind: { type: string, enum: [direct, group, unknown] }
        title: { type: string, minLength: 1, maxLength: 160 }
        statement: { type: string, minLength: 1, maxLength: 400 }
        valid_from: { type: string, format: date-time }
        valid_to: { type: string, format: date-time }
        source_record_version_ids: { type: array, minItems: 1, maxItems: 256, uniqueItems: true, items: { type: string, format: uuid } }

    CompactJournalPreview:
      type: object
      additionalProperties: false
      required: [status, groups]
      properties:
        status: { type: string, enum: [ready, unavailable] }
        groups:
          type: array
          maxItems: 12
          items:
            type: object
            additionalProperties: false
            required: [id, title, summary, parents]
            properties:
              id: { type: string, format: uuid }
              title: { type: string }
              summary: { type: string }
              parents:
                type: array
                minItems: 1
                maxItems: 16
                items: { $ref: '#/components/schemas/JournalEntry' }

    PreAIGoldInspectResponse:
      type: object
      additionalProperties: false
      required: [data_layer, target_kind, target_id, lineage, value]
      properties:
        read_window:
          $ref: '#/components/schemas/JournalReadWindow'
        data_layer: { type: string, const: pre_ai_gold_v1 }
        target_kind: { type: string, enum: [activity, object, relationship, source_version] }
        target_id: { type: string, format: uuid }
        identity_resolution: { type: [string, 'null'] }
        lineage:
          type: object
          additionalProperties: false
          required: [source_kind, source_plane, source_record_uuid, source_record_version_id, fact_table]
          properties:
            source_kind: { type: string }
            source_plane: { type: string, enum: [native, customer, historical] }
            source_record_uuid: { type: string, format: uuid }
            source_record_version_id: { type: string, format: uuid }
            source_event_id: { type: [string, 'null'], format: uuid }
            fact_id: { type: [string, 'null'], format: uuid }
            fact_table: { type: string }
        value: { type: object, additionalProperties: true }

    # ─── REQUESTS: where the knobs live ────────────────────────────────────────

    BrainCreate:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: { $ref: '#/components/schemas/Slug' }
        name: { type: string }
        self:
          $ref: '#/components/schemas/SelfEntity'

    # ─── SUPPORTING SHAPES (not resources, not knobs) ──────────────────────────

    Slug:
      type: string
      minLength: 1
      maxLength: 64
      pattern: '^[A-Za-z0-9._:-]+$'
      description: A non-empty, URL-safe identifier. Empty is not an identifier.

    SelfEntity:
      type: object
      description: |
        The principal this Brain is a memory OF. Resolves "my" and "our". Omit it and every
        possessive question abstains — the safe default is also the honest one.
      required: [type, name]
      properties:
        type: { type: string, enum: [person, org] }
        name: { type: string }

    DeletionReceipt:
      type: object
      required: [deleted, rederiving, hash]
      properties:
        deleted: { type: object, additionalProperties: { type: integer } }
        rederiving: { type: object, additionalProperties: { type: integer } }
        hash: { type: string }
        completed_at: { type: string, format: date-time }

    Error:
      type: object
      required: [type, code, message, request_id]
      properties:
        type: { type: string, enum: [invalid_request, authentication, permission, not_found, conflict, rate_limit, policy, internal] }
        code: { type: string }
        message: { type: string }
        request_id: { type: string, examples: ['req_01K7Q...'] }
