{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artwaste.land/data/hansard-reactions/schema.json",
  "title": "Hansard reaction notation cue record",
  "description": "One extracted reaction notation cue. The two acquisition regimes have different source affordances, so provenance fields that do not exist in a regime are absent rather than invented.",
  "type": "object",
  "required": [
    "kind",
    "date",
    "cue_text",
    "preceding_text"
  ],
  "properties": {
    "kind": {
      "type": "string",
      "enum": [
        "bracketed_cue_html",
        "bracketed_cue",
        "attributed_speech"
      ],
      "description": "Extraction route. bracketed_cue_html is a bracketed match in sampled historic HTML; bracketed_cue is a bracketed match inside an XML speech; attributed_speech is an XML speech whose speaker label matches the frozen generic-member predicate."
    },
    "date": {
      "type": "string",
      "description": "Source-supplied sitting date. XML records use YYYY-MM-DD. Historic HTML records preserve the source path house/YYYY/mon/DD because that archive can contain internally inconsistent dates.",
      "anyOf": [
        {
          "format": "date",
          "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
        },
        {
          "pattern": "^(commons|lords)/[0-9]{4}/(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/(0[1-9]|[12][0-9]|3[01])$"
        }
      ]
    },
    "house": {
      "type": "string",
      "enum": [
        "commons",
        "lords"
      ],
      "description": "House named by the historic HTML path. Absent from ParlParse XML rows because the harvester did not emit it."
    },
    "url": {
      "type": "string",
      "format": "uri",
      "pattern": "^https://api\\.parliament\\.uk/historic-hansard/",
      "description": "Exact historic HTML section URL fetched for this row. This is the strongest available HTML provenance locator."
    },
    "section": {
      "type": "string",
      "minLength": 1,
      "description": "Final path component of the historic HTML section URL. It is a source label, not a globally unique identifier."
    },
    "file": {
      "type": "string",
      "pattern": "^debates[0-9]{4}-[0-9]{2}-[0-9]{2}[^/]*\\.xml$",
      "description": "Exact ParlParse source filename. Resolve it against the XML collection URL in sources.json to retrieve the source bytes."
    },
    "sha256": {
      "type": "string",
      "pattern": "^[0-9a-f]{16}$",
      "description": "First 16 lowercase hexadecimal characters of the SHA-256 digest of the fetched XML source bytes. The current harvester discarded the remaining 48 characters, so this is a digest prefix, not a complete SHA-256 value. Absent from historic HTML rows."
    },
    "speech_id": {
      "type": "string",
      "minLength": 1,
      "description": "The id attribute of the containing or matched XML speech element. Together with file, sha256, and colnum it walks an XML row back to its source speech. Absent from historic HTML rows because the source and harvester expose no equivalent identifier."
    },
    "colnum": {
      "type": "string",
      "minLength": 1,
      "description": "The source XML speech element's colnum attribute, preserved as text because column labels are archival identifiers rather than quantities. Absent from historic HTML rows."
    },
    "heading": {
      "type": "string",
      "description": "Flattened text of the latest major-heading element before the XML speech. Empty when no earlier major heading was found. Absent from historic HTML rows."
    },
    "cue_speaker": {
      "type": "string",
      "description": "For an XML bracketed cue, speakername on its containing speech; for attributed_speech, speakername on that speech. Empty means the attribute was missing or empty. This is archival metadata and is not a claim of identity. Absent from historic HTML rows."
    },
    "cue_text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 400,
      "description": "Flattened matched notation. It preserves OCR errors and incompletely decoded character references. XML bracketed and historic HTML values are truncated to 300 code units; attributed XML speech values are truncated to 400. Brackets themselves are not included for bracketed kinds."
    },
    "preceding_speaker": {
      "type": "string",
      "description": "For an XML bracketed cue, the containing speech's speakername; for attributed_speech, the previous non-empty XML speech's speakername. Empty means unavailable. Absent from historic HTML rows."
    },
    "preceding_text": {
      "type": "string",
      "maxLength": 400,
      "description": "Up to the final 400 code units of flattened text before the cue. For an attributed XML speech this comes from the previous non-empty speech. For a bracketed cue it comes from text earlier in the same speech or HTML section. Adjacency does not establish causation."
    },
    "preceding_id": {
      "type": "string",
      "minLength": 1,
      "description": "XML speech id associated with preceding_text. It equals speech_id for a bracketed cue and identifies the previous non-empty speech for attributed_speech. Absent from historic HTML rows."
    }
  },
  "additionalProperties": false,
  "oneOf": [
    {
      "title": "Seeded historic HTML sample row",
      "properties": {
        "kind": {
          "const": "bracketed_cue_html"
        },
        "date": {
          "pattern": "^(commons|lords)/[0-9]{4}/(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/(0[1-9]|[12][0-9]|3[01])$"
        }
      },
      "required": [
        "house",
        "url",
        "section"
      ],
      "not": {
        "anyOf": [
          { "required": ["file"] },
          { "required": ["sha256"] },
          { "required": ["speech_id"] },
          { "required": ["colnum"] },
          { "required": ["heading"] },
          { "required": ["cue_speaker"] },
          { "required": ["preceding_speaker"] },
          { "required": ["preceding_id"] }
        ]
      }
    },
    {
      "title": "ParlParse XML row",
      "properties": {
        "kind": {
          "enum": [
            "bracketed_cue",
            "attributed_speech"
          ]
        },
        "date": {
          "format": "date",
          "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
        }
      },
      "required": [
        "file",
        "sha256",
        "speech_id",
        "colnum",
        "heading",
        "cue_speaker",
        "preceding_speaker",
        "preceding_id"
      ],
      "not": {
        "anyOf": [
          { "required": ["house"] },
          { "required": ["url"] },
          { "required": ["section"] }
        ]
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "kind": {
                "const": "bracketed_cue"
              }
            }
          },
          "then": {
            "properties": {
              "cue_text": {
                "maxLength": 300
              }
            }
          }
        }
      ]
    }
  ]
}
