{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.revenexx.com/search.schema.json",
  "title": "revenexx search.json (v2)",
  "description": "Declarative search contract for a revenexx app. One app ships one search.json. The platform turns each collection into (1) a flat, denormalised projection table maintained by Baseline, (2) a Typesense collection + Sequin sink (record_only) in the Search service, and later (3) Superset datasets. The app developer only declares WHAT is searchable and WHERE each field comes from via `from` locators — never SQL, triggers, transforms, or CDC wiring.",
  "type": "object",
  "required": ["version", "collections"],
  "additionalProperties": false,
  "properties": {
    "$schema": { "type": "string" },
    "version": {
      "description": "Contract major version. v2 introduces `from` locators + projection-based resolution.",
      "const": "2"
    },
    "collections": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/collection" }
    }
  },
  "$defs": {
    "collection": {
      "type": "object",
      "required": ["name", "source", "fields"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "description": "Logical collection name. The Typesense collection is namespaced per tenant as {tenant}__{name}.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "source": {
          "description": "The Baseline-managed entity this collection is built from (the entity name from schema.json, e.g. \"products\"). Baseline emits the projection table {vendor}__{app}__{source}__search and points the sink at it.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "key": {
          "description": "Primary key column on the source entity. Document id in Typesense. The projection is keyed (tenant_id, key[, locale]).",
          "type": "string",
          "default": "id",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "scope": {
          "$ref": "#/$defs/scope"
        },
        "fields": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/field" }
        },
        "default_sort": {
          "description": "Field used as Typesense default_sorting_field. Must name a sortable field above.",
          "type": "string"
        },
        "depends_on": {
          "description": "Other source entities whose changes invalidate a document (e.g. product_categories, categories, assets, reference_entity_records). Baseline derives the trigger/dirty-queue set from this list so the projection stays fresh without the app writing triggers.",
          "type": "array",
          "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$" },
          "default": []
        },
        "typesense": {
          "description": "Optional Typesense collection tuning passed through verbatim.",
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "token_separators": { "type": "array", "items": { "type": "string" } },
            "symbols_to_index": { "type": "array", "items": { "type": "string" } },
            "enable_nested_fields": { "type": "boolean" }
          }
        }
      }
    },
    "scope": {
      "description": "How channel/locale visibility shapes the index. `facet` (default) keeps one document per (key, locale) and exposes scope dimensions as filterable facets — visibility is filtered at query time. `materialize` produces one document per (key, locale, <scope values>) for hard per-scope isolation (heavier; only when needed).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "by": {
          "type": "array",
          "items": { "type": "string", "enum": ["channel", "locale"] },
          "default": []
        },
        "mode": {
          "type": "string",
          "enum": ["facet", "materialize"],
          "default": "facet"
        }
      }
    },
    "field": {
      "type": "object",
      "required": ["name", "from"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "description": "Field name in the Typesense document and projection column.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "from": {
          "description": "Declarative source locator. Verbs Baseline understands:\n  column:<col>                          a scalar column on the source entity\n  attribute:common.<key>                a key under attribute_values.common\n  attribute:common.*                    all remaining common keys, as a jsonb bucket\n  attribute:locale.<key>                a key under attribute_values.locale_specific.<locale>\n  relation:<join>→<entity>.<col>[]       aggregate a column over a join (array)\n  relation:<join>→<entity>.<col>.<lk>[]  aggregate a localised label (jsonb → <locale>)\n  reference:<attr>.label                 resolve a reference_entity_record label by its code held in attribute_values\n  asset:<slot>.url                       resolve an asset URL linked to the product\n  computed:<name>                        a platform-provided computed value (e.g. completeness)",
          "type": "string",
          "pattern": "^(column:[a-z][a-z0-9_]*|attribute:(common|locale)\\.([a-z0-9_]+|\\*)|relation:[a-z][a-z0-9_]*→[a-z][a-z0-9_]*\\.[a-z0-9_]+(\\.[a-z0-9_<>]+)?\\[\\]|reference:[a-z][a-z0-9_]*\\.label|asset:[a-z][a-z0-9_]*\\.url|computed:[a-z][a-z0-9_]*)$"
        },
        "type": {
          "description": "Typesense field type. Inferred when omitted (text for strings, int64 for timestamps, string[] for relation arrays).",
          "type": "string",
          "enum": ["string", "string[]", "int32", "int64", "float", "bool", "bool[]", "object", "object[]", "auto", "geopoint"]
        },
        "facet": {
          "description": "Expose as a Typesense facet (filter/aggregate).",
          "type": "boolean",
          "default": false
        },
        "sort": {
          "description": "Allow sorting on this field.",
          "type": "boolean",
          "default": false
        },
        "optional": {
          "description": "Field may be absent/null.",
          "type": "boolean",
          "default": false
        },
        "locale": {
          "description": "Field is locale-specific: its value is resolved per the document's locale. Forces one document per available locale.",
          "type": "boolean",
          "default": false
        },
        "index": {
          "description": "Whether Typesense indexes the field for search (default true). Set false for retrieve-only fields like image URLs.",
          "type": "boolean",
          "default": true
        }
      }
    }
  }
}
