{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://trialschema.org/v1/export.schema.json",
  "title": "TrialSchema Export",
  "description": "A portable, agent-friendly representation of clinical-trial eligibility designed for automated patient matching. TrialSchema makes explicit what to check (criteria concepts and assertions), where to look (document types, default visit order, and criterion-level routing), how to evaluate (constraints, thresholds, polarity, rank, and fallbacks), when evidence counts (optional criterion timing), and which normalized care path a trial belongs to (care_path_ids). Domain-neutral (oncology, cardiology, metabolic, rare disease, ...) and standards-aligned: a deliberate subset of HL7 FHIR R5 ResearchStudy with eligibility-focused additions, FHIR-shaped Codings, UCUM units, and an explicit user-defined document routing profile. Free-text fallbacks remain valid everywhere so end users never have to hand-curate ontology codes.",
  "type": "object",
  "required": ["format", "format_version", "generated_at", "routing_profile", "trials"],
  "additionalProperties": false,
  "properties": {
    "format":          { "const": "trialschema" },
    "format_version":  { "type": "string", "pattern": "^1\\.\\d+$", "description": "Semantic version of the format. Minor versions are additive." },
    "generated_at":    { "type": "string", "format": "date-time" },
    "generator":       { "$ref": "#/$defs/Generator" },
    "routing_profile": { "$ref": "#/$defs/RoutingProfile", "description": "The export-level answer to where a matching agent may look: available patient-document types, default scan set, and default visit order." },
    "care_path_catalog": { "type": "array", "items": { "$ref": "#/$defs/CarePathCatalogEntry" }, "description": "Optional normalized care paths active when this export was generated. Trials reference these via care_path_ids as a first-pass matching filter before criterion evaluation. Persisted so English display names, assignments, and original source-value aliases survive a re-import." },
    "trial_count":     { "type": "integer", "minimum": 0 },
    "trials":          { "type": "array", "items": { "$ref": "#/$defs/Trial" } },
    "extensions":      { "$ref": "#/$defs/Extensions" }
  },

  "$defs": {

    "Generator": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name":    { "type": "string" },
        "version": { "type": "string" }
      },
      "additionalProperties": false
    },

    "Extensions": {
      "type": "object",
      "description": "Free-form namespaced extension bag. Implementers SHOULD prefix keys with their own reverse-DNS namespace, e.g. \"org.example.foo\".",
      "additionalProperties": true
    },

    "RoutingProfile": {
      "type": "object",
      "description": "The document universe that answers where to look. It defines available patient-document types, the default maximum scan set for most criteria, and the default visit order used when a criterion has no more specific routing.",
      "required": ["id", "document_type_system", "default_scan_set", "default_visit_order", "document_types"],
      "additionalProperties": false,
      "properties": {
        "id":                   { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "Stable profile id, e.g. default-clinical." },
        "label":                { "type": "string" },
        "document_type_system": { "type": "string", "format": "uri", "description": "Canonical URI for the document-type id namespace." },
        "default_scan_set":     { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/DocumentType" }, "uniqueItems": true, "description": "Default maximum document set to scan for most criteria. Agents should inspect these document types first, ordered by default_visit_order, unless criterion.routing gives a more specific primary/fallback route." },
        "default_visit_order":  { "type": "array", "items": { "$ref": "#/$defs/DocumentType" }, "uniqueItems": true, "description": "General order in which a matching agent should visit patient documents. Criterion-level routing may narrow the set, but this order remains the tie-breaker." },
        "document_types":       { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/RoutingDocumentType" } }
      }
    },

    "RoutingDocumentType": {
      "type": "object",
      "description": "A patient-document type available to the extractor and downstream matching agent. Labels and hints explain what content is usually found there; order lives in routing_profile.default_visit_order, while criterion-level routing states where each rule should look.",
      "required": ["id", "label"],
      "additionalProperties": false,
      "properties": {
        "id":    { "$ref": "#/$defs/DocumentType" },
        "label": { "type": "string", "description": "Human-readable document type name." },
        "hint":  { "type": "string", "description": "Agent-facing guidance describing typical content and when to scan this document type." }
      }
    },

    "Coding": {
      "type": "object",
      "description": "FHIR-shaped reference to a concept in a terminology. Every field is optional; `text` alone is a valid free-text fallback so end users are never required to fill in ontology codes.",
      "properties": {
        "system":  { "type": "string", "format": "uri", "description": "Canonical URI of the terminology, e.g. http://loinc.org, http://snomed.info/sct, http://www.nlm.nih.gov/research/umls/rxnorm, http://hl7.org/fhir/sid/icd-10, http://www.genenames.org" },
        "code":    { "type": "string" },
        "display": { "type": "string", "description": "Human-readable label as it appears in the terminology." },
        "text":    { "type": "string", "description": "User-facing free-text label. Always allowed; required when neither system+code nor display are set." }
      },
      "anyOf": [
        { "required": ["system", "code"] },
        { "required": ["text"] },
        { "required": ["display"] }
      ],
      "additionalProperties": false
    },

    "Identifier": {
      "type": "object",
      "required": ["system", "value"],
      "properties": {
        "system": { "type": "string", "format": "uri" },
        "value":  { "type": "string" }
      },
      "additionalProperties": false
    },

    "Quantity": {
      "type": "object",
      "description": "FHIR-shaped numeric quantity. UCUM is recommended for `system`+`code`; `unit` is the human-readable label.",
      "required": ["value"],
      "properties": {
        "value":  { "type": "number" },
        "unit":   { "type": "string", "description": "Human-readable unit (e.g. \"mL/min\", \"years\", \"%\")." },
        "system": { "type": "string", "format": "uri", "default": "http://unitsofmeasure.org" },
        "code":   { "type": "string", "description": "UCUM (or other system) code, e.g. \"a\" for years, \"mg/dL\", \"L/min\"." }
      },
      "additionalProperties": false
    },

    "Trial": {
      "type": "object",
      "$comment": "FHIR R5: ResearchStudy.",
      "required": ["id", "kind", "enabled", "criteria"],
      "additionalProperties": false,
      "properties": {
        "id":          { "type": "string", "description": "Stable primary key. Typically an NCT id; any unique string is allowed.", "$comment": "FHIR R5: ResearchStudy.identifier (primary)." },
        "kind":        { "const": "trial" },
        "enabled":     { "type": "boolean", "default": true, "description": "Local TrialSchema matching switch. If false, agents should skip this trial regardless of dates, care path, or criteria. This is not a recruitment status." },
        "title":       { "type": "string", "$comment": "FHIR R5: ResearchStudy.title." },
        "identifiers": { "type": "array", "items": { "$ref": "#/$defs/Identifier" }, "$comment": "FHIR R5: ResearchStudy.identifier." },

        "sponsor": {
          "type": "object",
          "$comment": "FHIR R5: ResearchStudy.associatedParty[role=sponsor].",
          "additionalProperties": false,
          "properties": {
            "name":       { "type": "string" },
            "identifier": { "$ref": "#/$defs/Identifier" }
          }
        },

        "conditions":    { "type": "array", "items": { "$ref": "#/$defs/Coding" }, "description": "Trial-level scope: ClinicalTrials.gov-style conditions, diagnoses, disorders, or study focus. This is not by itself an eligibility rule; explicit patient requirements still belong in criteria.", "$comment": "FHIR R5: ResearchStudy.condition." },
        "interventions": { "type": "array", "items": { "$ref": "#/$defs/Intervention" }, "description": "Trial-level scope: investigational or protocol interventions such as drugs, devices, procedures, radiation, diagnostics, or behavioral interventions. This is not by itself an eligibility rule; prior/current therapy requirements still belong in criteria.", "$comment": "FHIR R5: ResearchStudy.focus (with type=intervention)." },

        "lifecycle": {
          "type": "object",
          "$comment": "FHIR R5: ResearchStudy.period. TrialSchema v1 keeps only matcher-relevant open/close dates.",
          "additionalProperties": false,
          "properties": {
            "start_date":      { "$ref": "#/$defs/PartialDate", "description": "Optional matching-window open date. If present and in the future, agents should not match patients before this date." },
            "completion_date": { "$ref": "#/$defs/PartialDate", "description": "Optional matching-window close date. If present and in the past, agents should skip this trial." }
          }
        },

        "care_path_ids": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$" }, "uniqueItems": true, "description": "Optional but first-class trial-level matching filter. Ids of the normalized care paths (from care_path_catalog) this trial belongs to, for example breast_cancer or heart_failure. Matching agents should evaluate criteria only when the patient care path matches at least one of these ids; if omitted, care-path filtering cannot be applied. Persisted so manual assignments survive a re-import." },
        "criteria":  { "type": "array", "items": { "$ref": "#/$defs/Criterion" }, "description": "Ordered, agent-friendly eligibility rules. Each item should represent one patient-level inclusion or exclusion requirement and make explicit what to check, how to evaluate it, and where to look. Do not use criteria for mere trial scope such as a listed condition, intervention, or care-path assignment. Array index === run order; the first criterion runs first.", "$comment": "FHIR R5: ResearchStudy.recruitment.eligibility (resolved). v1 represents each criterion inline rather than via a Group reference." },

        "provenance": { "$ref": "#/$defs/TrialProvenance" },
        "extensions": { "$ref": "#/$defs/Extensions" }
      }
    },

    "PartialDate": {
      "type": "string",
      "description": "ISO-8601 date with optional precision: YYYY, YYYY-MM, or YYYY-MM-DD. Empty string is allowed for editable drafts.",
      "pattern": "^$|^\\d{4}(-\\d{2}(-\\d{2})?)?$"
    },

    "Intervention": {
      "type": "object",
      "required": ["type", "label"],
      "additionalProperties": false,
      "properties": {
        "type":  {
          "type": "string",
          "enum": ["drug", "device", "procedure", "biological", "behavioral", "radiation", "diagnostic-test", "other"]
        },
        "label": { "type": "string" },
        "code":  { "$ref": "#/$defs/Coding" }
      }
    },

    "CarePathCatalogEntry": {
      "type": "object",
      "description": "A normalized care path in the export-level enum, used as a first-pass matching filter before evaluating criteria. TrialSchema v1 stores stable care-path references such as breast_cancer, heart_failure, or type_2_diabetes; broad source buckets that mean not tied to one disease area should be represented as the English cross-domain/cross-care-path entry cross_care_path, not expanded into every disease path. Detailed pathway phases or actions can be modeled in extensions or future versions when needed. Trials reference the path by id via care_path_ids. The id and label are English; aliases capture original source values and cross-language spellings used for auto-assignment.",
      "required": ["id", "label"],
      "additionalProperties": false,
      "properties": {
        "id":      { "type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$", "description": "Stable English snake_case id." },
        "label":   { "type": "string", "description": "English human-readable display name." },
        "aliases": { "type": "array", "items": { "type": "string" }, "description": "Original source care-path values plus synonyms/spellings used for alias-based auto-assignment." },
        "concept": { "$ref": "#/$defs/Coding", "description": "Optional condition-domain anchor (typically a SNOMED CT code)." }
      }
    },

    "Criterion": {
      "type": "object",
      "required": ["id", "kind", "enabled", "rank", "source_text", "concept", "assertion", "constraint", "routing"],
      "additionalProperties": false,
      "properties": {
        "id":             { "type": "string", "description": "Stable identifier within the trial. Default convention: \"INC-01\", \"EXC-01\", …" },
        "kind":           { "type": "string", "enum": ["inclusion", "exclusion"] },
        "enabled":        { "type": "boolean", "default": true, "description": "User toggle. Disabled criteria MUST be skipped by the matcher." },
        "rank":           { "type": "integer", "minimum": 1, "description": "1..N position in the run order. Lower runs first, allowing a matching agent to check cheap or decisive criteria earlier." },

        "source_text":     { "type": "string", "description": "Verbatim original criterion text in the source language. Immutable after extraction; do not overwrite it with a clarified, translated, or manually rewritten version." },
        "clarified_text":  { "type": ["string", "null"], "description": "Optional agent-facing rewrite of the same criterion, typically in English, made explicit and unambiguous for patient matching. It MUST preserve the exact clinical meaning, thresholds, negation, and logic of source_text. Consumers SHOULD prefer `clarified_text ?? source_text` for matching while retaining source_text for audit, debugging, and reprocessing." },

        "concept":   { "$ref": "#/$defs/Concept" },
        "assertion": {
          "type": "string",
          "enum": ["present", "absent", "unknown"],
          "description": "Whether the criterion asserts the concept's presence (`present`), its absence (`absent`, e.g. \"no history of …\"), or makes no assertion (`unknown`)."
        },

        "constraint":  { "$ref": "#/$defs/Constraint" },
        "timing": { "$ref": "#/$defs/Timing", "description": "Optional since v1.1. Which part of the patient's history counts as evidence. Omission means unspecified, not current." },

        "routing":   { "$ref": "#/$defs/Routing" },
        "guidance":  { "type": "string", "description": "Free-text guidance for the matching agent when source_text, clarified_text, concept, assertion, constraint, and routing are not enough. Strongly recommended when the routing includes \"other\"." },

        "provenance": { "$ref": "#/$defs/Provenance" },
        "extensions": { "$ref": "#/$defs/Extensions" }
      }
    },

    "Timing": {
      "type": "object",
      "required": ["scope"],
      "additionalProperties": false,
      "description": "Evidence timing, independent of assertion, constraint, and document routing. All windows count back from screening. Use clinical event/measurement dates, not merely the date of the document mentioning them. If omitted, consumers must interpret the criterion text; they must not assume current-only evidence.",
      "properties": {
        "scope": { "type": "string", "enum": ["current", "ever", "window"], "description": "current: state at screening; ever: any time in the patient's history up to screening; window: within lookback before screening. A recent document may describe historical evidence." },
        "lookback": { "$ref": "#/$defs/Lookback" },
        "description": { "type": "string", "description": "Original timing wording, optionally in the source language. Preserve boundary details here and in criterion text. Do not encode other anchors, future intervals, minimum washouts, or complex timelines as a simple lookback." }
      },
      "if": { "properties": { "scope": { "const": "window" } }, "required": ["scope"] },
      "then": { "required": ["lookback"] },
      "else": { "not": { "required": ["lookback"] } }
    },

    "Lookback": {
      "type": "object",
      "required": ["value", "unit", "code"],
      "additionalProperties": false,
      "description": "Positive duration before screening, using UCUM time units. Do not convert calendar months/years into an assumed number of days.",
      "properties": {
        "value": { "type": "number", "exclusiveMinimum": 0 },
        "unit": { "type": "string", "enum": ["hours", "days", "weeks", "months", "years"] },
        "code": { "type": "string", "enum": ["h", "d", "wk", "mo", "a"] },
        "system": { "const": "http://unitsofmeasure.org" }
      },
      "oneOf": [
        { "properties": { "unit": { "const": "hours" }, "code": { "const": "h" } } },
        { "properties": { "unit": { "const": "days" }, "code": { "const": "d" } } },
        { "properties": { "unit": { "const": "weeks" }, "code": { "const": "wk" } } },
        { "properties": { "unit": { "const": "months" }, "code": { "const": "mo" } } },
        { "properties": { "unit": { "const": "years" }, "code": { "const": "a" } } }
      ]
    },

    "Concept": {
      "type": "object",
      "required": ["domain"],
      "additionalProperties": false,
      "description": "What the criterion is about. This answers what to check and is decoupled from where to look (which lives in routing) and how to evaluate (which lives in constraint/assertion).",
      "properties": {
        "domain": {
          "type": "string",
          "description": "High-level clinical category. Influences how the matcher fetches data.",
          "enum": [
            "demographics",
            "condition",
            "observation",
            "medication",
            "procedure",
            "adverse-event",
            "genomic",
            "consent",
            "logistic",
            "other"
          ]
        },
        "code": { "$ref": "#/$defs/Coding", "description": "Best-effort code in a recognised terminology (LOINC for observations, SNOMED CT for conditions/procedures, RxNorm/ATC for medications, HGNC for genes, ICD-10/-11 for diagnoses). Optional." },
        "text": { "type": "string", "description": "Human-readable concept name (e.g. \"Creatinine clearance\", \"ECOG performance status\"). Always allowed." }
      }
    },

    "Constraint": {
      "type": "object",
      "description": "Tagged union describing how the criterion evaluates against patient data: comparison, range, code-set, boolean, existence, or text fallback. Use `kind: \"text\"` as a graceful fallback when no machine-evaluable form is available.",
      "discriminator": { "propertyName": "kind" },
      "oneOf": [
        {
          "type": "object",
          "required": ["kind", "operator", "quantity"],
          "additionalProperties": false,
          "properties": {
            "kind":     { "const": "comparison" },
            "operator": { "type": "string", "enum": ["<", "<=", "=", ">=", ">", "!="] },
            "quantity": { "$ref": "#/$defs/Quantity" }
          }
        },
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "range" },
            "low":  { "$ref": "#/$defs/Quantity" },
            "high": { "$ref": "#/$defs/Quantity" }
          },
          "anyOf": [{ "required": ["low"] }, { "required": ["high"] }]
        },
        {
          "type": "object",
          "required": ["kind", "values"],
          "additionalProperties": false,
          "properties": {
            "kind":   { "const": "code-set" },
            "match":  { "type": "string", "enum": ["any", "all", "none"], "default": "any" },
            "values": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Coding" } }
          }
        },
        {
          "type": "object",
          "required": ["kind", "value"],
          "additionalProperties": false,
          "properties": {
            "kind":  { "const": "boolean" },
            "value": { "type": "boolean" }
          }
        },
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "exists" }
          }
        },
        {
          "type": "object",
          "required": ["kind", "description"],
          "additionalProperties": false,
          "description": "Free-text fallback. Always valid; the matcher will treat the criterion as LLM-judgement-required.",
          "properties": {
            "kind":        { "const": "text" },
            "description": { "type": "string" }
          }
        }
      ]
    },

    "Routing": {
      "type": "object",
      "description": "Criterion-specific answer to where to look. It references the export's top-level routing_profile and tells the matcher which patient-record document types to search first (`primary`) before optional secondary routes (`fallback`). Empty arrays mean no criterion-specific preference; use routing_profile.default_scan_set first, ordered by routing_profile.default_visit_order.",
      "required": ["primary", "fallback"],
      "additionalProperties": false,
      "properties": {
        "primary":  { "type": "array", "items": { "$ref": "#/$defs/DocumentType" }, "uniqueItems": true },
        "fallback": { "type": "array", "items": { "$ref": "#/$defs/DocumentType" }, "uniqueItems": true }
      }
    },

    "DocumentType": {
      "type": "string",
      "description": "Document type id declared in routing_profile.document_types[].id. Default ids use system https://trialschema.org/v1/document-types, but profiles may define additional ids.",
      "pattern": "^[a-z][a-z0-9-]*$"
    },

    "Provenance": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "extracted_at":  { "type": "string", "format": "date-time" },
        "extracted_by":  { "type": "string", "description": "Identifier of the model / tool that produced this criterion, e.g. \"openai/gpt-4o-mini\" or \"manual\"." },
        "clarified_at":  { "type": "string", "format": "date-time" },
        "clarified_by":  { "type": "string" }
      }
    },

    "TrialProvenance": {
      "type": "object",
      "additionalProperties": false,
      "description": "Trial-level edit provenance. Records whether the trial was structured by AI and/or hand-edited by a reviewer, so a resume/merge of a newer raw upload can preserve reviewed manual work instead of redoing it.",
      "properties": {
        "ai_processed":    { "type": "boolean", "description": "True if an AI model produced or updated this trial." },
        "manually_edited": { "type": "boolean", "description": "True if a human reviewer edited this trial in the workspace." },
        "ai_at":           { "type": "string", "format": "date-time", "description": "Timestamp of the most recent AI update." },
        "ai_by":           { "type": "string", "description": "Identifier of the AI model / tool, e.g. \"openai/gpt-4o-mini\" or \"manual-paste\"." },
        "edited_at":       { "type": "string", "format": "date-time", "description": "Timestamp of the most recent manual edit." }
      }
    }

  }
}
