> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aitasker.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Brand Intelligence Content Group Insights Route

> Cohort content analysis for one competitor Brand Group (INSIGHTS tab).

Includes the content ↔ demand join, which reads the SAME cohort's stored
SEO keyword universe. No new provider spend on either side.



## OpenAPI

````yaml /openapi.json post /api/v1/brands/{brand_id}/brand-intelligence/content-group-insights
openapi: 3.1.0
info:
  title: AITasker
  version: 0.1.0
servers: []
security: []
paths:
  /api/v1/brands/{brand_id}/brand-intelligence/content-group-insights:
    post:
      tags:
        - Brand Intelligence
        - Brand Intelligence
      summary: Get Brand Intelligence Content Group Insights Route
      description: |-
        Cohort content analysis for one competitor Brand Group (INSIGHTS tab).

        Includes the content ↔ demand join, which reads the SAME cohort's stored
        SEO keyword universe. No new provider spend on either side.
      operationId: >-
        get_brand_intelligence_content_group_insights_route_api_v1_brands__brand_id__brand_intelligence_content_group_insights_post
      parameters:
        - name: brand_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
            title: Brand Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContentGroupInsightsRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentGroupInsightsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
        - HTTPBearer: []
components:
  schemas:
    ContentGroupInsightsRequest:
      properties:
        domains:
          items:
            type: string
          type: array
          maxItems: 60
          title: Domains
        group_label:
          anyOf:
            - type: string
              maxLength: 120
            - type: 'null'
          title: Group Label
        rollup_mode:
          type: string
          enum:
            - auto
            - frozen
          title: Rollup Mode
          default: auto
        rollup_pillars:
          items:
            type: string
          type: array
          maxItems: 25
          title: Rollup Pillars
        rollup_limit:
          anyOf:
            - type: integer
              maximum: 25
              minimum: 1
            - type: 'null'
          title: Rollup Limit
      type: object
      title: ContentGroupInsightsRequest
      description: |-
        Content cohort plus the vocabulary used by the pillar roll-up.

        ``auto`` derives a deterministic starting vocabulary from the stored
        cohort evidence and ignores ``rollup_pillars``. ``frozen`` uses exactly
        the supplied vocabulary. Carrying that vocabulary in the request keeps a
        saved comparison stable when later acquisitions discover new terms.
    ContentGroupInsightsResponse:
      properties:
        module:
          type: string
          const: content
          title: Module
          default: content
        brand_id:
          type: string
          format: uuid
          title: Brand Id
        generated_at:
          type: string
          format: date-time
          title: Generated At
        group_key:
          type: string
          title: Group Key
        group_label:
          anyOf:
            - type: string
            - type: 'null'
          title: Group Label
        brand_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Domain
        domains:
          items:
            type: string
          type: array
          title: Domains
        cohort_domains:
          items:
            type: string
          type: array
          title: Cohort Domains
        coverage:
          $ref: '#/components/schemas/ModuleCoverageStrip'
        position:
          items:
            $ref: '#/components/schemas/ModuleDimensionRank'
          type: array
          title: Position
        source_health:
          $ref: '#/components/schemas/ContentSourceHealthPanel'
        cadence:
          $ref: '#/components/schemas/ContentCadencePanel'
        format_mix:
          $ref: '#/components/schemas/ContentFormatMixPanel'
        pillars:
          $ref: '#/components/schemas/ContentPillarPanel'
        pillar_rollup:
          $ref: '#/components/schemas/ContentPillarRollupPanel'
        headline_patterns:
          $ref: '#/components/schemas/ContentHeadlinePatternsPanel'
        lead_magnets:
          $ref: '#/components/schemas/ContentLeadMagnetPanel'
        demand_join:
          $ref: '#/components/schemas/ContentDemandJoinPanel'
        ads_join:
          $ref: '#/components/schemas/ContentAdsJoinPanel'
        ai_visibility:
          anyOf:
            - $ref: '#/components/schemas/ContentAiVisibilityPanel'
            - type: 'null'
        page_performance:
          anyOf:
            - $ref: '#/components/schemas/ContentPagePerformancePanel'
            - type: 'null'
        ai_citations:
          anyOf:
            - $ref: '#/components/schemas/ContentAiCitationsPanel'
            - type: 'null'
        tech_stack:
          anyOf:
            - $ref: '#/components/schemas/ContentTechStackPanel'
            - type: 'null'
        publisher_evidence:
          items:
            $ref: '#/components/schemas/ContentPublisherEvidenceRow'
          type: array
          maxItems: 25
          title: Publisher Evidence
        comparison_basis:
          $ref: '#/components/schemas/ContentComparisonBasisPanel'
        findings:
          items:
            $ref: '#/components/schemas/ModuleRuleFinding'
          type: array
          title: Findings
      type: object
      required:
        - brand_id
        - generated_at
        - group_key
      title: ContentGroupInsightsResponse
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ModuleCoverageStrip:
      properties:
        targets_total:
          type: integer
          title: Targets Total
          default: 0
        measured:
          type: integer
          title: Measured
          default: 0
        missing:
          items:
            type: string
          type: array
          title: Missing
        fetch_failed:
          items:
            type: string
          type: array
          title: Fetch Failed
        persist_failed:
          items:
            type: string
          type: array
          title: Persist Failed
        skipped_cost:
          items:
            type: string
          type: array
          title: Skipped Cost
        last_run_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Last Run At
        brand_measured:
          type: boolean
          title: Brand Measured
          default: false
        brand_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Domain
        brand_not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Not Measured Reason
        near_duplicate_pairs:
          items:
            $ref: '#/components/schemas/ModuleCoverageNearDuplicatePair'
          type: array
          title: Near Duplicate Pairs
        near_duplicate_scan_truncated:
          type: boolean
          title: Near Duplicate Scan Truncated
          default: false
        near_duplicate_scanned_count:
          type: integer
          title: Near Duplicate Scanned Count
          default: 0
      type: object
      title: ModuleCoverageStrip
      description: >-
        N-of-M acquisition coverage for one module's cohort (the ★S0/★C0 strip).


        ``measured`` counts cohort domains that produced a stored metric row.

        ``missing`` are targets that never did — their fetch or persist failed,
        or

        they have not been acquired yet. For Content, ``fetch_failed`` is a
        typed

        subset of ``missing``, not an additional shortfall, so clients must

        de-duplicate rather than sum those lengths. A competitor with a failed

        acquisition must render as a failure chip, never as a measured zero.
    ModuleDimensionRank:
      properties:
        dimension:
          type: string
          title: Dimension
        label:
          type: string
          title: Label
        higher_is_better:
          type: boolean
          title: Higher Is Better
          default: true
        brand_value:
          anyOf:
            - type: number
            - type: 'null'
          title: Brand Value
        brand_rank:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Rank
        tied_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Tied Count
        measured_count:
          type: integer
          title: Measured Count
          default: 0
        cohort_total:
          type: integer
          title: Cohort Total
          default: 0
        best_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Best Domain
        best_tied_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Best Tied Count
        best_value:
          anyOf:
            - type: number
            - type: 'null'
          title: Best Value
        median_value:
          anyOf:
            - type: number
            - type: 'null'
          title: Median Value
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
      type: object
      required:
        - dimension
        - label
      title: ModuleDimensionRank
      description: >-
        The user brand's standing on ONE measured dimension (★S1 / ★C1).


        ``brand_rank`` is 1-based over the domains that carry a value on this

        dimension — ``measured_count``. It is ``None`` when the brand itself is
        not

        measured on it, which is different from ranking last.


        It is a COMPETITION rank ("1224"): only strictly-better domains push the

        brand down, so tied domains share a place. ``tied_count`` is how many

        measured domains hold the brand's exact value, INCLUDING the brand — so
        it

        is 1 when the brand stands alone. Rank alone cannot carry a "you lead"

        claim (a brand tied at the top ranks 1, and so does a brand tied at zero

        with a cohort that measured zero); check ``tied_count == 1`` for that.


        ``best_tied_count`` is the same disclosure for ``best_domain``, which is

        ONE name chosen alphabetically out of the leaders. It counts inclusively

        too, so 1 means the named leader stands alone and a UI may safely name
        it.

        Both are ``None`` — never 0 — when there is nothing measured to count.
    ContentSourceHealthPanel:
      properties:
        sources_total:
          type: integer
          title: Sources Total
          default: 0
        sources_fetched:
          type: integer
          title: Sources Fetched
          default: 0
        sources_failed:
          type: integer
          title: Sources Failed
          default: 0
        domains_with_failures:
          type: integer
          title: Domains With Failures
          default: 0
        failures:
          items:
            $ref: '#/components/schemas/ContentSourceFailureRow'
          type: array
          title: Failures
      type: object
      title: ContentSourceHealthPanel
      description: |-
        ★C0 — per-competitor source health rolled up for the cohort.

        Failed sources used to be DELETED from the UI, which made a brand whose
        blog was blocked look identical to one with no blog.
    ContentCadencePanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentCadenceRow'
          type: array
          title: Rows
        measured_count:
          type: integer
          title: Measured Count
          default: 0
        cohort_median_posts:
          anyOf:
            - type: number
            - type: 'null'
          title: Cohort Median Posts
        brand_posts_last_30d:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Posts Last 30D
        brand_rank:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Rank
        brand_tied_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Tied Count
        rows_with_suppressed_cadence:
          anyOf:
            - type: integer
            - type: 'null'
          title: Rows With Suppressed Cadence
        measured:
          type: boolean
          title: Measured
          default: false
      type: object
      title: ContentCadencePanel
      description: >-
        ★C1 — publishing velocity vs measured competitors.


        ``measured_count`` and ``cohort_median_posts`` exclude the user's brand;

        ``brand_rank`` ranks the brand across the combined measured population.


        ``brand_rank`` is a COMPETITION rank ("1224"): domains that published
        the

        same number of posts share a place, and only a domain that published
        MORE

        pushes the brand down one. So rank 1 means "nobody published more" — NOT

        "published more than everyone", which a tie also satisfies.

        ``brand_tied_count`` includes the brand itself and makes that shared
        place

        explicit to every renderer.


        ``rows_with_suppressed_cadence`` counts over the FULL cohort, not the

        truncated ``rows`` list, and is ``None`` rather than 0 when nothing was

        suppressed: ``module_group_commentary._flatten_refs`` skips ``None`` but

        flattens 0 into evidence the narrator may quote, so a 0 would hand the

        model a "0 rows suppressed" fact about every cohort.
    ContentFormatMixPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentFormatMixRow'
          type: array
          title: Rows
        cohort_items_classified:
          type: integer
          title: Cohort Items Classified
          default: 0
        brand_items_classified:
          type: integer
          title: Brand Items Classified
          default: 0
        missing_formats:
          items:
            type: string
          type: array
          title: Missing Formats
        measured:
          type: boolean
          title: Measured
          default: false
      type: object
      title: ContentFormatMixPanel
      description: ★C2 — article/video/gated split across the cohort vs the brand.
    ContentPillarPanel:
      properties:
        shared:
          items:
            $ref: '#/components/schemas/ContentPillarRow'
          type: array
          title: Shared
        whitespace:
          items:
            $ref: '#/components/schemas/ContentPillarRow'
          type: array
          title: Whitespace
        brand_only:
          items:
            $ref: '#/components/schemas/ContentPillarRow'
          type: array
          title: Brand Only
        cohort_domains_measured:
          type: integer
          title: Cohort Domains Measured
          default: 0
        threshold_domains:
          type: integer
          title: Threshold Domains
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
      type: object
      title: ContentPillarPanel
      description: ★C3 — shared topics (table stakes) vs uncontested ground.
    ContentPillarRollupPanel:
      properties:
        mode:
          type: string
          enum:
            - auto
            - frozen
          title: Mode
          default: auto
        rollup_pillars:
          items:
            type: string
          type: array
          maxItems: 25
          title: Rollup Pillars
        limit:
          type: integer
          maximum: 25
          minimum: 1
          title: Limit
          default: 12
        rows:
          items:
            $ref: '#/components/schemas/ContentPillarRollupRow'
          type: array
          maxItems: 25
          title: Rows
        competitor_domains_total:
          type: integer
          minimum: 0
          title: Competitor Domains Total
          default: 0
        competitor_domains_included:
          type: integer
          maximum: 60
          minimum: 0
          title: Competitor Domains Included
          default: 0
        competitor_domains_truncated:
          type: integer
          minimum: 0
          title: Competitor Domains Truncated
          default: 0
        competitor_domains_measured:
          type: integer
          minimum: 0
          title: Competitor Domains Measured
          default: 0
        brand_measured:
          type: boolean
          title: Brand Measured
          default: false
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Counts and shares use each domain's bounded stored latest-item
            sample; pillar-evidence coverage can also use stored strategy
            labels; the median excludes your brand and missing samples are never
            zero.
      type: object
      title: ContentPillarRollupPanel
      description: >-
        Request-scoped pillar vocabulary rolled across stored item samples.


        Unlike the sibling full-cohort panels, this comparison deliberately caps

        competitors before vocabulary and aggregate computation. The explicit

        total/included/truncated accounting prevents that bounded denominator
        from

        masquerading as complete cohort coverage.
    ContentHeadlinePatternsPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentHeadlinePatternRow'
          type: array
          title: Rows
        length_buckets:
          items:
            $ref: '#/components/schemas/ContentHeadlineLengthBucketRow'
          type: array
          title: Length Buckets
        cohort_titles_measured:
          type: integer
          title: Cohort Titles Measured
          default: 0
        cohort_domains_measured:
          type: integer
          title: Cohort Domains Measured
          default: 0
        brand_titles_measured:
          type: integer
          title: Brand Titles Measured
          default: 0
        cohort_median_title_chars:
          anyOf:
            - type: integer
            - type: 'null'
          title: Cohort Median Title Chars
        cohort_median_title_words:
          anyOf:
            - type: integer
            - type: 'null'
          title: Cohort Median Title Words
        brand_median_title_chars:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Median Title Chars
        brand_median_title_words:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Median Title Words
        min_cohort_titles:
          type: integer
          title: Min Cohort Titles
          default: 0
        min_brand_titles:
          type: integer
          title: Min Brand Titles
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
        brand_measured:
          type: boolean
          title: Brand Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
      type: object
      title: ContentHeadlinePatternsPanel
      description: >-
        ★C10 — the shape of the cohort's headlines, from stored titles only.


        Fully deterministic: every field is arithmetic over

        ``CompetitorContentMetric.latest_items[*].title``, which is already

        persisted. No provider call, no model call, no acquisition — see

        ``content_headline_patterns.py`` for why an LLM was declined.


        Four states, deliberately distinguishable:


        * ``measured=False`` + ``not_measured_reason="no_content_measured"``;

        * ``measured=False`` + ``"no_titles_stored"`` (read, but zero titles);

        * ``measured=False`` + ``"insufficient_titles"`` (below the floor — the
          counts are still carried so the reader sees "12 of 20 needed");
        * ``measured=True`` with a ``0.0`` share — a measured zero.


        A measured panel carries a row for EVERY pattern, including unused ones.

        Filtering the zeros would make a real "nobody does this"
        indistinguishable

        from "we did not check".
    ContentLeadMagnetPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentLeadMagnetRow'
          type: array
          title: Rows
        missing_plays:
          items:
            type: string
          type: array
          title: Missing Plays
        cohort_domains_measured:
          type: integer
          title: Cohort Domains Measured
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
      type: object
      title: ContentLeadMagnetPanel
      description: ★C4 — the cohort's tried-and-true capture patterns.
    ContentDemandJoinPanel:
      properties:
        validated_pillars:
          items:
            $ref: '#/components/schemas/ContentDemandTerm'
          type: array
          title: Validated Pillars
        demand_you_are_missing:
          items:
            $ref: '#/components/schemas/ContentDemandTerm'
          type: array
          title: Demand You Are Missing
        publishing_without_demand:
          items:
            $ref: '#/components/schemas/ContentDemandTerm'
          type: array
          title: Publishing Without Demand
        cohort_demand:
          items:
            $ref: '#/components/schemas/ContentDemandTerm'
          type: array
          title: Cohort Demand
        brand_side_measured:
          type: boolean
          title: Brand Side Measured
          default: false
        brand_side_withheld_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Side Withheld Reason
        seo_domains_measured:
          type: integer
          title: Seo Domains Measured
          default: 0
        content_domains_measured:
          type: integer
          title: Content Domains Measured
          default: 0
        seo_keyword_universe_size:
          type: integer
          title: Seo Keyword Universe Size
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Computed over the stored keyword samples for each competitor, not
            the full domain-wide keyword population.
      type: object
      title: ContentDemandJoinPanel
      description: >-
        ★C8 — the content ↔ demand join, benchmarked on the SAME cohort.


        Three buckets: pillars the brand publishes that the cohort's SEO keyword

        universe confirms have demand; demand the cohort captures that the brand

        publishes nothing for; and pillars the brand publishes with no demand

        evidence anywhere in the cohort's keyword universe.


        ``measured`` is False unless BOTH sides were measured for this cohort —

        a missing SEO capture must never render as "no demand".


        ``cohort_demand`` is the brand-side-WITHHELD half of the join: the
        demand

        the cohort captures, with ``brand_publishes=None`` on every row. It is

        populated ONLY when the SEO side is measured and the brand side is not,

        which is the default new-cohort state — a fully-measured cohort should
        not

        render as a blank panel just because the brand has not been read yet.
        When

        ``measured`` is True this list is empty and the three comparison buckets

        carry the answer, so no term is ever rendered twice.
    ContentAdsJoinPanel:
      properties:
        validated_pillars:
          items:
            $ref: '#/components/schemas/ContentAdsTerm'
          type: array
          title: Validated Pillars
        ad_spend_you_are_missing:
          items:
            $ref: '#/components/schemas/ContentAdsTerm'
          type: array
          title: Ad Spend You Are Missing
        publishing_without_ad_backing:
          items:
            $ref: '#/components/schemas/ContentAdsTerm'
          type: array
          title: Publishing Without Ad Backing
        cohort_ad_spend:
          items:
            $ref: '#/components/schemas/ContentAdsTerm'
          type: array
          title: Cohort Ad Spend
        brand_side_measured:
          type: boolean
          title: Brand Side Measured
          default: false
        brand_side_withheld_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Side Withheld Reason
        ads_domains_measured:
          type: integer
          title: Ads Domains Measured
          default: 0
        content_domains_measured:
          type: integer
          title: Content Domains Measured
          default: 0
        ads_keyword_universe_size:
          type: integer
          title: Ads Keyword Universe Size
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Computed over the stored keyword samples for each competitor, not
            the full domain-wide keyword population.
      type: object
      title: ContentAdsJoinPanel
      description: >-
        ★C8b — the content ↔ paid-ads join, benchmarked on the SAME cohort.


        Three buckets: pillars the brand publishes that the cohort's Google Ads

        keyword universe confirms someone is backing with paid spend; paid

        keywords the cohort bids on that the brand publishes no content for; and

        pillars the brand publishes with no paid-keyword evidence anywhere in
        the

        cohort's Ads universe.


        ``measured`` is False unless BOTH sides were measured for this cohort —

        a missing Ads capture must never render as "no paid demand".


        ``cohort_ad_spend`` is the brand-side-WITHHELD half of the join: the
        paid

        keywords the cohort backs, with ``brand_publishes=None`` on every row.
        It

        is populated ONLY when the ads side is measured and the brand side is

        not, which is the default new-cohort state — a fully-measured cohort

        should not render as a blank panel just because the brand has not been

        read yet. When ``measured`` is True this list is empty and the three

        comparison buckets carry the answer, so no term is ever rendered twice.
    ContentAiVisibilityPanel:
      properties:
        status:
          type: string
          enum:
            - measured
            - missing
            - resultless
            - read_failed
          title: Status
        scope:
          type: string
          const: brand_category_projected
          title: Scope
          default: brand_category_projected
        scope_notice:
          type: string
          title: Scope Notice
          default: >-
            Projected onto this selected cohort from the brand's weekly
            brand-category capture. This is not a group- or pillar-specific
            capture.
        measured:
          type: boolean
          title: Measured
          default: false
        expected_answers:
          type: integer
          maximum: 3
          minimum: 0
          title: Expected Answers
          default: 0
        measured_answers:
          type: integer
          maximum: 3
          minimum: 0
          title: Measured Answers
          default: 0
        missing_answers:
          type: integer
          maximum: 3
          minimum: 0
          title: Missing Answers
          default: 0
        mention_denominator:
          anyOf:
            - type: integer
              maximum: 3
              minimum: 0
            - type: 'null'
          title: Mention Denominator
        citation_denominator:
          anyOf:
            - type: integer
              maximum: 3
              minimum: 0
            - type: 'null'
          title: Citation Denominator
        citation_denominator_basis:
          anyOf:
            - type: string
              enum:
                - source_list_observed
                - valid_citation_hosts_legacy
            - type: 'null'
          title: Citation Denominator Basis
        cohort_domains_total:
          type: integer
          minimum: 0
          title: Cohort Domains Total
          default: 0
        cohort_domains_measured:
          type: integer
          minimum: 0
          title: Cohort Domains Measured
          default: 0
        missing_domains:
          items:
            type: string
          type: array
          title: Missing Domains
        rows_truncated:
          type: integer
          minimum: 0
          title: Rows Truncated
          default: 0
        provider:
          anyOf:
            - type: string
              const: dataforseo
            - type: 'null'
          title: Provider
        platform:
          type: string
          maxLength: 32
          title: Platform
          default: ''
        location_code:
          anyOf:
            - type: integer
              minimum: 1
            - type: 'null'
          title: Location Code
        language_code:
          anyOf:
            - type: string
              maxLength: 16
              minLength: 1
            - type: 'null'
          title: Language Code
        captured_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Captured At
        stale:
          type: boolean
          title: Stale
          default: false
        rows:
          items:
            $ref: '#/components/schemas/ContentAiVisibilityRow'
          type: array
          maxItems: 60
          title: Rows
        evidence:
          items:
            $ref: '#/components/schemas/ContentAiVisibilityEvidence'
          type: array
          maxItems: 3
          title: Evidence
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Mention share is answers naming the domain divided by answers whose
            mention inputs were observed. Citation share is answers citing the
            domain divided by answers whose source list was observed; an
            explicit empty source list is a measured zero, while an omitted
            source list is unmeasured. The denominators are separate and are
            never combined.
      type: object
      required:
        - status
      title: ContentAiVisibilityPanel
      description: >-
        Weekly observed AI-answer mentions and citations for this cohort.


        This is a read-only projection of the already-bounded weekly SEO
        capture;

        opening Content Insights never triggers a provider request.
    ContentPagePerformancePanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentPagePerformanceRow'
          type: array
          maxItems: 25
          title: Rows
        domains_measured:
          type: integer
          title: Domains Measured
          default: 0
        domains_in_cohort:
          type: integer
          title: Domains In Cohort
          default: 0
        pages_sampled:
          type: integer
          title: Pages Sampled
          default: 0
        pages_matched:
          type: integer
          title: Pages Matched
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Per-URL organic traffic and link counts for the content pieces
            already in each publisher's stored sample, matched against that
            domain's top pages by organic traffic and by backlinks. Traffic is a
            provider ESTIMATE, not analytics. A blank cell is not measured; zero
            is measured. An unmatched piece is absent from the page indexes we
            bought, which is not the same as earning nothing.
      type: object
      title: ContentPagePerformancePanel
      description: >-
        Content's first MEASURED performance signal (design 9.2).


        Every other Content panel counts what a publisher SHIPPED. This one
        reports

        what individual pieces EARNED, joined by URL onto the same bounded item

        sample the rest of the module already renders.


        Three states, and collapsing any two is the defect this panel exists to

        avoid:


        - ``measured=False`` - no publisher in this cohort has a stored
        page-index
          capture. Nothing can be said. This is the state a cohort sits in while
          the acquisition flag is off, and it must never render as zeros.
        - ``measured=True`` with ``rows == []`` - a page index WAS read for at
          least one publisher and none of the sampled URLs appear in it. A real
          finding, sized by ``pages_sampled``, not a failure.
        - ``measured=True`` with rows - render them, remembering that a blank
        cell
          inside a row is still "not measured".
    ContentAiCitationsPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentPagePerformanceRow'
          type: array
          maxItems: 25
          title: Rows
        domains_measured:
          type: integer
          title: Domains Measured
          default: 0
        domains_in_cohort:
          type: integer
          title: Domains In Cohort
          default: 0
        pages_sampled:
          type: integer
          title: Pages Sampled
          default: 0
        pages_cited:
          type: integer
          title: Pages Cited
          default: 0
        platform:
          anyOf:
            - type: string
              maxLength: 32
            - type: 'null'
          title: Platform
        platform_span:
          type: string
          enum:
            - single
            - multiple
            - unknown
          title: Platform Span
          default: unknown
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            How often an AI assistant cited each individual page, for the
            content pieces already in each publisher's stored sample. This is a
            PAGE-level citation count from DataForSEO's LLM Mentions index - not
            the same measurement as this module's brand-level AI visibility
            panel, which asks whether the BRAND is named in an answer. A brand
            can be named constantly with none of its pages cited. A blank cell
            is not measured; zero is measured.
      type: object
      title: ContentAiCitationsPanel
      description: >-
        Which of a publisher's PAGES an AI assistant cites (design 9.3).


        Deliberately its own panel rather than three more columns on

        `ContentPagePerformancePanel`, and deliberately NOT merged into the

        module's brand-level AI visibility panel. The three are different
        claims:


        - brand-level AI visibility (C5) - is this BRAND named when an assistant
        is
          asked about the category. Re-read of the SEO module's `llm_scraper`
          capture.
        - per-page citations (this) - is this URL cited. DataForSEO LLM
        Mentions,
          keyed per page, different denominator.
        - per-page organic/link performance - did this URL earn search traffic
        and
          links. No AI surface involved at all.

        A reader must be able to tell which is which, so each gets its own
        heading,

        its own basis notice and its own measured/unmeasured verdict. `platform`

        is required reading alongside any count: "cited by ChatGPT" and "cited
        in

        Google's AI Overviews" are not interchangeable.
    ContentTechStackPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ContentTechStackRow'
          type: array
          maxItems: 25
          title: Rows
        competitors_scanned:
          type: integer
          title: Competitors Scanned
          default: 0
        competitors_in_cohort:
          type: integer
          title: Competitors In Cohort
          default: 0
        brand_scanned:
          type: boolean
          title: Brand Scanned
          default: false
        technologies_detected:
          type: integer
          title: Technologies Detected
          default: 0
        rows_truncated:
          type: boolean
          title: Rows Truncated
          default: false
        oldest_detected_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Oldest Detected At
        newest_detected_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Newest Detected At
        stale_domain_count:
          type: integer
          title: Stale Domain Count
          default: 0
        stale_after_days:
          type: integer
          title: Stale After Days
          default: 45
        measured:
          type: boolean
          title: Measured
          default: false
        not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Not Measured Reason
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Technologies a fingerprinting scan DETECTED on each domain, counted
            only where they were found. Detection is not declaration: a tool can
            be in use and go undetected because it runs server-side, sits behind
            a login, is proxied through a CDN, or simply has no signature in the
            provider's set. So "not detected" is not evidence a publisher does
            not use something, and this panel never counts an absence. Each
            domain is scanned separately and the scan dates differ, so treat the
            oldest date as the age of the comparison.
      type: object
      title: ContentTechStackPanel
      description: >-
        What the tracked publishers BUILD with, versus what the brand builds
        with.


        Every other Content panel measures output. This one measures the content

        stack behind the output — CMS, static-site generator, analytics, tag

        manager, personalisation, email platform — so a reader can see that four
        of

        the publishers they track run a headless CMS and they do not.


        **The claim is bounded on three axes, and each one is on the payload.**


        1. *Evidence direction.* Only detections are counted. ``detected_count``
        is
           out of ``competitors_scanned``, never out of ``competitors_in_cohort``:
           a domain nobody scanned is not a domain that lacks the technology.
        2. *Sample size.* ``competitors_scanned`` / ``competitors_in_cohort``
        sizes
           the comparison, and ``brand_scanned`` says whether the "versus you" half
           is expressible at all. When it is False the differential is withheld —
           enforced below, not merely documented.
        3. *Age.* Detections carry the provider's own ``last_visited``, which
        can
           be materially older than the refresh that fetched it.
           ``oldest_detected_at`` bounds how current the comparison is and
           ``stale_domain_count`` says how much of it is past
           ``stale_after_days``.

        Three states, as everywhere else in this module: ``measured=False`` is
        "no

        cohort domain has a stored scan" and must never render as "no
        technologies";

        ``measured=True`` with ``rows == []`` is "we scanned and detected
        nothing we

        report", which is genuinely rare and still not proof of an empty stack.
    ContentPublisherEvidenceRow:
      properties:
        domain:
          type: string
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        measured:
          type: boolean
          title: Measured
          default: false
        latest_items:
          items:
            $ref: '#/components/schemas/ContentEvidenceItem'
          type: array
          maxItems: 5
          title: Latest Items
        latest_items_source:
          anyOf:
            - type: string
              enum:
                - recent_30d
                - fallback_top_items
            - type: 'null'
          title: Latest Items Source
        strategy_summary:
          anyOf:
            - type: string
              maxLength: 600
            - type: 'null'
          title: Strategy Summary
        strategy_source:
          anyOf:
            - type: string
              enum:
                - llm
                - deterministic
            - type: 'null'
          title: Strategy Source
        captured_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Captured At
      type: object
      required:
        - domain
      title: ContentPublisherEvidenceRow
      description: >-
        Stored execution evidence for one member of the Content cohort.


        ``measured=False`` forces the evidence fields empty in the service. 
        That

        keeps an old item or strategy summary from surviving a failed
        acquisition

        and reading as current evidence.
    ContentComparisonBasisPanel:
      properties:
        measured:
          type: boolean
          title: Measured
          default: false
        publishers_with_snapshots:
          type: integer
          title: Publishers With Snapshots
          default: 0
        publishers_with_prior_snapshot:
          type: integer
          title: Publishers With Prior Snapshot
          default: 0
        has_prior_snapshot:
          type: boolean
          title: Has Prior Snapshot
          default: false
      type: object
      title: ContentComparisonBasisPanel
      description: >-
        Whether a period-over-period comparison is possible for this cohort AT
        ALL.


        Three states. Collapsing any two of them is the defect this panel exists
        to

        prevent, because each one licenses a different sentence on screen:


        - ``measured=False`` — no tracked publisher has a stored capture, so we
          cannot say anything about history. Not a first generation; an unknown.
        - ``measured=True`` and ``has_prior_snapshot=False`` — captures exist
        but
          none has a predecessor on its own measurement series. This is the
          FIRST-EVER generation for the group: there is nothing to compare
          against, which is emphatically not "we compared and nothing changed".
        - ``has_prior_snapshot=True`` — a real comparison basis exists, so an
        empty
          change feed is a measured absence of qualifying movement.

        The series grain matches ``content_insight_events``'s own baseline test

        (``detect_content_publisher_delta`` returns ``None`` when the previous

        snapshot on the same ``domain``/``source``/``source_url`` series is

        missing): a publisher needs two captures on ONE series before any delta

        can exist. Normalization here is deliberately no tighter than the
        emitter's

        so that a mismatch can only over-count history — which suppresses the

        first-generation copy and falls back to the existing wording, never the

        other way round.
    ModuleRuleFinding:
      properties:
        rule_id:
          type: string
          title: Rule Id
        module:
          type: string
          enum:
            - seo
            - content
            - reviews
            - social
            - google-ads
            - audits
          title: Module
        panel:
          type: string
          title: Panel
        severity:
          type: string
          enum:
            - info
            - opportunity
            - risk
          title: Severity
          default: info
        title:
          type: string
          title: Title
        detail:
          type: string
          title: Detail
        evidence_refs:
          items:
            type: string
          type: array
          title: Evidence Refs
      type: object
      required:
        - rule_id
        - module
        - panel
        - title
        - detail
      title: ModuleRuleFinding
      description: |-
        One deterministic finding emitted by the rule engine.

        Every LLM recommendation must cite one of these ``rule_id``s, so the
        narrative can neither contradict nor exceed the measured layer.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
    ModuleCoverageNearDuplicatePair:
      properties:
        domain_a:
          type: string
          title: Domain A
        domain_b:
          type: string
          title: Domain B
        edit_distance:
          type: integer
          title: Edit Distance
      type: object
      required:
        - domain_a
        - domain_b
        - edit_distance
      title: ModuleCoverageNearDuplicatePair
      description: |-
        One "these might be the same brand" hint (audit Tier 0.3).

        Two cohort domains within ``edit_distance`` of each other on their
        REGISTRABLE (eTLD+1) form — e.g. ``acme.com``/``acm3.com`` (a typo), or
        ``blog.acme.com``/``shop.acme.com`` (two subdomains of the same apex,
        both normalizing to ``acme.com``, distance 0). This is what closed the
        ``warbybarker.com`` vs ``warbyparker.com`` gap the audit's D5 finding
        named (distance 1, exact-string dedup never caught it).

        **Advisory only.** The audit's own phrasing is "surface, don't
        auto-merge" — no code path may read this field to combine, drop, or
        otherwise alter which domains belong to a cohort. It exists purely so a
        coverage strip can ask the operator/user "are these two the same
        brand?" instead of silently double-counting or silently guessing.
    ContentSourceFailureRow:
      properties:
        domain:
          type: string
          title: Domain
        url:
          type: string
          title: Url
          default: ''
        source_type:
          type: string
          title: Source Type
          default: unknown
        error:
          anyOf:
            - type: string
            - type: 'null'
          title: Error
      type: object
      required:
        - domain
      title: ContentSourceFailureRow
    ContentCadenceRow:
      properties:
        domain:
          type: string
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        measured:
          type: boolean
          title: Measured
          default: false
        posts_last_30d:
          anyOf:
            - type: integer
            - type: 'null'
          title: Posts Last 30D
        avg_cadence_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Avg Cadence Days
        cadence_span_days:
          anyOf:
            - type: number
            - type: 'null'
          title: Cadence Span Days
        cadence_suppressed_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Cadence Suppressed Reason
      type: object
      required:
        - domain
      title: ContentCadenceRow
      description: >-
        One domain's publishing velocity.


        ``avg_cadence_days`` is the average interval between DATED POSTS
        OBSERVED,

        not a rate over the 30-day window: eight posts inside a 1.4-day burst

        publish 0.2, which is correct and is not a 30-day rate.
        ``cadence_span_days``

        carries that basis so the number never travels without it.


        INVARIANT (``cadence_consistency.check_cadence``): ``avg_cadence_days``
        and

        ``cadence_span_days`` are both set or both ``None``, and

        ``cadence_suppressed_reason`` is non-``None`` only in the second case —
        it

        names a PROVABLE contradiction with ``posts_last_30d``, never a merely

        surprising value.
    ContentFormatMixRow:
      properties:
        content_format:
          type: string
          title: Content Format
        cohort_count:
          type: integer
          title: Cohort Count
          default: 0
        cohort_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Cohort Share Pct
        cohort_domains:
          type: integer
          title: Cohort Domains
          default: 0
        brand_count:
          type: integer
          title: Brand Count
          default: 0
        brand_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Brand Share Pct
      type: object
      required:
        - content_format
      title: ContentFormatMixRow
    ContentPillarRow:
      properties:
        pillar:
          type: string
          title: Pillar
        cohort_domains:
          type: integer
          title: Cohort Domains
          default: 0
        brand_covers:
          type: boolean
          title: Brand Covers
          default: false
        example_domains:
          items:
            type: string
          type: array
          title: Example Domains
        evidence:
          type: string
          title: Evidence
          default: title_term
        demand_keywords:
          items:
            type: string
          type: array
          title: Demand Keywords
        search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Search Volume
      type: object
      required:
        - pillar
      title: ContentPillarRow
      description: >-
        One topic, and how well it is EVIDENCED.


        ``evidence`` is the strongest source any covering domain had for this
        term:


        * ``strategy_label`` — the domain's own stored content-strategy pillar.
        A
          deliberate, tool-forced classification.
        * ``item_tag`` — a tag on a published item.

        * ``title_term`` — a token or adjacent pair that RECURS across that
          domain's headlines. Real evidence, but inferred, and the source of the
          fragments ("answer children") that used to fill the whitespace bucket.

        Rendering the three identically is what made an inferred fragment read
        as

        an authored pillar, so the field is carried to the UI rather than being

        collapsed here.
    ContentPillarRollupRow:
      properties:
        pillar:
          type: string
          maxLength: 120
          minLength: 1
          title: Pillar
        evidence:
          anyOf:
            - type: string
              enum:
                - strategy_label
                - item_tag
                - title_term
            - type: 'null'
          title: Evidence
        demand_keywords:
          items:
            type: string
          type: array
          maxItems: 5
          title: Demand Keywords
        search_volume:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Search Volume
        cpc:
          anyOf:
            - type: number
              minimum: 0
            - type: 'null'
          title: Cpc
        competitor_domains_measured:
          type: integer
          minimum: 0
          title: Competitor Domains Measured
          default: 0
        competitor_domains_covering:
          type: integer
          minimum: 0
          title: Competitor Domains Covering
          default: 0
        competitor_count_total:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Competitor Count Total
        competitor_median_count:
          anyOf:
            - type: number
              minimum: 0
            - type: 'null'
          title: Competitor Median Count
        brand_measured:
          type: boolean
          title: Brand Measured
          default: false
        brand_covers:
          type: boolean
          title: Brand Covers
          default: false
        brand_count:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Brand Count
        brand_share_pct:
          anyOf:
            - type: number
              maximum: 100
              minimum: 0
            - type: 'null'
          title: Brand Share Pct
        brand_rank:
          anyOf:
            - type: integer
              minimum: 1
            - type: 'null'
          title: Brand Rank
        brand_tied_count:
          anyOf:
            - type: integer
              minimum: 1
            - type: 'null'
          title: Brand Tied Count
        gap_domains:
          items:
            type: string
          type: array
          maxItems: 60
          title: Gap Domains
        examples:
          items:
            $ref: '#/components/schemas/ContentPillarRollupExample'
          type: array
          maxItems: 5
          title: Examples
        domain_cells:
          items:
            $ref: '#/components/schemas/ContentPillarRollupDomainCell'
          type: array
          maxItems: 61
          title: Domain Cells
      type: object
      required:
        - pillar
      title: ContentPillarRollupRow
      description: >-
        One frozen topic vocabulary row across the selected Content cohort.


        Counts describe the bounded ``latest_items`` sample, not all-time
        output.

        ``brand_rank`` compares per-domain sample shares, not raw item counts,
        so

        unequal bounded sample sizes remain like-for-like.

        A domain with a measured zero stays in the competitor median and
        ranking;

        a domain whose item sample was unavailable is excluded and disclosed by

        ``competitor_domains_measured``. The median is competitors-only so the

        brand never moves its own benchmark.
    ContentHeadlinePatternRow:
      properties:
        pattern:
          type: string
          title: Pattern
        cohort_count:
          type: integer
          title: Cohort Count
          default: 0
        cohort_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Cohort Share Pct
        cohort_domains:
          type: integer
          title: Cohort Domains
          default: 0
        brand_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Count
        brand_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Brand Share Pct
      type: object
      required:
        - pattern
      title: ContentHeadlinePatternRow
      description: >-
        One headline SHAPE, and how much of each corpus carries it.


        The classes are NOT mutually exclusive — "7 Ways To Fix X — Which
        Works?"

        is number-led AND a question AND subtitled — so these shares can sum
        above

        100%. ``ContentHeadlineLengthBucketRow`` is the exclusive partition.


        ``brand_count`` and ``brand_share_pct`` are ``None``, never ``0``, when
        the

        brand's own corpus is below the panel's ``min_brand_titles`` floor. A

        ``0.0`` share on a measured corpus is a real finding ("none of their

        headlines ask a question") and must stay distinguishable from it.
    ContentHeadlineLengthBucketRow:
      properties:
        bucket:
          type: string
          title: Bucket
        cohort_count:
          type: integer
          title: Cohort Count
          default: 0
        cohort_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Cohort Share Pct
        brand_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Count
        brand_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Brand Share Pct
      type: object
      required:
        - bucket
      title: ContentHeadlineLengthBucketRow
      description: >-
        One character-length band. Exclusive and exhaustive: the cohort counts

        sum back to ``cohort_titles_measured`` exactly.


        Bounds are keyed to the ~60-character point where search results and
        most

        social cards truncate a title.
    ContentLeadMagnetRow:
      properties:
        play:
          type: string
          title: Play
        cohort_domains:
          type: integer
          title: Cohort Domains
          default: 0
        cohort_count:
          type: integer
          title: Cohort Count
          default: 0
        brand_count:
          type: integer
          title: Brand Count
          default: 0
        brand_uses:
          type: boolean
          title: Brand Uses
          default: false
      type: object
      required:
        - play
      title: ContentLeadMagnetRow
    ContentDemandTerm:
      properties:
        term:
          type: string
          title: Term
        content_domains:
          type: integer
          title: Content Domains
          default: 0
        brand_publishes:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Brand Publishes
        demand_keywords:
          items:
            type: string
          type: array
          title: Demand Keywords
        search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Search Volume
        cpc:
          anyOf:
            - type: number
              minimum: 0
            - type: 'null'
          title: Cpc
        competitor_ranking_domains:
          type: integer
          title: Competitor Ranking Domains
          default: 0
        gap_domains:
          items:
            type: string
          type: array
          title: Gap Domains
      type: object
      required:
        - term
      title: ContentDemandTerm
    ContentAdsTerm:
      properties:
        term:
          type: string
          title: Term
        content_domains:
          type: integer
          title: Content Domains
          default: 0
        brand_publishes:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Brand Publishes
        ad_keywords:
          items:
            type: string
          type: array
          title: Ad Keywords
        search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Search Volume
        cpc:
          anyOf:
            - type: number
              minimum: 0
            - type: 'null'
          title: Cpc
        competitor_advertiser_domains:
          type: integer
          title: Competitor Advertiser Domains
          default: 0
      type: object
      required:
        - term
      title: ContentAdsTerm
    ContentAiVisibilityRow:
      properties:
        brand:
          type: string
          maxLength: 300
          title: Brand
        domain:
          type: string
          maxLength: 253
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        answers_naming:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Answers Naming
        mention_share:
          anyOf:
            - type: number
              maximum: 1
              minimum: 0
            - type: 'null'
          title: Mention Share
        answers_citing:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Answers Citing
        citation_share:
          anyOf:
            - type: number
              maximum: 1
              minimum: 0
            - type: 'null'
          title: Citation Share
      type: object
      required:
        - brand
        - domain
      title: ContentAiVisibilityRow
      description: Observed assistant visibility for one selected Content-cohort domain.
    ContentAiVisibilityEvidence:
      properties:
        prompt:
          type: string
          maxLength: 2000
          title: Prompt
        provider:
          anyOf:
            - type: string
              const: dataforseo
            - type: 'null'
          title: Provider
        platform:
          type: string
          maxLength: 32
          title: Platform
          default: ''
        model:
          type: string
          maxLength: 120
          title: Model
          default: ''
        mention_observed:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Mention Observed
        sources_observed:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Sources Observed
        check_url:
          type: string
          maxLength: 2048
          title: Check Url
          default: ''
        citations:
          anyOf:
            - items:
                $ref: '#/components/schemas/CompetitorAiSovCitationEvidence'
              type: array
              maxItems: 51
            - type: 'null'
          title: Citations
      type: object
      required:
        - prompt
      title: ContentAiVisibilityEvidence
      description: Bounded provider/source provenance; raw assistant prose is excluded.
    ContentPagePerformanceRow:
      properties:
        domain:
          type: string
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        page_url:
          type: string
          maxLength: 2000
          title: Page Url
        title:
          anyOf:
            - type: string
              maxLength: 300
            - type: 'null'
          title: Title
        organic_etv:
          anyOf:
            - type: number
              minimum: 0
            - type: 'null'
          title: Organic Etv
        organic_keyword_count:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Organic Keyword Count
        organic_top_3:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Organic Top 3
        organic_top_10:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Organic Top 10
        backlinks:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Backlinks
        referring_domains:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Referring Domains
        ai_mentions:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Ai Mentions
        ai_search_volume:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Ai Search Volume
        ai_platform:
          anyOf:
            - type: string
              maxLength: 32
            - type: 'null'
          title: Ai Platform
        sources:
          items:
            type: string
          type: array
          maxItems: 4
          title: Sources
      type: object
      required:
        - domain
        - page_url
      title: ContentPagePerformanceRow
      description: >-
        One content URL with whatever the page indexes could say about it.


        Every metric is independently nullable. The organic half and the link
        half

        come from two separately-gated providers, so a row carrying organic
        numbers

        and blank link numbers is the normal shape when

        ``dataforseo_backlinks_enabled`` is off. ``sources`` names which
        providers

        actually answered for this URL so a reader never has to infer that from
        a

        pattern of nulls.
    ContentTechStackRow:
      properties:
        technology:
          type: string
          title: Technology
        category:
          type: string
          title: Category
        category_label:
          type: string
          title: Category Label
        detected_domains:
          items:
            type: string
          type: array
          title: Detected Domains
        detected_count:
          type: integer
          title: Detected Count
          default: 0
        on_brand:
          anyOf:
            - type: boolean
            - type: 'null'
          title: On Brand
      type: object
      required:
        - technology
        - category
        - category_label
      title: ContentTechStackRow
      description: >-
        One detected technology, and which cohort domains it was found on.


        ``detected_domains`` is a POSITIVE-evidence list. There is deliberately
        no

        ``absent_domains`` counterpart: that list would be the set of domains
        where

        the scan happened not to fire, which is not the set of domains that do
        not

        use the technology, and no amount of surrounding copy survives a reader

        seeing the two rendered as symmetrical columns.
    ContentEvidenceItem:
      properties:
        title:
          type: string
          maxLength: 300
          minLength: 1
          title: Title
        url:
          anyOf:
            - type: string
              maxLength: 2000
            - type: 'null'
          title: Url
        published_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Published At
        media_type:
          anyOf:
            - type: string
              maxLength: 40
            - type: 'null'
          title: Media Type
        tags:
          items:
            type: string
          type: array
          maxItems: 5
          title: Tags
      type: object
      required:
        - title
      title: ContentEvidenceItem
      description: One stored publisher example safe to render as supporting evidence.
    ContentPillarRollupExample:
      properties:
        domain:
          type: string
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        title:
          type: string
          maxLength: 300
          minLength: 1
          title: Title
        url:
          anyOf:
            - type: string
              maxLength: 2000
            - type: 'null'
          title: Url
        published_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Published At
      type: object
      required:
        - domain
        - title
      title: ContentPillarRollupExample
      description: One bounded stored item that was classified into a roll-up pillar.
    ContentPillarRollupDomainCell:
      properties:
        domain:
          type: string
          title: Domain
        is_brand:
          type: boolean
          title: Is Brand
          default: false
        measured:
          type: boolean
          title: Measured
          default: false
        covers:
          type: boolean
          title: Covers
          default: false
        count:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Count
        share_pct:
          anyOf:
            - type: number
              maximum: 100
              minimum: 0
            - type: 'null'
          title: Share Pct
        evidence:
          anyOf:
            - type: string
              enum:
                - strategy_label
                - item_tag
                - title_term
            - type: 'null'
          title: Evidence
      type: object
      required:
        - domain
      title: ContentPillarRollupDomainCell
      description: >-
        One (pillar, domain) cell of the coverage heatmap (audit Tier 3.4).


        This is the exact per-domain ``_PillarDomainRollup`` sample the service

        already computes internally for every cohort domain to build

        ``competitor_domains_covering`` / ``competitor_median_count`` /

        ``examples`` on the parent row — the cell just stops discarding it
        before

        the response is serialized. Not a frontend recomputation from row-level

        aggregates; every field here traces to the same stored bounded item

        sample the row's own aggregates are drawn from.


        ``measured=False`` means this domain had no honest bounded item sample

        for this pillar's row (never a claim of zero coverage — mirrors

        ``_pillar_item_sample_measured``). ``count`` and ``share_pct`` are

        ``None`` in that case; a renderer MUST show "not measured", never a

        false-zero cell. ``covers`` is the same stronger "has evidence" bar used

        by ``competitor_domains_covering`` / ``brand_covers`` on the parent row
        —

        a domain can have ``count > 0`` with ``covers=False`` (an incidental

        headline hit, not yet a recurring topic).
    CompetitorAiSovCitationEvidence:
      properties:
        url:
          type: string
          maxLength: 2048
          title: Url
        title:
          type: string
          maxLength: 300
          title: Title
          default: ''
      type: object
      required:
        - url
      title: CompetitorAiSovCitationEvidence
  securitySchemes:
    HTTPBearer:
      type: http
      scheme: bearer

````