{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://data.ungovr.org/v1/schema/ungovr.ai-laws-2.schema.json",
  "title": "UnGovr AI-law corpus (ungovr.ai-laws/2)",
  "description": "Validation contract for the AI and scraping law corpus published at https://data.ungovr.org/v1/ai-laws/. This is a definitions document: it asserts nothing at the root, so reference the subschema you are validating against by JSON pointer. The prose contract this schema formalizes is https://github.com/ungovr/ungovr/blob/main/docs/architecture/ai-laws-contract.md and the generating owner is data_api/ai_law_builders.py.",
  "$comment": "Targets: '#/$defs/jurisdictionRecord' for /v1/ai-laws/{slug}.json, '#/$defs/index' for /v1/ai-laws/index.json, '#/$defs/domainShard' for /v1/ai-laws/domains/{key}.json, '#/$defs/instrumentRow' for one line of ai-laws/export/instruments.jsonl, '#/$defs/jurisdictionRow' for one line of ai-laws/export/jurisdictions.jsonl, and '#/$defs/exportManifest' for ai-laws/export/manifest.json.",
  "x-ungovr-contract": "ungovr.ai-laws/2",
  "x-ungovr-license": "https://www.ungovr.org/open-data/license",

  "$defs": {
    "contractVersion": {
      "title": "Contract version",
      "description": "The versioned contract every record is stamped with. Refuse an unrecognised MAJOR loudly rather than degrading: a legal lint that cannot read its corpus has to say so. A new minor may add fields, so do not set additionalProperties false on the record objects below (see the note on openness in jurisdictionRecord).",
      "const": "ungovr.ai-laws/2"
    },

    "nonBlankString": {
      "title": "A string with actual content",
      "description": "minLength 1 is NOT sufficient for these fields. A single space passes it while being truthy in both Python and JavaScript, so a whitespace-only citation would sail through the schema, through a consumer's own validation, and publish as a cited finding citing nothing. bin/build_data_api.py aborts the build on exactly that condition using .strip(); this pattern is the same rule expressed where a third party can enforce it.",
      "type": "string",
      "pattern": "\\S"
    },

    "jurisdictionSlug": {
      "title": "Jurisdiction slug",
      "description": "Lowercase, slash-separated, most-specific-wins: 'eu', 'us', 'us/ca'. Resolution walks toward the root, so 'us/ca/ventura' is answered by 'us/ca' when no deeper record exists.",
      "type": "string",
      "pattern": "^[a-z0-9]+(/[a-z0-9-]+)*$",
      "examples": ["eu", "us", "us/ca", "ie"]
    },

    "isoDate": {
      "title": "Calendar date or null",
      "description": "ISO 8601 calendar date. Null means the corpus holds no date for this field, never that the field is current.",
      "type": ["string", "null"],
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
    },

    "jurisdictionRef": {
      "title": "Jurisdiction identity",
      "type": "object",
      "properties": {
        "slug": { "$ref": "#/$defs/jurisdictionSlug" },
        "name": {
          "description": "The jurisdiction's name in English.",
          "$ref": "#/$defs/nonBlankString"
        },
        "level": { "$ref": "#/$defs/jurisdictionLevel" }
      },
      "required": ["slug", "name", "level"]
    },

    "jurisdictionLevel": {
      "title": "Jurisdiction level",
      "description": "Deliberately an open string rather than a closed enum. The three values below are what the corpus carries today, but the level vocabulary is not part of the documented contract vocabulary (unlike instrument status and behaviour category), so a fourth value is a data addition rather than a contract event and must not fail third-party ingest. Open does not mean blank: the value still has to say something.",
      "type": "string",
      "pattern": "\\S",
      "examples": ["national", "subnational", "supranational"]
    },

    "confidence": {
      "title": "Research confidence",
      "description": "How much weight the research carries. Open for the same reason as jurisdictionLevel. Null means unrecorded, never high.",
      "type": ["string", "null"],
      "examples": ["high", "medium", "low"]
    },

    "instrumentStatus": {
      "title": "Instrument status",
      "description": "CLOSED enum, and closed on purpose. The contract states that a new status is a contract event and not a data update, because every consumer maps these onto its own policy and a value it has no mapping for is an obligation it cannot classify. Closing the enum makes a validator refuse the payload loudly instead of a consumer guessing; 'dead' reported as a live duty is the failure this prevents. Enforced upstream by the ai_law_instruments_status_check constraint and pinned to INSTRUMENT_STATUSES in data_api/ai_law_builders.py.",
      "enum": ["enacted", "proposed", "in_committee", "dead"]
    },

    "behaviorCategory": {
      "title": "Behaviour category",
      "description": "CLOSED enum, pinned to INSTRUMENT_BEHAVIOR_CATEGORIES in data_api/ai_law_builders.py. Unlike status this is not enforced by a database constraint, so bin/build_data_api.py logs a warning before publish when the corpus emits a value outside the set.",
      "enum": ["agent_txn", "auto_decision", "crawler_training", "disclosure"]
    },

    "appliesTo": {
      "title": "Who the instrument binds",
      "description": "CLOSED enum, enforced upstream by the ai_law_instruments_applies_to_check constraint. Null is permitted because the builder passes an absent value through as null rather than inventing a default.",
      "enum": ["government", "private", "both", null]
    },

    "accessVerdict": {
      "title": "Access verdict for one scenario",
      "type": "object",
      "properties": {
        "verdict": { "$ref": "#/$defs/nonBlankString" },
        "basis": { "type": "string" },
        "note": { "type": "string" }
      },
      "required": ["verdict"]
    },

    "accessMatrix": {
      "title": "Per-scenario access verdicts",
      "description": "Keyed by access scenario. The six the corpus carries today are public_unauthenticated, behind_login, tos_accepted, robots_disallowed, after_cease_and_desist, and after_technical_circumvention. An empty object means the jurisdiction has a record but no scenario verdicts yet, which is not the same as 'permitted'.",
      "type": "object",
      "additionalProperties": { "$ref": "#/$defs/accessVerdict" }
    },

    "crawlPolicy": {
      "title": "Verdict-level crawl posture",
      "description": "Keyed by policy dimension, values are verdict strings. The six the corpus carries today are public_crawl, authenticated_crawl, robots_compliance, tdm_indexing_use, personal_data, and overall_risk_tier. An empty object means no posture recorded, not a permissive posture.",
      "type": "object",
      "additionalProperties": { "type": "string" }
    },

    "accessLegality": {
      "title": "The ten whitelisted legality fields",
      "description": "Exactly ten keys, always present, individually nullable. privacy_regime, disclosure_type, and disclosure_contexts are deliberately absent from the contract: prose, and columns with no migration.",
      "type": "object",
      "properties": {
        "authorization_test": { "type": ["string", "null"] },
        "public_page_carveout": { "type": ["string", "null"] },
        "tos_browsewrap_enforceable": { "type": ["string", "null"] },
        "tos_clickwrap_enforceable": { "type": ["string", "null"] },
        "copyright_exception_model": { "type": ["string", "null"] },
        "tdm_commercial_status": { "type": ["string", "null"] },
        "tdm_optout_mechanism": { "type": ["string", "null"] },
        "robots_txt_legal_weight": { "type": ["string", "null"] },
        "ai_training_specific_law": { "type": ["string", "null"] },
        "trespass_to_chattels": { "type": ["string", "null"] }
      },
      "required": [
        "authorization_test",
        "public_page_carveout",
        "tos_browsewrap_enforceable",
        "tos_clickwrap_enforceable",
        "copyright_exception_model",
        "tdm_commercial_status",
        "tdm_optout_mechanism",
        "robots_txt_legal_weight",
        "ai_training_specific_law",
        "trespass_to_chattels"
      ]
    },

    "provenance": {
      "title": "Freshness and research state",
      "description": "The load-bearing block for legal data. A downstream model answering from this corpus without an as-of date is a liability problem for both parties, so every record carries one.",
      "type": "object",
      "properties": {
        "as_of_date": {
          "$ref": "#/$defs/isoDate",
          "description": "The canonical freshness date for this record."
        },
        "confidence": { "$ref": "#/$defs/confidence" },
        "fast_moving": {
          "description": "The jurisdiction's law is changing quickly, so the as-of date decays faster than the staleness threshold suggests.",
          "type": "boolean"
        },
        "version": {
          "description": "Monotonic research revision for the jurisdiction.",
          "type": ["integer", "null"]
        },
        "stale": {
          "description": "The publisher's own verdict AT BUILD TIME, true when as_of_date is absent or older than the 60-day threshold. True is a reason to re-check, not a reason to discard.",
          "type": "boolean"
        },
        "review_pending": {
          "description": "Research not yet human-verified. A consumer must not present a review_pending record as a clean result.",
          "type": "boolean"
        },
        "source": { "$ref": "#/$defs/nonBlankString" }
      },
      "required": [
        "as_of_date",
        "confidence",
        "fast_moving",
        "version",
        "stale",
        "review_pending",
        "source"
      ]
    },

    "notice": {
      "title": "Not-legal-advice notice",
      "description": "Rides every record rather than the response envelope, because a bulk consumer strips envelope context on ingest.",
      "type": "object",
      "properties": {
        "disclaimer": { "$ref": "#/$defs/nonBlankString" },
        "client_side_note": { "$ref": "#/$defs/nonBlankString" }
      },
      "required": ["disclaimer", "client_side_note"]
    },

    "license": {
      "title": "License and attribution envelope",
      "description": "Travels on every record for the same reason the notice does. Receipt of a record conveys no license; terms are by agreement at the url below.",
      "type": "object",
      "properties": {
        "name": { "$ref": "#/$defs/nonBlankString" },
        "url": { "type": "string", "format": "uri" },
        "holder": { "$ref": "#/$defs/nonBlankString" },
        "attribution": { "$ref": "#/$defs/nonBlankString" },
        "grant": { "$ref": "#/$defs/nonBlankString" }
      },
      "required": ["name", "url", "holder", "attribution", "grant"]
    },

    "instrument": {
      "title": "One legal instrument, as it appears inside a jurisdiction record",
      "type": "object",
      "properties": {
        "name": { "$ref": "#/$defs/nonBlankString" },
        "citation": {
          "description": "Official citation, never empty. bin/build_data_api.py aborts the whole build rather than publish a blank or whitespace-only citation, because a cited finding citing nothing is worse than a missing record.",
          "$ref": "#/$defs/nonBlankString"
        },
        "url": { "type": ["string", "null"], "format": "uri" },
        "status": { "$ref": "#/$defs/instrumentStatus" },
        "effective_date": {
          "$ref": "#/$defs/isoDate",
          "description": "When the instrument takes or took effect. A future date on an enacted instrument means it is law but not yet a live duty."
        },
        "applies_to": { "$ref": "#/$defs/appliesTo" },
        "behavior_categories": {
          "type": "array",
          "items": { "$ref": "#/$defs/behaviorCategory" },
          "uniqueItems": true
        },
        "summary": { "type": "string" },
        "as_of_date": {
          "$ref": "#/$defs/isoDate",
          "description": "This instrument's own research date, which may be absent even when the jurisdiction has one."
        }
      },
      "required": [
        "name",
        "citation",
        "url",
        "status",
        "effective_date",
        "applies_to",
        "behavior_categories",
        "summary",
        "as_of_date"
      ]
    },

    "jurisdictionRecord": {
      "title": "Jurisdiction record: /v1/ai-laws/{slug}.json",
      "description": "The per-jurisdiction file. Note that additionalProperties is deliberately NOT false here or on any record object in this schema: the contract states that adding a field is not a major bump, and a consumer validating with closed objects would reject a conforming payload the moment a field is added, which would make that documented rule false in practice.",
      "type": "object",
      "properties": {
        "schema": { "$ref": "#/$defs/contractVersion" },
        "jurisdiction": { "$ref": "#/$defs/jurisdictionRef" },
        "crawl_policy": { "$ref": "#/$defs/crawlPolicy" },
        "access_matrix": { "$ref": "#/$defs/accessMatrix" },
        "access_legality": { "$ref": "#/$defs/accessLegality" },
        "summary": { "type": "string" },
        "instruments": {
          "description": "Sorted by (citation, name) so the ordering is stable across reloads. An empty array means this jurisdiction has verdict-level research and no instruments, which the index makes visible as instrument_count 0.",
          "type": "array",
          "items": { "$ref": "#/$defs/instrument" }
        },
        "provenance": { "$ref": "#/$defs/provenance" },
        "notice": { "$ref": "#/$defs/notice" },
        "license": { "$ref": "#/$defs/license" }
      },
      "required": [
        "schema",
        "jurisdiction",
        "crawl_policy",
        "access_matrix",
        "access_legality",
        "summary",
        "instruments",
        "provenance",
        "notice",
        "license"
      ]
    },

    "indexEntry": {
      "title": "One row of the coverage index",
      "type": "object",
      "properties": {
        "slug": { "$ref": "#/$defs/jurisdictionSlug" },
        "name": { "$ref": "#/$defs/nonBlankString" },
        "level": { "$ref": "#/$defs/jurisdictionLevel" },
        "as_of_date": { "$ref": "#/$defs/isoDate" },
        "confidence": { "$ref": "#/$defs/confidence" },
        "stale": { "type": "boolean" },
        "instrument_count": {
          "description": "Present so that 'the export shipped zero instruments' is one fetch away instead of invisible.",
          "type": "integer",
          "minimum": 0
        }
      },
      "required": [
        "slug",
        "name",
        "level",
        "as_of_date",
        "confidence",
        "stale",
        "instrument_count"
      ]
    },

    "index": {
      "title": "Coverage index: /v1/ai-laws/index.json",
      "description": "One row per covered jurisdiction, sorted by slug.",
      "type": "array",
      "items": { "$ref": "#/$defs/indexEntry" }
    },

    "domainShard": {
      "title": "Domain shard: /v1/ai-laws/domains/{key}.json",
      "description": "Maps a bare domain (one leading 'www.' already stripped) to the jurisdiction that governs it. The value is an OBJECT, never a slug string: a consumer that returns the whole object where a slug is expected produces output its own other tools cannot consume.",
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "jurisdiction_slug": { "$ref": "#/$defs/jurisdictionSlug" },
          "entity_slug": {
            "description": "The specific government entity that owns the domain, when exactly one claims it. Null when several entities in the same country do, in which case jurisdiction_slug has already fallen back to the national level.",
            "type": ["string", "null"]
          }
        },
        "required": ["jurisdiction_slug", "entity_slug"]
      }
    },

    "exportRowCommon": {
      "title": "Fields every bulk-export line carries",
      "description": "The bulk export is denormalized so that one line is a self-contained retrieval chunk: it names its own jurisdiction, its own freshness, its own notice, and its own license, because an embedding pipeline preserves none of the surrounding context.",
      "type": "object",
      "properties": {
        "schema": { "$ref": "#/$defs/contractVersion" },
        "record_type": { "enum": ["instrument", "jurisdiction"] },
        "record_id": {
          "description": "Stable within a build and across builds for an unchanged record. Derived from the jurisdiction slug plus, for an instrument, its citation and name. Not a database identifier.",
          "$ref": "#/$defs/nonBlankString"
        },
        "jurisdiction": { "$ref": "#/$defs/jurisdictionRef" },
        "provenance": { "$ref": "#/$defs/exportProvenance" },
        "notice": { "$ref": "#/$defs/notice" },
        "license": { "$ref": "#/$defs/license" }
      },
      "required": [
        "schema",
        "record_type",
        "record_id",
        "jurisdiction",
        "provenance",
        "notice",
        "license"
      ]
    },

    "exportProvenance": {
      "title": "Freshness on a bulk-export line",
      "description": "The jurisdiction's freshness verdict verbatim, plus a resolved as_of_date so a retrieval chunk always has one date to quote. No new staleness semantics are computed per instrument.",
      "type": "object",
      "allOf": [{ "$ref": "#/$defs/provenance" }],
      "properties": {
        "as_of_source": {
          "description": "Which date as_of_date came from. 'instrument' means the instrument carries its own research date; 'jurisdiction' means it was inherited from the jurisdiction record, so it describes the jurisdiction's last review and not this instrument's. Naming the source is the point: an inherited date presented as the instrument's own is a freshness claim the corpus cannot support.",
          "enum": ["instrument", "jurisdiction", "none"]
        },
        "instrument_as_of_date": {
          "$ref": "#/$defs/isoDate",
          "description": "The instrument's own date, unresolved. Null on a jurisdiction row and on an instrument that carries no date of its own."
        },
        "jurisdiction_as_of_date": {
          "$ref": "#/$defs/isoDate",
          "description": "The jurisdiction record's date, unresolved."
        }
      },
      "required": ["as_of_source", "instrument_as_of_date", "jurisdiction_as_of_date"]
    },

    "instrumentRow": {
      "title": "One line of ai-laws/export/instruments.jsonl",
      "description": "One legal instrument as a self-contained record. record_type is 'instrument'.",
      "type": "object",
      "allOf": [{ "$ref": "#/$defs/exportRowCommon" }],
      "properties": {
        "record_type": { "const": "instrument" },
        "name": { "$ref": "#/$defs/nonBlankString" },
        "citation": { "$ref": "#/$defs/nonBlankString" },
        "url": { "type": ["string", "null"], "format": "uri" },
        "status": { "$ref": "#/$defs/instrumentStatus" },
        "effective_date": { "$ref": "#/$defs/isoDate" },
        "applies_to": { "$ref": "#/$defs/appliesTo" },
        "behavior_categories": {
          "type": "array",
          "items": { "$ref": "#/$defs/behaviorCategory" },
          "uniqueItems": true
        },
        "summary": { "type": "string" }
      },
      "required": [
        "name",
        "citation",
        "url",
        "status",
        "effective_date",
        "applies_to",
        "behavior_categories",
        "summary"
      ]
    },

    "jurisdictionRow": {
      "title": "One line of ai-laws/export/jurisdictions.jsonl",
      "description": "One jurisdiction's verdict-level research as a self-contained record: the crawl posture, the access matrix, and the ten legality fields, which is what a scraping-legality question is answered from. Its instruments are NOT nested here; they are separate lines of instruments.jsonl, joinable on jurisdiction.slug, and instrument_count says how many to expect. record_type is 'jurisdiction'.",
      "type": "object",
      "allOf": [{ "$ref": "#/$defs/exportRowCommon" }],
      "properties": {
        "record_type": { "const": "jurisdiction" },
        "crawl_policy": { "$ref": "#/$defs/crawlPolicy" },
        "access_matrix": { "$ref": "#/$defs/accessMatrix" },
        "access_legality": { "$ref": "#/$defs/accessLegality" },
        "summary": { "type": "string" },
        "instrument_count": { "type": "integer", "minimum": 0 }
      },
      "required": [
        "crawl_policy",
        "access_matrix",
        "access_legality",
        "summary",
        "instrument_count"
      ]
    },

    "exportManifest": {
      "title": "Bulk-export descriptor: ai-laws/export/manifest.json",
      "description": "The envelope for the JSONL files. Fetch this first: each entry carries the row count and the SHA-256 of the file it names, so a consumer can tell a complete corpus from one caught mid-publish, and a zero row count is one fetch away instead of invisible.",
      "type": "object",
      "properties": {
        "schema": { "$ref": "#/$defs/contractVersion" },
        "schema_url": {
          "description": "This document's canonical URL, which is also its $id.",
          "type": "string",
          "format": "uri"
        },
        "built_at": {
          "description": "UTC timestamp of the build that produced these files.",
          "type": "string"
        },
        "license": { "$ref": "#/$defs/license" },
        "notice": { "$ref": "#/$defs/notice" },
        "vocabulary": {
          "description": "The closed vocabularies as this build emitted them, so a consumer pins its own mappings to the publisher's rather than restating them from memory.",
          "type": "object",
          "properties": {
            "statuses": { "type": "array", "items": { "$ref": "#/$defs/instrumentStatus" } },
            "behavior_categories": {
              "type": "array",
              "items": { "$ref": "#/$defs/behaviorCategory" }
            }
          },
          "required": ["statuses", "behavior_categories"]
        },
        "files": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "properties": {
              "path": { "$ref": "#/$defs/nonBlankString" },
              "url": { "type": "string", "format": "uri" },
              "record_type": { "enum": ["instrument", "jurisdiction"] },
              "row_schema": {
                "description": "JSON pointer into this schema that one line of the file validates against.",
                "type": "string",
                "format": "uri-reference"
              },
              "rows": { "type": "integer", "minimum": 0 },
              "bytes": { "type": "integer", "minimum": 0 },
              "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
            },
            "required": [
              "path",
              "url",
              "record_type",
              "row_schema",
              "rows",
              "bytes",
              "sha256"
            ]
          }
        }
      },
      "required": [
        "schema",
        "schema_url",
        "built_at",
        "license",
        "notice",
        "vocabulary",
        "files"
      ]
    }
  }
}
