{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.rasd.dev/form/v1.json",
  "title": "Rasd Form Definition (RFD) v1",
  "description": "Machine-readable contract for a Rasd Forms form definition: one self-contained JSON document that carries layout (pages/elements), logic (REL v1 expressions), translations (localized strings), choice lists, dataset references, settings, theme hints and vendor payload (`ext`). Property names are camelCase; element `type` values are snake_case (XLSForm-compatible where one exists) or `x:<name>` for host-registered custom elements. Everything that a JSON Schema cannot express (name uniqueness, references between elements/lists/datasets/pages, expression parsing, calculation cycles) is enforced by the semantic pass of `validateFormDefinition()` in @rasd/core and by `rasd validate` (@rasd/cli). This schema is deliberately stricter than the runtime: the runtime ignores-and-preserves unknown properties within a MAJOR (warning W_UNKNOWN_PROPERTY), the schema rejects them so authors and CI catch typos early. `https://schemas.rasd.dev/form/v1.json` always resolves to the latest 1.x; frozen copies live at /form/v1.0.json, /form/v1.1.json, ….",
  "$comment": "Normative sources: docs/00-decisions-and-conventions.md §4–§5 and docs/04-form-schema-spec.md. In the @rasd/core repository this file is generated from the zod schemas (pnpm schema:build) and CI fails when the two drift. Discriminated unions (elements, validators, trigger actions, matrix columns) are expressed as if/then chains keyed on `type` so validators produce precise errors instead of a 30-branch oneOf failure. Custom annotation keywords `x-rasd-expr` and `x-rasd-localized` mark the two shared primitives (Expr, LocalizedString); strict validators must register them as a vocabulary (see schema/README.md).",
  "type": "object",
  "required": ["rasd", "id", "version", "meta", "settings", "pages"],
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri-reference",
      "description": "URI of the RFD JSON Schema this document was written against, normally https://schemas.rasd.dev/form/v1.json (or a frozen minor such as /form/v1.0.json, or a relative path in a repository). Ignored by the runtime; used by editors for autocompletion and by CI."
    },
    "rasd": {
      "type": "string",
      "pattern": "^1\\.(0|[1-9][0-9]*)$",
      "description": "RFD spec version this document targets, as MAJOR.MINOR. This schema covers MAJOR 1. Consumers on the same MAJOR must ignore-and-preserve unknown properties; MAJOR bumps ship a converter; deprecations live at least 12 months."
    },
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$",
      "description": "Stable form slug, unique per organisation (e.g. \"pdm-gfd-2026\"). Changing it creates a new form; all version history and submissions stay attached to the old id."
    },
    "version": {
      "type": "string",
      "minLength": 1,
      "maxLength": 32,
      "description": "Monotonically increasing version string per `id` (numeric-aware segment compare on '.', '-', '_'). A published (id, version) pair is immutable; the server rejects re-publishing an existing version and any content change to it. Every submission records the formVersion and the definitionHash it was captured with."
    },
    "requires": { "$ref": "#/$defs/Requires" },
    "meta": { "$ref": "#/$defs/Meta" },
    "settings": { "$ref": "#/$defs/Settings" },
    "choiceLists": {
      "type": "object",
      "description": "Named, reusable choice lists referenced by select_one / select_multiple / rank / matrix elements through props.list. Keys are list names (element-name grammar). Elements may instead carry inline props.choices with the identical Choice shape.",
      "propertyNames": { "$ref": "#/$defs/Name" },
      "additionalProperties": { "$ref": "#/$defs/ChoiceList" }
    },
    "datasets": {
      "type": "array",
      "description": "Reference tables pulled to the device (or embedded inline) and queried by dataset-backed choice lists and by pulldata(). Datasets are never pinned to a form version; a newer dataset is always allowed on device.",
      "items": { "$ref": "#/$defs/Dataset" },
      "maxItems": 100
    },
    "pages": {
      "type": "array",
      "description": "Ordered screens (navigation \"paged\") or headed sections (navigation \"scroll\"). Pages never nest; at least one page is required. More than 200 pages is a runtime warning, more than 1000 an error.",
      "items": { "$ref": "#/$defs/Page" },
      "minItems": 1,
      "maxItems": 1000
    },
    "logic": { "$ref": "#/$defs/Logic" },
    "ext": { "$ref": "#/$defs/Ext" }
  },

  "$defs": {
    "LocalizedString": {
      "title": "LocalizedString",
      "description": "Human-readable text: either a plain string (interpreted in settings.defaultLocale) or a map from BCP 47 locale to string, e.g. { \"en\": \"Yes\", \"ar\": \"نعم\" }. Resolution: exact tag → language-only parent → defaultLocale → any locale with a value. Strings may use the Rasd Mini-Message ICU subset ({name}, {n, plural, …}, {x, select, …}, # and '' escaping) and a Markdown-safe subset (emphasis, links, lists, line breaks). Raw HTML is stripped, never rendered.",
      "x-rasd-localized": true,
      "anyOf": [
        { "type": "string", "maxLength": 65536 },
        {
          "type": "object",
          "minProperties": 1,
          "propertyNames": { "$ref": "#/$defs/Bcp47" },
          "additionalProperties": { "type": "string", "maxLength": 65536 }
        }
      ]
    },
    "Expr": {
      "title": "Expr",
      "description": "Rasd Expression Language (REL) v1 source text. Field references are ${name} (sibling in the current repeat instance), ${../name} (parent scope), ${/name} (absolute), ${repeat[2].field} (1-based index), ${repeat[].field} (array); ${meta.*} for preload metadata; '.' is the element's own value inside constraint/validators; bare identifiers are only legal inside choiceFilter where they name a column of the candidate choice. Parsed statically at load time (never eval'd); parse failures are E_EXPR_PARSE. Limits: 4,000 characters, 5,000 AST nodes.",
      "x-rasd-expr": true,
      "type": "string",
      "minLength": 1,
      "maxLength": 4000
    },
    "Bcp47": {
      "title": "Bcp47",
      "description": "BCP 47 language tag such as \"en\", \"ar\", \"ar-JO\", \"ckb\", \"zh-Hant\". Direction (rtl/ltr) is derived from the language subtag unless overridden in settings.localeMeta.",
      "type": "string",
      "pattern": "^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$"
    },
    "Name": {
      "title": "Name",
      "description": "Identifier grammar shared by element names, choice-list keys, dataset names, dataset columns and choice attribute keys: ^[a-zA-Z_][a-zA-Z0-9_]*$, at most 64 characters. Names are case-sensitive but must be unique case-insensitively within their namespace (SQL/CSV consumers are not case-sensitive).",
      "type": "string",
      "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
      "maxLength": 64
    },
    "DataKey": {
      "title": "DataKey",
      "description": "An element or calculated-value name: it becomes the storage key in submission.data, so reserved keys used by Rasd, XLSForm metadata and Kobo/ODK export columns are forbidden, as is anything starting with two underscores (reserved for Rasd). Names starting with a single underscore are legal but trigger W_UNDERSCORE_NAME. Uniqueness within the repeat scope and the no-shadowing rule are enforced by the semantic pass.",
      "allOf": [
        { "$ref": "#/$defs/Name" },
        {
          "type": "string",
          "not": {
            "anyOf": [
              {
                "type": "string",
                "enum": ["meta", "id", "formId", "formVersion", "status", "data", "attachments", "audit", "ext", "instanceID", "deprecatedID", "instanceName", "formhub", "start", "end", "today", "deviceid", "username", "email", "phonenumber", "subscriberid", "simserial", "_id", "_uuid", "__version__", "_submission_time", "_index", "_parent_index", "_parent_table_name", "KEY", "PARENT_KEY"]
              },
              { "type": "string", "pattern": "^__" }
            ]
          }
        }
      ]
    },
    "PageId": {
      "title": "PageId",
      "description": "Identifier grammar for page ids and trigger ids: ^[a-zA-Z_][a-zA-Z0-9_-]*$, at most 64 characters, unique within its own namespace.",
      "type": "string",
      "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$",
      "maxLength": 64
    },
    "ChoiceValue": {
      "title": "ChoiceValue",
      "description": "Stored value of a choice: 1–128 characters without whitespace (select_multiple values are space-separated on XForm export), unique within its list.",
      "type": "string",
      "pattern": "^[^\\s]{1,128}$"
    },
    "Ext": {
      "title": "Ext",
      "description": "Custom payload that Rasd never interprets and always round-trips (load → builder → save → hash → diff → storage → sync → XLSForm export). Keys are vendor keys — reverse-DNS (\"org.wfp.moda\") or an org slug (\"unrwa\"); values are any JSON. Reserved vendor keys written by Rasd tooling: org.getodk.xpath, org.getodk.xlsform, org.kobotoolbox, dev.rasd.builder, dev.rasd.geo. Prototype-pollution keys are rejected. Runtime warns when a single node's ext exceeds 16 KiB or all ext exceeds 25 % of the document.",
      "type": "object",
      "propertyNames": {
        "type": "string",
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$",
        "not": { "type": "string", "enum": ["constructor", "prototype", "__proto__"] }
      },
      "additionalProperties": {
        "description": "Opaque vendor payload (any JSON value)."
      }
    },
    "IsoDateTime": {
      "title": "IsoDateTime",
      "description": "ISO-8601 date-time string, UTC on the wire (e.g. \"2026-08-15T10:00:00Z\").",
      "type": "string",
      "format": "date-time"
    },
    "Media": {
      "title": "Media",
      "description": "Per-language media shown with a label or a choice for low-literacy respondents. Values are relative asset paths (\"assets/food_ar.mp3\", resolved by the host's resolveMedia and prefetched by the sync engine), https URLs on an allow-listed origin (W_MEDIA_REMOTE_URL), or data:image/* URIs up to 32 KiB for icons.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "image": { "$ref": "#/$defs/LocalizedString", "description": "Image reference (PNG/JPEG/SVG/WebP), per language when a map." },
        "audio": { "$ref": "#/$defs/LocalizedString", "description": "Audio clip reference (read-aloud label), per language when a map." },
        "video": { "$ref": "#/$defs/LocalizedString", "description": "Video reference, per language when a map." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "Severity": {
      "title": "Severity",
      "description": "\"error\" blocks finalize (default); \"warning\" is shown and logged as an audit event but does not block; \"info\" is shown only.",
      "type": "string",
      "enum": ["error", "warning", "info"]
    },

    "Requires": {
      "title": "Requires",
      "description": "Hard requirements. A consumer that cannot satisfy them MUST refuse to open the form (E_REQUIRES_UNMET) instead of degrading gracefully. Omit a feature to let old clients render a placeholder instead.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "rasd": {
          "type": "string",
          "maxLength": 32,
          "description": "Semver range on the RFD spec that the consumer must support, e.g. \">=1.2\"."
        },
        "features": {
          "type": "array",
          "uniqueItems": true,
          "maxItems": 64,
          "description": "Feature ids the consumer must implement: type:<elementType> (e.g. type:consent), fn:<function> (fn:pulldata), x:<name> (x:beneficiary-lookup), cap:<capability> (cap:encryption).",
          "items": { "type": "string", "pattern": "^(type|fn|x|cap):[a-zA-Z0-9_:-]{1,64}$" }
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Meta": {
      "title": "Meta",
      "description": "Descriptive metadata about the form. `updatedAt` is excluded from definitionHash; everything else is covered.",
      "type": "object",
      "required": ["title"],
      "additionalProperties": false,
      "properties": {
        "title": { "$ref": "#/$defs/LocalizedString", "description": "Form title shown in form lists and as the default page header. At most 200 characters per locale." },
        "description": { "$ref": "#/$defs/LocalizedString", "description": "Longer description (Markdown-safe subset), shown in form lists and the builder." },
        "tags": {
          "type": "array",
          "description": "Free-form tags used by the host's form list for filtering (e.g. [\"pdm\", \"gfd\"]).",
          "items": { "type": "string", "minLength": 1, "maxLength": 40 },
          "maxItems": 32,
          "uniqueItems": true
        },
        "author": { "type": "string", "maxLength": 200, "description": "Free text or email of the author/team." },
        "createdAt": { "$ref": "#/$defs/IsoDateTime", "description": "When this form id was first created (set by the builder)." },
        "updatedAt": { "$ref": "#/$defs/IsoDateTime", "description": "Last modification time (set by the builder). Not part of definitionHash." },
        "changelog": { "$ref": "#/$defs/LocalizedString", "description": "Human note for this version; the builder pre-fills it from diffDefinitions()." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Settings": {
      "title": "Settings",
      "description": "Form-wide behaviour: locales, navigation, autosave, instance naming, audit, encryption and theme hint. Semantic rules: defaultLocale must be a member of locales (E_DEFAULT_LOCALE_NOT_IN_LOCALES).",
      "type": "object",
      "required": ["defaultLocale", "locales"],
      "additionalProperties": false,
      "properties": {
        "defaultLocale": { "$ref": "#/$defs/Bcp47", "description": "Fallback locale for every LocalizedString. Must be one of `locales`." },
        "locales": {
          "type": "array",
          "description": "Ordered list of locales offered to the enumerator (first is the default UI order). Up to 20; more than 10 is a warning.",
          "items": { "$ref": "#/$defs/Bcp47" },
          "minItems": 1,
          "maxItems": 20,
          "uniqueItems": true
        },
        "navigation": {
          "type": "string",
          "enum": ["paged", "scroll"],
          "default": "paged",
          "description": "\"paged\" = one page per screen with Back/Next and per-page validation; \"scroll\" = one long screen where pages render as headed sections (skipTo actions become no-ops)."
        },
        "showProgress": { "type": "boolean", "default": true, "description": "Show a progress indicator (relevant pages completed / total)." },
        "allowDrafts": { "type": "boolean", "default": true, "description": "false hides the explicit \"Save draft\" control; autosave still writes a single recovery draft (never lose data) that is discarded on finalize or explicit abandon." },
        "autosaveMs": {
          "type": "integer",
          "minimum": 0,
          "maximum": 60000,
          "default": 2000,
          "description": "Autosave debounce in milliseconds. 0 = write on every change; values 1–249 are raised to 250 by the engine."
        },
        "instanceName": { "$ref": "#/$defs/Expr", "description": "REL expression evaluated on every change (root scope); its string result (≤ 200 chars) becomes submission.meta.instanceName — the human label shown in submission lists, e.g. concat(${hh_id}, ' – ', ${site}). Avoid bind.sensitive fields here (W_SENSITIVE_IN_INSTANCE_NAME)." },
        "submissionIdPrefix": {
          "type": "string",
          "pattern": "^[A-Z0-9]{1,8}$",
          "description": "Prefix for the human-readable short id shown in lists (e.g. \"PDM\" → PDM-7F3K2). The UUID v7 remains the real submission id."
        },
        "numbering": {
          "type": "string",
          "enum": ["latn", "native"],
          "default": "latn",
          "description": "Digit display for numeric inputs: Latin (ASCII) or the locale's native digits. Inputs always normalise to ASCII on save; display-only strings use native digits per locale."
        },
        "calendar": {
          "type": "string",
          "enum": ["gregorian", "islamic-umalqura"],
          "default": "gregorian",
          "description": "Default calendar for date display/pickers (display only; stored dates are always ISO Gregorian). Per-element props.calendar overrides it."
        },
        "localeMeta": {
          "type": "object",
          "description": "Per-locale overrides keyed by BCP 47 tag: text direction, numbering system and calendar. `dir` defaults from the language subtag (ar, ckb, fa, ps, ur, he, sd, ug → rtl).",
          "propertyNames": { "$ref": "#/$defs/Bcp47" },
          "additionalProperties": { "$ref": "#/$defs/LocaleMeta" }
        },
        "audit": { "$ref": "#/$defs/AuditSettings" },
        "encryption": { "$ref": "#/$defs/EncryptionSettings" },
        "theme": { "$ref": "#/$defs/ThemeSettings" },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "LocaleMeta": {
      "title": "LocaleMeta",
      "description": "Overrides for one locale.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "dir": { "type": "string", "enum": ["rtl", "ltr"], "description": "Layout direction for this locale (mirrors progress, icons, sliders; sets dir on web / useDirection() on native)." },
        "numbering": { "type": "string", "enum": ["latn", "native"], "description": "Digit system for this locale; overrides settings.numbering." },
        "calendar": { "type": "string", "enum": ["gregorian", "islamic-umalqura"], "description": "Calendar for this locale; overrides settings.calendar." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "AuditSettings": {
      "title": "AuditSettings",
      "description": "Audit trail configuration. When enabled, the engine emits the ODK-compatible event vocabulary (form start/exit/resume/save/finalize, question, jump, add/delete repeat, constraint error, …) into submission.audit. Consent events are always recorded regardless of this setting.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": false, "description": "Emit audit events into submission.audit." },
        "trackChanges": { "type": "boolean", "default": false, "description": "Log old/new values on every value event (bind.trackChanges overrides per element)." },
        "changeReasons": { "type": "string", "enum": ["never", "onEdit"], "default": "never", "description": "\"onEdit\" asks the user for a reason when editing a previously finalized submission (ODK track-changes-reasons=on-form-edit)." },
        "identifyUser": { "type": "boolean", "default": false, "description": "Include meta.userId in every audit event." },
        "location": { "$ref": "#/$defs/AuditLocation" },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "AuditLocation": {
      "title": "AuditLocation",
      "description": "Background location trail written into the audit log while the form is open (ODK audit location-priority / min-interval semantics).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": false, "description": "Record a location trail while the form is open (requires location permission; never blocks data entry)." },
        "priority": { "type": "string", "enum": ["no-power", "low-power", "balanced", "high-accuracy"], "default": "balanced", "description": "Provider priority (ODK names): trades battery for accuracy." },
        "minSeconds": { "type": "integer", "minimum": 1, "maximum": 3600, "default": 60, "description": "Minimum seconds between two recorded fixes." },
        "minMeters": { "type": "number", "minimum": 0, "maximum": 10000, "default": 50, "description": "Minimum displacement in metres between two recorded fixes." }
      }
    },
    "EncryptionSettings": {
      "title": "EncryptionSettings",
      "description": "Encryption intent for finalized data. \"field\": values of elements with bind.sensitive are encrypted at rest and in the payload with the key identified by publicKeyId; the server can read everything else. \"submission\": the whole finalized data + attachments are enveloped; the server can only store and forward. Key material never lives in the definition — the host key provider resolves publicKeyId.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": { "type": "string", "enum": ["none", "field", "submission"], "default": "none", "description": "Encryption mode." },
        "publicKeyId": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Identifier of the public key (resolved by the host). Required when mode is not \"none\" (E_ENCRYPTION_KEY_MISSING)." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "if": { "properties": { "mode": { "enum": ["field", "submission"] } }, "required": ["mode"] },
      "then": { "required": ["publicKeyId"] }
    },
    "ThemeSettings": {
      "title": "ThemeSettings",
      "description": "Theme hint only: the host's <RasdProvider theme> wins and a renderer may ignore it entirely.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "themeId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", "description": "Id of a theme known to the host or one of the built-ins: rasd-light, rasd-dark, rasd-high-contrast, rasd-field (big-touch outdoor)." },
        "overrides": {
          "type": "object",
          "description": "Partial theme applied on top of themeId. Must validate against #/$defs/ThemePartial of https://schemas.rasd.dev/theme/v1.json (tokens, modes, components, assets, ext — no identity keys); checked by `rasd theme check`."
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Choice": {
      "title": "Choice",
      "description": "One option of a choice list (inline or in choiceLists). The shape is identical wherever choices appear.",
      "type": "object",
      "required": ["value", "label"],
      "additionalProperties": false,
      "properties": {
        "value": { "$ref": "#/$defs/ChoiceValue", "description": "Stored value; unique within the list; no whitespace." },
        "label": { "$ref": "#/$defs/LocalizedString", "description": "Display label (localizable)." },
        "media": { "$ref": "#/$defs/Media", "description": "Per-language image/audio for the option." },
        "attrs": {
          "type": "object",
          "description": "Filter attributes (XLSForm extra choice columns) addressed as bare identifiers inside props.choiceFilter, e.g. attrs { \"gov_code\": \"JO-AM\" } with choiceFilter \"gov_code = ${governorate}\".",
          "propertyNames": { "$ref": "#/$defs/Name" },
          "additionalProperties": {
            "anyOf": [ { "type": "string", "maxLength": 512 }, { "type": "number" }, { "type": "boolean" } ]
          }
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "ChoiceList": {
      "title": "ChoiceList",
      "description": "A named choice list: either inline `choices` or a `source` pointing at a dataset (exactly one of the two — E_CHOICELIST_SOURCE). Dataset lists need valueKey/labelKey; filterKeys are the dataset columns compared with '=' in choiceFilter and are pushed down to the storage index (WHERE key = ? on SQLite, indexed on Dexie).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "choices": {
          "type": "array",
          "description": "Inline options. Warning above 500 items, error above 10,000 — use a dataset instead. Duplicate values are E_CHOICE_DUPLICATE.",
          "items": { "$ref": "#/$defs/Choice" },
          "minItems": 1,
          "maxItems": 10000
        },
        "source": {
          "type": "object",
          "description": "Dataset source: rows of the named dataset become candidate choices.",
          "required": ["type", "dataset"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "dataset", "description": "Only \"dataset\" exists in v1." },
            "dataset": { "$ref": "#/$defs/Name", "description": "Name of an entry in the root `datasets` array (E_DATASET_NOT_FOUND otherwise)." }
          }
        },
        "valueKey": { "$ref": "#/$defs/Name", "description": "Dataset column that supplies Choice.value. Required with `source`." },
        "labelKey": { "$ref": "#/$defs/Name", "description": "Dataset column that supplies Choice.label (a plain string; per-locale label columns are a 1.x extension). Required with `source`." },
        "filterKeys": {
          "type": "array",
          "description": "Dataset columns used with '=' in choiceFilter (cascading selects). Lists over 5,000 rows without filterKeys are W_FILTERKEYS_MISSING.",
          "items": { "$ref": "#/$defs/Name" },
          "uniqueItems": true,
          "maxItems": 8
        },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "oneOf": [
        { "required": ["choices"] },
        { "required": ["source"] }
      ],
      "dependentRequired": { "source": ["valueKey", "labelKey"] }
    },

    "Dataset": {
      "title": "Dataset",
      "description": "Reference table available offline: pulled through the Rasd Sync Protocol (source \"server\": GET /v1/datasets/{name}?since=…), fetched once from an https URL and cached (\"url\"), or embedded (\"inline\"). Queried by dataset-backed choice lists and pulldata('name', 'column', 'keyColumn', key).",
      "type": "object",
      "required": ["name", "source", "keyField"],
      "additionalProperties": false,
      "properties": {
        "name": { "$ref": "#/$defs/Name", "description": "Dataset name; unique within `datasets`; the first argument of pulldata()." },
        "source": { "type": "string", "enum": ["server", "inline", "url"], "description": "Where rows come from." },
        "keyField": { "$ref": "#/$defs/Name", "description": "Column holding the unique row key (ODK entity `name`); default key column for pulldata()." },
        "inline": {
          "type": "array",
          "description": "Rows when source is \"inline\". Warning above 2,000 rows, error above 10,000 — large tables belong on the server. Cell values are string, number, boolean or null.",
          "maxItems": 10000,
          "items": {
            "type": "object",
            "propertyNames": { "$ref": "#/$defs/Name" },
            "additionalProperties": {
              "anyOf": [ { "type": "string", "maxLength": 4000 }, { "type": "number" }, { "type": "boolean" }, { "type": "null" } ]
            }
          }
        },
        "url": {
          "type": "string",
          "pattern": "^https://",
          "maxLength": 2048,
          "description": "One-shot fetch location when source is \"url\" (https only; response is JSON array of row objects or CSV; cached in storage)."
        },
        "columns": {
          "type": "array",
          "description": "Optional declared schema. When present, valueKey/labelKey/filterKeys/pulldata column names are checked against it (E_DATASET_COLUMN_UNKNOWN / W_DATASET_COLUMN_UNKNOWN).",
          "items": { "$ref": "#/$defs/DatasetColumn" },
          "maxItems": 200
        },
        "minVersion": { "type": "string", "maxLength": 64, "description": "Refuse to render choices from a dataset version older than this (validation provenance)." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "allOf": [
        {
          "if": { "properties": { "source": { "const": "inline" } }, "required": ["source"] },
          "then": { "required": ["inline"] },
          "else": { "not": { "required": ["inline"] } }
        },
        {
          "if": { "properties": { "source": { "const": "url" } }, "required": ["source"] },
          "then": { "required": ["url"] },
          "else": { "not": { "required": ["url"] } }
        }
      ]
    },
    "DatasetColumn": {
      "title": "DatasetColumn",
      "description": "Declared dataset column.",
      "type": "object",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": { "$ref": "#/$defs/Name", "description": "Column name as used in valueKey/labelKey/filterKeys/pulldata and as a bare identifier in choiceFilter." },
        "type": { "type": "string", "enum": ["string", "number", "boolean"], "default": "string", "description": "Cell type. Key comparison in pulldata/choiceFilter is strict string equality after string() so P-codes like '05' never collide with 5." },
        "label": { "$ref": "#/$defs/LocalizedString", "description": "Human label for the column (builder/inspector)." }
      }
    },

    "Page": {
      "title": "Page",
      "description": "A screen (navigation \"paged\") or a headed section (\"scroll\"). Pages do not nest; use group for sub-structure. A page with zero elements is a warning (W_EMPTY_PAGE).",
      "type": "object",
      "required": ["id", "elements"],
      "additionalProperties": false,
      "properties": {
        "id": { "$ref": "#/$defs/PageId", "description": "Page id, unique across pages; the target of skipTo trigger actions." },
        "title": { "$ref": "#/$defs/LocalizedString", "description": "Page heading." },
        "description": { "$ref": "#/$defs/LocalizedString", "description": "Text under the heading (Markdown-safe subset)." },
        "relevant": { "$ref": "#/$defs/Expr", "description": "Skip logic for the whole page: when false the page is hidden, not validated, and its values are excluded on finalize (kept in the draft)." },
        "elements": {
          "type": "array",
          "description": "Elements in display order.",
          "items": { "$ref": "#/$defs/Element" },
          "maxItems": 2000
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Logic": {
      "title": "Logic",
      "description": "Form-level logic that has no position in the layout: calculated values and triggers.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "calculated": {
          "type": "array",
          "description": "Form-scope computed values (like calculate elements without a page). Warning above 500, error above 1,000.",
          "items": { "$ref": "#/$defs/Calculated" },
          "maxItems": 1000
        },
        "triggers": {
          "type": "array",
          "description": "Edge-triggered rules: when `when` transitions from falsy to truthy the actions run in order. Warning above 200, error above 500.",
          "items": { "$ref": "#/$defs/Trigger" },
          "maxItems": 500
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "Calculated": {
      "title": "Calculated",
      "description": "A named form-scope calculation. Shares the element namespace; referenced as ${name} in expressions and {name} in labels. Cannot live in a repeat scope (use a calculate element inside the repeat).",
      "type": "object",
      "required": ["name", "calculate"],
      "additionalProperties": false,
      "properties": {
        "name": { "$ref": "#/$defs/DataKey", "description": "Unique name (element namespace)." },
        "calculate": { "$ref": "#/$defs/Expr", "description": "REL expression recomputed whenever a dependency changes (topological order; cycles are E_CALC_CYCLE)." },
        "includeInData": { "type": "boolean", "default": false, "description": "true emits the value into submission.data under `name`." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "Trigger": {
      "title": "Trigger",
      "description": "Rule that fires actions on the rising edge of `when` (or on the given lifecycle event while `when` is truthy). setValue writes through the engine and may fire other triggers; cascades are capped at 8 (E_TRIGGER_LOOP / RASD_TRIGGER_LOOP).",
      "type": "object",
      "required": ["id", "when", "actions"],
      "additionalProperties": false,
      "properties": {
        "id": { "$ref": "#/$defs/PageId", "description": "Trigger id, unique among triggers; appears in audit events as source \"trigger:<id>\"." },
        "when": { "$ref": "#/$defs/Expr", "description": "Boolean REL expression, e.g. \"${consent}.granted = false\"." },
        "on": { "type": "string", "enum": ["change", "pageLeave", "finalize"], "default": "change", "description": "When the rule is evaluated: on any dependency change (default), when leaving the page that contains the dependencies, or at finalize." },
        "once": { "type": "boolean", "default": false, "description": "true fires at most once per submission; false fires on every false→true edge." },
        "actions": {
          "type": "array",
          "description": "Actions executed in order.",
          "items": { "$ref": "#/$defs/TriggerAction" },
          "minItems": 1,
          "maxItems": 20
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "TriggerAction": {
      "title": "TriggerAction",
      "description": "One trigger action, discriminated by `type`: setValue, clearValue, complete, skipTo, showMessage, custom.",
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "type": "string", "enum": ["setValue", "clearValue", "complete", "skipTo", "showMessage", "custom"], "description": "Action kind." }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "setValue" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/SetValueAction" } },
        { "if": { "properties": { "type": { "const": "clearValue" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ClearValueAction" } },
        { "if": { "properties": { "type": { "const": "complete" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CompleteAction" } },
        { "if": { "properties": { "type": { "const": "skipTo" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/SkipToAction" } },
        { "if": { "properties": { "type": { "const": "showMessage" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ShowMessageAction" } },
        { "if": { "properties": { "type": { "const": "custom" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CustomAction" } }
      ]
    },
    "SetValueAction": {
      "title": "SetValueAction",
      "description": "Write a value into a question through the engine (audit event `value`, source trigger:<id>). Exactly one of `value` / `expr`. The target must be an existing question that has no calculate expression (E_TRIGGER_TARGET_UNKNOWN / E_TRIGGER_TARGET_CALCULATED); readonly targets are legal.",
      "type": "object",
      "required": ["type", "target"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "setValue" },
        "target": { "$ref": "#/$defs/DataKey", "description": "Name of the target question." },
        "value": { "description": "Literal JSON value to write (must match the target's value type)." },
        "expr": { "$ref": "#/$defs/Expr", "description": "REL expression whose result is written." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "oneOf": [ { "required": ["value"] }, { "required": ["expr"] } ]
    },
    "ClearValueAction": {
      "title": "ClearValueAction",
      "description": "Clear a question's value (sets null, audit event `value`).",
      "type": "object",
      "required": ["type", "target"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "clearValue" },
        "target": { "$ref": "#/$defs/DataKey", "description": "Name of the target question." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "CompleteAction": {
      "title": "CompleteAction",
      "description": "Finalize now: remaining pages are skipped and their validation is not run; the message is shown on the end screen (\"Thank you, the interview ends here\").",
      "type": "object",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "complete" },
        "message": { "$ref": "#/$defs/LocalizedString", "description": "End-screen message." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "SkipToAction": {
      "title": "SkipToAction",
      "description": "Navigate to a page by id (paged navigation only; no-op in scroll). Unknown page ids are E_SKIPTO_UNKNOWN_PAGE.",
      "type": "object",
      "required": ["type", "page"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "skipTo" },
        "page": { "$ref": "#/$defs/PageId", "description": "Target page id." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "ShowMessageAction": {
      "title": "ShowMessageAction",
      "description": "Show a toast (default) or a blocking modal to the enumerator.",
      "type": "object",
      "required": ["type", "message"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "showMessage" },
        "message": { "$ref": "#/$defs/LocalizedString", "description": "Message text (Mini-Message interpolation allowed)." },
        "severity": { "$ref": "#/$defs/Severity", "description": "Visual severity; default \"info\"." },
        "blocking": { "type": "boolean", "default": false, "description": "true shows a modal that must be dismissed; false shows a toast." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "CustomAction": {
      "title": "CustomAction",
      "description": "Dispatched to the host's onTriggerAction(id, payload, ctx) handler; unknown ids are ignored with a console warning.",
      "type": "object",
      "required": ["type", "id"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "custom" },
        "id": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_.:-]{0,63}$", "description": "Host action id." },
        "payload": { "description": "Any JSON passed to the host handler." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Validator": {
      "title": "Validator",
      "description": "One entry of an element's validators[] array, discriminated by `type`: regex, range, length, expr, custom. Every validator accepts `severity` and a localized `message`. At most 20 per element.",
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "type": "string", "enum": ["regex", "range", "length", "expr", "custom"], "description": "Validator kind." }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "regex" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RegexValidator" } },
        { "if": { "properties": { "type": { "const": "range" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RangeValidator" } },
        { "if": { "properties": { "type": { "const": "length" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/LengthValidator" } },
        { "if": { "properties": { "type": { "const": "expr" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ExprValidator" } },
        { "if": { "properties": { "type": { "const": "custom" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CustomValidator" } }
      ]
    },
    "RegexValidator": {
      "title": "RegexValidator",
      "description": "JS RegExp test on string values (text, barcode, select values). Matched UNANCHORED (RegExp.test) — add ^…$ yourself; the XLSForm importer anchors JavaRosa patterns and emits W_REGEX_UNANCHORED. Pattern ≤ 500 chars, compiled with the `u` flag, step-guarded.",
      "type": "object",
      "required": ["type", "pattern"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "regex" },
        "pattern": { "type": "string", "minLength": 1, "maxLength": 500, "format": "regex", "description": "ECMAScript regular expression source." },
        "message": { "$ref": "#/$defs/LocalizedString", "description": "Message shown when the test fails." },
        "severity": { "$ref": "#/$defs/Severity" },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "RangeValidator": {
      "title": "RangeValidator",
      "description": "Inclusive bounds for number, rating, range and date/time/datetime values (ISO strings compare lexicographically). At least one of min/max.",
      "type": "object",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "range" },
        "min": { "anyOf": [ { "type": "number" }, { "type": "string", "maxLength": 40 } ], "description": "Lower bound (number or ISO date/time string)." },
        "max": { "anyOf": [ { "type": "number" }, { "type": "string", "maxLength": 40 } ], "description": "Upper bound (number or ISO date/time string)." },
        "message": { "$ref": "#/$defs/LocalizedString" },
        "severity": { "$ref": "#/$defs/Severity" },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "anyOf": [ { "required": ["min"] }, { "required": ["max"] } ]
    },
    "LengthValidator": {
      "title": "LengthValidator",
      "description": "Inclusive length bounds: characters for strings, item count for arrays (select_multiple, repeat, multiple attachments). At least one of min/max.",
      "type": "object",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "length" },
        "min": { "type": "integer", "minimum": 0, "description": "Minimum length / count." },
        "max": { "type": "integer", "minimum": 0, "description": "Maximum length / count." },
        "message": { "$ref": "#/$defs/LocalizedString" },
        "severity": { "$ref": "#/$defs/Severity" },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "anyOf": [ { "required": ["min"] }, { "required": ["max"] } ]
    },
    "ExprValidator": {
      "title": "ExprValidator",
      "description": "Arbitrary REL predicate; '.' is the element's own value. Unlike `constraint` (always error severity), validators may be warnings that are shown and audited but do not block finalize.",
      "type": "object",
      "required": ["type", "expr"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "expr" },
        "expr": { "$ref": "#/$defs/Expr", "description": "Predicate; falsy result = validation failure." },
        "message": { "$ref": "#/$defs/LocalizedString" },
        "severity": { "$ref": "#/$defs/Severity" },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "CustomValidator": {
      "title": "CustomValidator",
      "description": "Host validator registered on the provider by id (sync or async). Unknown ids at render time are W_CUSTOM_VALIDATOR_UNKNOWN and treated as pass.",
      "type": "object",
      "required": ["type", "id"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "const": "custom" },
        "id": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_.:-]{0,63}$", "description": "Host validator id." },
        "message": { "$ref": "#/$defs/LocalizedString" },
        "severity": { "$ref": "#/$defs/Severity" },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Appearance": {
      "title": "Appearance",
      "description": "Presentation hints. Renderers ignore unknown values — never an error — so this object is an open extension point (additional keys allowed). Variant vocabulary per type: select_one/select_multiple radio|dropdown|chips|buttons|likert; date picker|no-calendar|month-year|year; group section|card|collapsible|field-list; matrix likert.",
      "type": "object",
      "properties": {
        "variant": { "type": "string", "maxLength": 40, "description": "Named visual variant for the element type (see vocabulary above)." },
        "columns": { "type": "integer", "minimum": 1, "maximum": 6, "description": "Number of columns for option grids / field-lists on wide screens." },
        "size": { "type": "string", "enum": ["sm", "md", "lg"], "description": "Control size; \"lg\" is the big-touch outdoor size." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "Bind": {
      "title": "Bind",
      "description": "Storage and behaviour flags for the element's value.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "sensitive": { "type": "boolean", "default": false, "description": "Value is PII: masked in read-only views, redacted from logs and instanceName, field-encrypted when settings.encryption.mode is \"field\" (W_SENSITIVE_WITHOUT_ENCRYPTION when mode is none)." },
        "saveIncomplete": { "type": "boolean", "default": true, "description": "Write the value to the draft even while it fails constraint/validators. false keeps an invalid value in memory only (sensitive identifiers that must not touch disk half-typed)." },
        "trackChanges": { "type": "boolean", "description": "Log old/new values in audit events for this element; inherits settings.audit.trackChanges when omitted." },
        "index": { "type": "boolean", "default": false, "description": "Create a queryable storage index on this field for list/search screens. Index columns stay cleartext under encryption (W_INDEX_ON_SENSITIVE if both flags are set)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "Default": {
      "title": "Default",
      "description": "Initial value applied once when the submission (or repeat instance) is created — once() semantics. Exactly one of `value` (literal matching the element's value type, E_DEFAULT_TYPE_MISMATCH otherwise) or `expr` (REL, e.g. today(), ${meta.username}). Not allowed on calculate elements.",
      "type": "object",
      "oneOf": [
        {
          "required": ["value"],
          "additionalProperties": false,
          "properties": { "value": { "description": "Literal JSON value." } }
        },
        {
          "required": ["expr"],
          "additionalProperties": false,
          "properties": { "expr": { "$ref": "#/$defs/Expr", "description": "REL expression evaluated once at creation." } }
        }
      ]
    },
    "ElementType": {
      "title": "ElementType",
      "description": "Built-in element type (snake_case) or a host-registered custom type x:<kebab-name>. Unknown built-in-looking types are E_UNKNOWN_TYPE; unregistered x: types render a placeholder and never crash.",
      "anyOf": [
        {
          "type": "string",
          "enum": ["text", "number", "date", "time", "datetime", "select_one", "select_multiple", "rank", "rating", "range", "checkbox", "consent", "matrix", "geopoint", "geotrace", "geoshape", "image", "audio", "video", "file", "barcode", "signature", "note", "hidden", "calculate", "group", "repeat"]
        },
        { "type": "string", "pattern": "^x:[a-z][a-z0-9-]*$", "maxLength": 64 }
      ]
    },

    "ElementBase": {
      "title": "ElementBase",
      "description": "Properties shared by every element type. Type-specific `props` and per-type rules are layered on by the *Element definitions; `Element` dispatches on `type`.",
      "type": "object",
      "required": ["type", "name"],
      "additionalProperties": false,
      "properties": {
        "type": { "$ref": "#/$defs/ElementType", "description": "Element type." },
        "name": { "$ref": "#/$defs/DataKey", "description": "Storage key of the value; unique within its repeat scope; must not shadow an outer name. Moving an element between pages/groups keeps its name and is therefore non-breaking." },
        "label": { "$ref": "#/$defs/LocalizedString", "description": "Question label / note body. Required for questions and notes; optional for hidden, calculate, group and repeat. Missing on a question is an accessibility warning (W_LABEL_MISSING)." },
        "hint": { "$ref": "#/$defs/LocalizedString", "description": "Short help shown under the label, always visible." },
        "guidance": { "$ref": "#/$defs/LocalizedString", "description": "Collapsible enumerator guidance (XLSForm guidance_hint)." },
        "media": { "$ref": "#/$defs/Media", "description": "Image/audio/video shown with the label." },
        "required": {
          "description": "true, false, or a REL expression. Enforced only when the element is relevant. On repeat: at least props.min instances; ignored on group (W_REQUIRED_ON_GROUP).",
          "anyOf": [ { "type": "boolean" }, { "$ref": "#/$defs/Expr" } ]
        },
        "requiredMessage": { "$ref": "#/$defs/LocalizedString", "description": "Message shown when a required value is missing (renderer default otherwise)." },
        "relevant": { "$ref": "#/$defs/Expr", "description": "Skip logic (ODK semantics): when false the element is hidden, not validated, and its value is EXCLUDED from the finalized submission (retained in the draft so toggling back restores it)." },
        "readonly": {
          "description": "true, false, or a REL expression. Displayed but not editable; still receives calculate, default and trigger setValue.",
          "anyOf": [ { "type": "boolean" }, { "$ref": "#/$defs/Expr" } ]
        },
        "default": { "$ref": "#/$defs/Default", "description": "Initial value." },
        "calculate": { "$ref": "#/$defs/Expr", "description": "When set, the value is computed whenever a dependency changes and the element becomes read-only. Cycles are E_CALC_CYCLE. Mandatory on type calculate." },
        "constraint": { "$ref": "#/$defs/Expr", "description": "Predicate over the element's own value ('.'), evaluated only when the value is non-empty, on change and on finalize; failure blocks finalize (severity error)." },
        "constraintMessage": { "$ref": "#/$defs/LocalizedString", "description": "Message shown when `constraint` fails." },
        "validators": {
          "type": "array",
          "description": "Additional typed validators (regex, range, length, expr, custom) with per-validator severity. Warning above 10, error above 20.",
          "items": { "$ref": "#/$defs/Validator" },
          "maxItems": 20
        },
        "appearance": { "$ref": "#/$defs/Appearance", "description": "Presentation hints." },
        "bind": { "$ref": "#/$defs/Bind", "description": "Storage/behaviour flags." },
        "props": { "type": "object", "description": "Type-specific properties; see the *Props definitions. Unknown keys are rejected by this schema (the runtime only warns: W_UNKNOWN_PROPERTY)." },
        "elements": {
          "type": "array",
          "description": "Child elements — only on group and repeat (E_ELEMENTS_ON_LEAF otherwise). Container nesting deeper than 6 is an error; repeats nest at most 3 deep.",
          "items": { "$ref": "#/$defs/Element" },
          "maxItems": 2000
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },

    "Element": {
      "title": "Element",
      "description": "Any node in pages[].elements or inside a group/repeat: a question (stores a value), a note (display only), a group (transparent container) or a repeat (container producing object[]). Validation dispatches on `type` to the matching *Element definition; `elements` is required on group/repeat and forbidden elsewhere.",
      "type": "object",
      "required": ["type", "name"],
      "properties": {
        "type": { "$ref": "#/$defs/ElementType" }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "text" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/TextElement" } },
        { "if": { "properties": { "type": { "const": "number" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/NumberElement" } },
        { "if": { "properties": { "type": { "const": "date" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/DateElement" } },
        { "if": { "properties": { "type": { "const": "time" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/TimeElement" } },
        { "if": { "properties": { "type": { "const": "datetime" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/DatetimeElement" } },
        { "if": { "properties": { "type": { "const": "select_one" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/SelectOneElement" } },
        { "if": { "properties": { "type": { "const": "select_multiple" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/SelectMultipleElement" } },
        { "if": { "properties": { "type": { "const": "rank" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RankElement" } },
        { "if": { "properties": { "type": { "const": "rating" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RatingElement" } },
        { "if": { "properties": { "type": { "const": "range" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RangeElement" } },
        { "if": { "properties": { "type": { "const": "checkbox" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CheckboxElement" } },
        { "if": { "properties": { "type": { "const": "consent" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ConsentElement" } },
        { "if": { "properties": { "type": { "const": "matrix" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/MatrixElement" } },
        { "if": { "properties": { "type": { "const": "geopoint" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/GeopointElement" } },
        { "if": { "properties": { "type": { "const": "geotrace" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/GeotraceElement" } },
        { "if": { "properties": { "type": { "const": "geoshape" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/GeoshapeElement" } },
        { "if": { "properties": { "type": { "const": "image" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ImageElement" } },
        { "if": { "properties": { "type": { "const": "audio" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/AudioElement" } },
        { "if": { "properties": { "type": { "const": "video" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/VideoElement" } },
        { "if": { "properties": { "type": { "const": "file" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/FileElement" } },
        { "if": { "properties": { "type": { "const": "barcode" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/BarcodeElement" } },
        { "if": { "properties": { "type": { "const": "signature" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/SignatureElement" } },
        { "if": { "properties": { "type": { "const": "note" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/NoteElement" } },
        { "if": { "properties": { "type": { "const": "hidden" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/HiddenElement" } },
        { "if": { "properties": { "type": { "const": "calculate" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CalculateElement" } },
        { "if": { "properties": { "type": { "const": "group" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/GroupElement" } },
        { "if": { "properties": { "type": { "const": "repeat" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/RepeatElement" } },
        { "if": { "properties": { "type": { "type": "string", "pattern": "^x:" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/CustomElement" } },
        {
          "if": { "properties": { "type": { "enum": ["group", "repeat"] } }, "required": ["type"] },
          "then": { "required": ["elements"] },
          "else": { "not": { "required": ["elements"] } }
        }
      ]
    },

    "TextElement": {
      "title": "text",
      "description": "Free text. Value: string (empty string is stored as null). Renderer: dir=auto / first-strong alignment; LTR island for mask, phone, email, url.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "text" }, "props": { "$ref": "#/$defs/TextProps" } } }
      ]
    },
    "TextProps": {
      "title": "TextProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "multiline": { "type": "boolean", "default": false, "description": "Textarea / multiline TextInput, auto-growing to 6 lines." },
        "format": { "type": "string", "enum": ["none", "email", "phone", "url"], "default": "none", "description": "Sets keyboard/inputmode and adds a built-in format check (severity error; message overridable through validators)." },
        "maxLength": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4000, "description": "Hard character cap; a counter is shown above 80 %." },
        "mask": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Input mask: # digit, A letter, * any, other characters literal (e.g. \"AA-######\"). The stored value is the raw input without mask literals." },
        "placeholder": { "$ref": "#/$defs/LocalizedString", "description": "Placeholder text." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "NumberElement": {
      "title": "number",
      "description": "Numeric input. Value: JSON number (integer kind rejects fractions). Values beyond ±2^53 or > 15 significant digits are rejected at input — identifiers belong in text with a mask. Renderer: numeric keypad, Arabic-Indic digits normalised to ASCII on save.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "number" }, "props": { "$ref": "#/$defs/NumberProps" } } }
      ]
    },
    "NumberProps": {
      "title": "NumberProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "kind": { "type": "string", "enum": ["integer", "decimal"], "default": "decimal", "description": "integer rejects fractional input." },
        "min": { "type": "number", "description": "Built-in inclusive lower bound (error severity)." },
        "max": { "type": "number", "description": "Built-in inclusive upper bound (error severity)." },
        "step": { "type": "number", "exclusiveMinimum": 0, "description": "Stepper increment (default 1 for integer, any for decimal)." },
        "unit": { "$ref": "#/$defs/LocalizedString", "description": "Unit suffix, e.g. { \"en\": \"kg\", \"ar\": \"كغ\" }." },
        "thousandsSeparator": { "type": "boolean", "default": false, "description": "Display grouping separators (display only)." },
        "placeholder": { "$ref": "#/$defs/LocalizedString", "description": "Placeholder text." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "DateElement": {
      "title": "date",
      "description": "Calendar date. Value: \"YYYY-MM-DD\". Use constraint (e.g. \". <= today()\") for dynamic bounds; comparisons in REL are lexicographic on the ISO string.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "date" }, "props": { "$ref": "#/$defs/DateProps" } } }
      ]
    },
    "TimeElement": {
      "title": "time",
      "description": "Time of day. Value: \"HH:mm[:ss]\" local wall time without offset.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "time" }, "props": { "$ref": "#/$defs/DateProps" } } }
      ]
    },
    "DatetimeElement": {
      "title": "datetime",
      "description": "Date and time. Value: full ISO-8601 with the device offset preserved (e.g. \"2026-08-15T10:00:00+03:00\").",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "datetime" }, "props": { "$ref": "#/$defs/DateProps" } } }
      ]
    },
    "DateProps": {
      "title": "DateProps",
      "description": "Shared by date, time and datetime.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "min": { "type": "string", "maxLength": 40, "description": "Static ISO lower bound (same value grammar as the element)." },
        "max": { "type": "string", "maxLength": 40, "description": "Static ISO upper bound." },
        "calendar": { "type": "string", "enum": ["gregorian", "hijri"], "description": "Display/picker calendar (display only; stored value is always Gregorian ISO). Defaults from settings.calendar." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "SelectOneElement": {
      "title": "select_one",
      "description": "Single choice from a list or dataset. Value: string (a Choice.value); a value not in the unfiltered list fails at finalize (W_VALUE_NOT_IN_LIST in drafts, e.g. after a dataset update). Renderer: radiogroup semantics, ≥ 48 px touch targets, <bdi> around Latin labels in RTL lists.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label", "props"], "properties": { "type": { "const": "select_one" }, "props": { "$ref": "#/$defs/SelectOneProps" } } }
      ]
    },
    "SelectOneProps": {
      "title": "SelectOneProps",
      "description": "Exactly one of `list` (choiceLists key) or inline `choices` (E_SELECT_SOURCE).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "list": { "$ref": "#/$defs/Name", "description": "Key into root choiceLists (E_LIST_NOT_FOUND if missing)." },
        "choices": { "type": "array", "items": { "$ref": "#/$defs/Choice" }, "minItems": 1, "maxItems": 10000, "description": "Inline options (same Choice shape as choiceLists)." },
        "choiceFilter": { "$ref": "#/$defs/Expr", "description": "Predicate over candidate choices; bare identifiers name choice attributes / dataset columns, ${name} names form values — e.g. \"gov_code = ${governorate}\" (XLSForm choice_filter parses unchanged). Changing the filter does not clear an already-selected value." },
        "search": { "type": "boolean", "description": "Searchable list with Arabic-normalised matching. Default: automatic above 12 choices." },
        "other": {
          "type": "object",
          "description": "Adds a free-text \"Other\" option; the typed text is stored under the reserved sibling key <name>_other (XLSForm or_other convention).",
          "required": ["enabled"],
          "additionalProperties": false,
          "properties": {
            "enabled": { "type": "boolean", "description": "Enable the Other option." },
            "label": { "$ref": "#/$defs/LocalizedString", "description": "Label of the Other option (default: localized \"Other\")." },
            "value": { "$ref": "#/$defs/ChoiceValue", "description": "Stored value for the Other option; default \"other\"." }
          }
        },
        "randomize": { "type": "boolean", "default": false, "description": "Shuffle option order per submission (seeded by submission id, stable across re-renders)." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "oneOf": [ { "required": ["list"] }, { "required": ["choices"] } ]
    },
    "SelectMultipleElement": {
      "title": "select_multiple",
      "description": "Multiple choice. Value: string[] in selection order ([] is stored as null on finalize). Use selected(${x}, 'v') / countSelected(${x}) in REL.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label", "props"], "properties": { "type": { "const": "select_multiple" }, "props": { "$ref": "#/$defs/SelectMultipleProps" } } }
      ]
    },
    "SelectMultipleProps": {
      "title": "SelectMultipleProps",
      "description": "As SelectOneProps plus minSelected / maxSelected / exclusive.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "list": { "$ref": "#/$defs/Name", "description": "Key into root choiceLists." },
        "choices": { "type": "array", "items": { "$ref": "#/$defs/Choice" }, "minItems": 1, "maxItems": 10000, "description": "Inline options." },
        "choiceFilter": { "$ref": "#/$defs/Expr", "description": "Predicate over candidate choices (see SelectOneProps)." },
        "search": { "type": "boolean", "description": "Searchable list." },
        "other": {
          "type": "object",
          "description": "Adds a free-text \"Other\" option stored under <name>_other.",
          "required": ["enabled"],
          "additionalProperties": false,
          "properties": {
            "enabled": { "type": "boolean", "description": "Enable the Other option." },
            "label": { "$ref": "#/$defs/LocalizedString", "description": "Label of the Other option (default: localized \"Other\")." },
            "value": { "$ref": "#/$defs/ChoiceValue", "description": "Stored value for the Other option; default \"other\"." }
          }
        },
        "randomize": { "type": "boolean", "default": false, "description": "Shuffle option order per submission." },
        "minSelected": { "type": "integer", "minimum": 0, "description": "Minimum number of selected options (enforced when the value is non-empty; use required for at-least-one)." },
        "maxSelected": { "type": "integer", "minimum": 1, "description": "Maximum number of selected options." },
        "exclusive": { "type": "array", "items": { "$ref": "#/$defs/ChoiceValue" }, "uniqueItems": true, "description": "Values that clear all others when chosen (e.g. [\"none\", \"dont_know\"])." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "oneOf": [ { "required": ["list"] }, { "required": ["choices"] } ]
    },
    "RankElement": {
      "title": "rank",
      "description": "Order every option. Value: string[] containing every choice value exactly once when non-empty (ODK odk:rank); partial rankings fail the constraint. Renderer: drag with keyboard/menu alternative (WCAG 2.2 SC 2.5.7).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label", "props"], "properties": { "type": { "const": "rank" }, "props": { "$ref": "#/$defs/RankProps" } } }
      ]
    },
    "RankProps": {
      "title": "RankProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "list": { "$ref": "#/$defs/Name", "description": "Key into root choiceLists." },
        "choices": { "type": "array", "items": { "$ref": "#/$defs/Choice" }, "minItems": 2, "maxItems": 50, "description": "Inline options." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "oneOf": [ { "required": ["list"] }, { "required": ["choices"] } ]
    },
    "RatingElement": {
      "title": "rating",
      "description": "Discrete rating scale. Value: integer 1..max. Renderer: radiogroup semantics, options ≥ 44 px, never colour-only.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "rating" }, "props": { "$ref": "#/$defs/RatingProps" } } }
      ]
    },
    "RatingProps": {
      "title": "RatingProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "max": { "type": "integer", "minimum": 2, "maximum": 10, "default": 5, "description": "Number of steps (W_RATING_MAX_LARGE above 10 — use range or select_one)." },
        "icon": { "type": "string", "enum": ["star", "number", "smiley"], "default": "star", "description": "Icon style." },
        "labels": {
          "type": "object",
          "additionalProperties": false,
          "description": "End labels.",
          "properties": {
            "min": { "$ref": "#/$defs/LocalizedString", "description": "Label under the lowest step." },
            "max": { "$ref": "#/$defs/LocalizedString", "description": "Label under the highest step." }
          }
        },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "RangeElement": {
      "title": "range",
      "description": "Slider. Value: number within [min, max] on the step grid. Renderer: fills from inline-start; a numeric input alternative must exist.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "range" }, "props": { "$ref": "#/$defs/RangeProps" } } }
      ]
    },
    "RangeProps": {
      "title": "RangeProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "min": { "type": "number", "default": 0, "description": "Lower end." },
        "max": { "type": "number", "default": 100, "description": "Upper end." },
        "step": { "type": "number", "exclusiveMinimum": 0, "default": 1, "description": "Step size." },
        "showValue": { "type": "boolean", "default": true, "description": "Show the numeric value next to the slider." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "CheckboxElement": {
      "title": "checkbox",
      "description": "Single acknowledgement / yes-no toggle. Value: boolean (null when untouched); required: true means it must be checked. XLSForm export maps to acknowledge. No props.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "checkbox" }, "props": { "$ref": "#/$defs/EmptyProps" } } }
      ]
    },
    "EmptyProps": {
      "title": "EmptyProps",
      "description": "Element types without type-specific props (checkbox, hidden, calculate) accept an empty object (or an object with only ext).",
      "type": "object",
      "additionalProperties": false,
      "properties": { "ext": { "$ref": "#/$defs/Ext" } }
    },
    "ConsentElement": {
      "title": "consent",
      "description": "Informed-consent capture. Value: { granted: boolean, at: ISO, textVersion: string, locale: string, method: \"tap\"|\"signature\"|\"verbal\", signature?: AttachmentRef }. required means granted must be true to continue; forms that must proceed on refusal use a trigger (\"${consent}.granted = false\" → complete). Always audited; anchor for data-protection reporting.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label", "props"], "properties": { "type": { "const": "consent" }, "props": { "$ref": "#/$defs/ConsentProps" } } }
      ]
    },
    "ConsentProps": {
      "title": "ConsentProps",
      "type": "object",
      "required": ["text", "textVersion"],
      "additionalProperties": false,
      "properties": {
        "text": { "$ref": "#/$defs/LocalizedString", "description": "The full consent statement read/shown to the respondent (Markdown-safe subset; ≤ 32,000 chars)." },
        "textVersion": { "type": "string", "minLength": 1, "maxLength": 32, "description": "Version tag of `text`, stored with every consent value; bump when the text changes (W_CONSENT_TEXT_CHANGED_SAME_VERSION)." },
        "method": { "type": "string", "enum": ["tap", "signature", "verbal"], "default": "tap", "description": "How consent is captured: tap a control, sign on an embedded pad, or enumerator attestation of verbal consent." },
        "allowWithdraw": { "type": "boolean", "default": false, "description": "Show a \"withdraw consent\" control on later pages." },
        "onWithdraw": { "type": "string", "enum": ["clearSensitive", "keep"], "default": "clearSensitive", "description": "On withdrawal: clear values of bind.sensitive elements (audit event consent.withdrawn) or keep them." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "MatrixElement": {
      "title": "matrix",
      "description": "Grid of rows sharing one column specification (Likert grid, per-row number or text). Value: { [rowValue]: value } where value follows the column type; unanswered rows are absent. Renderer: table on wide screens, one card per row below 480 px.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label", "props"], "properties": { "type": { "const": "matrix" }, "props": { "$ref": "#/$defs/MatrixProps" } } }
      ]
    },
    "MatrixProps": {
      "title": "MatrixProps",
      "type": "object",
      "required": ["rows", "columns"],
      "additionalProperties": false,
      "properties": {
        "rows": {
          "type": "array",
          "description": "Row definitions (≥ 1, unique values).",
          "minItems": 1,
          "maxItems": 100,
          "items": {
            "type": "object",
            "required": ["value", "label"],
            "additionalProperties": false,
            "properties": {
              "value": { "$ref": "#/$defs/ChoiceValue", "description": "Row key in the stored object." },
              "label": { "$ref": "#/$defs/LocalizedString", "description": "Row label." },
              "relevant": { "$ref": "#/$defs/Expr", "description": "Hide this row (and exclude its value) when false." },
              "ext": { "$ref": "#/$defs/Ext" }
            }
          }
        },
        "columns": { "$ref": "#/$defs/MatrixColumns" },
        "requiredRows": { "type": "string", "enum": ["all", "any", "none"], "description": "How many rows must be answered when the element is required (default \"all\" when required, \"none\" otherwise)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "MatrixColumns": {
      "title": "MatrixColumns",
      "description": "One column specification applied to every row, discriminated by `type`: select_one (choices become the visual columns), number, or text.",
      "type": "object",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "type": "string", "enum": ["select_one", "number", "text"], "description": "Cell type." },
        "list": { "$ref": "#/$defs/Name", "description": "select_one only: choiceLists key." },
        "choices": { "type": "array", "items": { "$ref": "#/$defs/Choice" }, "minItems": 1, "maxItems": 50, "description": "select_one only: inline options." },
        "kind": { "type": "string", "enum": ["integer", "decimal"], "description": "number only." },
        "min": { "type": "number", "description": "number only: inclusive lower bound." },
        "max": { "type": "number", "description": "number only: inclusive upper bound." },
        "maxLength": { "type": "integer", "minimum": 1, "maximum": 4000, "description": "text only: character cap." },
        "ext": { "$ref": "#/$defs/Ext" }
      },
      "allOf": [
        {
          "if": { "properties": { "type": { "const": "select_one" } }, "required": ["type"] },
          "then": { "oneOf": [ { "required": ["list"] }, { "required": ["choices"] } ], "not": { "anyOf": [ { "required": ["kind"] }, { "required": ["min"] }, { "required": ["max"] }, { "required": ["maxLength"] } ] } }
        },
        {
          "if": { "properties": { "type": { "const": "number" } }, "required": ["type"] },
          "then": { "not": { "anyOf": [ { "required": ["list"] }, { "required": ["choices"] }, { "required": ["maxLength"] } ] } }
        },
        {
          "if": { "properties": { "type": { "const": "text" } }, "required": ["type"] },
          "then": { "not": { "anyOf": [ { "required": ["list"] }, { "required": ["choices"] }, { "required": ["kind"] }, { "required": ["min"] }, { "required": ["max"] } ] } }
        }
      ]
    },
    "GeopointElement": {
      "title": "geopoint",
      "description": "Single location. Value: { lat, lng, alt?, accuracy?, capturedAt } (lat ∈ [-90, 90], lng ∈ [-180, 180]). Renderer: live accuracy readout, 60 s timeout with fallback, offline basemap when configured; a mocked-provider sidecar is written to submission.meta.ext[\"dev.rasd.geo\"].",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "geopoint" }, "props": { "$ref": "#/$defs/GeopointProps" } } }
      ]
    },
    "GeopointProps": {
      "title": "GeopointProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "accuracyThreshold": { "type": "number", "minimum": 0, "maximum": 10000, "default": 5, "description": "Metres: auto-accept a fix at or below this accuracy (ODK capture-accuracy). 0 = manual accept only." },
        "warningThreshold": { "type": "number", "minimum": 0, "maximum": 100000, "default": 100, "description": "Metres: non-blocking warning above this accuracy (ODK warning-accuracy)." },
        "autoCapture": { "type": "boolean", "default": false, "description": "Start capturing as soon as the element is revealed." },
        "allowManual": { "type": "boolean", "default": true, "description": "Allow placing the point on a map or typing coordinates." },
        "map": { "type": "boolean", "default": true, "description": "Show a basemap." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "GeotraceElement": {
      "title": "geotrace",
      "description": "Open line. Value: Geo[] with ≥ 2 points.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "geotrace" }, "props": { "$ref": "#/$defs/GeoLineProps" } } }
      ]
    },
    "GeoshapeElement": {
      "title": "geoshape",
      "description": "Closed polygon. Value: Geo[] closed ring (first == last, ≥ 4 entries) so area() and XForm export are unambiguous.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "geoshape" }, "props": { "$ref": "#/$defs/GeoLineProps" } } }
      ]
    },
    "GeoLineProps": {
      "title": "GeoLineProps",
      "description": "Shared by geotrace and geoshape.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": { "type": "string", "enum": ["manual", "auto"], "default": "manual", "description": "manual = tap-to-place / record vertex on demand; auto = record a vertex every intervalSeconds." },
        "intervalSeconds": { "type": "integer", "minimum": 1, "maximum": 3600, "default": 10, "description": "Auto-record interval (mode auto)." },
        "accuracyThreshold": { "type": "number", "minimum": 0, "maximum": 10000, "default": 5, "description": "Metres per vertex, as for geopoint." },
        "warningThreshold": { "type": "number", "minimum": 0, "maximum": 100000, "default": 100, "description": "Metres per vertex, non-blocking warning." },
        "allowManual": { "type": "boolean", "default": true, "description": "Allow tap-to-place vertices on the map." },
        "map": { "type": "boolean", "default": true, "description": "Show a basemap (required for manual placement)." },
        "minPoints": { "type": "integer", "minimum": 2, "maximum": 10000, "description": "Minimum vertices (default 2 for geotrace, 3 unique for geoshape)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "ImageElement": {
      "title": "image",
      "description": "Photo capture/pick. Value: AttachmentRef (or AttachmentRef[] when multiple) — { attachmentId, name?, mime?, bytes?, sha256?, capturedAt?, geo? }; bytes live in the attachment store and upload independently via tus. EXIF is stripped; geotag is captured separately.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "image" }, "props": { "$ref": "#/$defs/ImageProps" } } }
      ]
    },
    "ImageProps": {
      "title": "ImageProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "source": { "type": "string", "enum": ["camera", "gallery", "both"], "default": "camera", "description": "Where the image may come from." },
        "maxPixels": { "type": "integer", "minimum": 64, "maximum": 8192, "default": 1280, "description": "Long-edge cap in pixels; proportional resize before storing (ODK max-pixels)." },
        "quality": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "default": 0.7, "description": "JPEG quality 0–1." },
        "annotate": { "type": "boolean", "default": false, "description": "Allow drawing on the photo." },
        "geotag": { "type": "boolean", "default": false, "description": "Attach a sidecar geo fix to the AttachmentRef." },
        "multiple": { "type": "boolean", "default": false, "description": "Allow several photos (value becomes an array)." },
        "maxCount": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5, "description": "Maximum photos when multiple." },
        "maxBytes": { "type": "integer", "minimum": 1024, "default": 5242880, "description": "Per-file size cap in bytes (default 5 MiB)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "AudioElement": {
      "title": "audio",
      "description": "Audio recording/pick. Value: AttachmentRef (or AttachmentRef[]).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "audio" }, "props": { "$ref": "#/$defs/AudioProps" } } }
      ]
    },
    "AudioProps": {
      "title": "AudioProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "maxDurationSeconds": { "type": "integer", "minimum": 1, "maximum": 14400, "default": 600, "description": "Recording cap." },
        "accept": { "type": "array", "items": { "type": "string", "maxLength": 100 }, "uniqueItems": true, "description": "MIME allow-list for picked files (e.g. [\"audio/mpeg\", \"audio/mp4\"])." },
        "maxBytes": { "type": "integer", "minimum": 1024, "default": 20971520, "description": "Per-file size cap in bytes (default 20 MiB)." },
        "multiple": { "type": "boolean", "default": false, "description": "Allow several recordings." },
        "maxCount": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5, "description": "Maximum items when multiple." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "VideoElement": {
      "title": "video",
      "description": "Video recording/pick. Value: AttachmentRef (or AttachmentRef[]).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "video" }, "props": { "$ref": "#/$defs/VideoProps" } } }
      ]
    },
    "VideoProps": {
      "title": "VideoProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "maxDurationSeconds": { "type": "integer", "minimum": 1, "maximum": 3600, "default": 600, "description": "Recording cap." },
        "accept": { "type": "array", "items": { "type": "string", "maxLength": 100 }, "uniqueItems": true, "description": "MIME allow-list for picked files." },
        "maxBytes": { "type": "integer", "minimum": 1024, "default": 52428800, "description": "Per-file size cap in bytes (default 50 MiB)." },
        "multiple": { "type": "boolean", "default": false, "description": "Allow several videos." },
        "maxCount": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5, "description": "Maximum items when multiple." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "FileElement": {
      "title": "file",
      "description": "Arbitrary file pick. Value: AttachmentRef (or AttachmentRef[]).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "file" }, "props": { "$ref": "#/$defs/FileProps" } } }
      ]
    },
    "FileProps": {
      "title": "FileProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "accept": { "type": "array", "items": { "type": "string", "maxLength": 100 }, "uniqueItems": true, "description": "MIME allow-list, e.g. [\"application/pdf\"]." },
        "maxBytes": { "type": "integer", "minimum": 1024, "default": 20971520, "description": "Per-file size cap in bytes (default 20 MiB)." },
        "multiple": { "type": "boolean", "default": false, "description": "Allow several files." },
        "maxCount": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5, "description": "Maximum items when multiple." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "BarcodeElement": {
      "title": "barcode",
      "description": "Barcode/QR scan. Value: string (or string[] when multiple — batch scanning appends). Renderer: BarcodeDetector → self-hosted WASM ponyfill on web; expo-camera on native.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "barcode" }, "props": { "$ref": "#/$defs/BarcodeProps" } } }
      ]
    },
    "BarcodeProps": {
      "title": "BarcodeProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "formats": {
          "type": "array",
          "description": "Accepted symbologies (BarcodeDetector names). Default [\"qr_code\", \"code_128\", \"ean_13\"].",
          "items": { "type": "string", "enum": ["qr_code", "code_128", "code_39", "code_93", "codabar", "ean_13", "ean_8", "upc_a", "upc_e", "itf", "pdf417", "data_matrix", "aztec"] },
          "minItems": 1,
          "uniqueItems": true
        },
        "allowManual": { "type": "boolean", "default": true, "description": "Allow typing the code when scanning fails." },
        "multiple": { "type": "boolean", "default": false, "description": "Batch scanning; value becomes string[]." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "SignatureElement": {
      "title": "signature",
      "description": "Signature pad. Value: AttachmentRef of a trimmed PNG (≤ 256 KiB by default).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["label"], "properties": { "type": { "const": "signature" }, "props": { "$ref": "#/$defs/SignatureProps" } } }
      ]
    },
    "SignatureProps": {
      "title": "SignatureProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "penColor": { "type": "string", "pattern": "^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})|(rgb|rgba|hsl|hsla)\\([^)]*\\))$", "default": "#111", "description": "Stroke colour (hex or rgb()/hsl())." },
        "maxBytes": { "type": "integer", "minimum": 1024, "maximum": 2097152, "default": 262144, "description": "PNG size cap in bytes." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "NoteElement": {
      "title": "note",
      "description": "Display-only text: `label` is the body (Markdown-safe subset, {name} interpolation for read-back), `hint` is secondary text. Stores nothing — required, constraint, default and calculate are not allowed (E_NOTE_WITH_VALUE_PROPS).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        {
          "type": "object",
          "required": ["label"],
          "properties": { "type": { "const": "note" }, "props": { "$ref": "#/$defs/NoteProps" } },
          "not": { "anyOf": [ { "required": ["required"] }, { "required": ["constraint"] }, { "required": ["default"] }, { "required": ["calculate"] } ] }
        }
      ]
    },
    "NoteProps": {
      "title": "NoteProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "style": { "type": "string", "enum": ["info", "warning", "success"], "default": "info", "description": "Visual style." },
        "collapsible": { "type": "boolean", "default": false, "description": "Render collapsed with an expander." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "HiddenElement": {
      "title": "hidden",
      "description": "No UI; value comes from `default` (typically { \"expr\": \"${meta.deviceId}\" }) or host-injected initialData. Value: any JSON. Never required (W_REQUIRED_HIDDEN).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "properties": { "type": { "const": "hidden" }, "props": { "$ref": "#/$defs/EmptyProps" } } }
      ]
    },
    "CalculateElement": {
      "title": "calculate",
      "description": "Computed value with no UI. `calculate` is mandatory (E_CALCULATE_MISSING) and `default` is forbidden (E_DEFAULT_ON_CALCULATE). Value: whatever the expression returns. Volatile functions (now(), random(), uuid()) re-evaluate on every dependency change — wrap in once() for stable ids.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        {
          "type": "object",
          "required": ["calculate"],
          "properties": { "type": { "const": "calculate" }, "props": { "$ref": "#/$defs/EmptyProps" } },
          "not": { "required": ["default"] }
        }
      ]
    },
    "GroupElement": {
      "title": "group",
      "description": "Transparent container: child values are stored at the parent level unless props.nestData is true. `relevant` hides the whole subtree and excludes all descendant values. appearance.variant: section (default) | card | collapsible | field-list (all children on one screen even in paged mode).",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["elements"], "properties": { "type": { "const": "group" }, "props": { "$ref": "#/$defs/GroupProps" } } }
      ]
    },
    "GroupProps": {
      "title": "GroupProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "nestData": { "type": "boolean", "default": false, "description": "true stores children as { child: value } under the group's own name (needed for 1:1 XForm nesting)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "RepeatElement": {
      "title": "repeat",
      "description": "Repeating container (household roster, issues list). Value: object[] — one object per instance keyed by child names (groups inside stay transparent). ${rep[].field} yields an array for sum()/count(); position() is the 1-based instance index. Repeats nest at most 3 deep. Renderer: instances collapsed above 5, Add pinned at the end, delete requires confirmation.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        { "type": "object", "required": ["elements"], "properties": { "type": { "const": "repeat" }, "props": { "$ref": "#/$defs/RepeatProps" } } }
      ]
    },
    "RepeatProps": {
      "title": "RepeatProps",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "min": { "type": "integer", "minimum": 0, "default": 0, "description": "Minimum instances (enforced when required). min > max is E_REPEAT_MIN_GT_MAX." },
        "max": { "type": "integer", "minimum": 1, "default": 200, "description": "Maximum instances (W_REPEAT_MAX_LARGE above 500)." },
        "count": { "$ref": "#/$defs/Expr", "description": "Fixed instance count (ODK repeat_count); add/remove controls are hidden; shrinking hides extra instances rather than deleting them." },
        "addLabel": { "$ref": "#/$defs/LocalizedString", "description": "Add-instance button label." },
        "removeLabel": { "$ref": "#/$defs/LocalizedString", "description": "Remove-instance button label." },
        "itemLabel": { "$ref": "#/$defs/Expr", "description": "REL evaluated per instance for the collapsed header, e.g. concat(${m_name}, ' (', ${m_age}, ')'); position() is available." },
        "keyField": { "$ref": "#/$defs/Name", "description": "Child element whose value must be unique across instances (W_REPEAT_KEY_DUPLICATE in drafts, constraint failure at finalize)." },
        "confirmDelete": { "type": "boolean", "default": true, "description": "Ask before deleting an instance." },
        "allowReorder": { "type": "boolean", "default": false, "description": "Reorder UI (drag with a non-drag alternative)." },
        "ext": { "$ref": "#/$defs/Ext" }
      }
    },
    "CustomElement": {
      "title": "x:<name>",
      "description": "Host-defined element registered with defineElement({ type: 'x:foo', component, builder?, valueSchema? }). `props` are host-defined and preserved untouched; all base properties (relevant, required, calculate, bind, …) apply unchanged. Unregistered at render → placeholder card, never a crash; declare requires.features [\"x:foo\"] to make old clients refuse instead.",
      "allOf": [
        { "$ref": "#/$defs/ElementBase" },
        {
          "type": "object",
          "properties": {
            "type": { "type": "string", "pattern": "^x:[a-z][a-z0-9-]*$", "maxLength": 64, "description": "Custom type id: \"x:\" followed by a kebab-case name, e.g. \"x:beneficiary-lookup\" (E_CUSTOM_TYPE_NAME otherwise)." },
            "props": {
              "type": "object",
              "description": "Host-defined properties (any JSON object). Prototype-pollution keys are rejected.",
              "propertyNames": { "type": "string", "not": { "type": "string", "enum": ["constructor", "prototype", "__proto__"] } }
            }
          }
        }
      ]
    }
  }
}
