{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/mlltx/Invaract/main/contract/schema/invaract-contract.schema.json",
  "title": "Invaract Data Contract",
  "description": "Structural shape of a contract document Invaract's `contract` module (com.invaract.contract.ContractParser/ContractValidator) accepts, modeled after the Open Data Contract Standard (ODCS) - see docs/CONTRACT_MODEL.md. This schema is deliberately permissive at every level (`additionalProperties: true`) to match the parser's own design: an unrecognized key is preserved verbatim rather than rejected, so a contract authored for other ODCS-based tooling stays valid input here too. It also intentionally sits between two layers of the real Scala implementation: some constraints here (id, version, dataset name/location, field name/type) mirror ContractParser's hard parse failures; others (outputs being present and non-empty) mirror ContractValidator's Error-level checks rather than the parser alone, because a document that merely parses but fails validation isn't a useful contract either. Two things this schema does NOT and cannot enforce, both still Scala-side responsibilities: duplicate-name detection (dataset names, field names) and cross-field business rules (e.g. ContractValidator's required-and-nullable warning) - see docs/CONTRACT_MODEL.md's \"JSON Schema\" section for the full list of what's schema-level versus what stays engine-only.",
  "type": "object",
  "required": ["id", "version", "outputs"],
  "additionalProperties": true,
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "description": "Contract identity. Must be non-empty (ContractParser hard failure otherwise). ContractValidator additionally warns (not an error) if it doesn't start with a letter and contain only alphanumerics, '.', '_', '-' - not enforced here since the parser still accepts other values."
    },
    "version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$",
      "description": "MAJOR.MINOR[.PATCH], e.g. \"1.0.0\" or \"1.0\" (PATCH defaults to 0). Any other shape is a ContractParser hard failure (ContractVersion.parse)."
    },
    "status": {
      "type": "string",
      "description": "Free-form; Invaract does not constrain or interpret this value. Defaults to \"active\" if the key is omitted."
    },
    "inputs": {
      "type": "array",
      "default": [],
      "items": { "$ref": "#/$defs/dataset" },
      "description": "Datasets this transformation reads from. Optional - omitted or empty means no declared inputs."
    },
    "outputs": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/dataset" },
      "description": "Datasets this transformation writes to. Required and non-empty: a contract with no outputs parses but fails ContractValidator with an Error (\"Contract must declare at least one output dataset\"), so this schema requires it up front rather than accepting a document ContractValidator would immediately reject."
    },
    "rules": {
      "type": "array",
      "default": [],
      "items": { "$ref": "#/$defs/rule" },
      "description": "Declarative rules attached to the contract. Most rule types (e.g. compatibility mode) are recorded but not yet verified against a transformation - see ROADMAP.md. Three types ARE interpreted and enforced during verification (com.invaract.contract.RuleType/ContractRule.interpret, checked by spark-adapter's RuleVerifier against a real Spark row-level DML operation): 'merge_condition' (columns: a MERGE's ON clause must reference these), 'forbid_unconditional_delete' (no properties: a DELETE may never omit a filtering predicate), and 'allowed_update_columns' (columns: an UPDATE may only assign these). A known rule type with malformed properties for its shape is a ContractValidator Error, not silently ignored. A fourth way to get a rule type interpreted: name it in `customRuleTypes` (below), the reflective escape hatch for a DML rule type outside these three built-in ones."
    },
    "extensions": {
      "type": "object",
      "additionalProperties": true,
      "description": "Explicit bucket for metadata Invaract doesn't interpret (owners, SLAs, quality rules, ...). Any top-level key not listed in this schema's `properties` is preserved here too by ContractParser, whether or not it's nested under this explicit `extensions` map."
    },
    "customRuleTypes": {
      "type": "object",
      "default": {},
      "additionalProperties": { "type": "string", "minLength": 1 },
      "description": "Maps a rule `type` name this contract's own `rules` use to the fully-qualified class name of a com.invaract.verification.CustomRuleVerifier implementation that verifies it - the reflective, no-source-change extension point for a DML rule type outside the three built-in ones described above, resolved by com.invaract.verification.CustomRuleVerifierFactory the same way spark-adapter's NotificationSinkFactory resolves a notification sink.class, and contract/org-policy's own customPolicyTypes resolves a CustomPolicyEvaluator. A key here that collides with one of the three built-in rule types is dead (the built-in interpretation always wins) and is flagged by ContractValidator as a Warning, not an Error; unlike customPolicyTypes, a rule `type` matching neither a built-in type nor a customRuleTypes entry is NOT itself flagged - most of this document's `rules` are routinely recorded-but-uninterpreted (e.g. 'compatibility'), so that shape is normal here, not a likely typo."
    }
  },
  "$defs": {
    "dataset": {
      "type": "object",
      "required": ["name", "location"],
      "additionalProperties": true,
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "Logical name used to look up this dataset (Contract.input/output). Must be unique within inputs and within outputs - not enforced by this schema, only by ContractValidator."
        },
        "location": {
          "type": "string",
          "minLength": 1,
          "description": "Physical location: table name, file path, topic, etc."
        },
        "format": {
          "type": "string",
          "description": "Optional storage/serialization format, e.g. \"table\", \"parquet\", \"delta\". Not a closed set - any value is accepted."
        },
        "saveMode": {
          "type": "string",
          "description": "Optional expected write behavior for an output dataset, the canonical modes are \"append\", \"overwrite\", \"ignore\" and \"error\" (com.invaract.contract.SaveModes), compared case-insensitively. Any other string is accepted but draws a validation warning. Meaningless for an input dataset; only checked by StructuralVerifier against a plan's Write node."
        },
        "catalog": { "$ref": "#/$defs/catalog" },
        "description": {
          "type": "string",
          "description": "Optional free-form human-readable explanation of what this dataset is/contains - purely documentary, never checked by RuleVerifier/StructuralVerifier. An organizational policy can require every dataset to declare one (com.invaract.contract.PolicyType.RequireDatasetDescription)."
        },
        "type": {
          "type": "string",
          "enum": ["DATA_ASSET", "SOURCE", "CONTROL"],
          "description": "Optional declared semantic role (com.invaract.contract.DatasetType), matched case-insensitively by ContractParser though only the canonical uppercase form is listed here: DATA_ASSET (a governed, shareable data product this contract may itself produce or consume), SOURCE (data entering this contract's pipeline from outside its own responsibility - never valid as this contract's own output; ContractValidator warns if declared there), or CONTROL (data used to operate/control processing rather than being the business data produced, e.g. a processing calendar or watermark). A closed three-value set for the initial implementation - an unrecognized value is a ContractParser hard failure, unlike the open-vocabulary `format` above. Whether declaring this is mandatory is an organizational choice, not enforced here: see com.invaract.contract.PolicyType.RequireDatasetType."
        },
        "derivedFrom": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Optional, meaningful only on an output: the names of this contract's declared `inputs` this output is derived from. Absent means the original assumption - derived from ALL declared inputs; present scopes spark-adapter's per-write input checks (MISSING_INPUT, UNDECLARED_INPUT) to just these inputs whenever a write to this output is checked, so a contract governing several writes can say e.g. 'orders_summary uses orders and customers, audit_log uses only events'. An empty list means derived from no declared input. Many-to-many: an input may appear under several outputs. A name matching no declared input is a ContractValidator Error - not enforceable by this schema, since it needs the sibling `inputs` list."
        },
        "schema": { "$ref": "#/$defs/schema" }
      }
    },
    "catalog": {
      "type": "object",
      "required": ["required"],
      "additionalProperties": true,
      "properties": {
        "required": {
          "type": "boolean",
          "description": "Whether this dataset must be registered in a catalog at all. `false` records an expected shape without gating anything (ContractValidator warns if other catalog sub-fields are also declared). This is a ContractParser hard requirement within a present 'catalog' block, mirroring 'schema.fields' being required within a dataset."
        },
        "technology": {
          "type": "string",
          "description": "Optional catalog implementation, e.g. \"hive\", \"delta\", \"iceberg\". Not a closed set - any value is accepted."
        },
        "catalogName": {
          "type": "string",
          "description": "Optional Spark-level catalog plugin/session-catalog name, e.g. \"spark_catalog\" - a local alias, not necessarily unique across an organization."
        },
        "location": {
          "type": "string",
          "description": "Optional catalog service's own network address, e.g. a Hive metastore's \"thrift://host:port\" URI - the durable, technology-specific answer to \"which Hive\", distinct from catalogName's local alias. Only ever compared for Hive-backed datasets; always unset (and never compared) for Delta/Iceberg/JDBC today, since no reflective accessor exists for those catalogs' own network endpoints."
        },
        "namespace": {
          "type": "array",
          "default": [],
          "items": { "type": "string" },
          "description": "The database/schema path within the catalog, as an ordered list, e.g. [\"default\"]."
        },
        "table": {
          "type": "string",
          "description": "Optional table name within the declared namespace."
        }
      }
    },
    "schema": {
      "type": "object",
      "required": ["fields"],
      "additionalProperties": true,
      "properties": {
        "fields": {
          "type": "array",
          "items": { "$ref": "#/$defs/field" },
          "description": "The 'fields' key itself is a ContractParser hard requirement within a present 'schema'. A dataset with no 'schema', or with no fields, declares no columns: its content is not governed and its columns are not checked (ContractValidator warns, so a schema left out by mistake is never silent)."
        }
      }
    },
    "field": {
      "type": "object",
      "required": ["name", "type"],
      "additionalProperties": true,
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "Field name. Must be unique among sibling fields (within the same schema, or the same struct's properties) - not enforced by this schema, only by ContractValidator."
        },
        "type": {
          "type": "string",
          "minLength": 1,
          "description": "Field type. Recognized values (ContractValidator.KnownTypes): string, integer, long, short, byte, double, float, decimal, boolean, date, timestamp, binary, struct, array, map. An unrecognized value is only a ContractValidator Warning, not a parse or validation Error, so it is not enforced here as an enum - a contract using a type this version of Invaract doesn't yet know about still parses and still matches this schema. A field with a non-empty `properties` array is treated as a struct regardless of this value."
        },
        "required": {
          "type": "boolean",
          "default": false,
          "description": "Whether the field must be present. Defaults to false if omitted."
        },
        "nullable": {
          "type": "boolean",
          "description": "Whether the field's value may be null when present. Defaults to the opposite of `required` if omitted (i.e. a required field defaults to non-nullable). ContractValidator warns, but does not error, if both `required` and `nullable` are explicitly true together."
        },
        "properties": {
          "type": "array",
          "default": [],
          "items": { "$ref": "#/$defs/field" },
          "description": "Nested fields, for a struct/record type. A field with a non-empty `properties` list is treated as a struct regardless of its declared `type`."
        },
        "sensitivityTags": {
          "type": "array",
          "default": [],
          "items": { "type": "string" },
          "description": "Open-vocabulary governance labels for this field (e.g. \"pii\", \"financial\", \"restricted\") - Invaract does not define or restrict which tags exist. spark-adapter's SensitivityLineage propagates whichever tags an input field carries to every output column that transitively derives from it, for audit/governance visibility - this is reporting, not enforcement, so a tagged field does not by itself cause verification to fail. A non-list value for this key is a ContractParser hard failure, not silently ignored."
        },
        "constraints": {
          "type": "array",
          "default": [],
          "items": { "$ref": "#/$defs/fieldConstraint" },
          "description": "Statically-verifiable value-domain requirements on this field (equals a constant, one of a finite set, a numeric range) - see docs/STATIC_DATA_QUALITY_VERIFICATION.md. NOT NULL needs no entry here: the existing `nullable: false` above already states it. On an input field, a constraint is an axiom the static analysis trusts; on an output field, it's an obligation the analysis attempts to prove."
        }
      }
    },
    "fieldConstraint": {
      "type": "object",
      "required": ["type"],
      "additionalProperties": true,
      "properties": {
        "type": {
          "type": "string",
          "minLength": 1,
          "description": "Constraint kind: \"equals\" (a `value` property), \"oneOf\" (a `values` list), or \"range\" (at least one of `gte`/`gt`/`lte`/`lt`, not both gte+gt or both lte+lt). Every other key on a constraint object is constraint-specific and captured verbatim (FieldConstraint.properties). See FieldConstraintType/FieldConstraint.interpret."
        }
      }
    },
    "rule": {
      "type": "object",
      "required": ["type"],
      "additionalProperties": true,
      "properties": {
        "type": {
          "type": "string",
          "minLength": 1,
          "description": "Rule kind, e.g. \"compatibility\", \"merge_condition\", \"forbid_unconditional_delete\", \"allowed_update_columns\", \"required_group_by\", \"forbid_cross_join\", \"required_join_columns\", \"required_filter_columns\". Every other key on a rule object is rule-specific and captured verbatim (ContractRule.properties). The three DML types and four plan-shape types are interpreted and enforced today (see RuleType/ContractRule.interpret) - every other rule type is recorded but not yet verified (see ROADMAP.md)."
        }
      }
    }
  }
}
