{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://excsv.dev/schema/excsv-0.6.schema.json",
  "title": "ExCSV JSON form (.excsv.json)",
  "description": "The JSON form of an ExCSV document, carried in a .excsv.json file (media type application/excsv+json). Carries the same vocabulary as the text form (#!excsv header, #@ metadata, #column schema, #chart, #note/#link, #% aggregates, #$ SQL) so the two round-trip losslessly. The CSV text form stays canonical; the JSON form is for JSON-native contexts (APIs, config, LLM structured output).",
  "type": "object",
  "required": ["excsv"],
  "additionalProperties": false,
  "properties": {
    "excsv": {
      "description": "Format version. Mirrors #!excsv version=.",
      "type": "string",
      "examples": ["0.5"]
    },
    "layout": {
      "description": "Document shape. inline/sidecar are single-table (use root-level columns/data/reference); pack is multi-table (use tables[]).",
      "type": "string",
      "enum": ["inline", "sidecar", "pack"]
    },
    "csv": {
      "description": "How to reconstruct the canonical CSV text form. All optional; omit to accept defaults. Purely a round-trip hint — it does not affect the JSON data.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "delim": { "type": "string", "default": "comma", "description": "Delimiter name (comma|tab|pipe|semicolon) or a literal." },
        "quote": { "type": "string", "default": "none", "description": "Quote name (none|double|single) or a literal." },
        "header": { "type": "boolean", "default": true, "description": "Whether the canonical CSV carries a header row (column names come from `columns`)." },
        "encoding": { "type": "string", "default": "UTF-8" },
        "null": {
          "description": "Extra non-empty strings that also mean null in the CSV form. In JSON, null is the literal null.",
          "type": "array",
          "items": { "type": "string" }
        }
      }
    },
    "meta": {
      "description": "File-level metadata (#@ lines). Known keys below; any additional key is allowed.",
      "type": "object",
      "properties": {
        "source": { "type": "string", "description": "Origin system, table, or file." },
        "author": { "type": "string" },
        "comment": { "type": "string" },
        "grain": { "type": "string", "description": "What one data row represents, e.g. 'one row per order'." },
        "created": { "type": "string", "format": "date-time" },
        "exported": { "type": "string", "format": "date-time" },
        "license": { "type": "string" },
        "tool": { "type": "string" },
        "tags": {
          "oneOf": [
            { "type": "array", "items": { "type": "string" } },
            { "type": "string" }
          ]
        }
      },
      "additionalProperties": true
    },
    "columns": {
      "description": "Column schema (#column lines). Each entry MUST include `index`: the zero-based position of that column's value in every `data` row array (`data[row][index]`). The array SHOULD be sorted by `index`; each entry's array position SHOULD equal its `index`.",
      "type": "array",
      "items": { "$ref": "#/$defs/column" }
    },
    "charts": {
      "description": "Chart suggestions (#chart / #chart-<engine> lines). Advisory — every channel MUST reference a name present in `columns`. See docs/implementation/charts.md.",
      "type": "array",
      "items": { "$ref": "#/$defs/chart" }
    },
    "notes": {
      "description": "Notes (#note lines) on a cell, row, column, or the table. Advisory; file order preserved. See docs/implementation/notes.md.",
      "type": "array",
      "items": { "$ref": "#/$defs/note" }
    },
    "links": {
      "description": "One-off cell links (#link lines). Advisory; file order preserved. See docs/implementation/notes.md.",
      "type": "array",
      "items": { "$ref": "#/$defs/link" }
    },
    "aggregates": {
      "description": "Pre-computed aggregates (#% lines). Each key is an aggregation name; each value is an array with one entry per physical column (null = not applicable / not computed). The array length should equal the number of physical columns.",
      "type": "object",
      "propertyNames": {
        "anyOf": [
          { "enum": ["count_nonnull", "count_null", "count_distinct", "sum", "avg", "min", "max", "len_min", "len_max"] },
          { "type": "string" }
        ]
      },
      "additionalProperties": {
        "type": "array",
        "items": { "$ref": "#/$defs/cell" }
      }
    },
    "sql": {
      "description": "SQL companions (#$ lines).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "ddl": {
          "description": "Schema statements, executed in array order.",
          "type": "array",
          "items": { "$ref": "#/$defs/statement" }
        },
        "dql": {
          "description": "Queries (SELECT) for provenance; not executed.",
          "type": "array",
          "items": { "$ref": "#/$defs/statement" }
        }
      }
    },
    "checksum": {
      "description": "Integrity fingerprint of the data, '<algorithm>:<hex-digest>'. Advisory.",
      "type": "string",
      "pattern": "^[a-z0-9-]+:[0-9a-f]+$"
    },
    "rows": {
      "description": "Number of physical data rows (excluding the header). Required except in a pack (tables[] carries per-table rows instead). For inline data this should equal data.length.",
      "type": "integer",
      "minimum": 0
    },
    "column_count": {
      "description": "Total physical column count — stored plus materialized:true computed columns; virtual computed columns don't count. Optional. Named distinctly from `columns` (the #column schema array) to avoid collision; mirrors text-form `columns=`.",
      "type": "integer",
      "minimum": 0
    },
    "reference": {
      "description": "Sidecar mode: relative path to an external CSV/TSV data file. Mutually exclusive with `data`.",
      "type": "string"
    },
    "data": {
      "description": "Data rows as an array of arrays (one inner array per row). No header row is embedded — names live in `columns`. Each row's cell at position `i` is the value for the column with `index` = `i`. Absent in sidecar or header-only stubs.",
      "type": "array",
      "items": {
        "type": "array",
        "items": { "$ref": "#/$defs/cell" }
      }
    },
    "tables": {
      "description": "Pack (multi-table) payload. One entry per table. When present, root-level columns/aggregates/sql/data/reference are not used — those live inside each table.",
      "type": "array",
      "items": { "$ref": "#/$defs/table" }
    },
    "fk": {
      "description": "Pack foreign keys — an informational relationship map (#fk lines). Not enforced.",
      "type": "array",
      "items": { "$ref": "#/$defs/fk" }
    }
  },
  "allOf": [
    {
      "not": {
        "description": "A document cannot be both inline (data) and sidecar (reference).",
        "required": ["data", "reference"]
      }
    },
    {
      "not": {
        "description": "A pack (tables) cannot also carry root-level inline data.",
        "required": ["tables", "data"]
      }
    },
    {
      "not": {
        "description": "A pack (tables) cannot also carry a root-level sidecar reference.",
        "required": ["tables", "reference"]
      }
    },
    {
      "if": {
        "not": { "required": ["tables"] }
      },
      "then": {
        "description": "Non-pack documents (inline, sidecar, header-only stub) MUST declare rows. A pack carries rows per-table instead.",
        "required": ["rows"]
      }
    }
  ],
  "$defs": {
    "cell": {
      "description": "One CSV cell. JSON null is a true null. For decimal/long/int64, encode as string to avoid IEEE-754 precision loss; the column `type` tells consumers how to read it.",
      "type": ["string", "number", "boolean", "null"]
    },
    "scalar": {
      "type": ["string", "number", "boolean", "null"]
    },
    "column": {
      "type": "object",
      "properties": {
        "name": { "type": "string", "description": "Column identifier. No spaces." },
        "title": { "type": "string", "description": "Human-readable display name." },
        "description": { "type": "string" },
        "index": { "type": "integer", "minimum": 0, "description": "Zero-based position of this column's value in each `data` row array (`data[row][index]`). REQUIRED on every physical (stored or materialized) column; MUST NOT be present on a virtual computed column (see `formula`)." },
        "formula": { "type": "string", "description": "Computed-column definition: derives this column's values from other columns instead of storing them, using one portable expression language (no dialect selector) so any conforming tool can evaluate it to materialize the result. Never dropped, virtual or materialized." },
        "materialized": { "type": "boolean", "description": "For a `formula` column: true = values are also cached as real data (present in `data`, has `index`). Absent/false = virtual (no data cells, no `index`)." },
        "type": {
          "type": "string",
          "enum": ["string", "int", "long", "float", "double", "decimal", "boolean", "date", "time", "datetime", "uuid", "binary"]
        },
        "format": { "type": "string", "description": "Display/parse hint, e.g. '0.00'." },
        "unit": { "type": "string", "description": "Unit of measurement, e.g. USD, kg, ms." },
        "role": {
          "type": "string",
          "enum": ["id", "dimension", "measure", "time"],
          "description": "Analytical role, independent of storage type."
        },
        "agg": {
          "type": "string",
          "enum": ["sum", "avg", "min", "max", "none"],
          "description": "Additivity hint for role=measure."
        },
        "order": { "type": "string", "enum": ["none", "asc", "desc"] },
        "separator": { "type": "string", "description": "Sub-field separator inside a single cell." },
        "link": { "type": "string", "description": "URL template making each non-null value a link: {$} = this cell, {name} = another column in the row, {N} = column index (header=0), {{ = literal {. See docs/implementation/notes.md." },
        "enum": {
          "description": "Closed set of allowed non-null values (typed per `type`). In the text form this is a pipe-separated string; in JSON it is an array.",
          "type": "array",
          "items": { "$ref": "#/$defs/scalar" }
        },
        "pattern": { "type": "string", "description": "Regex the values match." },
        "regexp_dialect": {
          "type": "string",
          "enum": ["ecmascript", "pcre", "posix_ere", "re2"],
          "default": "ecmascript"
        },
        "min": { "$ref": "#/$defs/scalar", "description": "Minimum value (numeric/date)." },
        "max": { "$ref": "#/$defs/scalar", "description": "Maximum value (numeric/date)." },
        "len_min": { "type": "integer", "minimum": 0 },
        "len_max": { "type": "integer", "minimum": 0 },
        "unique": { "type": "boolean", "description": "Descriptive uniqueness hint." },
        "required": { "type": "boolean", "description": "false = nullable, true = not null." },
        "default": { "$ref": "#/$defs/scalar", "description": "Schema/DDL default. Not applied when reading data." }
      },
      "patternProperties": {
        "^x-": { "description": "Custom, tool-specific attribute." }
      },
      "additionalProperties": true,
      "allOf": [
        {
          "if": {
            "required": ["formula"],
            "properties": { "materialized": { "not": { "const": true } } }
          },
          "then": {
            "properties": { "index": false }
          },
          "else": {
            "required": ["index"]
          }
        }
      ]
    },
    "chart": {
      "description": "One #chart suggestion. Either the compact form (type= + channels + modifiers) or the #chart-vega escape hatch (raw Vega-Lite unit-spec). See docs/implementation/charts.md.",
      "type": "object",
      "oneOf": [
        {
          "required": ["type", "name"],
          "not": { "required": ["vega"] },
          "properties": {
            "type": {
              "type": "string",
              "description": "Chart mark. Unrecognized values MUST be preserved/ignored (chart_unknown_type), not rejected.",
              "examples": ["bar", "line", "area", "point", "circle", "arc", "rect", "tick", "boxplot", "text", "sparkline"]
            },
            "title": { "type": "string" },
            "name": { "type": "string", "description": "Required. Unique identifier for this chart line, for tooling to address it (e.g. `excsv chart --name`). Missing = chart_missing_name; duplicate across lines = chart_duplicate_name (last wins)." },
            "x": { "type": "string" },
            "y": { "type": "string" },
            "x2": { "type": "string" },
            "y2": { "type": "string" },
            "color": { "type": "string" },
            "size": { "type": "string" },
            "theta": { "type": "string" },
            "radius": { "type": "string" },
            "shape": { "type": "string" },
            "opacity": { "type": "string" },
            "column": { "type": "string", "description": "Facet column (small multiples)." },
            "row": { "type": "string", "description": "Facet row (small multiples)." },
            "detail": { "type": "string" },
            "order": { "type": "string" },
            "tooltip": {
              "oneOf": [
                { "type": "string" },
                { "type": "array", "items": { "type": "string" } }
              ]
            },
            "text": { "type": "string" },
            "aggregate": { "type": "string", "enum": ["sum", "avg", "min", "max", "count", "count_distinct"] },
            "bin": { "oneOf": [{ "type": "boolean" }, { "type": "integer", "minimum": 1 }] },
            "stack": { "oneOf": [{ "type": "boolean" }, { "const": "normalize" }] },
            "sort": { "type": "string", "description": "'asc', 'desc', or a column name." },
            "limit": { "type": "integer", "minimum": 1 },
            "hole": { "type": "number", "minimum": 0, "maximum": 1 }
          },
          "additionalProperties": true
        },
        {
          "required": ["vega"],
          "properties": {
            "vega": { "type": "object", "description": "Raw Vega-Lite unit-spec, from #chart-vega:." }
          },
          "additionalProperties": false
        }
      ]
    },
    "anchor": {
      "description": "Address shared by #note and #link. row and key are mutually exclusive.",
      "type": "object",
      "properties": {
        "col": {
          "oneOf": [{ "type": "string" }, { "type": "integer", "minimum": 0 }],
          "description": "Column: a name, or a zero-based index when header=0."
        },
        "row": { "type": "integer", "minimum": 0, "description": "Zero-based data row, not counting the header row." },
        "key": { "type": "string", "description": "Raw value of the row's id column (role=id, else unique=1)." }
      },
      "not": { "required": ["row", "key"] }
    },
    "note": {
      "description": "One #note line. Which address fields are present decides the target: row/key + col = cell, col = column, row/key = row, none = table.",
      "type": "object",
      "required": ["text"],
      "allOf": [{ "$ref": "#/$defs/anchor" }],
      "properties": {
        "col": true,
        "row": true,
        "key": true,
        "text": { "type": "string", "description": "The note. Plain single-line text." },
        "author": { "type": "string" },
        "created": { "type": "string", "format": "date-time" }
      },
      "patternProperties": {
        "^x-": { "description": "Custom, tool-specific attribute." }
      },
      "additionalProperties": false
    },
    "link": {
      "description": "One #link line: a URL on a single cell, overriding the column's link= template.",
      "type": "object",
      "required": ["col", "href"],
      "allOf": [
        { "$ref": "#/$defs/anchor" },
        { "anyOf": [{ "required": ["row"] }, { "required": ["key"] }] }
      ],
      "properties": {
        "col": true,
        "row": true,
        "key": true,
        "href": { "type": "string", "description": "The URL. Rendered as a link only for http, https, mailto." }
      },
      "patternProperties": {
        "^x-": { "description": "Custom, tool-specific attribute." }
      },
      "additionalProperties": false
    },
    "statement": {
      "description": "A single SQL statement, optionally tagged by dialect and version.",
      "type": "object",
      "required": ["stmt"],
      "additionalProperties": false,
      "properties": {
        "dialect": { "type": "string", "description": "e.g. ansi, mysql, postgres, clickhouse. Omitted = ansi / header default." },
        "version": { "type": "string", "description": "Optional dialect version, e.g. '18'." },
        "stmt": { "type": "string", "description": "The SQL statement (single statement, no embedded newlines needed)." }
      }
    },
    "table": {
      "description": "One table inside a pack. Mirrors a single-table document, plus a name.",
      "type": "object",
      "required": ["name", "rows"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "description": "Table name. Unique within the pack." },
        "meta": {
          "type": "object",
          "additionalProperties": true,
          "description": "Table-level metadata (#@ lines in the table's _header)."
        },
        "csv": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "delim": { "type": "string" },
            "quote": { "type": "string" },
            "header": { "type": "boolean" },
            "encoding": { "type": "string" },
            "null": { "type": "array", "items": { "type": "string" } }
          }
        },
        "columns": {
          "type": "array",
          "items": { "$ref": "#/$defs/column" }
        },
        "charts": {
          "type": "array",
          "items": { "$ref": "#/$defs/chart" }
        },
        "notes": {
          "type": "array",
          "items": { "$ref": "#/$defs/note" }
        },
        "links": {
          "type": "array",
          "items": { "$ref": "#/$defs/link" }
        },
        "aggregates": {
          "type": "object",
          "additionalProperties": {
            "type": "array",
            "items": { "$ref": "#/$defs/cell" }
          }
        },
        "sql": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "ddl": { "type": "array", "items": { "$ref": "#/$defs/statement" } },
            "dql": { "type": "array", "items": { "$ref": "#/$defs/statement" } }
          }
        },
        "checksum": { "type": "string", "pattern": "^[a-z0-9-]+:[0-9a-f]+$" },
        "rows": { "type": "integer", "minimum": 0 },
        "column_count": { "type": "integer", "minimum": 0 },
        "data": {
          "type": "array",
          "items": {
            "type": "array",
            "items": { "$ref": "#/$defs/cell" }
          }
        }
      }
    },
    "fk": {
      "description": "An informational foreign key: from <table>.<column> to <table>.<column>.",
      "type": "object",
      "required": ["from", "to"],
      "additionalProperties": false,
      "properties": {
        "from": { "type": "string", "description": "e.g. 'orders.customer_id'." },
        "to": { "type": "string", "description": "e.g. 'customers.id'." }
      }
    }
  }
}
