{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.revenexx.com/analytics.schema.json",
  "title": "revenexx analytics.json (v1)",
  "description": "Declarative analytics contract for a revenexx app. One app ships one analytics.json. The platform turns each view into a flat, denormalised SQL view over the app's complex source entities — collapsing scoped JSONB attribute_values, relations and references into analysable columns. Reporting and BI tools read these flat views as datasets. Tenant isolation is NOT the app's concern and cannot be opted out of: the platform always creates every view WITH (security_invoker = true) so the base tables' Row-Level Security is enforced as the querying role (analytics_reader). Storage strategy (plain vs. materialized) is likewise a platform/ops decision, not an app one. The app developer only declares WHAT is analysable and WHERE each column comes from via `from` locators (the same vocabulary as search.json) — never SQL, never security or storage knobs. The sibling of search.json: search projects for full-text, analytics projects for reporting.",
  "type": "object",
  "required": ["version", "views"],
  "additionalProperties": false,
  "properties": {
    "$schema": { "type": "string" },
    "version": {
      "description": "Contract major version.",
      "const": "1"
    },
    "views": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/view" }
    }
  },
  "$defs": {
    "view": {
      "type": "object",
      "required": ["name", "source", "columns"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "description": "Logical view name. The platform emits analytics.{vendor}__{app}__{name} in the apps database.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "source": {
          "description": "The platform-managed entity this view is built from (the entity name from schema.json, e.g. \"products\").",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "key": {
          "description": "Primary key column on the source entity.",
          "type": "string",
          "default": "id",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "default_locale": {
          "description": "Locale used to resolve attribute:locale.<key> columns into a single scalar (analytics is one row per key, not per locale). Per-locale breakouts can be added as explicit columns.",
          "type": "string",
          "default": "en",
          "pattern": "^[a-z]{2}(-[A-Z]{2})?$"
        },
        "columns": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/column" }
        },
        "depends_on": {
          "description": "Other source entities this view reads from (joins/relations). Documents the data dependencies; reserved for the platform to drive caching/refresh if it later chooses to materialise a view.",
          "type": "array",
          "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$" },
          "default": []
        },
        "title": {
          "description": "Human title for the view in analytics UIs (Reports catalog). Defaults to a humanised name.",
          "type": "string"
        },
        "description": {
          "description": "Short description shown for the view in the analytics Reports catalog.",
          "type": "string"
        },
        "roles": {
          "description": "Semantic roles that drive the analytics UIs (Report shell date-range/trend, cohorts, segments). All optional — the studio falls back to column-type heuristics when a role is absent. Names must refer to columns declared above.",
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "time": {
              "description": "Primary time column for date-range scoping and time-series trends.",
              "type": "string",
              "pattern": "^[a-z][a-z0-9_]{0,62}$"
            },
            "entity": {
              "description": "Entity key for cohort analysis (e.g. the customer or product id).",
              "type": "string",
              "pattern": "^[a-z][a-z0-9_]{0,62}$"
            },
            "measures": {
              "description": "Numeric columns meaningful to aggregate (sum/avg) as report metrics.",
              "type": "array",
              "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$" },
              "default": []
            },
            "dimensions": {
              "description": "Columns meaningful to group or segment by.",
              "type": "array",
              "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$" },
              "default": []
            }
          }
        }
      }
    },
    "column": {
      "type": "object",
      "required": ["name", "from"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "description": "Column name in the flat view.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,62}$"
        },
        "from": {
          "description": "Declarative source locator (same vocabulary as search.json, with scalar relations added for joins). Verbs the platform 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 common keys as a jsonb bucket\n  attribute:locale.<key>                a key under attribute_values.locale_specific.<default_locale>\n  relation:<fk>→<entity>.<col>           scalar: follow an FK column to one row\n  relation:<join>→<entity>.<col>[]       array: aggregate a column over a join table\n  relation:<join>→<entity>.<col>.<lk>[]  array of a localised label (jsonb → <default_locale>)\n  reference:<attr>.label                 resolve a reference_entity_record label by code held in attribute_values\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|computed:[a-z][a-z0-9_]*)$"
        },
        "type": {
          "description": "Postgres type the column is cast to. Inferred when omitted (text for strings/attributes, the source column type for column:, text[] for array relations).",
          "type": "string",
          "enum": ["text", "text[]", "int", "bigint", "numeric", "bool", "timestamptz", "date", "uuid", "jsonb"]
        },
        "optional": {
          "description": "Column may be null (no error if the source attribute/relation is absent).",
          "type": "boolean",
          "default": true
        }
      }
    }
  }
}
